/* Layout and structural styles */

html, body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--bp-paper);
  color: var(--text);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, "Apple Color Emoji", "Segoe UI Emoji";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  display: grid;
  /* header · flashes · main — and everything after `main` (the modal frame, the
     session / support modals, the site footer) takes an implicit `auto` row.

     🚩 THIS WAS `auto auto auto 1fr auto`, written for five in-flow children
     with the skip link first. #2310 made `.skip-link` `position: fixed`, which
     takes it OUT of grid flow, so every child slid up a row: `main` landed in
     an `auto` row and the `1fr` — the only row that absorbs slack — went to the
     zero-height `#session-expired` wrapper. (Not `turbo-frame#modal`, which
     comes first: `components.css` gives eager turbo frames `display: contents`,
     so that frame has no box and is not a grid item at all.)

     ⚠️ WHAT THAT COST, MEASURED RATHER THAN ASSUMED (#2447). Not a fixed band:
     the stray `1fr` row soaked up ALL the slack whenever a page was shorter than
     the window, so `main` stopped short of the footer by 0px at 1280×720 (the
     page scrolls, there is no slack), 9px at 1440×900 and 209px at 1440×1100.
     That band was blank paper, and it still is below short content — so this
     frees no space a student can use. It restores `main` as the flexible row the
     shell was designed around: its box, its scroll area and its focus outline
     now reach the footer on every page, however tall the window.

     `minmax(0, 1fr)` rather than bare `1fr`: a track's automatic minimum is its
     content, and `main` must never grow to its content height — it is the app's
     one scroll container, and the footer's place on the bottom edge depends on
     `main` scrolling rather than stretching (`app_shell_footer_scroll_test`). */
  grid-template-rows: auto auto minmax(0, 1fr);
  grid-template-columns: 1fr;
}

.container {
  width: min(90%, 2000px);
  margin-inline: auto;
}

/* Skip link (#2310) — the first tab stop on every page, and the route into the
   `main` scroll container that the layout made focusable.

   Copied deliberately, not shared. The identical rule lives in
   public/site/styles.css for the two `layout: false` pages (the landing page and
   the institutions page), which do not load this bundle at all. There is no
   import seam between a Propshaft bundle and a static stylesheet in public/, so
   the choice was duplicate the rule or build one; a change to either
   copy should be made to both.

   `transform`, never `display: none` or `visibility: hidden`: an element the
   browser cannot focus cannot be a skip link. It is on-screen and focusable at
   all times and simply parked above the viewport until it takes focus.

   `position: fixed` rather than `absolute` because `body` is the shell's grid
   and an absolute here would be laid out into a grid row. */
.skip-link {
  position: fixed;
  top: var(--bp-s-2);
  left: var(--bp-s-2);
  /* Its own rung. `--bp-z-chrome` would NOT do: `.app-bar` is on that rung and
     comes later in the DOM, so an equal z-index puts the chrome on top and the
     skip link renders underneath the thing it exists to skip past. */
  z-index: var(--bp-z-skip);
  padding: 10px 14px;
  border-radius: var(--bp-r-md);
  background: var(--bp-ink);
  color: var(--bp-paper);
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform var(--bp-dur-fast) var(--bp-easing);
}

/* `:focus`, not `:focus-visible`. The whole point is to appear the moment it
   takes focus, and the only way it ever does is a Tab press. */
.skip-link:focus {
  transform: translateY(0);
}

/* Signed-out header: main/_header is shared with the public website.
   Keep these scoped rules aligned with public/site/styles.css; loading that
   entire stylesheet here would restyle authentication forms and product pages. */
.site-header {
  --bp-accent: #087bbd;
  --bp-ink-2: #30362f;
  --bp-rule: rgba(26, 26, 26, 0.16);
  --bp-rule-soft: rgba(26, 26, 26, 0.08);
  font-size: 15px;
  line-height: 1.6;
  position: sticky;
  top: 0;
  z-index: var(--bp-z-chrome);
  border-bottom: 1px solid var(--bp-rule-soft);
  background: rgba(255, 255, 255, 0.96);
  backdrop-filter: blur(14px);
}

.site-header .header-inner {
  padding: 0;
  display: flex;
  align-items: center;
  gap: 28px;
  width: calc(100% - 56px);
  min-height: 76px;
  margin-inline: auto;
}

:is(.site-header, .marketing-footer) .wordmark {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}

:is(.site-header, .marketing-footer) .wordmark__mark {
  width: 32px;
  height: 32px;
  object-fit: contain;
}

:is(.site-header, .marketing-footer) .wordmark__text {
  width: 112px;
  height: 20px;
  object-fit: contain;
}

.site-header .nav-menu {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: flex-end;
  gap: 24px;
}

.site-header .nav-links {
  display: flex;
  align-items: center;
  gap: 24px;
}

.site-header .nav-links a {
  padding: 0;
  border-radius: 0;
  font-weight: 400;
  color: var(--bp-ink-2);
  font-size: 14px;
  line-height: 23px;
  text-decoration: none;
}

.site-header .nav-links a:hover {
  color: var(--bp-accent);
}

.site-header .header-login {
  padding-block: 10px;
  min-height: 46px;
  padding-inline: 20px;
  font-size: 16px;
  font-weight: 700;
  line-height: 24px;
}

.site-header .nav-toggle {
  display: none;
  width: 42px;
  height: 42px;
  margin-left: auto;
  padding: 10px;
  border: 1px solid var(--bp-rule);
  border-radius: var(--bp-r-md);
  background: var(--bp-paper);
  cursor: pointer;
}

.site-header,
.site-header *,
.site-header *::before,
.site-header *::after {
  box-sizing: border-box;
}

.site-header img {
  display: block;
}

/* ⚠️ DARKENS ON HOVER, AND IT CANNOT BRIGHTEN. This was `filter: brightness(1.1)`,
   which took white 16px/700 text on the accent from 4.59:1 to 3.90:1 — under the
   4.5:1 AA needs for text this size (16px bold is not "large text"; that starts
   at 18.66px bold).

   🚩 There is no smaller version of this fix. AA against white needs a background
   luminance <= 0.1833 and `--bp-accent` is already at 0.1789, so the largest
   `brightness()` multiplier that still passes is 1.01 — no hover state at all.

   📌 The value `.bp-btn--primary:hover` in components.css already uses, and now
   the same one in public/site/styles.css. This rule only exists because its
   higher specificity was overriding that correct one. 5.67:1. */
.site-header .header-login:hover {
  background: color-mix(in srgb, var(--bp-accent) 85%, var(--bp-ink));
  border-color: color-mix(in srgb, var(--bp-accent) 85%, var(--bp-ink));
}

.site-header a[aria-current="page"] {
  color: var(--bp-accent);
  text-decoration: underline;
  text-underline-offset: var(--bp-s-1);
}

.site-header .nav-toggle:focus-visible {
  outline: 2px solid var(--bp-accent);
  outline-offset: 2px;
}

@media (max-width: 900px) {
  .site-header .nav-toggle {
    display: grid;
    place-items: center;
  }

  .site-header .nav-menu {
    position: absolute;
    top: 76px;
    left: 0;
    display: none;
    width: 100%;
    padding: 18px 24px 24px;
    border-bottom: 1px solid var(--bp-rule);
    background: var(--bp-paper);
  }

  .site-header .nav.is-open .nav-menu {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 14px;
  }

  .site-header .nav-links {
    display: grid;
    gap: 0;
  }

  .site-header .nav-links a {
    padding: 12px 0;
    border-bottom: 1px solid var(--bp-rule-soft);
  }
}

@media (max-width: 640px) {
  .site-header .header-inner {
    width: calc(100% - 28px);
  }

  :is(.site-header, .marketing-footer) .wordmark__text {
    width: 100px;
    height: auto;
  }
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  font-weight: 700;
  color: var(--text);
  text-decoration: none;
}
.brand img {
  display: block;
  height: 36px;
  width: auto;
}

/* Flash messages container */
.flashes {
  padding: .8rem 0;
}

/* 🚩 AN EMPTY FLASH ROW COSTS NOTHING (#2447). `#flashes` is a row of the body
   grid on every page, and its padding used to apply whether or not it held a
   message — measured at 26px of blank space between the app-bar and `main` on
   every screen, with no flash in it. The container itself has to stay: JS
   inserts messages into it (`batch_details_controller`), and `:has()` re-applies
   the padding the moment one arrives. `:empty` would not work here — the ERB
   leaves whitespace inside the element even when both conditionals are false. */
.flashes:not(:has(> *)) {
  padding-block: 0;
}

/* Main content */
main {
  /* Named so the one element that must cancel it can read it rather than copy
     it: the Flow A progress bar pins flush to the scroller's top edge by
     offsetting exactly this padding (#2447). A copied number would drift the
     day someone changes this one, and the gap it closes would silently return. */
  --main-pad-top: 1.2rem;
  padding: var(--main-pad-top) 0 2rem;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: auto;
  /* A scroll container must also be a positioning context, or it does not
     actually contain what it appears to. `overflow` only clips descendants whose
     containing block is inside the scroller; an absolutely positioned one
     resolves its containing block to the nearest POSITIONED ancestor, and with
     `main` static that is the initial containing block. So every visually-hidden
     absolute in the app — `.sr-only`, `.questionnaire-field__label.is-suppressed`,
     the `.capabilities-filter__toggle-option` inputs — escaped the clip and was
     laid out at its static position in the document, hundreds of pixels below the
     fold on a long page.
     The shell has a fixed `height: 100%` and parks `.site-footer` in the last
     body-grid row, so `<html>` is meant to have nothing to scroll. Those escaped
     spans gave it a scroll range with nothing visible in it, and a fast flick
     that ran `main` out of scroll chained the residual delta to the document —
     sliding the whole shell up and stranding the footer mid-screen over blank
     page. Guarded by test/system/app_shell_footer_scroll_test.rb. */
  position: relative;
}

/* `main` is focusable (`tabindex="0"` in the layout, #2310) because it is the
   app's scroll container and a scroll container that cannot take focus cannot
   be scrolled from the keyboard. Focusable means it needs a visible focus
   indicator — WCAG 2.4.7 — and the app's ring is `2px solid var(--bp-accent)`.

   INSET, unlike every other ring in the app. `main` is full-bleed: it spans the
   viewport's width and, in the shell's grid, the row between chrome and footer.
   A positive `outline-offset` on it draws the ring outside the viewport on the
   left and right, where a keyboard user cannot see it — the indicator would be
   present and invisible, which is the failure the rule exists to prevent. A
   negative offset draws it just inside the scroll container's own edge instead.

   `:focus-visible`, never `:focus`. Chrome focuses the nearest focusable
   ancestor on mousedown, so a plain `:focus` rule would outline the entire
   content area every time anyone clicked on ordinary text. */
main:focus-visible {
  outline: 2px solid var(--bp-accent);
  outline-offset: -3px;
}

/* Footer */
.site-footer {
  border-top: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.8);
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: .8rem 1.2rem;
  align-items: center;
  justify-content: space-between;
  /* Trimmed from `1rem 0` (#2447). The footer is pinned to the bottom edge of
     every signed-in screen — `main` is the one scroller and the footer is the
     body grid's last row — so its height is paid on every screen, all the time.
     Measured 52px → 34px, and everything on it stays: Contact support, Privacy,
     Terms. Unpinning it would free all 52px but changes how every page scrolls,
     and `app_shell_footer_scroll_test` guards that deliberately; trimming it
     does not. */
  padding: .45rem 0;
  color: var(--muted);
  font-size: .95rem;
}

.footer-nav {
  display: flex;
  align-items: center;
  gap: .8rem;
}

/* Separators between footer items (#2322 walk).
   Without them the row is whitespace-delimited and every adjacent pair runs
   together — "Privacy Terms" always did, harmlessly. It stopped being harmless
   when a crisis route joined the row: "In crisis? Contact support" read as a
   question followed by its answer. That route left the footer on 2026-09-06,
   but the separators stay — they are what stops the next pair doing the same
   thing, and the row is still a control followed by two documents.

   ⚠️ A real element rather than `a::after`. A pseudo-element on the link puts
   the character INSIDE the accessible name, so the control announces as
   "Privacy middle dot" — and `aria-hidden` cannot be applied to a pseudo. This
   span carries `aria-hidden` in the markup and is skipped entirely. */
.footer-nav__sep {
  color: var(--bp-ink-subtle);
  user-select: none;
}

.footer-nav a {
  color: inherit;
  text-decoration: none;
}

/* The route to support (customer-support PRD, R1). It sits in the footer's own
   row and has to read as a control rather than as a third document, so it takes
   one step of TYPE WEIGHT and nothing else — no colour, no fill, no rule. The
   design contract is explicit that contrast comes from weight, rules and
   whitespace, and that colour is not for emphasis.

   The reset is what a <button> needs to sit in a row of links without the
   browser's default chrome. `:focus-visible` is here rather than inherited
   because this is the only keyboard-operable control in the footer. */
.footer-nav__support {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 600;
  color: inherit;
  cursor: pointer;
}

.footer-nav__support:hover {
  color: var(--bp-ink);
}

.footer-nav__support:focus-visible {
  outline: 2px solid var(--bp-accent);
  outline-offset: 2px;
  border-radius: var(--bp-r-sm);
}

/* Principle 9: hit targets >= 44px on the tier that is actually touched. The
   row is a support control a stuck person has to be able to press on a phone,
   and at its natural 19px line box it is not one. Applied to the whole row so
   the three items stay on one baseline, which also lifts Privacy and Terms to a
   real target for the first time. Mobile only — desktop keeps the footer's
   existing density. */
@media (max-width: 620px) {
  /* The separators are decorative; hiding them below the wrap point keeps a
     wrapped row from starting or ending a line with a stray dot. */
  .footer-nav__sep { display: none; }

  .footer-nav a,
  .footer-nav__support {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

/* Hero section */
.hero {
  padding: 3.2rem 0 2rem;
}

.hero-inner {
  text-align: center;
}

.hero-title {
  font-size: clamp(1.8rem, 3.6vw, 2.6rem);
  letter-spacing: 0;
  margin: 0 0 .5rem;
}

.hero-subtitle {
  color: var(--muted);
  font-size: clamp(1rem, 1.4vw, 1.15rem);
  margin: 0 auto 1rem;
  max-width: 60ch;
}

.hero-ctas {
  display: flex;
  gap: .6rem;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: .6rem;
}

/* Info section */
.info-section {
  padding: 1.2rem 0 2.2rem;
}

.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
}

.info-icon {
  font-size: 1.3rem;
}

.info-title {
  font-weight: 700;
  margin: 0;
}

.info-text {
  color: var(--text);
  opacity: .9;
  margin: 0;
}

/* Page header utility */
.page-header {
  display: grid;
  gap: .25rem;
  margin-bottom: .8rem;
}

/* Content wrapper utility */
.content {
  display: grid;
  gap: .6rem;
}

/* Public footer on authentication pages, matching main/_footer on the website.

   🚩 NO `body.signed-out` SHELL OVERRIDE HERE, AND IT MUST NOT COME BACK.

   One shipped that switched the signed-out body to `display: flex` with
   `height: auto`, and gave `main` `overflow: visible`, to hold the footer down
   on a short page. The shell already does that: `body` is a grid whose
   `grid-template-rows` ends `1fr auto`, so `main` takes the slack and
   `.site-footer` sits in the last row. Measured on /users/sign_in with the
   override removed — the footer's bottom edge is still exactly the viewport's.

   ⚠️ What the override DID do was give signed-out pages a second scroll model.
   `main` stopped being the scroll container (`overflow: visible`), so
   `main.scrollTop` was pinned at 0, and `height: auto` let the body outgrow the
   viewport and raise a document scrollbar — which narrowed the phone tier by
   15px and broke SharedDrawerTest's 390px measurement at 375. Neither symptom
   looked like CSS, and the second one is a real layout change on every
   signed-out page, not just a test artefact. `main` is the app's ONE scroller:
   see keyboard_scroll_test.rb, app_shell_footer_scroll_test.rb, and the note on
   `main` above.

   📌 BUT THE OVERRIDE WAS SOLVING A REAL PROBLEM, and the rule below is that
   problem fixed narrowly. `body`'s `grid-template-rows` is POSITIONAL — it was
   `auto auto auto 1fr auto` when this was written (see the note on `body` for
   what it is now and why) — and was written for the signed-in child count. A
   signed-out page has one fewer in-flow child (the empty `turbo-frame#modal` is
   not laid out), so every item shifts up a row and the FOOTER lands on the
   `1fr`. On a short page like /users/sign_in that stretched it from its natural
   125px to 152px, which is the sag the flex override was reaching for.

   Moving the flexible row onto `main` fixes it without touching how anything
   scrolls: measured on /users/sign_in, the footer is back to 125px — the same
   height it has on the website — its bottom edge is still the viewport's, and
   `main` still owns the overflow.

   ⚠️ Still positional, because the shell is. If a signed-out page ever gains or
   loses a body-level child this shifts with it; navigation_test.rb's
   `footer_appearance` compares the website's footer against the signed-out
   app's computed styles and is what catches that drift. */
body.signed-out {
  grid-template-rows: auto auto 1fr auto auto;
}

.marketing-footer {
  --bp-accent: #087bbd;
  --bp-ink-muted: #666d67;
  --bp-rule: rgba(26, 26, 26, 0.16);
  padding: var(--bp-s-6) 0;
  border-top: 1px solid var(--bp-rule);
  background: var(--bp-paper);
  color: var(--bp-ink);
  font-size: 15px;
  line-height: 1.6;
}

.marketing-footer,
.marketing-footer * {
  box-sizing: border-box;
}

.marketing-footer img {
  display: block;
}

.marketing-footer a {
  color: inherit;
  text-decoration: none;
}

.marketing-footer .footer-grid {
  width: min(1180px, calc(100% - 48px));
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: var(--bp-s-6);
  row-gap: var(--bp-s-2);
}

.marketing-footer .section-stack {
  grid-row: span 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--bp-s-2);
}

.marketing-footer .small-copy {
  margin: 0;
  color: var(--bp-ink-muted);
  font-size: 13px;
  line-height: 20px;
}

/* No `grid-column` here on purpose: there are TWO of these navs now (Explore and
   Support), so auto-placement is what puts them in columns 2 and 3. Pinning them
   to a column stacked both in the same one. Auto-placement is also what makes the
   single-column phone tier below need no override. */
.marketing-footer .footer-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--bp-s-5);
}

.marketing-footer .footer-links a {
  font-size: 15px;
  line-height: 26px;
}

/* The legal row sits under the wayfinding one and reads quieter, the way the
   signed-in footer's own legal row does. */
.marketing-footer .footer-links--legal a {
  color: var(--bp-ink-muted);
  font-size: 14px;
}

.marketing-footer .footer-links a:hover {
  color: var(--bp-accent);
}



.marketing-footer a[aria-current="page"] {
  color: var(--bp-accent);
  text-decoration: underline;
  text-underline-offset: var(--bp-s-1);
}

@media (max-width: 640px) {
  .marketing-footer .footer-grid {
    width: calc(100% - 28px);
    grid-template-columns: 1fr;
    gap: 36px;
  }

  /* No `.section-stack` gap override at this tier. The column footer needed one
     because its brand block stood alone above three tall columns; the compact
     footer's brand block is two tight lines at every width, and the site
     stylesheet has no such override — a 24px gap here and 8px there is a 16px
     drift between two copies of one footer, which is exactly what
     navigation_test.rb's `footer_appearance` comparison exists to catch. */
}
