/* careers.css — the public career surface: the job board (/, /careers/<slug>,
 * /careers/job/<code>) and the application funnel (/apply).
 *
 * Public, unauthenticated, and served at /static/careers/careers.css. It is the
 * ONLY stylesheet either surface loads. There is no build step, no framework, no
 * CDN and no preprocessor: what is in this file is what the browser gets.
 *
 * ## Why this file exists at all, instead of reusing what we have
 *
 * Palette is the MARKETING system — navy #0A1857, orange #F97316, #F8FAFC page,
 * DM Sans headings over Nunito body. An applicant who clicks through from
 * stepaheadaba.com must land on the same brand, not on a second product wearing
 * the same logo. So this file deliberately does NOT import or extend
 * frontend/leap/tokens.css: that is the internal tool's cool-slate ramp with a
 * burnt-orange accent, it is a different audience on a different task, and one
 * shared token file would force every future change on one product through the
 * other one's review.
 *
 * Nothing here is imported by the app shell either. If you find yourself adding
 * a rule to this file for a screen behind /login, it belongs in leap/app.css.
 *
 * ## Three colours from the approved mockup are NOT in this file
 *
 * David approved the mockup; the mockup has three measured contrast failures on
 * the surfaces those colours are actually used on. Each replacement is marked
 * FIXED below with both numbers, computed with the WCAG relative-luminance
 * formula (the same maths as ui-craft/scripts/audit.py: contrast_ratio), not
 * eyeballed. Do not restore them from the mockup:
 *
 *     muted   #707070 -> #5B6472   (4.47:1 -> 5.40:1 on --icon-bg)
 *     orange-as-text #F97316 -> #B4460E  (2.80:1 -> 5.49:1 on --surface)
 *     tick    #10B981 -> #047857   (2.41:1 -> 5.21:1 on --ok-bg)
 *
 * The incident behind the rule: the 2026-08-14 design brief records #8A8A8A at
 * 3.31:1 on this exact page family, where the least readable text on the page
 * was the sentence explaining what a list was hiding. #8A8A8A appears nowhere in
 * this file and neither does any colour outside the contrast table in SPEC §4.1.
 *
 * ## CSP
 *
 * script-src 'self' and font-src 'self' are enforced and tested
 * (main.py CSP, test_http_auth.test_csp_is_present_and_restrictive). That means:
 * no @import from a font host, no Google Fonts <link>, no remote image, no
 * data-URI font. Every url() in this file is a same-origin relative path.
 *
 * ## The rules a reviewer should hold this file to
 *
 * One interactive hue (navy). One ornament (orange), which is never text and
 * never a fill under white ink. Two semantic colours, each used by exactly one
 * component. Spacing from the 4px scale only — a 13px padding is the single
 * clearest signal a machine composed a layout, because a human would have reused
 * a value. Every interactive box is at least 44x44. Every input is at least 16px
 * so iOS does not zoom the page on focus. Mobile-first: the base rules ARE the
 * 390px design and every media query is min-width, so a narrow or unknown
 * viewport gets the simple layout rather than a desktop layout it cannot hold.
 */

/* ------------------------------------------------------------------ fonts
 * Four self-hosted woff2 files, already on disk at frontend/careers/fonts/ and
 * verified there by sha256 against the marketing site's copies.
 *
 * The url()s are RELATIVE. This file is served from /static/careers/careers.css,
 * so "fonts/dm-sans-latin.woff2" resolves against the stylesheet's own URL to
 * /static/careers/fonts/dm-sans-latin.woff2 — which the StaticFiles mount at
 * /static -> frontend/ serves from frontend/careers/fonts/. An absolute
 * /static/... path would work today and break the day this surface moves behind
 * a different mount point; a relative path moves with the file.
 *
 * THE SUBSET MAPPING IS NOT WHAT ADJACENCY SUGGESTS. It was parsed per
 * @font-face block out of get-started-app/assets/fonts.css, not read off by
 * eye — reading the unicode-range lines by adjacency gets it exactly backwards.
 * The failure mode is silent and expensive: ship the Cyrillic subset as "Nunito
 * latin" and it matches no character on an English page, so font-display: swap
 * resolves to nothing, Nunito falls back to the system stack, and the network
 * tab looks perfectly healthy while the brand is gone.
 *
 *     dm-sans-latin.woff2      62,556 B   was f01_…   U+0000-00FF
 *     dm-sans-latin-ext.woff2  31,312 B   was f00_…   U+0100-02BA
 *     nunito-latin.woff2       39,152 B   was f07_…   U+0000-00FF
 *     nunito-latin-ext.woff2   35,464 B   was f06_…   U+0100-02BA
 *
 * latin-ext ships on purpose and is not padding: the Hawaiian kahakō (ā ē ī ō ū,
 * U+0101 and friends) lives there and Hawaii is our largest state. The ʻokina
 * (U+02BB) is in the basic-latin subset of both families, so Waiʻanae, Waipahu
 * and Kāneʻohe render from the latin file alone.
 *
 * font-weight is a RANGE because both shipped files are variable, so 400/600/700
 * all come out of the one file per subset. Verify in a real browser: if 700
 * renders as synthetic bold, split into one block per weight against the same
 * URL, exactly as the marketing fonts.css does. That failure is also silent.
 */
@font-face {
  font-family: "DM Sans";               /* headings, buttons, labels */
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;                   /* text is readable before the font lands */
  src: url("fonts/dm-sans-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("fonts/dm-sans-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Nunito";                /* body copy, all prose */
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("fonts/nunito-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Nunito";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("fonts/nunito-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ================================================================= tokens
 * Decided once, here. No component below re-decides a colour, a radius or a
 * spacing value; re-deciding is what produces the assembled-from-parts look.
 *
 * Every token carries where it is used and, where the distinction is
 * load-bearing, what it must NOT be used for. The "must not" half is the half
 * that gets dropped in a hurry and it is the half that causes the incident.
 */
:root {
  /* ---- surfaces -------------------------------------------------------
   * Four neutral steps and four tinted fills. A tinted fill is always SOFT and
   * always paired with a border or a rail: soft fill means a state you read,
   * solid fill means a thing you click. Break that and the stale band starts
   * getting tapped. */
  --page:      #F8FAFC;  /* body only. Never a card fill — #fff on #fff has no edge */
  --surface:   #FFFFFF;  /* card, header bar, job card, funnel card, input      */
  --sunken:    #F1F5F9;  /* skeleton fill, disabled fill, neutral badge, hover  */
  --icon-bg:   #F0F3FF;  /* 44px icon well, pay badge, selected choice/case     */
  --orange-bg: #FFF3E9;  /* distance chip, stale band, <noscript>. NOT a button */
  --orange-br: #FDE0C6;  /* the 1px border that always accompanies --orange-bg  */
  --ok-bg:     #ECFDF5;  /* exactly one element: the funnel's completion disc   */
  --danger-bg: #FEF3F2;  /* field error and error-section fill. Nothing else    */

  /* ---- ink ------------------------------------------------------------
   * FIXED: the mockup's muted #707070 measures 4.47:1 on --icon-bg and 4.54:1
   * on --orange-bg — under AA on two of the four surfaces it is used on — and
   * 4.73:1 on --page is a rounding error away from failing. #5B6472 clears
   * 5.40:1 on every surface in this file. Secondary text carries the counts and
   * the exclusions, which is the text a candidate most needs to read. */
  --ink:   #0A1857;  /* h1/h2/h3, wordmark, primary fill, focus ring (light)  */
  --ink-2: #1B2B4B;  /* h3 on a tinted fill, footer headings                  */
  --body:  #333333;  /* every sentence, card title, choice label              */
  --muted: #5B6472;  /* counts, meta, help, provenance, placeholder           */

  /* ---- the one interactive hue: navy ----------------------------------
   * Anything a person can click or that shows selection is navy. There is no
   * second interactive colour, which is why an orange element is never
   * mistaken for a control. --on-nav exists because `color: #fff` written
   * inline does not survive the dark block: a fill token needs a paired ink
   * token or it is only correct in the theme it was written in. */
  --nav:        #0A1857;  /* primary fill, selected border, focus ring        */
  --nav-hover:  #0D2070;
  --nav-active: #081142;  /* :active — pressed reads darker, never lighter    */
  --on-nav:     #FFFFFF;  /* ink ON --nav: 16.39:1 light, 8.08:1 dark         */

  /* ---- orange: ornament, plus one readable stop -----------------------
   * #F97316 measures 2.80:1 on --surface, 2.68:1 on --page and 2.57:1 on
   * --orange-bg. So --orange may NEVER be text, never carry white text, and
   * never be the only boundary of a control. It is a shape, not a value.
   * FIXED: the mockup used it for the "4 miles away" line and a 12px icon
   * stroke inside --orange-bg. Both of those are --orange-ink here. */
  --orange:     #F97316;  /* ornament only: the 3px rail on a stale band, the
                           * 2px underline under the h1's last word          */
  --orange-ink: #B4460E;  /* orange when it must be READ: the distance band,
                           * the freshness line, the dark-mode focus ring    */

  /* ---- semantic: one component each ----------------------------------
   * FIXED: the mockup's #10B981 is 2.54:1 on white and 2.41:1 on --ok-bg — as
   * a glyph it is not there. --ok is the funnel completion tick and the
   * "Applied" badge and nothing else; --danger is validation and error blocks
   * and nothing else. A semantic colour used decoratively stops meaning
   * anything, and then a real error looks like a style. */
  --ok:     #047857;
  --danger: #B42318;

  /* ---- borders: two tokens because they have two jobs ------------------
   * FIXED: the mockup puts a 2px #E2E8F0 border on inputs and choice cards. At
   * 1.23:1 that is a component boundary nobody with low vision can find, and
   * WCAG 1.4.11 asks 3:1 of exactly this. So controls get --line-control
   * (3.58:1) and decorative dividers keep --line. The visual weight lost is
   * bought back by keeping the mockup's 2px width on controls.
   * --line is decorative BY DEFINITION: never put it on something clickable. */
  --line:         #E2E8F0;  /* dividers, hairlines, card edge, footer rule   */
  --line-control: #7C889C;  /* input, choice card, chip, secondary button    */
  --track:        #E5E7EB;  /* progress track and :active fill. navy on it
                             * measures 13.24:1                             */

  /* ---- spacing: a 4px scale, no exceptions -----------------------------
   * Nine steps is enough for two pages. If a layout seems to need a tenth, the
   * layout is wrong, not the scale. Off-scale values are what the ui-craft
   * audit flags first and they are the tell it is right about. */
  --s1:  4px;   --s2:  8px;   --s3: 12px;  --s4: 16px;
  --s5: 24px;   --s6: 32px;   --s7: 48px;  --s8: 64px;  --s9: 96px;
  /* --s9 is currently unused. It stays because a spacing scale with a hole in
   * it is worse than an unused step: the next person needing a big gap invents
   * 100px rather than finding 96px missing and asking why. */

  /* Touch floor. Every interactive box asserts this, not only the ones that
   * look small — the failures are always the control nobody measured. The
   * mockup's availability cell was 2.4rem (38px), which is the whole reason
   * this token is written down instead of remembered. */
  --tap: 44px;

  /* ---- type -----------------------------------------------------------
   * Two families, which is the maximum, and both are already the marketing
   * site's. The fallback stacks are real rather than decorative: with
   * font-display: swap the first paint is rendered in them, so a stack that
   * ends at `sans-serif` alone would reflow visibly on every cold load. */
  --font-head: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
               Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-body: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI",
               Roboto, "Helvetica Neue", Arial, sans-serif;

  /* A 1.25 ratio rounded to integers. 12px is the floor for any UI text and
   * 16px the floor for prose — body text under 16px on a desktop is a tell,
   * and under 16px in an INPUT makes iOS zoom the page on focus and jump the
   * layout, which is why .input and .input--search are both >= 16px. */
  --t-meta: 13px;  /* provenance, badge text, footer legal, breadcrumb      */
  --t-ui:   14px;  /* count line, chips, card meta, labels, hints           */
  --t-body: 17px;  /* every sentence, card title, choice label              */
  --t-lead: 18px;  /* hero lead, funnel input text, question sub-line       */
  --t-h3:   20px;  /* section heading, answered-group value                 */
  --t-h2:   25px;  /* funnel question, and the board h1 from 320 to 767px   */
  --t-h1:   36px;  /* the board h1 at >= 768px only                         */

  --lh-tight: 1.25;  /* headings                                            */
  --lh-body:  1.55;  /* prose — the marketing site's measure                 */
  --lh-dense: 1.4;   /* meta and card lines, where 1.55 wastes card height  */
  --track-head: -0.015em;

  /* ---- radius ---------------------------------------------------------
   * Base 4. Nested law: inner = outer − padding, pills exempt. Worked example,
   * the availability grid: wrapper --r (12) with --s1 (4) of padding contains
   * cells at --r-sm (8), and 12 − 4 = 8, so the curves are concentric. Where
   * the subtraction reaches zero — a 16px card with 16px of padding — the
   * contents are NOT boxed at all: dividers and proximity, which is the honest
   * answer to the law rather than an invented small radius. */
  --r-sm: 8px;   --r: 12px;   --r-lg: 16px;   --r-pill: 999px;

  /* ---- elevation ------------------------------------------------------
   * One light source, above and slightly forward. Layered, navy-tinted,
   * nothing above 8%. Borders — not shadows — for anything that is not
   * actually floating: a job card and a filter bar are attached to the page.
   * The funnel card and the sticky mobile action bar do float. */
  --sh1: 0 1px 2px rgb(10 24 87 / .04), 0 1px 3px rgb(10 24 87 / .06);
  --sh2: 0 2px 4px rgb(10 24 87 / .04), 0 4px 8px rgb(10 24 87 / .06);
  /* --sh3 is currently unused: nothing on either surface is a modal, a popover
   * or a drag preview, which are the only things that earn the top step. It is
   * declared so that if one ever ships it does not get a hand-rolled shadow. */
  --sh3: 0 4px 8px rgb(10 24 87 / .04), 0 12px 24px rgb(10 24 87 / .08);

  /* ---- motion ---------------------------------------------------------
   * Motion explains causality or it is noise. --fast for a control's own
   * state, --mid for a thing arriving or leaving, --slow only for the funnel's
   * question change. Nothing in this file exceeds 300ms.
   *
   * EVERY duration in this file is one of these three tokens. That is not
   * tidiness: the prefers-reduced-motion block at the bottom neutralises
   * motion by redefining these three, which is how it reaches rules it does
   * not name without a specificity fight. If you add a literal duration
   * anywhere below, add it to that block too or it will keep animating for
   * someone who asked it not to. */
  --fast: 120ms;   --mid: 200ms;   --slow: 300ms;
  --pulse: 1.4s;                                 /* skeleton, deliberately slow */
  --ease-out: cubic-bezier(.2, .8, .3, 1);       /* entering */
  --ease-in:  cubic-bezier(.4, 0, 1, 1);         /* leaving  */
  /* --slow and --ease-in are currently unused, and that is the correct
   * outcome rather than an omission. --slow was reserved for the funnel's
   * question change; that change was then specified as a 200ms rise and fade
   * instead of the mockup's 300ms horizontal slide, because a horizontal slide
   * on iOS competes with the back-navigation gesture. --ease-in has no user
   * because nothing on either surface animates OUT: a filtered card takes
   * [hidden] and leaves immediately, since a card fading away is a card
   * someone tries to tap. */

  /* ---- geometry -------------------------------------------------------- */
  --header-h:    56px;   /* 44px tap target with 6px above and below         */
  --shell-max: 1120px;   /* page gutter-to-gutter cap                       */
  --list-max:   720px;   /* the posting list column. ONE column, never a
                          * grid of identical cards: an even grid has no
                          * focal point, which is both a craft tell and a
                          * lie about which of 116 postings matters.        */
  --card-max:   672px;   /* funnel card — the mockup's 42rem, kept          */
  --rail-w:     320px;   /* the >= 1024px right rail                        */
}

/* ============================================================ dark theme
 * DELIBERATE DECISION, not a default: ship the full dark theme. Job-board
 * traffic is overwhelmingly phone traffic, a large share of those phones sit in
 * system dark mode, and a forced-light page on a dark phone at 11pm is a real
 * cost to a real audience. `color-scheme: light only` would also put this
 * surface out of step with login.html and leap/tokens.css, both already
 * theme-aware.
 *
 * Dark is NOT an inversion. Surfaces lift off a navy-black; text stops short of
 * white; shadow gives way to a lighter surface, because shadow is invisible on
 * dark. Exactly one role changes: --nav. Navy cannot be both the page and the
 * button, so the primary fill lightens to #3A4A96 and keeps white ink at
 * 8.08:1. The brand survives that because a deep brand navy is good at being a
 * surface.
 *
 * BOTH BLOCKS DEFINE THE SAME TOKENS AND MUST BE EDITED TOGETHER. A colour
 * defined in only one of them vanishes for whoever has the other setting, and
 * the symptom is one unreadable component that nobody can reproduce.
 *
 * There is no in-page theme toggle on a public marketing page — the system
 * preference is the whole contract — so unlike leap/tokens.css there is no
 * third [data-theme] block to keep in step. If a toggle is ever added, this
 * becomes three blocks, not two. */
@media (prefers-color-scheme: dark) {
  :root {
    --page:      #0B1024;
    --surface:   #131A38;
    --sunken:    #1B2450;
    --icon-bg:   #1B2450;
    --orange-bg: #2A1A0E;
    --orange-br: #4A2E17;
    --ok-bg:     #082B21;
    --danger-bg: #2E1213;

    --ink:   #EAECF5;
    --ink-2: #D3D8EA;
    --body:  #D3D8EA;
    --muted: #A2ACC8;

    --nav:        #3A4A96;
    --nav-hover:  #4256B0;
    --nav-active: #2E3E86;
    --on-nav:     #FFFFFF;

    --orange:     #FB923C;
    --orange-ink: #FDBA74;

    --ok:     #6EE7B7;
    --danger: #FCA5A5;

    --line:         #2A3565;
    --line-control: #6B77AE;
    --track:        #2A3565;

    /* Shadow is nearly invisible on dark, so these stay only to give a
     * floating thing a soft edge. Separation is carried by --surface lifting
     * off --page, which is why --surface is a real step and not #0F1430. */
    --sh1: 0 1px 2px rgb(0 0 0 / .30);
    --sh2: 0 2px 6px rgb(0 0 0 / .35);
    --sh3: 0 8px 24px rgb(0 0 0 / .40);
  }
  /* ---- job page hero band: dark gradient -------------------------------
   * In dark mode the orange tint is softened to an 8% overlay so the warm
   * register survives without the band glowing. --surface is already a real
   * step off --page in dark, so the gradient reads as a lift, not a flash. */
  .hero-job {
    background: linear-gradient(135deg, var(--surface), rgba(249,115,22,0.08));
  }
}

/* ------------------------------------------------------------------- base */
* { box-sizing: border-box; }

html {
  /* Stops iOS inflating text in landscape, which silently breaks the 320px
   * column this whole file is measured at. */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--page);   /* explicit: a transparent body borrows a colour */
  color: var(--body);
  font: var(--t-body)/var(--lh-body) var(--font-body);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, .u-head {
  font-family: var(--font-head);
  font-weight: 600;
  letter-spacing: var(--track-head);
  line-height: var(--lh-tight);
  color: var(--ink);
  margin: 0;                 /* every gap in this file is deliberate, not UA */
  /* Headings carry a BUILT title containing a city that came out of Zoho, and
   * Zoho text is treated as dirty by every other guard on this surface. A
   * heading wraps rather than truncates — "Spotsylvania Courthouse, VA" is a
   * real 23-character city and going to three lines is the correct answer —
   * but a single unbroken token would push the page sideways at 320px, which
   * is the one thing the layout must never do. This breaks the word instead. */
  overflow-wrap: break-word;
}
h1 { font-size: 28px; }          /* mobile-first: 28px is the base, not 36px */
h2 { font-size: var(--t-h3); }
h3 { font-size: var(--t-body); }
@media (min-width: 768px) { h1 { font-size: var(--t-h1); } }

p { margin: 0; }
img { max-width: 100%; }         /* nothing here can push the page sideways */

a { color: var(--ink); }

/* ---- utilities ----
 * Three, and no more: a utility layer is where a design system goes to die. */

/* Every number a person compares or is paid by. Without this, a column of
 * figures — 116/30/29, $22–$23, 12 mi ±6 — visibly jitters between rows, which
 * reads as sloppiness even to people who cannot name what is wrong. Applied by
 * class here AND baked into .count, .chip-filter .n, .badge, .tail, .stepnum
 * and .states, so a template that forgets the class still gets it. */
.u-num { font-variant-numeric: tabular-nums; }

/* Visually hidden but reachable: the label on the board's town search, the
 * "current" marker on the active state chip. Never display:none — that takes it
 * out of the accessibility tree, which is the whole point of it existing. */
.u-sr {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ---- focus ----
 * ONE focus treatment, everywhere, no per-component exceptions.
 *
 * FIXED: the mockup's 4px navy-at-20% ring over white measures about 2:1, which
 * is decoration, not a focus indicator. This is 3px SOLID navy at 2px offset —
 * 16.39:1 on --surface, 15.67:1 on --page — and the offset leaves a real gap, so
 * it survives Windows high-contrast mode instead of merging into the control.
 *
 * :focus-visible, not :focus, so a mouse click on a card does not leave a ring
 * behind it; :where() so the whole thing has zero specificity and any component
 * rule below can still set its own border colour without fighting this. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--nav);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
@media (prefers-color-scheme: dark) {
  /* Navy is the button fill in dark mode, so a navy ring on a navy button is
   * invisible. --orange-ink is 10.10:1 on dark --surface and is not used as a
   * control colour anywhere, so it cannot be confused with a selected state. */
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline-color: var(--orange-ink);
  }
}

/* The one exclusion, and it is not a weakening. A step heading carries
 * `tabindex="-1"` for exactly one reason: `render()` moves focus to it so a
 * screen reader announces the new question instead of leaving the reader on a
 * button that no longer exists. It is NOT in the tab order, so no keyboard user
 * can land on it and there is nobody for a ring to inform — while the ring the
 * rule above painted put a 3px navy box around the question text, which reads
 * as an empty input field. Seen in Chrome on /apply?job=… at 390px, which is
 * the first screen every ad will land on.
 *
 * The announcement is the accessibility feature here; the outline is not. Drop
 * the outline, keep the focus move. */
:where(h1, h2, h3)[tabindex="-1"]:focus,
:where(h1, h2, h3)[tabindex="-1"]:focus-visible { outline: none; }

/* ============================================================== layout
 * Neither DESIGN.md nor SPEC §4.2 ships a layout layer — they specify the
 * components. These are the containers those components sit in, named after the
 * regions the SPEC §5 wireframes annotate, so a template author has one obvious
 * class per region instead of inventing one per page.
 *
 * Base = the 390px design. Every query below is min-width, so 320px and any
 * viewport we have not thought of gets this single-column layout. */

.shell {
  max-width: var(--shell-max);
  margin: 0 auto;
  padding: 0 var(--s4);      /* 16px gutters: at 320px that leaves 288px      */
}

/* ---- the board's marketing band ----
 * D-49: the marketing register and the data register are ZONED, not blended.
 * Everything above .filterbar is marketing, everything below it is a data
 * product, and the filter bar is the hard line. Whitespace is spent here and
 * withdrawn below — 116 postings at 32px of card padding is a page nobody
 * reaches the bottom of. */
.hero { padding: var(--s6) 0 var(--s5); }
.hero .lead { font-size: var(--t-lead); color: var(--body); margin-top: var(--s5); }
.hero .states-line { font-size: var(--t-ui); color: var(--muted); margin-top: var(--s4); }

/* Measured at 1280px before this cap existed: the lead ran the full 1088px
 * column at 18px, which is ~121 characters a line. Prose stops being readable
 * somewhere around 75 and the eye loses the line return. Capped in `ch` for the
 * same reason `.prose` is — measure is a property of the text, not of the
 * viewport — and left un-capped below 768px because there the column is already
 * narrower than the cap. */
.hero .lead { max-width: 68ch; }

/* The one ornamental use of full-strength orange: a 2px underline under the
 * h1's last word. It is a border, never a text colour — #F97316 as text is
 * 2.80:1. Removing it costs nothing but the brand's signature.
 *
 * `nowrap` because a border-bottom on a phrase that wraps renders as TWO
 * disconnected underlines, one under each fragment, which reads as a rendering
 * fault rather than as emphasis — seen at 390px, where "in 7 states." broke
 * after the 7. The marked phrase is two words by construction (the template
 * marks the state count), so it cannot be long enough to overflow 320px. */
.mark { border-bottom: 2px solid var(--orange); white-space: nowrap; }

/* The hero CTA is desktop-only: below 768px the one primary action lives in the
 * sticky .actionbar where a thumb is, and two copies of one primary action is
 * two primary actions. */
.hero .btn { display: none; }
@media (min-width: 768px) { .hero .btn { display: inline-flex; } }

/* ---- the data region ---- */
.list {
  max-width: var(--list-max);
  display: grid;
  gap: var(--s3);            /* 12px: cards are dense on purpose */
}
/* Left-aligned, never centred, and never a multi-column grid at any width.
 * See --list-max. */
.group + .group { margin-top: var(--s6); }
.group > h2 { font-size: var(--t-h3); margin-bottom: var(--s3); }

/* D-47: one line per state group replaces 68 thin city pages. It carries two
 * counts, so it is tabular. */
.tail {
  max-width: var(--list-max);
  margin-top: var(--s3);
  font-size: var(--t-ui);
  line-height: var(--lh-dense);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* End of a 116-card list. A labelled rule, so "there is no more" is stated
 * rather than inferred from running out of scroll. */
.eol {
  display: flex; align-items: center; gap: var(--s3);
  max-width: var(--list-max);
  margin: var(--s6) 0 var(--s4);
  font-size: var(--t-meta); color: var(--muted);
}
.eol::before, .eol::after {
  content: ""; flex: 1 1 auto; height: 1px; background: var(--line);
}

/* build_description()'s HTML — p, ul, li, strong — plus the Pay and training
 * sections on the job page. Capped by characters, not pixels: measure is a
 * property of the text, and ~68 characters is where a line stops being
 * comfortable regardless of viewport. */
.prose { max-width: 68ch; }
.prose p, .prose ul { margin: 0; }
.prose > * + * { margin-top: var(--s4); }
.prose ul { padding-left: var(--s5); }
.prose li + li { margin-top: var(--s2); }
.prose strong { font-weight: 700; color: var(--ink-2); }

.sect { margin-top: var(--s7); }
.sect > h2 { font-size: 22px; line-height: 28px; margin-bottom: var(--s3); }

/* Breadcrumb on the job page. 12px and muted: it is orientation, not
 * navigation, and the real way back is the 44px link above it. */
.crumb { font-size: var(--t-meta); color: var(--muted); line-height: var(--lh-dense); }
.crumb a { color: var(--muted); text-decoration: underline; text-underline-offset: 3px; }
.crumb a:hover { color: var(--ink); }

/* ---- >= 1024px: list plus a rail ----
 * The rail is ADDITIVE — the town filter, the seven state rows and the
 * straight-line note are all also present in the mobile layout. Nothing is
 * only available on a wide screen. */
@media (min-width: 1024px) {
  .cols {
    display: grid;
    grid-template-columns: minmax(0, var(--list-max)) var(--rail-w);
    gap: var(--s7);
    align-items: start;
  }
  .rail {
    position: sticky;
    top: var(--s4);          /* the board header is NOT sticky (D-48) */
    display: grid; gap: var(--s4);
    font-size: var(--t-ui);
  }
}

/* Openings-by-state table, >= 1024px only, additive: every figure in it is
 * also on a filter chip. Right-aligned tabular figures so 30/29/25/10 line up
 * on the digit rather than on the character. */
.states { width: 100%; border-collapse: collapse; font-size: var(--t-ui); }
.states th, .states td {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--line);
  text-align: left;
}
.states th { font: 600 var(--t-meta)/1.3 var(--font-head); color: var(--muted); }
.states td.n, .states th.n {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* ==================== skip link ====================
 * The first focusable thing in the DOM on both surfaces. Visually hidden until
 * focused — moved off the top, never display:none, which would take it out of
 * the tab order and defeat the entire point of it. z-index 60 so it lands over
 * the sticky filter bar and the sticky funnel header. */
.skip-nav {
  position: absolute; left: var(--s3); top: -100%; z-index: 60;
  display: inline-flex; align-items: center;
  min-height: var(--tap); padding: 0 var(--s4);
  background: var(--surface); color: var(--ink);
  border: 2px solid var(--nav); border-radius: var(--r);
  font: 600 var(--t-ui)/1 var(--font-head); text-decoration: none;
}
.skip-nav:focus-visible { top: var(--s2); }

/* ==================== header bar ====================
 * 56px, --surface, one hairline under it. NOT sticky on the board (D-48: three
 * sticky regions is 156px of chrome on a 390x844 screen, and the count line is
 * the one you need while scrolling past posting 60); sticky on the funnel via
 * .hdr--sticky, where the card is the whole page.
 *
 * Right side is the staff sign-in control and nothing else. David's instruction
 * puts the login button top-right, and two controls in one 320px corner is
 * neither of them — so the phone number lives in the footer. */
.hdr { background: var(--surface); border-bottom: 1px solid var(--line); }
.hdr--sticky { position: sticky; top: 0; z-index: 40; }
.hdr-in {
  max-width: var(--shell-max); margin: 0 auto; padding: 0 var(--s4);
  min-height: var(--header-h);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
}
.hdr-logo { display: flex; align-items: center; min-height: var(--tap); }
.hdr-logo img { height: 40px; width: auto; display: block; }
/* The wordmark beside the logo is desktop-only: at 320px it competes with the
 * one control that has to be findable. The logo carries alt="Step Ahead ABA",
 * so nothing is lost to a screen reader. */
.hdr-name { display: none; }
@media (min-width: 768px) {
  .hdr-name { display: inline; font: 600 var(--t-body)/1 var(--font-head); color: var(--ink); }
}

/* Secondary on purpose: there is exactly one primary action per screen and it
 * belongs to the applicant, not to the handful of staff who know this URL. The
 * label is "Staff sign-in" and never "Sign in" or "Log in" alone — the risk is
 * an applicant seeing a login box and concluding they need an account, and the
 * word "Staff" is what answers that. */
.btn-staff {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: var(--tap); padding: 0 var(--s4);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-control); border-radius: var(--r);
  font: 600 var(--t-ui)/1 var(--font-head);
  text-decoration: none; white-space: nowrap;
  transition: background var(--fast) var(--ease-out),
              border-color var(--fast) var(--ease-out);
}
.btn-staff:hover  { background: var(--sunken); border-color: var(--nav); }
.btn-staff:active { background: var(--track); }
@media (max-width: 359px) {
  /* The one max-width query in this file, and it earns it: at 320px the words
   * "Staff sign-in" and a 40px mark do not both fit beside the tighter
   * gutters. The control is NOT hidden and the label is NOT abbreviated —
   * being findable is the entire job of this button. The logo gives up 8px
   * instead. */
  .hdr-logo img { height: 32px; }
  .btn-staff { padding: 0 var(--s3); }
}

/* ==================== job card ====================
 * One posting. A whole-card link to /careers/job/<code> — navigation, not an
 * action, so it carries no button: 116 Apply buttons is 116 primary actions and
 * therefore none.
 *
 * Border, not shadow, because it is not floating. Card heights DIFFER, because
 * titles, schedules and city names differ and 24 of 116 postings have no
 * schedule at all; nothing is padded out to make a tidy grid. A row of
 * equal-height cards would be the clearest possible signal that the data was
 * invented. */
.card-job {
  display: block; position: relative;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r);
  padding: var(--s4);
  color: var(--body); text-decoration: none;
  transition: border-color var(--fast) var(--ease-out),
              box-shadow var(--fast) var(--ease-out);
}
.card-job:hover  { border-color: var(--nav); box-shadow: var(--sh1); }
.card-job:active { box-shadow: none; background: var(--sunken); }
/* Filtered out, not restyled: board.js sets [hidden] and the card leaves the
 * layout entirely. A dimmed non-match is a card you try to tap. */
.card-job[hidden] { display: none; }
.card-job h3 {
  font-size: var(--t-body); font-weight: 600; color: var(--ink);
  padding-right: var(--s6);          /* clears the absolute chevron */
  margin-bottom: var(--s1);
}
.card-job .where {
  font: 600 var(--t-ui)/var(--lh-dense) var(--font-body);
  color: var(--body); margin-bottom: var(--s2);
  overflow-wrap: break-word;         /* same reason as the heading rule */
}
.card-job .when {
  font-size: var(--t-ui); color: var(--muted); line-height: var(--lh-dense);
}
.card-job .chips { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s3); }
.card-job .foot {
  font-size: var(--t-meta); color: var(--muted); margin-top: var(--s3);
  font-variant-numeric: tabular-nums;   /* "One family · opened 19 Aug" */
}
/* The chevron is decorative and aria-hidden; the card's accessible name is the
 * heading plus the city line. */
.card-job .chev { position: absolute; right: var(--s4); top: var(--s4); color: var(--muted); }

/* ==================== filter chip ====================
 * State filters, with counts. Selected is never colour-only: it also carries
 * aria-pressed and a 2px border, so it reads in greyscale, in forced-colors and
 * in a screenshot pasted into a bug report. Also used for the funnel's
 * "All mornings" line buttons, which is why it is not called .chip-state. */
.chip-filter {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: var(--tap); padding: 0 var(--s4);
  background: var(--surface); color: var(--body);
  border: 2px solid var(--line-control); border-radius: var(--r-pill);
  font: 600 var(--t-ui)/1 var(--font-head);
  cursor: pointer; white-space: nowrap;
  transition: background var(--fast) var(--ease-out),
              border-color var(--fast) var(--ease-out);
}
.chip-filter .n { font-variant-numeric: tabular-nums; color: var(--muted); }
.chip-filter:hover  { border-color: var(--nav); background: var(--icon-bg); }
.chip-filter:active { background: var(--track); }
.chip-filter[aria-pressed="true"] {
  background: var(--nav); border-color: var(--nav); color: var(--on-nav);
}
.chip-filter[aria-pressed="true"] .n { color: var(--on-nav); opacity: .85; }
/* A chip that would yield 0 postings is NOT RENDERED at all: a chip you can tap
 * into an empty list is a lie about what is behind it. This disabled style
 * exists only for a state that empties while the page is open, and the template
 * must carry the reason in aria-description — a disabled control with no
 * explanation gets reported as a bug, every time. */
.chip-filter:disabled {
  background: var(--sunken); color: var(--muted);
  border-color: var(--line); cursor: not-allowed;
}

/* ==================== count line ====================
 * "Say what you excluded" lives here — `Showing 116 of 116 open postings ·
 * filled and on-hold postings are not shown`. ALWAYS rendered, even when
 * nothing is filtered, so its presence is never itself a signal that something
 * has been hidden. */
.count {
  font: var(--t-ui)/var(--lh-dense) var(--font-body);
  color: var(--body);
  font-variant-numeric: tabular-nums;
}
.count b { font-weight: 700; }
.count .why { color: var(--muted); }

/* The one-click way out of a filter, and the "Change" control on an answered
 * funnel group. One rule for both because they are the same control in two
 * places, and two rules would drift. --ink rather than --muted: this is the
 * escape hatch from an empty screen, so it has to be findable. */
.count-clear, .ans-chg {
  display: inline-flex; align-items: center;
  min-height: var(--tap); padding: 0 var(--s2);
  background: none; border: 0; cursor: pointer;
  font: 600 var(--t-ui)/1 var(--font-body);
  color: var(--ink); text-decoration: underline; text-underline-offset: 3px;
}
.count-clear:hover, .ans-chg:hover  { color: var(--nav-hover); }
.count-clear:active, .ans-chg:active { color: var(--nav-active); }

/* ==================== provenance / freshness ====================
 * R10: freshness is MAX(synced_at), never "posted N days ago". The board is not
 * allowed to imply it is live.
 *
 * Fresh is quiet. Stale escalates into the brand's warm register rather than
 * introducing an amber hue nobody chose: --orange is the 3px rail (ornament) and
 * --orange-ink carries the words at 5.03:1 on --orange-bg. Soft fill plus a
 * border, never a solid accent fill — solid fill means a thing you click.
 *
 * The incident: on 2026-08-13 the cache froze and 19 rows read In-progress that
 * were actually Filled. This band is that incident rendered at the point where
 * the consequence lands, and the template puts it INSIDE .filterbar so it
 * cannot be scrolled away from while the numbers it qualifies are on screen. */
.prov {
  font-size: var(--t-meta); color: var(--muted); line-height: var(--lh-dense);
  font-variant-numeric: tabular-nums;
}
.prov--stale {
  display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s3) var(--s4);
  background: var(--orange-bg);
  border: 1px solid var(--orange-br);
  border-left: 3px solid var(--orange);
  border-radius: var(--r-sm);
  color: var(--orange-ink); font-size: var(--t-ui);
}

/* ==================== choice card ====================
 * The funnel's answer button. Stacked below 640px, 2-up above. Selected is
 * border + fill + aria-pressed + an inset <svg> tick: three encodings, so it
 * survives greyscale and forced-colors. 72px tall, well above the 44px floor,
 * because at this size the whole card is the target and a thumb does not have
 * to be accurate. */
.choices { display: grid; gap: var(--s3); }
.choice {
  display: flex; align-items: center; gap: var(--s4);
  width: 100%; min-height: 72px; padding: var(--s4);
  background: var(--surface); color: var(--body);
  border: 2px solid var(--line-control); border-radius: var(--r-lg);
  text-align: left; cursor: pointer;
  font: var(--t-body)/var(--lh-dense) var(--font-body);
  transition: border-color var(--fast) var(--ease-out),
              background var(--fast) var(--ease-out),
              box-shadow var(--fast) var(--ease-out);
}
.choice:hover  { border-color: var(--nav); box-shadow: var(--sh1); }
.choice:active { box-shadow: none; background: var(--sunken); }
.choice[aria-pressed="true"] { border-color: var(--nav); background: var(--icon-bg); }
.choice:disabled {
  background: var(--sunken); color: var(--muted);
  border-color: var(--line); cursor: not-allowed;
}
.choice .well {
  flex: 0 0 var(--tap); width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  background: var(--icon-bg); border-radius: var(--r-pill); color: var(--ink);
}
/* `display: block` is load-bearing, not tidiness: both of these are <span>, so
 * without it the title and the description render on one run-together line —
 * "YesShow me what's open near me" — and `margin-top` on an inline box does
 * nothing either. Seen in Chrome at 390px on every choice card in the funnel. */
.choice .t {
  display: block;
  font-family: var(--font-head); font-weight: 600; color: var(--body);
}
.choice .d {
  display: block;
  font-size: var(--t-ui); color: var(--muted); margin-top: var(--s1);
}
@media (min-width: 640px) {
  .choices { grid-template-columns: 1fr 1fr; gap: var(--s4); }
  /* Column-oriented above 640: the icon well moves above the label, which is
   * what lets two cards sit side by side without the text going to 4 lines. */
  .choice { flex-direction: column; align-items: flex-start; min-height: 116px; }
}

/* ==================== input ====================
 * The label is always above and always visible; a placeholder is a hint and
 * never a label — it vanishes the moment someone types, which is exactly when
 * they need it, and it is not an accessible name.
 *
 * 52px in the funnel (one question on screen, thumb-first) and 48px on the
 * board filter. 18px text in the funnel and 16px on the board filter: both are
 * at or above the 16px floor under which iOS zooms the page on focus and jumps
 * the layout out from under the person typing. */
.field { display: block; margin-bottom: var(--s5); }
.field > label {
  display: block; margin-bottom: var(--s2);
  font: 600 var(--t-ui)/1.3 var(--font-head); color: var(--body);
}
.field .hint { margin-top: var(--s2); font-size: var(--t-ui); color: var(--muted); }
.input {
  display: block; width: 100%;
  min-height: 52px; padding: var(--s3) var(--s4);
  background: var(--surface); color: var(--body);
  border: 2px solid var(--line-control); border-radius: var(--r);
  font: var(--t-lead)/1.4 var(--font-body);
  transition: border-color var(--fast) var(--ease-out);
}
.input::placeholder { color: var(--muted); }   /* 5.98:1 — readable, not decor */
.input:hover { border-color: var(--nav); }
.input:focus-visible { border-color: var(--nav); }   /* plus the global ring */
.input:disabled {
  background: var(--sunken); color: var(--muted); cursor: not-allowed;
}
/* Colour is never the only signal: the template also sets aria-invalid and
 * aria-describedby pointing at the .err-field line. */
.input[aria-invalid="true"] { border-color: var(--danger); }
.input--zip { max-width: 10ch; }   /* the width is itself a hint: five digits */
.input--search { min-height: 48px; font-size: var(--t-body); }

/* ==================== primary CTA ====================
 * One per screen. Navy fill, white ink, 54px, full width up to 384px — the
 * mockup's proportions, kept. NEVER orange: white on #F97316 is 2.80:1, which
 * is why orange is not a button in this system at all. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  width: 100%; max-width: 384px;
  min-height: 54px; padding: 0 var(--s6);
  background: var(--nav); color: var(--on-nav);
  border: 2px solid var(--nav); border-radius: var(--r);
  font: 600 var(--t-lead)/1 var(--font-head);
  cursor: pointer; text-decoration: none;
  transition: background var(--fast) var(--ease-out),
              border-color var(--fast) var(--ease-out);
}
.btn:hover:not(:disabled)  { background: var(--nav-hover); border-color: var(--nav-hover); }
.btn:active:not(:disabled) { background: var(--nav-active); border-color: var(--nav-active); }
.btn:disabled {
  background: var(--sunken); color: var(--muted);
  border-color: var(--line-control); cursor: not-allowed;
}
/* Busy is a state, not a disabled button: the label stays, the template adds
 * the word ("Checking…") past 1s, and the control keeps its accessible name.
 * A button that disables itself on submit loses focus and strands a keyboard
 * user at the top of the document. */
.btn[aria-busy="true"] { cursor: progress; }
.btn--secondary {
  background: var(--surface); color: var(--ink); border-color: var(--line-control);
}
.btn--secondary:hover:not(:disabled) { background: var(--sunken); border-color: var(--nav); }
.btn--secondary:active:not(:disabled) { background: var(--track); }

/* ==================== skip (funnel) ====================
 * The plain underlined text button under the CTA. A real <button>, 44px tall,
 * and the copy always says what skipping costs. Muted, because it must be
 * clearly the lesser of the two paths without being hidden. */
.btn-skip {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0 var(--s3);
  background: none; border: 0; cursor: pointer;
  font: var(--t-ui)/1 var(--font-body); color: var(--muted);
  text-decoration: underline; text-underline-offset: 3px;
}
.btn-skip:hover  { color: var(--ink); }
.btn-skip:active { color: var(--nav-active); }

/* ==================== availability grid ====================
 * Real <button role="gridcell" aria-pressed> cells, never divs.
 *
 * FIXED from the mockup's 2.4rem (38px) cells, which fail the touch floor. And
 * transposed below 768px — days as ROWS, bands as columns — because seven 44px
 * columns plus a label column is 396px and does not fit in 390px, let alone
 * 320px. The mockup's answer was a horizontal scroller inside a vertical
 * scroller, which is two gestures fighting over one finger.
 *
 * Nested radius, worked: wrapper --r (12) with --s1 (4) of padding, cells at
 * --r-sm (8). 12 − 4 = 8, so the curves are concentric. */
.avail {
  display: grid; gap: var(--s1); padding: var(--s1);
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--r);
  /* Below 768: a label column plus the three bands. The label column is `auto`
   * — it hugs "Wed" — and the three band columns take `1fr` each, so the cells
   * fill the row instead of being pushed to the right edge.
   *
   * It was `minmax(0, 1fr)` on the label and `auto` on the cells, which is the
   * same thing backwards: `1fr` ate every spare pixel, so at 390px the day name
   * sat 280px away from its own three cells and the row read as two unrelated
   * things. Proximity is the only thing telling a reader that this "Mon" owns
   * those three squares. Cells stay at or above the touch floor because
   * minmax(var(--tap), 1fr) cannot go below --tap. */
  grid-template-columns: auto repeat(3, minmax(var(--tap), 1fr));
}
@media (min-width: 768px) {
  /* Un-transposed: bands as rows, the seven days as columns. Same grid, same
   * cells, same keyboard model — the arrow keys follow the visual order, which
   * is why the JS reads the orientation rather than assuming it. */
  .avail { grid-template-columns: minmax(0, 1fr) repeat(7, minmax(var(--tap), 1fr)); }
}
.cell {
  min-width: var(--tap); min-height: var(--tap);
  display: grid; place-items: center;
  background: var(--surface); color: var(--body);
  border: 2px solid var(--line-control); border-radius: var(--r-sm);
  font: 600 var(--t-ui)/1 var(--font-head); cursor: pointer;
  transition: background var(--fast) var(--ease-out),
              border-color var(--fast) var(--ease-out);
}
.cell:hover  { border-color: var(--nav); background: var(--icon-bg); }
.cell:active { background: var(--track); }
/* Selected is not colour-only: the cell also renders an <svg> tick when
 * pressed, so it survives greyscale, forced-colors and a screenshot. */
.cell[aria-pressed="true"] {
  background: var(--nav); border-color: var(--nav); color: var(--on-nav);
}
.cell:disabled {
  background: var(--sunken); color: var(--muted);
  border-color: var(--line); cursor: not-allowed;
}
.avail-lbl {
  font: 600 var(--t-ui)/1 var(--font-head); color: var(--muted);
  display: grid; place-items: center; min-height: var(--tap);
  /* start, not center: a row of day names reads down the left edge */
  justify-items: start; padding: 0 var(--s2);
}

/* ==================== case row ====================
 * One matched open case in the funnel: checkbox, region, slot phrase, both
 * rates, banded distance. Region — never a town: a case is PHI-adjacent, and
 * apply.PUBLIC_CASE_KEYS is the control that keeps a household off this row.
 *
 * Full-bleed dividers rather than nested boxes, because the parent .card is
 * --r-lg with --s4 of padding and inner = outer − padding leaves nothing to
 * round. Dividers and proximity is the honest answer to that. */
.case {
  display: grid; grid-template-columns: var(--tap) 1fr; gap: var(--s3);
  align-items: start; padding: var(--s4) 0;
  border-top: 1px solid var(--line); cursor: pointer;
}
.case:first-of-type { border-top: 0; }
.case:hover { background: var(--sunken); }
.case:has(input:checked) { background: var(--icon-bg); }
.case input[type="checkbox"] {
  /* 24px box inside 10px of margin = a 44px target around a control that is
   * only 24px of paint. The number nobody measures. */
  width: 24px; height: 24px; margin: 10px;
  accent-color: var(--nav);
}
.case input:focus-visible { outline: 3px solid var(--nav); outline-offset: 2px; }
.case .region { font: 600 var(--t-body)/var(--lh-dense) var(--font-head); color: var(--ink); }
.case .slots  { font-size: var(--t-ui); color: var(--muted); margin-top: var(--s1); }
.case .row    { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s2); }

/* ==================== badge ====================
 * Read-only. A pill with a soft fill and no border on the neutral variant, so
 * it never reads as a control. Every figure is tabular: rates, distances and
 * counts all appear stacked in a column down the cards. */
.badge {
  display: inline-flex; align-items: center; gap: var(--s1);
  padding: var(--s1) var(--s3);
  background: var(--sunken); color: var(--body);
  border-radius: var(--r-pill);
  font: 600 var(--t-meta)/1.4 var(--font-body);
  font-variant-numeric: tabular-nums;
}
/* Pay. Rendered ONLY as a pair — the non-billable figure is never on screen
 * without the billable one and vice versa; omitting the non-billable rate is a
 * named reason candidates said they felt misled. If STATE_RATES has no row for
 * the state, neither badge is rendered: never "$0", never "TBD", and never a
 * neighbouring state's number (five BCBA listings once carried Virginia's $85).
 * Never a ceiling either — the Leveling System is "a pay increase at each
 * level" and nothing more specific. */
.badge--pay { background: var(--icon-bg); color: var(--ink); }
/* Distance. Banded straight-line — "12 mi ±6" — never drive time. On Oahu that
 * difference is a resignation. The list carries geo.DISCLAIMER as visible text,
 * not a tooltip. Same-ZIP rows render "in your ZIP code" as words, because
 * "0 mi ±4.5" reads as broken rather than as near. */
.badge--dist   { background: var(--orange-bg); color: var(--orange-ink); }
.badge--single { background: var(--sunken); color: var(--muted); }   /* "One family" */
.badge--ok     { background: var(--ok-bg); color: var(--ok); }       /* "Applied"    */

/* ==================== the five screen states ====================
 * Named classes, decided here, so that nobody improvises one at build time.
 * A state that gets invented in a template is a state nobody designed.
 *
 *   skeleton       .skel  .skel--line  .skel--meta  .skel--row
 *   empty          .block  .block--empty
 *   stale          .prov--stale                     (above, with the incident)
 *   error-inline   .err-field                       (one field, on blur)
 *   error-section  .block  .block--err              (one region failed)
 *
 * There is deliberately no loading state for the board: the list is
 * server-rendered, filtering is client-side and lands in under 16ms, and a
 * skeleton for a synchronously-rendered list is theatre. The funnel has exactly
 * two async moments and both use .skel. */
.block {
  padding: var(--s5) var(--s4);
  background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r);
}
.block h3 { margin-bottom: var(--s2); }
.block p  { color: var(--muted); font-size: var(--t-body); }
.block .actions { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s4); }
/* Left-aligned, not centred: an empty state here is data ("no open postings in
 * Iowa right now · 108 are open in the other 6 states"), not a poster. Centring
 * it and adding an illustration is how an empty state stops being informative. */
.block--empty { text-align: left; }
/* One region failed and the rest of the page is fine. The 3px left rail is the
 * same device as .prov--stale, so "something is wrong here" has one shape
 * across both surfaces. */
.block--err {
  background: var(--danger-bg); border-color: var(--danger); border-left-width: 3px;
}
.block--err h3, .block--err p { color: var(--danger); }
/* Inline, one field, shown on blur and never per keystroke, and the input keeps
 * what was typed. Colour is never the only signal: the template renders an
 * alert glyph in here and points aria-describedby at it. */
.err-field {
  display: flex; gap: var(--s2); align-items: flex-start;
  margin-top: var(--s2); font-size: var(--t-ui); color: var(--danger);
}

/* Skeleton shapes match the FINAL layout exactly — same heights, same gaps — so
 * nothing jumps when the data lands. --pulse is deliberately slow and infinite,
 * which is why the motion budget below does not apply to it; the
 * reduced-motion block at the bottom switches it off completely. */
.skel {
  background: var(--sunken); border-radius: var(--r-sm);
  animation: skel-pulse var(--pulse) ease-in-out infinite;
}
.skel--line { height: 16px; }
.skel--meta { height: 14px; }
.skel--row  { height: 92px; border-radius: var(--r); }   /* == a .case row */
@keyframes skel-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .55 } }

/* ==================== job page redesign ====================
 * The job detail page (/careers/<slug>, /careers/job/<code>) gets its own
 * component layer here. The board cards reuse .card-job and its scoped
 * .where/.when/.chips/.foot rules; the job PAGE needs the same class names
 * styled at page scale, without the card's border or padding.
 *
 * Mobile-first: the facts rail stacks vertically at base and becomes the
 * sticky right rail at >= 1024px (the existing .rail rule handles that).
 * The sticky Apply bar is hidden on desktop and fixed to the bottom on
 * mobile so the primary action is always within thumb reach. */

/* ---- facts rail: mobile stack, desktop sticky rail ----
 * On the job page these classes sit outside .card-job, so the scoped rules
 * above do not reach them. These are the page-level equivalents: larger
 * type, no card chrome, and a column layout that the >= 1024px .rail rule
 * picks up for the sticky sidebar. */
.facts {
  display: flex; flex-direction: column; gap: 12px;
}
.where {
  font: 700 18px/1.3 var(--font-head);
  color: var(--ink);
}
.when {
  font: 600 15px/1.4 var(--font-body);
  color: var(--body);
}
.chips {
  display: flex; flex-wrap: wrap; gap: 8px;
}
.note {
  font: 400 13px/1.5 var(--font-body);
  color: var(--muted);
}
.cta-note {
  font: 600 14px/1.4 var(--font-body);
  color: var(--muted);
  margin-top: 8px;
}

/* ---- hero band ----
 * A warm-tinted panel that opens the job page. The gradient is --surface to
 * --orange-bg so it carries the brand's warm register without an orange fill
 * under text. Paired with --orange-br so the edge is real, not borrowed. */
.hero-job {
  background: linear-gradient(135deg, var(--surface), var(--orange-bg));
  border: 1px solid var(--orange-br);
  border-radius: var(--r-lg);
  padding: 24px;
  margin-bottom: 32px;
}

/* ---- feature card grid ----
 * Two-up above 640px, one column below. Each card is --surface with --line,
 * the same construction as .card-job but without the link behaviour. */
.features {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  margin: 24px 0;
}
@media (min-width: 640px) {
  .features { grid-template-columns: repeat(2, 1fr); }
}
.feature {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 16px;
}
.feature h3 {
  font: 700 16px/1.3 var(--font-head);
  color: var(--ink);
  margin-bottom: 4px;
}
.feature p {
  font: 400 15px/1.5 var(--font-body);
  color: var(--body);
  margin: 0;
}

/* ---- callout cards ----
 * Three variants, each a soft fill with a 4px left rail. The rail colour
 * matches the fill's semantic family: navy for info, orange for warm, green
 * for ok. Same device as .prov--stale and .block--err, so a callout reads as
 * the same shape across the surface. */
.callout {
  border-radius: var(--r-lg);
  padding: 16px 20px;
  margin: 16px 0;
}
.callout--info {
  background: var(--icon-bg);
  border-left: 4px solid var(--nav);
}
.callout--warm {
  background: var(--orange-bg);
  border-left: 4px solid var(--orange);
}
.callout--ok {
  background: var(--ok-bg);
  border-left: 4px solid var(--ok);
}

/* ---- sticky mobile Apply bar ----
 * Hidden by default (desktop). Shown at <= 1023px so the Apply CTA is always
 * within thumb reach on a phone. The .btn inside is full-width — the bar's
 * own padding provides the gutter. z-index 100 sits above the sticky filter
 * bar (30) and funnel header (40) but below an app-level overlay. */
.apply-bar { display: none; }
@media (max-width: 1023px) {
  .apply-bar {
    display: block;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    background: var(--surface);
    border-top: 1px solid var(--line);
    padding: 12px 16px;
    box-shadow: 0 -2px 8px rgba(10,24,87,0.08);
    z-index: 100;
  }
  .apply-bar .btn {
    width: 100%; max-width: none;
  }
}

/* ---- final CTA band ----
 * A warm callout that closes the page with the Apply button and a link back
 * to the board. Replaces the old <p class="sect"> + <p class="cta-note"> pair
 * with a single visually distinct band. */
.cta-band {
  text-align: center;
  padding: 8px 0;
}
.cta-band .btn {
  margin-bottom: 12px;
}

/* ---- job main column ----
 * On mobile this is a simple stacked column. At >=1024px the .cols grid
 * makes it the left column (minmax(0,720px)) and the .rail becomes the
 * sticky right column. */
.jobmain {
  min-width: 0;
}


/* ==================== footer ====================
 * The phone, the freshness line, staff sign-in and the EEO line. Not the
 * mockup's "nothing is submitted" — that was a review artefact. */
.ftr {
  margin-top: var(--s8);
  /* env() so the last line clears the iPhone home indicator. Falls back to 0
   * everywhere else, so this is safe unconditionally. */
  padding: var(--s6) var(--s4) calc(var(--s6) + env(safe-area-inset-bottom));
  background: var(--surface); border-top: 1px solid var(--line);
  font-size: var(--t-ui); color: var(--muted);
}
.ftr-in { max-width: var(--shell-max); margin: 0 auto; display: grid; gap: var(--s4); }
.ftr a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.ftr a:hover { color: var(--nav-hover); }
.ftr .legal { font-size: var(--t-meta); }
/* Every footer link is a 44px target too. Footer links are the ones that get
 * tapped one-handed on a bus. */
.ftr-link { display: inline-flex; align-items: center; min-height: var(--tap); }
@media (min-width: 768px) { .ftr-in { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .ftr-in { grid-template-columns: 1fr 1fr 1fr; } }

/* ==================== sticky regions ====================
 * Two on the board and they are chosen, not accumulated: the filter bar (which
 * carries the count line you need while scrolling past posting 60) and, below
 * 768px, the one primary action. The board header is NOT sticky — three sticky
 * regions is 156px of chrome on a 390x844 screen.
 *
 * z-index stays in a two-digit band on purpose: 30 for sticky page furniture,
 * 40 for the funnel's sticky header, 60 for the skip link. Nothing in this file
 * goes above 100, so an app-level overlay always wins. */
.filterbar {
  position: sticky; top: 0; z-index: 30;
  background: var(--page); border-bottom: 1px solid var(--line);
  /* Full-bleed band, shell-aligned CONTENTS. The band has to reach both edges
   * or the sticky rule leaves a stripe of scrolling page beside it; its
   * contents have to start where `.shell` starts or they do not line up with
   * the h1 above and the cards below. Measured at 1280px before this existed:
   * the search field and the count line began at x=16 while every other
   * element on the page began at x=96, which reads as a broken column.
   *
   * `max()` is what lets one rule do both: below the shell width the second
   * term goes negative and the 16px gutter wins, so 320px is untouched. */
  padding: var(--s3)
           max(var(--s4), calc((100% - var(--shell-max)) / 2 + var(--s4)));
  display: grid; gap: var(--s2);
}
.actionbar {
  position: sticky; bottom: 0; z-index: 30;
  padding: var(--s3) var(--s4) calc(var(--s3) + env(safe-area-inset-bottom));
  background: var(--surface); border-top: 1px solid var(--line);
  box-shadow: var(--sh2);
}
/* Dropped at >= 768px: the hero button is on screen often enough there, and a
 * permanent bar on a laptop is chrome for nothing. */
@media (min-width: 768px) { .actionbar { display: none; } }

/* The chip scroller, 320–767px. scroll-snap proximity rather than mandatory:
 * mandatory fights a flick that was meant to go three chips along. At 390px
 * about 2.5 chips are visible, which is what makes the row read as scrollable
 * without a scrollbar. */
.chiprow {
  display: flex; gap: var(--s2);
  overflow-x: auto; scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--s2);
}
.chiprow > * { scroll-snap-align: start; }

/* The funnel's three availability shortcuts are the one .chiprow that must NOT
 * scroll. The board's eight state chips scroll on purpose — 2.5 visible is what
 * says "there are more" — but with exactly three items a scroller hides the
 * third instead of hinting at it, and at 390px "All evenings" was cut off
 * mid-word with no reason for a reader to look for it. Three is few enough to
 * wrap onto a second line, and a wrapped option can be seen. */
#avail-chips { flex-wrap: wrap; overflow-x: visible; scroll-snap-type: none; }

@media (min-width: 768px) {
  /* One row, no scroller: at 768px all eight chips fit and a scroller with
   * nothing to scroll is a control that lies about there being more. */
  .chiprow { flex-wrap: wrap; overflow-x: visible; }
}

/* ==================== the funnel card ====================
 * One question on screen, answered groups collapsed and editable above it. The
 * card floats (--sh2) because it genuinely is a layer over the page — it is the
 * only thing on it.
 *
 * Padding is 16px at base and 24px from 640px. The wireframe annotates 20px;
 * 20px is not on the 4px scale, and the difference between 20 and 24 is
 * invisible while the difference between "on the scale" and "off it" is the
 * thing every reviewer notices. */
.card {
  max-width: var(--card-max); margin: 0 auto;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--s4);
  box-shadow: var(--sh2);
}
@media (min-width: 640px) { .card { padding: var(--s5); } }
/* Apply funnel: narrow and center the card so it sits where the eye lands,
 * not offset by the wider shell. The shell is 1120px; the card at 560px
 * centered reads as the focus of the page on every width. */
.shell > .card {
  max-width: 560px;
  margin-left: auto;
  margin-right: auto;
}
/* Nested radius, and the one place this file bends the law knowingly: the card
 * is --r-lg (16) with 16px of padding, so by inner = outer − padding its
 * contents should not be boxed at all — and .answers, .case and the dividers
 * obey that. .choice is the exception, boxed at --r-lg, because its 2px border
 * IS the control boundary and a control with no edge is not tappable. Anything
 * that is not a control uses a divider. */

/* Question N of 7 — no percentage. With four branches a percentage moves
 * backwards, and a progress indicator that lies is worse than none (D-56). The
 * counted spine is fixed at seven, so M never changes; every off-spine step
 * renders "One more thing —" instead of a number. */
.stepnum {
  font-size: var(--t-ui); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.progress {
  height: 10px; margin-top: var(--s2);
  background: var(--track); border-radius: var(--r-pill);
  overflow: hidden;                     /* the fill's own corners are clipped */
}
.progress > span {
  display: block; height: 100%;
  /* --ink, NOT --nav, and the difference only shows in dark mode. In light the
   * two tokens are the same #0A1857, so the bar is 13.24:1 on --track either
   * way. In dark, --nav lightens to #3A4A96 to work as a button fill while
   * --track is a navy tint at #2A3565 — and a navy fill on a navy track
   * measures 1.45:1, which is a progress bar that does not exist for anyone in
   * dark mode. --ink is 9.92:1 there. SPEC §4.1 measured this pair in light
   * only (its dark column is "—"), so this was an open gap rather than a
   * disagreement. The bar is something you READ, so ink is also the right
   * role. */
  background: var(--ink);
  border-radius: var(--r-pill);         /* pills are exempt from the nested law */
  transition: width var(--mid) var(--ease-out);
}

/* Answered groups: collapsed, visible, editable. Keeping them on screen is what
 * makes one-question-at-a-time honest rather than a black box. */
.answers { margin: var(--s5) 0; border-bottom: 1px solid var(--line); }
.ans {
  display: grid; grid-template-columns: 1fr auto;
  gap: var(--s1) var(--s3); align-items: center;
  padding: var(--s2) 0; border-top: 1px solid var(--line);
}
.ans-k { font-size: var(--t-meta); color: var(--muted); }
.ans-v {
  grid-column: 1 / -1;
  font: 600 var(--t-h3)/var(--lh-dense) var(--font-head); color: var(--ink);
}

/* The question itself. A 200ms 8px rise and fade — NOT the mockup's 300ms slide
 * from the right, because a horizontal slide on iOS competes with the
 * back-navigation gesture and the two together read as a stutter. */
.step { animation: step-in var(--mid) var(--ease-out) both; }
@keyframes step-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.step > h2 { font-size: var(--t-h2); }
.step > .sub {
  margin-top: var(--s3);
  font-size: var(--t-lead); color: var(--muted);
}
.step > .note {
  margin-top: var(--s4);
  font-size: var(--t-ui); color: var(--muted); line-height: var(--lh-dense);
}
.step .actions { display: grid; gap: var(--s3); margin-top: var(--s5); justify-items: start; }

/* The completion disc. --ok-bg + --ok is the ONE green in this file and this is
 * the only place it appears; --orange-bg + --orange-ink carries the
 * not-yet-registered branch, which is a fork and not a failure. */
.disc {
  width: var(--s8); height: var(--s8);
  display: grid; place-items: center; border-radius: var(--r-pill);
  margin-bottom: var(--s4);
}
.disc--ok   { background: var(--ok-bg); color: var(--ok); }
.disc--warm { background: var(--orange-bg); color: var(--orange-ink); }

/* The funnel cannot work without JS, so it says so and hands over the phone
 * number rather than rendering a dead form. Same warm register as .prov--stale:
 * a state you read, not a thing you click. */
.noscript {
  display: block; margin: var(--s4) auto; max-width: var(--card-max);
  padding: var(--s4);
  background: var(--orange-bg); color: var(--orange-ink);
  border: 1px solid var(--orange-br); border-left: 3px solid var(--orange);
  border-radius: var(--r-sm);
  font-size: var(--t-body);
}

/* ==================== icons ====================
 * One set, one weight, one size per context: the lucide single-path SVGs from
 * the marketing assets, 2px stroke, currentColor. 20px in chrome and cards,
 * 24px in a 44px well, 32px in a completion disc.
 *
 * Inlined as <svg><symbol> + <use> at the top of each template. Inline SVG is
 * markup, not script, so CSP script-src 'self' is untouched — and currentColor
 * only works inline, which is what lets one icon file serve both themes.
 *
 * NO EMOJI ANYWHERE. It is the single most recognisable generated-UI tell, and
 * an emoji is also read aloud by its unicode name. */
.ico {
  width: 20px; height: 20px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.ico--lg { width: 24px; height: 24px; }
.ico--xl { width: 32px; height: 32px; }

/* =========================================================== reduced motion
 * LAST IN THE FILE, and that is load-bearing.
 *
 * Every duration above is one of --fast / --mid / --slow / --pulse, so
 * redefining those four here neutralises every transition and animation in this
 * file without needing !important and without a specificity fight — a media
 * query adds no specificity, so :root here only wins because it comes later.
 * Move this block up and it stops working, silently.
 *
 * The two named overrides are the cases a duration cannot fix: an infinite
 * animation at 0.01ms still loops forever and would repaint continuously, and
 * `animation: … both` holds a from-state that must not be relied on. Both are
 * matched at the same specificity as the rules above, so source order decides —
 * same reason.
 *
 * The universal reset is the belt: it catches a literal duration added later by
 * someone who did not read the token comment. It only wins over rules that do
 * not name an element or class, so it is a safety net and not the mechanism. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --fast: .01ms; --mid: .01ms; --slow: .01ms; --pulse: .01ms;
  }
  .skel { animation: none; }
  .step { animation: none; }
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    transition-duration: .01ms;
    animation-duration: .01ms;
    animation-iteration-count: 1;
  }
}

/* ==================== the booking calendar ====================
 * The only third-party frame in Leap, and the only place `frame-src` is widened
 * (`main.CSP_EMBED`, one host, on `/apply` alone). `booking.py`'s docstring
 * carries the measurements; these rules exist for the three ways an embedded
 * calendar goes wrong on a phone.
 *
 * 1. **It arrives with no height.** The widget reports one over postMessage a
 *    beat after it loads, and `apply.js` sets it. Until then the frame needs a
 *    height that is not zero, or the page jumps by 560px the moment the message
 *    lands — layout shift reads as low quality even to people who cannot name
 *    what moved. 560px is the height the live widgets actually reported.
 * 2. **It is a scroll trap.** `scrolling="no"` in the vendor's snippet plus a
 *    fixed height is two scrollbars fighting one thumb. The height is driven by
 *    the frame's own content instead, so the page scrolls and the frame does
 *    not.
 * 3. **It is not ours and it will not match.** The widget renders its own type
 *    and colours from GoHighLevel's config, so the honest thing is a container
 *    that reads as a distinct panel rather than styling that pretends the frame
 *    is part of this page. Hence the border and the sunken ground: it looks
 *    embedded because it IS embedded. */
.bookwrap {
  margin-top: var(--s4);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r);
  /* --s1 of padding inside --r, so the frame's own corners sit concentric with
   * the wrapper's. Same nested-radius rule as the availability grid. */
  padding: var(--s1);
  overflow: hidden;
}
.bookframe {
  display: block;
  width: 100%;
  height: 560px;                 /* replaced by the reported height; see above */
  border: 0;
  border-radius: var(--r-sm);
  background: var(--surface);
  /* The frame is the tallest thing on this step and a phone keyboard can make
   * the viewport very short. Never taller than the viewport minus the header,
   * so the actions under it stay reachable without a hunt. */
  max-height: max(420px, calc(100vh - 96px));
}

/* ==================== the consent tick ====================
 * One checkbox with a long label, and the label is a legal record rather than a
 * hint. It gets more room than a normal field because the whole of it has to be
 * readable before somebody ticks it: consent to a sentence nobody read is the
 * thing this control exists to avoid.
 *
 * The box is 24px rather than the browser default, and the whole row is the hit
 * area via the <label for>, so a thumb aiming anywhere on the sentence toggles
 * it. That matters more here than anywhere else on the form: a mis-tap on a
 * consent box is either a refusal recorded as agreement or the reverse. */
.consent {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s3);
  align-items: start;
  margin-top: var(--s4);
  padding: var(--s4);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r);
}
.tick {
  width: 24px; height: 24px;
  margin: 0;
  accent-color: var(--nav);   /* the brand navy, not the platform blue */
  flex: 0 0 auto;
}
.consent-t {
  font-size: var(--t-ui);
  line-height: var(--lh-body);
  color: var(--body);
  cursor: pointer;
}
