/* SYSTEM: 2.0
 * ==========================================================================================
 * THE CLIENT INTAKE PORTAL -- STYLESHEET
 *
 * This is a CLIENT-FACING surface. Everything below follows from that one fact.
 *
 * 1. MOBILE FIRST, NOT MOBILE-TOLERANT. Every rule outside the one media query at the bottom
 *    is written for a phone held in one hand. The desktop layout is the adaptation, and it is
 *    twelve lines long. The client is going to open this from an email on a phone; that is the
 *    real screen and it gets the real design.
 *
 * 2. NO OPERATOR CHROME. There is no rail, no sidebar, no toolbar, no table. One column, one
 *    thing to do. The Client Manager stylesheet this palette comes from has all of those and
 *    they are all deliberately absent here.
 *
 * 3. THE STATUS COLOURS CARRY MEANING, AND ONLY THREE THINGS MAY SAY THEM.
 *      GREEN means done. Nothing else may be green -- not a heading, not a button, not a tick
 *      on something that is merely started. If green appears, a step is finished.
 *      BLUE means in progress. NEUTRAL means not started.
 *      THERE IS NO RED IN THIS FILE. Not as an error colour, not as a warning, not as a border.
 *      A client who cannot open their link has not done anything wrong, and a red screen tells
 *      them they have. The one failure surface in this project is styled exactly like the
 *      ordinary ones.
 *      *** THIS IS A DELIBERATE CLIENT-FACING CHOICE, NOT AN OMISSION. *** Both sibling 2.0
 *      tools define --red: #b3261e and both are right to -- an operator needs to be told a run
 *      failed. This surface has no failure that is the reader's fault, so it has no red, and
 *      the next person to reach for one should read this line first. Amber is absent on the same
 *      terms: nothing on a client screen has meant "warning" yet, and when something does it
 *      comes from the family's #9a6b00 rather than from a colour picker.
 *      DESIGN_REFERENCE.md §7b.
 *
 * 4. EVERY COLOUR IS A TOKEN. No hex below the token block. The brand navy and blue are copied
 *    character for character from itv-client-manager/css/app.css, which copied them from the
 *    company's own live stylesheet. If the brand changes it changes there first.
 *
 * 5. CONTRAST IS MEASURED, NOT ASSUMED. Every ratio in a comment below was computed off the two
 *    hex values it names, not estimated. The lowest pairing on the page is 4.61:1.
 *
 * 6. THE TWO TYPEFACES ARE THE FAMILY'S, SELF-HOSTED. Lexend for body, Bebas Neue for display.
 *    This file said "system font stack only" for the whole of its first month, and the argument
 *    was a good one -- see the TYPE block below for why it was still wrong, and what closes it.
 *
 * 7. EVERY VALUE THAT REPEATS IS A TOKEN, NOT JUST EVERY COLOUR. Sizes, leading, weights,
 *    tracking, radii, shadows and easing all live in the block below. There is no bare size in a
 *    rule any more; a rule that wanted a new one would have to add it here, in front of the
 *    others, which is the point.
 * ==========================================================================================
 */

/* ==========================================================================================
   00  THE TWO TYPEFACES -- SELF-HOSTED, AND WHY THE OLD RULE WAS WRONG
   ==========================================================================================

   THE RULE THIS REPLACES, IN FULL, BECAUSE IT WAS NOT A LAZY RULE:

       "System font stack only. A webfont is a second request before the client can read
        anything, and on a phone on a bad signal that is the difference between 'it opened'
        and 'it is broken'."

   Everything in that sentence is true and the conclusion still does not follow, for three
   reasons that only became available later:

   1. `font-display: swap` MEANS THE TEXT IS NEVER WAITING ON THE FONT. The fallback stack paints
      on the first frame and is swapped when the file lands. The failure the rule describes -- a
      client staring at nothing -- is the behaviour of `font-display: block`, which is the
      DEFAULT, and which is why the fear was well-founded before this line existed.

   2. THE FILES ARE HASHED AND IMMUTABLE (build.js, public/_headers). The second request is paid
      once, ever, per client. Every subsequent screen in the intake -- and there are four -- takes
      it from cache without a network round trip.

   3. IT IS 8.6KB, NOT A FONT SERVICE. Four woff2 subsets, in this repo, from
      `itv-aeo-tool/fonts` where they already were, `unicode-range`d so a client whose name is
      plain Latin never downloads the -ext half. No fonts.googleapis.com, no second origin, no
      render dependency on somebody else's uptime, and nothing to go stale.

   AND THE THING THE OLD RULE COST WAS NOT SMALL. Both sibling 2.0 tools name a real typeface
   first; this one named none, and that single fact was the largest reason a screenshot of this
   portal did not read as the same product as a screenshot of the AEO tool
   (DESIGN_REFERENCE.md §7a). A client-facing surface is the LAST place that should look like a
   different company.

   BOTH FAMILIES ARE OFL 1.1 AND THE LICENCE TEXT SHIPS BESIDE THEM -- public/fonts/OFL-Lexend.txt
   and public/fonts/OFL-BebasNeue.txt. That is what makes redistributing them legitimate rather
   than merely convenient; deleting either file is a licence violation, not a tidy-up.
   ========================================================================================== */

@font-face {
  font-family: 'Bebas Neue';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/bebas-neue-latin.441b026d.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: 'Bebas Neue';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/bebas-neue-latin-ext.a724a4b6.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;
}

/* Lexend ships as ONE VARIABLE FILE per subset covering 100-900, which is why a whole range is
   declared rather than four separate faces: the browser instances the weight it is asked for out
   of one download.
 *
 * THE RANGE STOPS AT 700 HERE AND AT 600 IN THE AEO TOOL, AND THAT IS DELIBERATE ON BOTH SIDES.
 * That file caps at 600 because 600 is the heaviest weight its design uses, and an open `100 900`
 * would let a stylesheet reach a weight nobody chose. The same argument gives a different number
 * here: THIS surface sets its titles, its wordmark and its card headings at 700 -- a client-facing
 * page has fewer words and they have to carry further. The cap is still "what the design uses".
 *
 * IT MATTERS THAT IT IS NOT LEFT AT 600. A variable face asked for a weight above its declared
 * range does not fail and does not synthesise -- it CLAMPS. Every 700 in this file would have
 * rendered at 600, the page would have looked slightly soft, and nothing anywhere would have said
 * so. Verified in the browser rather than assumed: see MASTER_DOC §25. */
@font-face {
  font-family: 'Lexend';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/lexend-latin.1ec8f6ee.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: 'Lexend';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/lexend-latin-ext.368c86e9.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;
}

/* ==========================================================================================
   01  TOKENS
   ========================================================================================== */

:root {
  /* ---- BRAND -- copied from itv-client-manager/css/app.css, not chosen here --------------- */
  --brand-navy:      #063a6c;   /* the header bar. THE company colour */
  --brand-blue:      #2494e0;   /* the bright half of the brand */
  --on-brand:        #ffffff;   /* text on navy -- 11.48:1 */
  --on-brand-dim:    #b9d9f5;   /* secondary text on navy -- 7.82:1 */

  /* ---- THE CALL TO ACTION: ORANGE. THE VIOLET IS DEAD. -------------------------------------
   *
   * Gerek's ruling, family-wide. The violet was this surface's own invention -- chosen so a
   * client-facing button would not look like an operator's Save -- and inventing a colour was the
   * mistake. THE COMPANY ALREADY HAS AN ACTION COLOUR and it is in the brand: legacy reserves
   * `--orange: #dd6b00` for "brand / CTA / action accents ONLY (RUN, Finalize, ...)". That is the
   * same rule the violet was carrying here, in the house colour instead of a made-up one.
   *
   * COPIED, NOT EYEBALLED, from `itv-aeo-tool/shared/itv-tokens.css` -- the file the AEO Article
   * Tool 2.0 draws from, which took them from the legacy design. --brand-navy and --brand-blue
   * below were ALREADY character for character identical to that file; the two tools have been
   * drawing from the same well the whole time and only the action colour differed.
   *
   * -------------------------------------------------------------------------------------------
   * THE TEXT ON IT IS BLACK, AND THAT IS A MEASUREMENT RATHER THAN A TASTE
   * -------------------------------------------------------------------------------------------
   *
   * WHITE ON #dd6b00 IS 3.40:1. It clears the 3:1 floor for LARGE text and fails the 4.5:1 floor
   * for normal text. There is no third answer: which of the two applies is decided by the SIZE and
   * the CASE of the type sitting on the fill, so this file now carries BOTH inks and each one is
   * locked to the condition that earns it.
   *
   * Measured across the candidates, on the fill AND on its hover, because a button under a cursor
   * still has to be readable:
   *
   *     ink        on #dd6b00   on #c25e00      both >= 4.5?
   *     #ffffff       3.40         4.29             no    <- NORMAL text. Legal only when large
   *     #101828       5.22         4.14             no    <- this file's own --ink
   *     #0d0a06       5.81         4.60             yes
   *     #000000       6.18         4.89             YES   <- and it invents nothing
   *
   * ORDINARY BUTTONS TAKE THE BLACK, and that arithmetic is unchanged: they are Lexend at the body
   * size, so the large-text allowance is not available to them and pure black is the only
   * candidate that clears 4.5:1 on the fill and on the hover without inventing a hex.
   *
   * THE COMMITTING BUTTON TAKES THE WHITE, AND ONLY BECAUSE IT NOW HAS A DISPLAY FACE. `.btn--cta`
   * sets Bebas Neue at --t-cta (24px) and `text-transform: uppercase` IN THE SAME RULE AS THE
   * FILL, so the size, the case and the ground cannot drift apart in a later edit. 24px is 18pt,
   * which is where large text begins, and every glyph on that button is at cap height -- so it is
   * genuinely large rather than nominally large.
   *
   * THIS IS THE ONE GENUINE ACCESSIBILITY FAILURE AVAILABLE IN THIS FILE, and it is available in
   * exactly one shape: white ink with the size taken off. DESIGN_REFERENCE.md §7b names it -- "do
   * not adopt white without adopting the display face". check.js does not take that on trust: it
   * reads the rendered size rule out of `.btn--cta` on every run, decides WHICH floor applies from
   * it, and fails if the size ever drops below the one that makes the white legal.
   *
   * Both brand orange values are kept exactly as the brand file defines them; the only thing
   * decided here is what is written on top of them, and under what condition. */
  --brand-orange:        #dd6b00;   /* legacy --orange / itv-tokens --cta. THE committing button */
  --brand-orange-strong: #c25e00;   /* hovered -- itv-tokens --cta-strong */
  --brand-orange-ink:    #b35700;   /* orange as TEXT or a border on a light ground -- 4.61:1 on --bg */
  --brand-orange-wash:   #fdf1e6;   /* the faintest orange */
  --on-orange:           #000000;   /* ORDINARY buttons -- 6.18:1 on the fill, 4.89:1 on the hover */
  /* THE NAME CARRIES THE PRECONDITION ON PURPOSE. This is not "the other white"; it is the ink
   * that is only legal under display type at 24px, and a rule reaching for it without setting
   * --font-display and --t-cta is reaching for a 3.40:1 failure. itv-tokens calls this --on-cta. */
  --on-orange-display:   #ffffff;   /* .btn--cta ONLY -- 3.40:1, which is AA LARGE and nothing else */

  /* THE FOCUS RING IS THE BRAND BLUE, from the same file. A ring is not an action, so it has no
   * business wearing the action colour -- and an orange ring around an orange button is invisible
   * at exactly the moment a keyboard user needs to see it. */
  --focus-ring:      rgba(36, 148, 224, .45);

  /* ---- SURFACES -------------------------------------------------------------------------- */
  --bg:              #f6f8fb;   /* the page */
  --panel:           #ffffff;   /* a card */
  --panel-2:         #f1f4f9;   /* a quiet fill */
  --line:            #e4e9f0;   /* hairline */
  --line-strong:     #d3dae4;
  --panel-3:         #e8edf4;   /* a quiet fill, pressed. Step 1's choice buttons on hover */

  /* ---- TEXT ------------------------------------------------------------------------------ */
  --ink:             #101828;   /* body     -- 17.75:1 on a card */
  --ink-dim:         #4e5a6a;   /* secondary --  7.01:1 on a card */
  --ink-mute:        #5a6675;   /* meta      --  5.85:1 on a card */

  /* ---- THE ACCENT: the brand's deep blue, for a CHOSEN state and for a link ----------------
   *
   * From `itv-aeo-tool/shared/itv-tokens.css`, character for character, like the rest. It is the
   * same #0e5aa6 as --doing-ink below, and they are kept as separate names on purpose: one means
   * "a client picked this", the other means "this step is in progress". A single token doing both
   * jobs is a token that cannot be changed for one of them.
   *
   * THIS BLOCK DID NOT EXIST UNTIL THE VIOLET WAS REMOVED, and its absence was a live defect for
   * one deploy: `.choice.is-on` and the new segmented control were both pointed at `var(--accent)`
   * on the assumption it was already here -- it is in Client Manager and in the AEO tool, not in
   * this repo. An undefined custom property does not error, it resolves to NOTHING, so a selected
   * button rendered transparent with dark text and still looked like a button. Caught by clicking
   * through the deployed page, not by reading it. See the token check in check.js, added the same
   * afternoon so it cannot happen twice. */
  --accent:          #0e5aa6;   /* white on it -- 6.93:1 */
  --accent-strong:   #063a6c;   /* hovered: straight to the company navy -- 11.48:1 */
  --on-accent:       #ffffff;

  /* ---- THE THREE STATUS COLOURS -----------------------------------------------------------
   *
   * ALL SIX OF THESE MOVED, AND NOT ONE OF THEM MOVED FAR. That is the entire point.
   *
   * Every value below used to be a NEAR miss on the family's: #116b39 against the family's
   * #1a7a4d, #e6f4ec against #e9f2ee, #e4eefb against #eef7fd, #eef1f6 against #e8edf4. Nobody
   * could see any one of them. What a viewer CAN see is the accumulation -- a green that is not
   * quite the green in the other two tools, on a page that is otherwise trying to be them -- and
   * A NEAR MISS READS AS SLOPPINESS IN A WAY A DELIBERATE DIFFERENCE DOES NOT, because there is
   * no way to tell it was a choice. DESIGN_REFERENCE.md §7a item 4.
   *
   * So they are the family's exact values now, copied from itv-client-manager/css/app.css and
   * itv-aeo-tool/shared/itv-tokens.css, which name them --green / --green-wash / --green-fill,
   * --blue-run / --blue-run-wash / --blue-run-fill and --neutral-ink / --neutral-bg. THE NAMES
   * STAY LOCAL because they say what the colour MEANS on this surface -- "done", not "green" --
   * and that is the rule §3 of this file's header depends on.
   *
   * RE-MEASURED, NOT ASSUMED. Every ratio below was recomputed against the new ground it sits on;
   * the done chip is the one that moved, from 5.81:1 to 4.68:1, and it still clears AA with room.
   * THE GREEN IS THE SACRED ONE AND IT IS NOW THE FAMILY'S GREEN -- #1a7a4d is what "done" means
   * in all three 2.0 tools, and white on it is 5.34:1, which is what the pressed-button flash
   * needs (see .btn.is-done). */
  --done-ink:        #1a7a4d;   /* family --green. On --done-bg -- 4.68:1. White on it -- 5.34:1 */
  --done-bg:         #e9f2ee;   /* family --green-wash */
  --done-edge:       #d1e4db;   /* family --green-fill */

  --doing-ink:       #0e5aa6;   /* family --blue-run, already exact. On --doing-bg -- 6.39:1 */
  --doing-bg:        #eef7fd;   /* family --blue-run-wash */
  --doing-edge:      #deeffa;   /* family --blue-run-fill */

  --todo-ink:        #4a5568;   /* family --neutral-ink. On --todo-bg -- 6.40:1 */
  --todo-bg:         #e8edf4;   /* family --neutral-bg */
  /* THE ONE EDGE THE FAMILY HAS NO VALUE FOR, so it is kept rather than invented. The family's
   * --neutral-line is #7d8a9c, a deliberately heavy 3.51:1 control boundary; taking it would have
   * made "not started" the loudest of the three chips, which is backwards. #dfe4ec sits a shade
   * under its own fill exactly as --done-edge and --doing-edge sit under theirs, so all three
   * chips still read as one set. */
  --todo-edge:       #dfe4ec;

  /* ---- THE BRAND'S PALE BLUE, ADDED ON FIRST NEED ------------------------------------------
   *
   * DESIGN_REFERENCE.md D30 lists the family's brand tints as absent here and says to add them
   * "on first need, from the family's values" rather than pre-emptively. Step 2's client
   * bubbles are that need, and this is the family's exact value -- both siblings carry it.
   *
   * ██ IT IS A BRAND TINT AND NOT A STATUS COLOUR, WHICH IS WHY IT HAD TO BE ADDED AT ALL ██
   *
   * The obvious blue for a bubble is `--doing-bg`, and it would have been wrong: that is one of
   * the three sacred status colours and it means A STEP IS IN PROGRESS (rule 3 in this file's
   * header). Spending it on a chat bubble would put "in progress" on every line a client has ever
   * typed, and the day somebody wonders why the interview looks like a status, the answer would be
   * that we borrowed a colour that already meant something. */
  --brand-blue-pale: #cfe8fc;

  /* ---- AMBER: THE FOURTH COLOUR, AND THE FIRST THING ON THIS SURFACE TO NEED ONE -----------
   *
   * DESIGN_REFERENCE.md §7b left amber out on purpose and said exactly when to add it: *"Nothing
   * on a client screen has meant 'warning' yet. Add it when something does, from the family's
   * value."* A refused finalize is that thing, so these are the family's values -- both siblings
   * agree on all three, character for character.
   *
   * ██ THESE KEEP THE FAMILY'S NAMES WHERE GREEN AND BLUE DID NOT, AND THE DIFFERENCE IS REAL ██
   *
   * §25.4 renamed the family's green to `--done-*` because green MEANS something narrower here
   * than it does in an operator tool: one specific step, finished. Amber means the same thing on
   * this surface as it does in the other two -- *attempted, and there is something to look at* --
   * so renaming it would invent a distinction that is not there.
   *
   * ██ IT IS NOT A STATUS COLOUR AND IT MUST NEVER BECOME ONE ██
   *
   * The three status colours in this file describe a STEP and are written only by the status code
   * (rule 3 in the header). Amber describes a CONTROL the client has not filled in yet. Nothing
   * amber may ever appear on a step card, a chip, or the progress strip -- the strip in particular
   * is progress and not error, and it stays blue while every mark on the page is amber.
   *
   * AND IT IS NOT RED. There is still no red in this file. A client with three questions left has
   * done nothing wrong; they have not finished, which is a different sentence.
   *
   * MEASURED, on every ground it actually lands on:
   *
   *     --amber-ink on --amber-wash   6.97:1        the marker's line of text
   *     --amber-ink on --panel        7.99:1        the same line inside a white card
   *     --amber-ink on --bg           7.51:1        and on the page itself
   *     --amber     on --panel        4.69:1        the rule down the side, as a boundary (>=3)
   *     white       on --amber        4.69:1        the refused button -- see .btn--cta.is-refused
   */
  --amber:      #9a6b00;   /* the rule down the side of a marked control, and its border */
  --amber-ink:  #6d4a00;   /* the line of text that says what is wanted */
  --amber-wash: #f5efe3;   /* the ground it sits on -- itv-tokens' opaque value, not the rgba one */

  /* ---- SHAPE -------------------------------------------------------------------------------
   *
   * THE FAMILY'S THREE, AT THE FAMILY'S VALUES, MEANING WHAT THE FAMILY MEANS BY THEM.
   *
   * These were 8 / 12 / 16 -- every radius exactly one step rounder than the other two 2.0 tools.
   * The rounding was not the real problem. THE NAMES WERE: this file's --r-md was 12px, which is
   * itv-client-manager's --r-lg, so the same token name pointed at a different size in two repos
   * that copy rules between each other. A rule pasted across would silently change shape, look
   * fine, and nobody would ever find it. DESIGN_REFERENCE.md §7a item 3.
   *
   * The comments are Client Manager's own, verbatim, because the meaning is the thing being
   * adopted here and not merely the number. Anything reaching for a radius on this surface has to
   * answer "is it a chip, a control, or a card?" and the answer is already written down. */
  --r-sm:   6px;    /* a chip, a tag, a small pill's cousin */
  --r-md:   8px;    /* a control: input, button */
  --r-lg:   12px;   /* a card, a panel, a drawer */
  --r-pill: 999px;

  /* THE RESTING CARD SITS ON THE PAGE. IT DOES NOT FLOAT ABOVE IT.
   *
   * This was `0 1px 2px /.04, 0 6px 20px /.06` -- twenty pixels of blur against Client Manager's
   * three, and the single largest visual difference between a card here and a card there
   * (DESIGN_REFERENCE.md §7a item 2). Twenty pixels of soft grey under every card is not a detail
   * that shows up on close inspection; it is the first thing a screenshot says, on every screen.
   *
   * This is itv-client-manager's --shadow-sm, character for character. The second layer is what
   * makes it read as a seated edge rather than a hairline: 2px of near-contact plus 3px of
   * settle. */
  --shadow-card:  0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .04);
  --shadow-lift:  0 2px 4px rgba(16, 24, 40, .06), 0 12px 28px rgba(16, 24, 40, .10);
  --shadow-bar:   0 -2px 16px rgba(16, 24, 40, .08);   /* the fixed bar, lit from below */

  /* THE MODAL BACKDROP, AND IT DIMS WITH NAVY RATHER THAN WITH BLACK.
   *
   * Dark enough that the form behind it is plainly out of reach, light enough that the client can
   * still see their own answers underneath and knows what they are about to lock.
   *
   * The colour is the change: this was `rgba(16, 24, 40, .45)`, the ink black. Both siblings dim
   * with the company navy at the same .45, and they agree with each other about it -- which makes
   * it one of the few values in this whole file where "the family" has a single unambiguous
   * answer. One token, and it is the difference between a dialog that feels like part of the
   * product and one that feels like a browser default. */
  --scrim-fill:   rgba(6, 58, 108, .45);

  /* ---- SPACE -------------------------------------------------------------------------------
   *
   * *** A DELIBERATE CLIENT-FACING CHOICE, AND THE NAME IS PART OF IT ***
   *
   * The rhythm matches the family up to 16px and then opens out: 4 8 12 16 -- 24 32 48, against
   * the operator tools' 4 8 12 16 -- 20 24 32 40 56. Generous spacing is in the client-facing
   * brief and it is KEPT, not closed (DESIGN_REFERENCE.md §7b). There is no 20px step here
   * because nothing on this surface wants one.
   *
   * AND THAT IS EXACTLY WHY THESE ARE NOT CALLED --s-*. The family's --s-5 is 20px; this file's
   * fifth step is 24px. Renaming --gap-5 to --s-5 would be the mechanical half of "match the
   * family" performed at the cost of the honest half: a rule copied from Client Manager into this
   * repo, or out of it, would keep its token name and silently change its spacing. A different
   * name for a different ladder is the thing that makes the divergence survivable.
   *
   * So: --gap-* stays, on purpose, and this comment is the reason it stays. */
  --gap-1: 4px;
  --gap-2: 8px;
  --gap-3: 12px;
  --gap-4: 16px;
  --gap-5: 24px;   /* the family's fifth step is 20px -- see above */
  --gap-6: 32px;   /* the family's sixth step is 24px */
  --gap-7: 48px;   /* the family's seventh step is 32px */

  /* ---- TYPE: THE TWO FACES ------------------------------------------------------------------
   *
   * The stacks are itv-aeo-tool/shared/itv-tokens.css's, character for character.
   *
   * --font is Lexend, and it is everything on this site a client reads.
   *
   * --font-display is Bebas Neue: condensed, all-caps, letter-spaced. It is never used for a
   * sentence. ON THIS SURFACE IT HAS EXACTLY ONE JOB -- the committing button -- and that is a
   * narrower brief than it has in the AEO tool, where it also carries the header, the section
   * labels and the stat values. The reason is the no-operator-chrome rule: there are no section
   * labels here to set, and a client's own business name is not a thing to put in a condensed
   * all-caps face. The token is family-standard so a later screen CAN reach for it; nothing but
   * .btn--cta does today.
   *
   * THE FALLBACK STACKS ARE REAL AND THEY ARE LOAD-BEARING, not politeness. `Arial Narrow` and
   * `Avenir Next Condensed` are on essentially every Mac and Windows machine, so the frame before
   * the woff2 lands is still condensed rather than a reflow out of and back into a normal-width
   * face. And .btn--cta sets `text-transform: uppercase` in its own rule rather than leaning on
   * Bebas Neue having no lowercase -- so the CASE that earns the white ink its large-text
   * allowance survives the font failing to load at all. */
  --font-display: 'Bebas Neue', 'Oswald', 'Avenir Next Condensed', 'Arial Narrow',
                  'Helvetica Neue Condensed', sans-serif;
  --font: 'Lexend', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text', 'Segoe UI',
          system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif;

  /* ---- TYPE: THE SCALE -----------------------------------------------------------------------
   *
   * THIS FILE HAD NO TYPE SCALE AT ALL UNTIL NOW. Every size was a literal in the rule that used
   * it, which is survivable on a small surface and stops being survivable the moment a visual
   * page gets built -- a page inventing sizes is exactly how eleven sizes become nineteen, one
   * reasonable decision at a time. Adding the ladder BEFORE Step 4 is cheaper than retrofitting
   * it after (DESIGN_REFERENCE.md §7c).
   *
   * These are the eleven sizes this surface already used, named -- not a new ladder imposed on it.
   * Three near-duplicates were folded in on the way (1.3 and 1.4 leading into 1.25 and 1.45, and
   * a stray .08em label tracking onto the .8px the other label already used), because two values
   * a pixel apart are not a scale, they are the absence of one.
   *
   * *** --t-base IS 17px AND THAT IS A DELIBERATE CLIENT-FACING CHOICE ***
   * The family's base is 15px. The operator tools are read all day by somebody fluent in them;
   * this is read once, on a phone, by a chiropractor who is not looking at screens for a living.
   * The size is the accessibility decision, not the aesthetic one, and it is KEPT
   * (DESIGN_REFERENCE.md §7b). It also happens to be the iOS zoom floor for a text input -- see
   * .q__box -- so on this surface those two arguments arrive at the same number. */
  --t-xxs:   13px;   /* a chip, a section label, the save line under the bar */
  --t-xs:    14px;   /* the footer, a why-line */
  --t-sm:    15px;   /* meta, a muted line, an answer already given */
  --t-md:    16px;   /* a segment, a dialog's body, the back link */
  --t-base:  17px;   /* THE BODY. See above -- 17, not 15, and not 16 */
  --t-lg:    18px;   /* the wordmark, a finalized answer */
  --t-xl:    19px;   /* a question */
  --t-2xl:   20px;   /* a card title */
  --t-3xl:   21px;   /* a dialog title; a question on a wide screen */
  --t-title:    27px;   /* the page title, on a phone */
  --t-title-lg: 34px;   /* the page title, from 720px up */

  /* THE DISPLAY SIZE, AND IT IS THE ONLY ONE. 24px is 18pt, which is where WCAG's large-text
   * allowance begins, which is the entire reason white is legal on the orange. It is not a
   * styling number and it is not free to change -- see --on-orange-display above, and .btn--cta
   * below, where it is set in the same rule as the fill so the two cannot drift apart. */
  --t-cta:   24px;

  /* ---- TYPE: LEADING, WEIGHT, TRACKING -------------------------------------------------------
   *
   * --lh-body IS THE SECOND HALF OF THE 17px DECISION and is deliberate for the same reason: the
   * family reads 1.5 and 1.6, and a page read once on a phone gets the air. */
  --lh-tight:  1.25;   /* a title, a segment label -- something that must not wrap loosely */
  --lh-snug:   1.45;   /* a question, an answer: a line or two of consequence */
  --lh-normal: 1.5;    /* a paragraph inside a card */
  --lh-body:   1.55;   /* THE BODY. A deliberate client-facing softening -- see above */

  /* The family runs 300/400/500/600; this one runs 400/500/600/700 and skips the light. A page
   * with forty words on it has nothing for a 300 to do, and its titles have to carry the screen
   * on their own. The 700 is why the Lexend @font-face above declares `300 700` rather than the
   * AEO tool's `300 600` -- a variable face CLAMPS above its range instead of failing. */
  --w-normal: 400;
  --w-medium: 500;
  --w-semi:   600;
  --w-bold:   700;

  /* TRACKING COMES IN TWO KINDS AND THEY ARE NOT INTERCHANGEABLE.
   *
   * --ls-3 is the FAMILY's display-face value, from itv-tokens, at the number itv-components.css
   * spends on its own committing button. Bebas Neue without tracking is a wall; every use of it in
   * every 2.0 tool is tracked.
   *
   * ONLY THE THIRD STEP IS DEFINED, AND IT KEEPS ITS NUMBER. The family's ladder is 1 / 1.5 / 2 /
   * 3px and this surface spends exactly one rung of it. Declaring the other three "for later"
   * would put three tokens in this file that nothing reads, and the numbering is what makes that
   * safe to do: --ls-3 means the same thing here as it does in the other two repos, so --ls-1 and
   * --ls-2 can arrive later, from the family's values, without renumbering anything.
   *
   * The three below are body-face optical corrections, which is a different job: a 34px title
   * needs pulling in, an 18px wordmark and a 13px all-caps label need pushing out. They are named
   * for what they sit ON rather than by step number, so nothing can mistake one kind for the
   * other. */
  --ls-3: 2px;      /* .btn--cta, matching itv-components.css */

  --ls-title:    -.2px;   /* a large title, pulled in */
  --ls-wordmark:  .2px;   /* the wordmark, opened up a hair */
  --ls-label:     .8px;   /* a 13px all-caps label -- the only place caps appear outside the CTA */

  /* ---- MOTION --------------------------------------------------------------------------------
   *
   * Colour and border only, on a hover or a focus. Nothing on this site slides, fades in or
   * appears, and all of it is off under prefers-reduced-motion (see the reset below).
   *
   * THE TWO SIBLINGS DISAGREE HERE -- Client Manager eases at 120ms on a cubic-bezier, the AEO
   * tool at a flat 200ms -- so this is a pick rather than a copy. --ease takes Client Manager's,
   * because that is the file whose header calls itself the template and because 120ms is what a
   * hover should cost: fast enough to feel like the control responding rather than the page
   * animating.
   *
   * --ease-slow is a separate token because it does a separate job. The progress strip's fill is
   * not a hover; it is a width crossing real distance to say "one more of your answers landed",
   * and at 120ms that reads as a jump rather than as movement. 200ms is the AEO tool's value and
   * it is the right one for the only thing on this site that actually travels. */
  --ease:      120ms cubic-bezier(.4, 0, .2, 1);
  --ease-slow: 200ms ease;
}

/* ==========================================================================================
   02  RESET AND BASE
   ========================================================================================== */

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

/* THE `hidden` ATTRIBUTE ACTUALLY HIDES THINGS. A SAFETY NET, NOT THE FIX.
 *
 * `hidden` is not a property of an element; it is a UA stylesheet rule, `[hidden] { display:
 * none }`, with effectively no specificity -- so ANY author rule that sets `display` on the same
 * element silently defeats it. That shipped: `.card3__summary { display: flex }` beat it, and an
 * empty chip and a live "Change" link rendered on every unanswered Step 3 card while the code
 * setting `hidden = true` ran correctly every time.
 *
 * The card was fixed by CONSTRUCTION -- it attaches one presentation or the other and never
 * hides anything (see showOnly() in js/step3-render.js). This rule is here so the NEXT element
 * somebody hides cannot fail the same way silently. It is a floor, and it is not permission to
 * go back to hiding things that should not be in the document at all. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;   /* iOS must not resize our type when the phone rotates */
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font);
  /* 17px, not the family's 15. A DELIBERATE CLIENT-FACING CHOICE -- see --t-base and --lh-body in
     the token block, where the argument and the divergence are both written down. */
  font-size: var(--t-base);
  line-height: var(--lh-body);
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* Nothing on this site is decorative-motion. One transition, and it is off for anybody who has
   asked their machine for less. */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ==========================================================================================
   03  THE SHELL
   ========================================================================================== */

.site-header {
  background: var(--brand-navy);
  color: var(--on-brand);
  padding: var(--gap-4) var(--gap-4);
}

.wordmark {
  margin: 0;
  font-size: var(--t-lg);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-wordmark);
  color: var(--on-brand);
}

/* *** 680px IS A DELIBERATE CLIENT-FACING CHOICE *** and so is the header above it having no
   sticky and no toolbar. The operator tools run to a 1680-1700px ceiling with a rail down one
   side, because they have things to navigate BETWEEN. This has one column and one thing to do, so
   a rail here would be furniture and a sticky bar would be chrome with nothing in it. Keeping the
   family's page ceiling would not have made this look more like the family; it would have made a
   single column of questions float in the middle of a very wide empty page.
   DESIGN_REFERENCE.md §7b. */
.wrap {
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  padding: var(--gap-6) var(--gap-4) var(--gap-7);
}

.site-footer {
  padding: 0 var(--gap-4) var(--gap-6);
  text-align: center;
  color: var(--ink-mute);
  font-size: var(--t-xs);
}

/* ==========================================================================================
   04  PAGE HEAD AND COPY
   ========================================================================================== */

.page-title {
  margin: 0 0 var(--gap-3);
  font-size: var(--t-title);
  line-height: var(--lh-tight);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-title);
  /* A name can be long and it can be one unbroken word. Neither may push the page sideways. */
  overflow-wrap: anywhere;
}

.lede {
  margin: 0 0 var(--gap-5);
  color: var(--ink-dim);
  font-size: var(--t-base);
}

/* THE ONE-TIME THANK-YOU, under the welcome, for somebody who finalized a step seconds ago.
 *
 * It replaced a pop-up the client had to dismiss (see js/step1.js), so its whole job is to cost
 * them nothing: one line, no card, no icon, no button. It uses --done-ink because that is what
 * this project's green means and this sentence is about something being done -- it is not a
 * fourth status colour and it is not a success banner for an ordinary load, which §10 still
 * forbids. 6.20:1 on the page. */
.thanks {
  margin: 0 0 var(--gap-4);
  color: var(--done-ink);
  font-size: var(--t-base);
  font-weight: var(--w-semi);
}

.section-label {
  margin: 0 0 var(--gap-3);
  font-size: var(--t-xxs);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* ==========================================================================================
   05  THE STEP LIST -- the whole point of the checklist screen
   ========================================================================================== */

.step-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

/*
 * ONE TAP TARGET PER STEP, AND IT IS THE WHOLE CARD.
 *
 * Not a "Start" link inside a card -- the card. A small link inside a large box is the single
 * most common way a phone screen becomes annoying: the thing that looks tappable is tappable
 * and the 90% of it that also looks tappable is not. min-height is 76px against a 44px floor,
 * because this is the only control on the screen and it can afford to be generous.
 */
.step-card {
  display: flex;
  align-items: center;
  gap: var(--gap-4);
  width: 100%;
  min-height: 76px;
  padding: var(--gap-4);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: box-shadow var(--ease), border-color var(--ease), transform var(--ease);
}

.step-card:hover {
  border-color: var(--line-strong);
  box-shadow: var(--shadow-lift);
}

.step-card:active { transform: translateY(1px); }

.step-card:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-color: var(--brand-blue);
}

/* The number. Quiet on purpose -- it is an ordering aid, not a label, and the client is looking
   for the words next to it. */
.step-num {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--r-pill);
  background: var(--panel-2);
  color: var(--ink-dim);
  font-size: var(--t-sm);
  font-weight: var(--w-bold);
}

.step-body {
  flex: 1 1 auto;
  min-width: 0;   /* lets a long name wrap instead of stretching the flex row */
}

.step-name {
  display: block;
  font-size: var(--t-base);
  font-weight: var(--w-semi);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.step-meta {
  display: block;
  margin-top: var(--gap-1);
  font-size: var(--t-xs);
  color: var(--ink-mute);
}

/* The chevron. A pure affordance; it says "this goes somewhere" without any words. */
.step-go {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  border-right: 2px solid var(--ink-mute);
  border-bottom: 2px solid var(--ink-mute);
  transform: rotate(-45deg);
}

/* ==========================================================================================
   06  THE STATUS CHIP -- THE ONLY THING ON THIS SITE THAT MAY USE THE STATUS COLOURS
   ========================================================================================== */

.chip {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  border: 1px solid;
  font-size: var(--t-xxs);
  font-weight: var(--w-semi);
  white-space: nowrap;
}

/* NOT STARTED. Neutral, and neutral is correct -- an untouched step is not a problem. */
.chip--todo  { color: var(--todo-ink);  background: var(--todo-bg);  border-color: var(--todo-edge); }

/* IN PROGRESS. Blue, the brand's own colour. */
.chip--doing { color: var(--doing-ink); background: var(--doing-bg); border-color: var(--doing-edge); }

/* DONE. The only green in the file. */
.chip--done  { color: var(--done-ink);  background: var(--done-bg);  border-color: var(--done-edge); }

/* ==========================================================================================
   07  CARDS AND BUTTONS
   ========================================================================================== */

.card {
  padding: var(--gap-5);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
}

.card > :first-child { margin-top: 0; }
.card > :last-child  { margin-bottom: 0; }

.card-title {
  margin: 0 0 var(--gap-3);
  font-size: var(--t-2xl);
  font-weight: var(--w-bold);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 0 var(--gap-5);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font: inherit;
  font-weight: var(--w-semi);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--ease), border-color var(--ease);
}

.btn:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* THE ORDINARY ORANGE BUTTON. Lexend, body size, BLACK ink -- 6.18:1 on the fill and 4.89:1 on
   the hover. "Try again" on a dead-end screen is the only thing wearing this today: it is the one
   button on that screen and it recovers rather than commits, so it takes the colour without
   taking the display face. */
.btn--primary {
  background: var(--brand-orange);
  color: var(--on-orange);
}
.btn--primary:hover { background: var(--brand-orange-strong); }

/* ------------------------------------------------------------------------------------------
 * THE COMMITTING BUTTON -- AND ITS SIZE IS IN THE SAME RULE AS ITS COLOUR, ON PURPOSE
 * ------------------------------------------------------------------------------------------
 *
 * "Save and finalize", and the "Yes, finalize" inside the confirmation dialog. The two controls
 * in this app that lock a client's answers, and the only two things wearing white on the orange.
 *
 * WHITE ON --brand-orange IS 3.40:1. That clears the 3:1 floor for LARGE text and fails the
 * 4.5:1 floor for normal text, so THE SIZE AND THE CASE BELOW ARE NOT STYLING -- THEY ARE WHAT
 * MAKES THE COLOUR LEGAL. --t-cta is 24px, which is 18pt, which is where large text begins.
 * `text-transform: uppercase` puts every glyph at cap height, so it is genuinely large rather
 * than nominally large -- and, unlike relying on Bebas Neue having no lowercase, it holds even
 * on the frame before the woff2 lands and on a machine where it never does.
 *
 * THEY ARE WRITTEN TOGETHER BECAUSE APART THEY WOULD DRIFT. A later edit dropping this to 17px
 * to match the quiet button beside it would take the one control that commits a client's answers
 * below AA, and NOTHING ON THE SCREEN WOULD LOOK WRONG. That is the whole failure mode, and it is
 * why check.js reads this rule's font-size out of the deployed stylesheet, decides which contrast
 * floor applies from the number it finds, and fails on the smaller one.
 *
 * The padding is the family's shape at this size; the 52px floor from .btn still governs the tap
 * target, which is what a phone actually needs.
 * ------------------------------------------------------------------------------------------ */
.btn--cta {
  background: var(--brand-orange);
  color: var(--on-orange-display);
  font-family: var(--font-display);
  font-size: var(--t-cta);          /* LOAD-BEARING. Do not lower without re-measuring the ink */
  font-weight: var(--w-normal);     /* Bebas Neue has one weight and it is 400 */
  letter-spacing: var(--ls-3);
  text-transform: uppercase;        /* LOAD-BEARING with the size above -- cap height is the point */
}
.btn--cta:hover { background: var(--brand-orange-strong); }

.btn--quiet {
  background: var(--panel);
  color: var(--ink-dim);
  border-color: var(--line-strong);
}
.btn--quiet:hover { background: var(--panel-2); }

.btn--block { display: flex; width: 100%; }

/* ------------------------------------------------------------------------------------------
 * A BUTTON THAT ANSWERS FOR ITSELF
 *
 * Feedback belongs ON the control that was pressed. The save state used to live in a grey line
 * in the opposite corner of the bar; it was correct, live, and far enough away that Gerek
 * pressed Save three times on the live site believing it was dead. See js/button-state.js.
 * ------------------------------------------------------------------------------------------ */

/* The label is a span so its text can be swapped without rebuilding the button -- rebuilding
   would destroy the spinner and, worse, drop the keyboard's focus. */
.btn__label { display: inline-block; }

/* THE SPINNER EXISTS AT REST AND IS MERELY INVISIBLE. Collapsed to zero width with no margin, so
   entering the working state does not reflow the bar and slide the other button under a finger
   that may be about to tap again. */
.btn__spin {
  width: 0;
  height: 14px;
  margin-right: 0;
  border: 2px solid currentColor;
  border-radius: var(--r-pill);
  border-right-color: transparent;
  opacity: 0;
  transition: none;
}

.btn.is-working .btn__spin {
  width: 14px;
  margin-right: var(--gap-2);
  opacity: .55;
  animation: btn-spin .7s linear infinite;
}

@keyframes btn-spin { to { transform: rotate(360deg); } }

/* WORKING. Dimmed and un-pressable, and the LABEL carries the whole message -- which is what
   keeps this honest under prefers-reduced-motion, where the reset at the top of this file stops
   the spinner turning and leaves a static ring. Somebody who has asked their machine for less
   motion still reads "Saving…". */
.btn.is-working {
  cursor: default;
  opacity: .85;
}

.btn--quiet.is-working { background: var(--panel-2); }

/* DONE. THE SANCTIONED GREEN, ruled by Gerek: an outcome the client asked for, on the control
   they pressed, for a second and a half.
 *
 * §10 says green means Done and nothing else may be green. That rule protects the STATUS CHIPS
 * -- it exists so a styling decision can never make a claim about a client's data. This is not a
 * new green: it is --done-ink itself, borrowed briefly, and it describes an event rather than a
 * record. White on it is 5.34:1 -- re-measured when --done-ink became the family's #1a7a4d. */
.btn.is-done,
.btn--quiet.is-done,
.btn--primary.is-done,
.btn--cta.is-done {
  background: var(--done-ink);
  border-color: var(--done-ink);
  color: var(--on-brand);
  opacity: 1;
}

/* The hover rules above are more specific than they look once a state class is on. Held, so a
   mouse resting on the button does not undo the green mid-flash. */
.btn--primary.is-done:hover,
.btn--cta.is-done:hover,
.btn--quiet.is-done:hover { background: var(--done-ink); }

/* NO TRANSITION INTO EITHER STATE, and this is a fix rather than a preference.
 *
 * `.btn` carries `transition: background var(--ease)` for hover, which is right for a hover and
 * wrong for this. Entering the done state sets the text to white and the background to green;
 * with the transition running, the background spends 150ms crossing from --panel to --done-ink
 * while the text is ALREADY white -- so the label is white-on-near-white and effectively
 * invisible for a fifth of a second. Measured in the browser: `backgroundColor` read
 * `rgb(255,255,255)` with `color` reading `rgb(255,255,255)` on the frame the class landed.
 *
 * The whole purpose of these two states is to be seen the instant they are entered, so they are
 * entered instantly. Leaving them keeps the transition -- fading back to the ordinary label is
 * the one moment here where a fade is the right answer. */
.btn.is-working,
.btn.is-done { transition: none; }

.btn:disabled { cursor: default; }

/* The back link on a step screen. A link, not a button -- going back is not an action with a
   consequence, and giving it a button's weight would suggest it is. It keeps a 44px target
   anyway, because it is the only way off this screen on a phone. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-2);
  min-height: 44px;
  margin-bottom: var(--gap-3);
  color: var(--doing-ink);
  font-size: var(--t-md);
  font-weight: var(--w-semi);
  text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
.back-link:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; border-radius: var(--r-sm); }

.back-arrow {
  width: 9px;
  height: 9px;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}

/* ==========================================================================================
   08  THE WAITING STATE
   ========================================================================================== */
/*
 * A calm line of text and nothing else. No spinner, no skeleton, no progress bar.
 *
 * It is also NOT announced the instant the page paints: the CSS delays it, so a fast lookup --
 * which is most of them -- shows nothing at all rather than flashing "one moment" for 80ms.
 * A flicker reads as a fault.
 */
.waiting {
  color: var(--ink-mute);
  font-size: var(--t-base);
  opacity: 0;
  animation: fade-in .2s ease .5s forwards;
}

@keyframes fade-in { to { opacity: 1; } }

/* ==========================================================================================
   09  UTILITIES
   ========================================================================================== */

.stack   { display: flex; flex-direction: column; gap: var(--gap-4); }
.mt-5    { margin-top: var(--gap-5); }
.mt-6    { margin-top: var(--gap-6); }
.muted   { color: var(--ink-mute); font-size: var(--t-sm); }

/* Visible to a screen reader, invisible on screen. Used for the live region that announces a
   screen change to somebody who cannot see that the page has redrawn. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================================
   10  WIDER SCREENS -- the adaptation, and it is deliberately small
   ========================================================================================== */

@media (min-width: 720px) {
  .site-header { padding: var(--gap-4) var(--gap-6); }
  .wrap        { padding: var(--gap-7) var(--gap-5) var(--gap-7); }
  .page-title  { font-size: var(--t-title-lg); }
  .step-card   { padding: var(--gap-5); min-height: 84px; }
  .btn--block  { display: inline-flex; width: auto; }
}

/* ==========================================================================================
   11  STEP 1 -- THE FORM
   ------------------------------------------------------------------------------------------
   Mobile first, and on this page that is not a slogan: twenty questions on a phone held in one
   hand, at the end of a working day. Every tap target here is at least 48px tall, every text
   box is big enough to see three lines of what you typed, and nothing is a small grey link.

   NO RED. Not for the secret steer, not for an unsaved state, not for a refusal. A client who
   pastes something they shouldn't, or loses their signal, has not done anything wrong -- and the
   one colour that says "you have made a mistake" is the one colour this site does not own.
   The steer is amber-free too: it uses the ordinary quiet fill and bold ink, which reads as
   "look at this" without reading as "you are in trouble".

   GREEN STAYS SACRED. It appears on this page in exactly one place: the finished note.
   ========================================================================================== */

.form { margin-top: var(--gap-5); }

.form__section { margin-bottom: var(--gap-6); }

.form__section-title {
  margin: 0 0 var(--gap-3);
  font-size: var(--t-xxs);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* ---- one question ---------------------------------------------------------------------- */

.q {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  padding: var(--gap-5) var(--gap-4);
  margin-bottom: var(--gap-4);
}

/* THE QUESTION IS THE BIGGEST THING IN THE CARD, and the pre-filled value is inside it. The
   whole design of this step is that a wrong value in front of somebody's eyes gets corrected in
   four seconds -- which does not happen if it is set at the size of body copy. */
.q__text {
  margin: 0 0 var(--gap-4);
  font-size: var(--t-xl);
  line-height: var(--lh-snug);
  font-weight: var(--w-semi);
  color: var(--ink);
}

.q__why {
  margin: var(--gap-4) 0 0;
  font-size: var(--t-xs);
  line-height: var(--lh-normal);
  color: var(--ink-mute);
}

.q__body { display: block; }

/* ---- the two buttons ------------------------------------------------------------------- */

.q__choices {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap-3);
}

.choice {
  min-height: 52px;
  padding: var(--gap-3) var(--gap-4);
  font: inherit;
  font-size: var(--t-md);
  font-weight: var(--w-semi);
  color: var(--ink);
  background: var(--panel-2);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
}

.choice--wide {
  display: block;
  width: 100%;
  margin-top: var(--gap-3);
  text-align: left;
  line-height: var(--lh-snug);
}

.choice:hover { background: var(--panel-3); }

.choice:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* THE CHOSEN ONE IS THE BRAND ACCENT BLUE, and it is deliberately NOT the orange.
 *
 * Orange is reserved for the one control on a screen that COMMITS something -- itv-tokens.css
 * says so in as many words, and this page would otherwise put nine orange fills above the single
 * orange button that actually finalizes. A picked choice is a state, not an action.
 *
 * And still not green: green on this site means a whole step is Done, and a picked button is not
 * a finished anything. White on --accent is 6.93:1. */
.choice.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.choice.is-on:hover { background: var(--accent-strong); border-color: var(--accent-strong); }

/* ---- the boxes -------------------------------------------------------------------------- */

.q__fix { margin-top: var(--gap-3); }

.q__box {
  display: block;
  width: 100%;
  box-sizing: border-box;
  min-height: 96px;
  padding: var(--gap-3) var(--gap-4);
  font: inherit;
  font-size: var(--t-base);          /* 17px or larger, or iOS zooms the whole page on focus */
  line-height: var(--lh-normal);
  color: var(--ink);
  background: var(--panel);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  resize: vertical;
}

.q__box:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 1px;
  border-color: var(--brand-blue);
}

.q__box--extra { margin-top: var(--gap-2); }

.q__extra-label {
  margin: var(--gap-4) 0 0;
  font-size: var(--t-sm);
  font-weight: var(--w-semi);
  color: var(--ink-dim);
}

/* ---- the secret steer -- a notice, never an alarm ---------------------------------------- */

.q__steer {
  margin: var(--gap-3) 0 0;
  padding: var(--gap-3) var(--gap-4);
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  font-weight: var(--w-semi);
  color: var(--ink);
  background: var(--panel-2);
  border: 1px solid var(--line-strong);
  border-left: 4px solid var(--brand-blue);
  border-radius: var(--r-sm);
}

/* ---- the services list ------------------------------------------------------------------ */

.q__checks { display: block; }

.check {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  min-height: 52px;
  padding: var(--gap-2) var(--gap-3);
  border-radius: var(--r-md);
  cursor: pointer;
}

.check:hover { background: var(--panel-2); }

/* A 24px box, because the default is about 13px and this is a phone. */
.check__input {
  width: 24px;
  height: 24px;
  flex: 0 0 auto;
  accent-color: var(--accent);
  cursor: pointer;
}

.check__label { font-size: var(--t-base); line-height: var(--lh-snug); color: var(--ink); }

/* ---- the bottom bar --------------------------------------------------------------------- */

/* ALWAYS ON SCREEN. A save button twenty questions down the page is a save button that is not
   found, and the client who does not find it is the client who loses their evening's work. */
.bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  padding: var(--gap-3) var(--gap-4) calc(var(--gap-3) + env(safe-area-inset-bottom));
  background: var(--panel);
  border-top: 1px solid var(--line);
  box-shadow: var(--shadow-bar);
}

/* `auto 1fr`, NOT `1fr 1fr`. Two equal columns on a 375px phone leaves about 150px a side, and
   "Save and finalize" does not fit in it -- it wrapped to two lines and sat half behind the edge
   of its own button. Seen on the deployed page at 375x812, not reasoned about. So "Save draft"
   takes the width its words need and the longer label gets everything else. */
/* THE LINE AND THE BUTTONS ARE ONE GROUP. This wrapper is the whole fix for the bottom-left
   problem: previously the bar's two children were pushed apart by `space-between` at desktop
   width, leaving the save text a screen away from the button it described. */
.bar__actions {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
}

/* ██ THE COMMITTING BUTTON NOW CARRIES A SENTENCE, AND THE ROW HAD TO LEARN IT ██
 *
 * These two sat side by side because every commit label was two or three short words. Gerek's
 * rehearsal ruling made it name its step, so the label is now "Finalize Step N" plus that step's
 * name from the payload -- and at 375px the longest of the four is 463px of un-wrappable display
 * face inside a 343px row. Measured on the deployed page: the button ran from x=155 to x=618,
 * 243px past the viewport.
 *
 * (This comment quoted the label in full, step name and all, until check.js failed the build on
 *  it. The catalogue grep reads the stylesheet -- §26.7 -- and a step name is catalogue wherever
 *  it is written. Ninth time today.)
 *
 * THE SIZE IS NOT AVAILABLE AS A FIX. `.btn--cta` is 24px because that is what makes its white ink
 * legal on the orange -- 3.40:1 is AA LARGE and nothing else (§25.2). Shrinking the text to make a
 * longer label fit would silently take the contrast below the floor, which is the one trade this
 * button may never make.
 *
 * SO THE ROW GIVES WAY INSTEAD. On a phone the two stack, the committing button takes the full
 * column, and its label is allowed to wrap. From 720px up they return to a row, where the label
 * has always fitted on one line. */
.bar__buttons { display: grid; grid-template-columns: 1fr; gap: var(--gap-3); }

.bar__buttons .btn {
  width: 100%;
  min-height: 52px;
  padding: var(--gap-2) var(--gap-4);
  white-space: nowrap;
}

/* THE ONE BUTTON THAT MAY WRAP. `--lh-tight` is the ladder's step for something that must not
 * wrap loosely, which is exactly a two-line label on a control. */
.bar__buttons .btn--cta {
  white-space: normal;
  line-height: var(--lh-tight);
}

/* QUIET. Small, grey, and it holds "Saved just now" -- no banner, no toast, no colour. A save
   that announces itself teaches somebody to watch for it, and then a save that is merely fast
   looks like a save that did not happen. It reserves its line so the buttons never jump. */
.bar__state {
  margin: 0;
  /* THE ONE LEADING IN THIS FILE THAT IS NOT A SCALE STEP, AND IT IS NOT AN OVERSIGHT. 18px is
     not "1.38 of 13px"; it is the same number as the min-height directly above it, which is what
     makes the reserved line and the line that arrives in it exactly the same height. Expressed as
     a ratio it would drift the moment --t-xxs moved, the row would gain a pixel, and the two
     buttons under it would step down the screen mid-save. A fixed leading matched to a fixed box
     is the correct tool here; a token would be the wrong one. */
  min-height: 18px;
  font-size: var(--t-xxs);
  line-height: 18px;
  color: var(--ink-mute);
  text-align: center;
}

/* The page has to be able to scroll clear of the fixed bar. */
body:has(.bar) .wrap { padding-bottom: 148px; }

/* ==========================================================================================
   STEP 3 -- THE ACCOUNT CARDS
   ==========================================================================================
   Nine questions, most of which are "no". The card starts as one question with three answers and
   grows only for the ones that turn out to be the client's -- a page showing nine account-name
   boxes at once reads as nine jobs. */

.card3-list {
  display: flex;
  flex-direction: column;
  gap: var(--gap-4);
  margin-top: var(--gap-5);
}

.card3 {
  padding: var(--gap-5);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
}

/* FOLDED, NOT HIDDEN. An answered card keeps its name and shows what was said, on one line, with
   a way back in. The client can see they answered it and can still change their mind.
   FOLDING IS INSTANT -- no height transition. A card that animates shut on every answer turns
   nine quick taps into nine little waits, and the movement is the thing being complained about,
   not the thing being enjoyed. */
.card3--folded { padding: var(--gap-4) var(--gap-5); background: var(--panel-2); box-shadow: none; }
.card3--folded .card3__why { display: none; }

.card3__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-2) var(--gap-3);
  margin: var(--gap-2) 0 0;
}

.card3__said { font-size: var(--t-sm); color: var(--ink-mute); overflow-wrap: anywhere; }

/* A LINK, NOT A BUTTON. Reopening a card undoes nothing and commits nothing; a button's weight
   would suggest otherwise. 44px so it is still a thumb target. */
.card3__change {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-left: auto;
  color: var(--accent);
  font-size: var(--t-sm);
  font-weight: var(--w-semi);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
}
.card3__change:hover { color: var(--accent-strong); }
.card3__change:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; border-radius: var(--r-sm); }

/* ==========================================================================================
   THE SEGMENTED CONTROL -- one switch, not three buttons
   ==========================================================================================
   Three flat buttons in a row read as three separate things a client could press in any
   combination. A segmented control reads as ONE question with one answer in it, which is what
   this actually is -- and it says so to a screen reader too: role="radiogroup" around
   role="radio", arrow keys to move, one tab stop for the whole group.

   THE SEGMENTS ARE EQUAL WIDTH BY FLEX-BASIS ZERO, not by content. That is what stops the tick
   appearing on selection from resizing the chosen segment and shoving the others sideways under
   a finger that is still on the screen. */

.seg {
  display: flex;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--panel);
}

.seg__opt {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-2);
  min-height: 56px;
  padding: var(--gap-2) var(--gap-3);
  border: 0;
  border-left: 1.5px solid var(--line-strong);
  background: transparent;
  font: inherit;
  font-size: var(--t-md);
  font-weight: var(--w-semi);
  line-height: var(--lh-tight);
  color: var(--ink);
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.seg__opt:first-child { border-left: 0; }
.seg__opt:hover { background: var(--panel-2); }

/* THE FOCUS RING IS DRAWN INSIDE, because the group clips its children -- an outline drawn
   outward would be cut off by `overflow: hidden` and a keyboard user would see nothing. */
.seg__opt:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: -3px;
}

/* SELECTED: the brand accent blue, and deliberately NOT the orange -- see .choice.is-on. */
.seg__opt[aria-checked="true"] {
  background: var(--accent);
  color: var(--on-accent);
}

.seg__opt[aria-checked="true"]:hover { background: var(--accent-strong); }

/* The tick is decoration; the aria-checked state is what is actually announced. */
/* `line-height: 1` is a glyph-box reset rather than a leading choice -- the tick is one character
   sitting beside a label and it must not add height to the row. Not a scale step, deliberately. */
.seg__tick { font-size: var(--t-sm); line-height: 1; }

/* ---- STACKED: the same control, turned ------------------------------------------------------
 * Four sentence-length options cannot be four columns. Side by side each one wraps to three lines
 * and the tick loses the label it belongs to, so the group becomes rows -- same roles, same
 * roving tabindex, same arrow keys, same selected fill.
 *
 * THE RULE IS THE OPTIONS' LENGTH, NOT THE VIEWPORT. A control that reflows between one shape and
 * another as the window moves is two controls to learn, and the narrow layout is the one that has
 * to work anyway.
 *
 * LEFT-ALIGNED, because these are sentences. Centred sentences of different lengths give a group
 * a ragged left edge and there is nothing for the eye to run down. */
.seg--stack { flex-direction: column; }
.seg--stack .seg__opt {
  justify-content: flex-start;
  min-height: 52px;
  border-left: 0;
  border-top: 1.5px solid var(--line-strong);
  text-align: left;
}
.seg--stack .seg__opt:first-child { border-top: 0; }

/* ---- GRID: the same control, wrapped ---------------------------------------------------------
 * Step 3 v2 serves cards with seven and nine tap-options, and those options are COMPANY NAMES
 * rather than sentences. A stack of nine names is a column of near-identical rows to hunt down; a
 * grid puts them where the eye can sweep, and a name is short enough to sit two or three across.
 *
 * IT IS STILL ONE CONTROL. Same radiogroup, same roving tabindex, same arrow keys -- only the
 * wrapping changes. See js/segmented.js for why a second component was the wrong answer.
 *
 * THE BORDER MOVES FROM THE SEGMENTS TO THE BUTTONS. A wrapped flex row cannot use the shared-edge
 * trick the row and stack shapes use -- there is no "first in the row" to hang it off once items
 * wrap -- so each option carries its own outline and the group carries none. */
.seg--grid {
  flex-wrap: wrap;
  gap: var(--gap-2);
  border: 0;
  background: transparent;
  overflow: visible;
}
.seg--grid .seg__opt {
  /* THE BASIS IS WHAT MAKES IT A GRID RATHER THAN A LIST. With `auto` the basis is the label's own
     width and a single long option takes the whole row, so a card of company names came out one
     per line on a phone -- see the breakpoint note further down, which is what actually caused it.
     A fixed basis says "about two of these fit", and `flex-grow` then shares out whatever is left
     so the row has no ragged edge. */
  flex: 1 1 8rem;
  min-width: 8rem;
  min-height: 52px;
  justify-content: center;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  text-align: center;
}
.seg--grid .seg__opt:focus-visible { outline-offset: 2px; }

/* ---- THE INVENTORY GROUP: one card on the screen, four asks inside it ----------------------- */
.card3--group { background: var(--panel-2); }
.card3__reassure {
  margin: var(--gap-2) 0 0;
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  color: var(--ink-mute);
}
.card3__kids {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
  margin-top: var(--gap-4);
}
/* A CHILD IS A CARD ON A CARD, so it takes the white and loses the shadow -- otherwise four
   raised cards inside a fifth read as five separate questions rather than one group of four. */
.card3--child {
  padding: var(--gap-4);
  background: var(--panel);
  box-shadow: none;
}
.card3--child .card3__name { font-size: var(--t-md); }

/* ---- A CARD THE SERVER FOLDED ---------------------------------------------------------------
 * It is folded and it says why, and the sentence is the whole point: a question that quietly
 * vanished when the one above it was answered reads as a page losing a question. Quiet, because
 * nothing is being asked -- this is the page explaining itself, not addressing the client. */
.card3--auto { background: var(--panel-2); box-shadow: none; }
.card3__because {
  margin: var(--gap-2) 0 0;
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  color: var(--ink-mute);
}

/* ---- THE INLINE NOTE: INFORMATION, NEVER A WARNING -------------------------------------------
 * One option carries one today -- the heads-up that a personal address is worth moving to their
 * own domain -- and it appears the moment that option is picked.
 *
 * ██ THE TONE IS THE DECISION. ██ The obvious styling is the amber §27.4 uses for a refused
 * finalize, and it would be wrong: amber on this site means SOMETHING IS WANTED FROM YOU, and
 * nothing is. The client answered honestly and the answer is fine. So it takes the brand's own
 * quiet blue rule -- the same shape as the scanner's steer, which is also a note rather than a
 * refusal -- and says its piece without telling anybody they made a mistake. */
.card3__note {
  margin: var(--gap-3) 0 0;
  padding: var(--gap-3) var(--gap-4);
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  color: var(--ink-dim);
  background: var(--doing-bg);
  border-left: 4px solid var(--brand-blue);
  border-radius: var(--r-sm);
}

/* ---- A FOLLOW-UP FIELD, NESTED INSIDE THE CHOICE THAT ASKED FOR IT ---------------------------- */
.card3__field + .card3__field { margin-top: var(--gap-3); }

/* ---- the nested panel: visually INSIDE the choice that opened it ---- */
.card3__nest {
  margin-top: var(--gap-4);
  padding: var(--gap-1) 0 0 var(--gap-4);
  border-left: 3px solid var(--brand-blue);
}

/* ---- ██ THE CONTROL THAT SHUTS A CARD THAT INVITES SEVERAL ANSWERS ██ ------------------------
 *
 * A multi-select card stays open until the client says it is finished (see THE STAY-OPEN LAW in
 * js/step3-render.js), and this is the saying-so. It is the LAST thing in the card, full width on
 * a phone, above the fold line the card draws for itself.
 *
 * ██ IT IS QUIET, AND THAT IS NOT AN OVERSIGHT. ██ `--brand-orange` is reserved for the one control
 * on a screen that COMMITS something (§20.2), and this commits nothing a client cannot undo by
 * pressing Change. It also sits ten cards above the finalize button on the same scroll, so an
 * orange Done on every multi card would put three orange buttons on the page and make the one that
 * locks the step look like a fourth. Quiet fill, brand-blue ink, 44px box.
 *
 * THE TOP RULE IS A SEPARATOR AND NOT A BOX. The card is already a box; a second border round its
 * own footer would be a card inside a card. */
.card3__doneRow {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--gap-5);
  padding-top: var(--gap-4);
  border-top: 1px solid var(--line);
}
/* ██ STYLED FROM SCRATCH RATHER THAN OFF `btn--quiet btn--small`, AND check.js IS WHY. ██
 *
 * The obvious reach was those two modifiers, and the honest-out rule refused them: it greps the
 * function that draws a card's options for `--quiet` and `--small`, because a control that demotes
 * itself inside a card is how fine print gets introduced to a question. Done is not an option, but
 * the check cannot tell one node in that function from another and it should not have to -- so the
 * button takes its own class and the rule stays strict. Ten declarations against weakening a check
 * is a good trade. */
.card3__done {
  min-height: 44px;
  min-width: 120px;
  padding: 0 var(--gap-5);
  background: var(--panel-2);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  font: inherit;
  font-size: var(--t-md);
  font-weight: var(--w-semi);
  color: var(--brand-blue);
  cursor: pointer;
  transition: background var(--ease), border-color var(--ease);
}
.card3__done:hover { background: var(--panel-3); }
.card3__done:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* On a phone it takes the full column: it is the one control that ends the card, and a 120px
   button floated right on a 375px screen is a target a thumb has to aim for. */
@media (max-width: 519px) {
  .card3__doneRow { justify-content: stretch; }
  .card3__done    { width: 100%; }
}

.card3__nest-gap { margin-top: var(--gap-4); }

/* ---- THE SECTION HEADING ------------------------------------------------------------------
 * 2.1E puts these cards under headings in the client's own language, and the WORDS ARE NOT IN
 * THIS REPO -- the payload carries no such field yet, so this rule waits for the day it does.
 * See sectionsOf() in js/step3-render.js.
 *
 * QUIET, AND IN THE SAME FAMILY. A signpost between groups of cards, not a second page title:
 * the label size, the label tracking, the muted ink. A heading here at title weight would
 * compete with the one thing on the page that is a title. */
.card3-section {
  margin: var(--gap-6) 0 0;
  font-size: var(--t-xxs);
  font-weight: var(--w-bold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink-mute);
}
.card3-section:first-child { margin-top: 0; }

/* ---- A FIELD LABEL, AND THE BOX UNDER IT ---------------------------------------------------
 * Every one of these labels is SERVED. `identifier_label`, `platform_label` and `extra_label` are
 * columns on the card's own row, so what a client reads above a box is Gerek's to reword with no
 * deploy here. */
.card3__label {
  display: block;
  margin: var(--gap-4) 0 var(--gap-2);
  font-size: var(--t-sm);
  font-weight: var(--w-semi);
  color: var(--ink-dim);
}
.card3__label:first-child { margin-top: 0; }

/* A ONE-LINE BOX. `.q__box` is a 96px textarea, which is the right shape for an answer and the
 * wrong one for an email address -- a three-line well under a one-line label invites three lines
 * of something that is one. 17px is inherited, because anything smaller makes iOS zoom the whole
 * page on focus.
 *
 * (This comment quoted one of the served identifier labels as its example, and check.js failed the
 *  build on it -- correctly. The catalogue grep reads the STYLESHEET too, which is the whole point
 *  of §26.7: the rule is about bytes this repo ships, not about which language they are in.) */
.card3__box { min-height: 52px; }

/* The free-text card IS an answer, so it keeps the tall box. */
.card3__area { min-height: 96px; }

/* The served explanation that sits between a label and its box. */
.card3__hint {
  margin: 0 0 var(--gap-2);
  font-size: var(--t-xs);
  line-height: var(--lh-normal);
  color: var(--ink-mute);
}

/* ---- THE GRANT BLOCK'S RULES ARE DELETED, LIKE ITS NODES ---------------------------------
 * The address, the walkthrough paragraph, the per-card note and the vendor link were styled here
 * for two builds. Gerek ruled the granting off the page on 2026-09-03 and the nodes ceased to
 * exist, so the rules did too -- rather than being left behind for a `.grant` that nothing draws.
 *
 * A STYLESHEET IS A LIST OF CLAIMS ABOUT WHAT THE PAGE CONTAINS. Dead rules are the cheapest kind
 * of lie to leave lying around and the most expensive to read past a year later, and check.js
 * greps the shipped bytes so a grant node cannot come back quietly on either side. */

.card3__promise { margin-top: var(--gap-2); }

/* ---- THE STICKY PROGRESS STRIP ----------------------------------------------------------
 * Slim, quiet, and it stays. On a nine-card page "how much is left" is the first thing to
 * scroll off, and it is the one thing a client wants while they are working.
 *
 * z-index BELOW the save bar's 20, deliberately: a sticky top strip and a fixed bottom bar
 * cannot meet at any sane viewport height, and if they ever did the bar is the one that must
 * win -- it holds the buttons. */
.progress-strip {
  position: sticky;
  top: 0;
  z-index: 15;
  margin: var(--gap-2) 0 var(--gap-5);
  padding: var(--gap-3) 0 var(--gap-3);
  /* OPAQUE, or the cards scroll through it. --bg is the page, so it reads as the page. */
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

/* At least body size and medium weight -- it is a status a client reads at a glance, not a
   caption. The old version was 15px --ink-mute and got lost against the why-lines. */
.progress-strip__label {
  margin: 0 0 var(--gap-2);
  font-size: var(--t-base);
  font-weight: var(--w-semi);
  color: var(--ink-dim);
}

.progress-strip__track {
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--panel-3);
  overflow: hidden;
}

/* BRAND BLUE, NEVER GREEN. Green means a whole step is Done, and answering the ninth card is
   not finishing the step -- finalizing is. A bar that filled to green would promise that. */
.progress-strip__fill {
  height: 100%;
  width: 0;
  background: var(--brand-blue);
  border-radius: var(--r-pill);
  /* The one movement on this strip, and it is the width following the number. The global
     reduced-motion reset at the top of this file switches it off. */
  transition: width var(--ease-slow);
}

/* ---- the finished step, read-only ---- */
.card3--done { padding: var(--gap-4) var(--gap-5); }
.card3__name-done {
  margin: 0 0 var(--gap-1);
  font-size: var(--t-md);
  font-weight: var(--w-semi);
  color: var(--ink-dim);
}
.card3__done-id {
  margin: var(--gap-2) 0 0;
  font-size: var(--t-sm);
  color: var(--ink-mute);
  overflow-wrap: anywhere;
}

/* ---- the finished step ------------------------------------------------------------------- */

/* THE ONE GREEN ON THIS PAGE. */
.note {
  margin-top: var(--gap-5);
  padding: var(--gap-4);
  background: var(--done-bg);
  border: 1px solid var(--done-edge);
  border-radius: var(--r-lg);
}

.note__title { margin: 0 0 var(--gap-1); font-size: var(--t-base); font-weight: var(--w-bold); color: var(--done-ink); }

.q__support { margin: 0; font-size: var(--t-sm); line-height: var(--lh-normal); color: var(--done-ink); }
.q__support a { color: var(--done-ink); font-weight: var(--w-semi); }

.q--done { padding: var(--gap-4); }
.q--done .q__text { font-size: var(--t-md); font-weight: var(--w-semi); color: var(--ink-dim); margin-bottom: var(--gap-2); }

.q__answer {
  margin: 0;
  font-size: var(--t-lg);
  line-height: var(--lh-snug);
  font-weight: var(--w-semi);
  color: var(--ink);
  white-space: pre-wrap;
}

.q__answer--none { font-weight: var(--w-medium); color: var(--ink-mute); }

/* ---- the modal --------------------------------------------------------------------------- */

.scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: var(--gap-4);
  background: var(--scrim-fill);
}

.dialog {
  width: 100%;
  max-width: 460px;
  padding: var(--gap-5) var(--gap-4) calc(var(--gap-4) + env(safe-area-inset-bottom));
  background: var(--panel);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift);
}

.dialog__title { margin: 0 0 var(--gap-3); font-size: var(--t-3xl); line-height: var(--lh-tight); color: var(--ink); }
.dialog__body  { margin: 0 0 var(--gap-5); font-size: var(--t-md); line-height: var(--lh-body); color: var(--ink-dim); }
.dialog__actions { display: grid; gap: var(--gap-3); }
.dialog__actions .btn { min-height: 52px; }

body.is-modal { overflow: hidden; }

@media (prefers-reduced-motion: no-preference) {
  .scrim  { animation: scrim-in .16s ease-out; }
  .dialog { animation: dialog-in .2s ease-out; }
}

@keyframes scrim-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes dialog-in { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } }

/* ---- wider screens ----------------------------------------------------------------------- */

/* NARROW: THE SEGMENTS STACK, AND THEY STACK INSIDE THE BORDER.
   The group keeps its single outline and its single radius; only the flow direction and which
   edge carries the divider change. That is the difference between a switch that got taller and
   three buttons that happen to be touching. Below 560px the longest of the three segment labels
   cannot share a row with the other two without hyphenating.

   (That sentence quoted the label itself until Step 4's check started grepping the STYLESHEET as
   well as the shipped modules, and caught it. The rule was never "no wording in JavaScript" -- it
   is that no wording the server owns lives in this repo at all, and a CSS comment ships too.
   Seventh time; MASTER_DOC §15.1.) */
/* ██ AND THE GRID IS EXEMPT, WHICH IT WAS NOT UNTIL IT WAS MEASURED ON A PHONE ██
 *
 * This rule turns a segmented ROW into a column on a narrow screen, and it predates the grid by
 * two builds. It matched `.seg` -- all of it -- so the moment Step 3 v2 introduced `.seg--grid`,
 * the grid became a single column at exactly the width the grid exists for. **It looked perfect on
 * a desktop**, where the rule does not apply, and every structural assertion about the grid passed
 * because the class and the `flex-wrap` were both really there.
 *
 * FOUND BY MEASURING THE ROWS AT 375px rather than by looking at the page: six options, six rows,
 * one per line. A screenshot would not have shown it as wrong -- a column of buttons looks like a
 * deliberate column of buttons.
 *
 * The grid wraps by itself and needs no help from a breakpoint; it is the ROW shape that cannot
 * survive a narrow screen. So the rule says which shapes it means. */
@media (max-width: 559px) {
  .seg:not(.seg--grid) { flex-direction: column; }
  .seg:not(.seg--grid) > .seg__opt {
    border-left: 0;
    border-top: 1.5px solid var(--line-strong);
    min-height: 52px;
  }
  .seg:not(.seg--grid) > .seg__opt:first-child { border-top: 0; }
}

@media (min-width: 720px) {
  .q            { padding: var(--gap-5); }
  .q__text      { font-size: var(--t-3xl); }
  .q__choices   { grid-template-columns: auto auto; justify-content: start; }
  .choice       { min-width: 190px; }
  /* RIGHT-ALIGNED AS ONE GROUP, not spread to the two corners. `flex-end` is doing the work that
     `space-between` used to undo: the quiet line now sits immediately beside the buttons, which
     is the only place a person who just clicked one is looking. */
  .bar          { flex-direction: row; align-items: center; justify-content: flex-end; padding: var(--gap-4) var(--gap-6); }
  .bar__actions { flex-direction: row; align-items: center; gap: var(--gap-4); }
  .bar__state   { text-align: right; }
  .bar__buttons { grid-template-columns: auto auto; }
  .card3        { padding: var(--gap-5) var(--gap-6); }
  .bar__buttons .btn { min-width: 170px; }
  body:has(.bar) .wrap { padding-bottom: 120px; }
  .scrim        { align-items: center; }
}

/* ==========================================================================================
   14  STEP 4 -- "the design step": nine sections, tiles, a gallery of nine builds, swatches,
       statements, and two drop spots
   ==========================================================================================

   THE MOST VISUAL PAGE IN THE PORTAL, AND THE ONE MOST LIKELY TO WANT A GRID. It does not get
   one. DESIGN_REFERENCE.md §3.2 settled that before this page existed: the family's answer to
   "several things side by side" is the AEO tool's two-column workbench, which is an OPERATOR
   pattern built for a screen somebody sits in front of all day, and the client translation of a
   gallery is a single-column list of large cards.

   ██ THE NINE BUILDS ARE THE TEST OF THAT RULE AND THEY PASS IT HARDER THAN THE SIX DID. ██ Every
   one of them is a SCREENSHOT OF A WEBSITE at roughly 2:1. Two-up inside a 680px column is 330px
   wide, which puts a whole homepage into 170px of height -- a client would be judging a blur and
   answering a question they cannot see. One column, full bleed to the card edge, is the only size
   at which the question can be answered, and on a phone that is the only layout there is anyway.
   ========================================================================================== */

/* ---- A QUESTION BLOCK GREW TWO MORE LINES, AND BOTH ARE COLUMNS RATHER THAN DECORATION ------
 *
 * `.q__text` (section 13) is the QUESTION. These two are the rows either side of it:
 *
 *   `.q__head`    the line ABOVE it. Four blocks on this page carry one, and it is not the
 *                 question -- putting it in the prompt would make itv-core's answers file print a
 *                 heading where it prints an ask. Set BELOW the question in size, because the
 *                 question is still the thing being answered; it is a signpost, not a title.
 *   `.q__helper`  the line BELOW it -- how many to pick, or why this one is not about their taste.
 *                 Distinct from `.q__why`, which is the reason WE ask and sits at the foot of the
 *                 block; a helper is an instruction and has to be read before the control.
 *
 * ██ AND ONE BLOCK ON THIS PAGE HAS THE SAME STRING IN BOTH COLUMNS. ██ The renderer prints it
 * once (see bodyPrompt()), so there is no rule here suppressing a duplicate -- a stylesheet that
 * hid a sentence would hide it from a screen reader too. */
.q__head {
  margin: 0 0 var(--gap-2);
  font-size: var(--t-base);
  font-weight: var(--w-bold);
  line-height: var(--lh-snug);
  color: var(--ink-dim);
}
.q__head + .q__text { margin-top: 0; }

.q__helper {
  margin: calc(var(--gap-4) * -1 + var(--gap-1)) 0 var(--gap-4);
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  color: var(--ink-dim);
}

/* WHEN THERE IS NO QUESTION ABOVE IT the helper is the only instruction in the block, so it takes
 * the space the question would have had rather than hanging off nothing. */
.q__head + .q__helper { margin-top: 0; }

/* ---- A SECTION ---------------------------------------------------------------------------
 *
 * Not a card. The questions inside are already cards, and a card of cards is a box drawn round
 * some boxes. A section is a group with air around it and a name over it. */
.sect { margin-bottom: var(--gap-7); }
.sect__body > .q + .q,
.sect__body > .q + .told,
.sect__body > .told + .q,
.sect__body > .told + .told { margin-top: var(--gap-4); }

/* ██ THE SECTION NAME, AND IT IS DELIBERATELY NOT UPPERCASED. ██
 *
 * Step 3's equivalent (`.card3-section`) is `text-transform: uppercase`, and copying that here
 * would break the one rule this page is given about case: the wording arrives capital where it
 * MEANS to be capital -- one question shouts a single word in the middle of a sentence, on purpose
 * -- and a stylesheet that shouts every heading is a stylesheet re-deciding something the owner
 * already decided. Uppercasing a section label would also uppercase an apostrophe'd word into
 * something no one wrote.
 *
 * So it earns its rank the way a heading should: size, weight and ink, in the reading face. */
.sect__label {
  margin: 0 0 var(--gap-2);
  font-size: var(--t-lg);
  font-weight: var(--w-bold);
  line-height: var(--lh-snug);
  color: var(--ink);
}

/* The line a section says before its first question. Only one section has one today; nothing here
 * knows that, and a second is a data change. */
.sect__lead {
  margin: 0 0 var(--gap-4);
  font-size: var(--t-base);
  line-height: var(--lh-normal);
  color: var(--ink-dim);
}

/* THE DEFERRAL NOTE. Quiet, and it does not say "skipped" -- somebody who has told us to take the
 * lead has made a decision, not failed to make one. THE SENTENCE IS THE SERVER'S NOW: it is the
 * section's own `deferral_message`, so the warmth is the owner's wording and not this repo's. */
.defer-note {
  margin-bottom: var(--gap-6);
  padding: var(--gap-4);
  background: var(--panel-2);
  border-left: 3px solid var(--brand-blue);
  border-radius: var(--r-sm);
}
.defer-note__body {
  margin: 0;
  font-size: var(--t-base);
  line-height: var(--lh-normal);
  color: var(--ink);
}

/* ---- THE FOLD: what a deferral shuts, and the one control that opens it ---------------------
 *
 * A LINK AND NOT A BUTTON-LOOKING THING. Everything else on this page that looks pressable is an
 * ANSWER; this presses nothing and records nothing, it just shows more of the form. Giving it the
 * tile or choice treatment would put a fourth kind of tappable box on a page that already has
 * three, and the client would reasonably expect it to mean something.
 *
 * IT STILL CLEARS 44px, because a thumb does not care what a control means. */
.fold__link {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-2);
  min-height: 44px;
  padding: 0 var(--gap-2);
  margin: 0 0 var(--gap-2) calc(var(--gap-2) * -1);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font: inherit;
  font-size: var(--t-base);
  font-weight: var(--w-semi);
  color: var(--brand-blue);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.fold__link:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* A chevron, drawn rather than typed, so it needs no icon font and no glyph coverage. It points
 * down when there is more to see and up when it is all open. */
.fold__link::after {
  content: "";
  width: 8px;
  height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.fold__link.is-open::after { transform: translateY(2px) rotate(-135deg); }

.fold__body > .q + .q { margin-top: var(--gap-4); }

/* ---- A CHOICE THAT IS A SENTENCE, AND SOMETIMES TWO -----------------------------------------
 *
 * Step 1's `.q__choices` is a fixed two-column grid, which is right for the answers it holds --
 * two or three words that pair naturally. Step 4's are whole sentences, and two of its questions
 * give every choice a SECOND sentence underneath saying what we will do about that answer. Two
 * columns of that at 375px is two tall narrow blocks of text nobody can compare.
 *
 * `.choice--wide` was NOT the fix, and it is worth writing down why since it looks like it: it
 * sets `width: 100%`, which inside a grid is 100% OF ITS CELL. It was written for a choice that
 * stands alone outside the grid, and dropping it into one produces exactly the two-column layout
 * it appears to prevent -- plus a `margin-top` fighting the grid's own gap.
 *
 * THE TRACK DOES THE WORK INSTEAD, and it needs no per-question logic: a 220px floor means one
 * column inside a phone's column and two once there is room for two. The compound selector is
 * deliberate -- it has to outrank the `.q__choices` rule in the 720px query further up. */
.q__choices.q__choices--fit { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

.choice--sentence {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-1);
  text-align: left;
  line-height: var(--lh-snug);
}
.choice__label { font-weight: var(--w-semi); }

/* THE ANSWER TO THE ANSWER. Smaller and quieter than the label, and it stays legible when the
 * button lights: `--on-accent` is white and this inherits it, so the pair keeps its contrast in
 * both states rather than a fixed grey disappearing into the blue. */
.choice__helper {
  font-size: var(--t-sm);
  font-weight: var(--w-normal);
  line-height: var(--lh-normal);
  color: var(--ink-dim);
  white-space: normal;
}
.choice.is-on .choice__helper { color: var(--on-accent); opacity: 0.92; }

/* ---- TILES: tap on, tap off, and a cap that says so ----------------------------------------
 *
 * A CONTROL, SO IT TAKES THE CONTROL RADIUS AND A 44px FLOOR. These are not chips -- a chip is
 * something the page tells you and a tile is something you press. They wrap, because the number of
 * them is data and the labels are sentences of unpredictable length. */
.tiles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-2);
}

.tile {
  min-height: 44px;
  padding: var(--gap-2) var(--gap-4);
  background: var(--panel-2);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  font: inherit;
  font-size: var(--t-md);
  font-weight: var(--w-semi);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--ease), border-color var(--ease), color var(--ease), opacity var(--ease);
}
.tile:hover { background: var(--panel-3); }
.tile:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* CHOSEN IS THE ACCENT BLUE AND NOT THE ORANGE. A tile is a STATE, not an action -- §20.2's
 * reservation, and a page of orange tiles above one orange finalize button would end it. */
.tile.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.tile.is-on:hover { background: var(--accent-strong); border-color: var(--accent-strong); }

/* ██ AT THE CAP: QUIET, NOT DEAD, AND STILL LEGIBLE. ██
 *
 * The tiles a client cannot add yet go flat -- the fill drops back to the page ground and the
 * border softens -- while the INK STAYS FULL STRENGTH. This is the distinction the obvious
 * `opacity: .4` gets wrong: dimming the whole tile makes the words hard to read, and a client at
 * the cap is often reading precisely to decide which one to swap in. So the BOX says unavailable
 * and the WORDS stay a sentence.
 *
 * `aria-disabled` and not `disabled`: the tile keeps its place in the tab order and a screen
 * reader is told it is unavailable, where a `disabled` button vanishes from the keyboard and
 * explains nothing. The cursor says it too. */
.tile.is-capped {
  background: var(--bg);
  border-color: var(--line);
  border-style: dashed;
  cursor: default;
}
.tile.is-capped:hover { background: var(--bg); }

/* The taps under a URL, and under a picked build, sit down a size -- they qualify the thing above
 * them rather than asking a question of their own.
 *
 * ██ THE TYPE GOES DOWN AND THE TARGET DOES NOT ██ This was 40px until the 375px pass measured it,
 * and 40px breaks the 44px floor this surface keeps on everything tappable (DESIGN_REFERENCE §5,
 * rule 7). A smaller LABEL is a hierarchy decision; a smaller TARGET is a thumb missing a control
 * on a phone, which is the only device that matters here. The padding shrinks, the box does not. */
.tiles--why { margin-top: var(--gap-2); }
.tiles--why .tile { min-height: 44px; font-size: var(--t-sm); padding: var(--gap-1) var(--gap-3); }

/* The line under a capped question. It is not a warning and it is not shown only once the cap
 * bites -- a rule the client can read before they hit it is a rule that never surprises. When it
 * DOES bite the same line changes to what to do about it, so the client's eye is already in the
 * right place: nothing new appears, one sentence is replaced. */
.q__cap {
  margin: var(--gap-3) 0 0;
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  color: var(--ink-mute);
}
.q__cap.is-full { color: var(--ink-dim); font-weight: var(--w-semi); }

/* A tap that could not land makes the sentence move ONCE. No colour and no shake: this is a page
 * doing what it said it would, not an error. `prefers-reduced-motion` turns it off entirely, and
 * the sentence is still correct standing still -- which is what makes that safe. */
@keyframes capNudge {
  0%   { transform: translateX(0); }
  30%  { transform: translateX(3px); }
  60%  { transform: translateX(-2px); }
  100% { transform: translateX(0); }
}
.q__cap.is-nudged { animation: capNudge 260ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .q__cap.is-nudged { animation: none; }
}

/* ---- THE GALLERY: nine sites we actually built ----------------------------------------------
 *
 * ONE COLUMN, ALWAYS. See the section header: two-up would make a website 330px wide.
 *
 * ██ THE WHOLE CARD IS THE BUTTON. ██ A tap target the size of a photograph is the easiest control
 * on a phone there is. A small "pick this" button under a picture would make the picture -- the
 * entire question -- decorative, and would be the one control on this page a thumb could miss. */
.gallery { display: block; }
.build + .build { margin-top: var(--gap-6); }

.build__pick {
  display: block;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.build__pick:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; border-radius: var(--r-md); }

/* ██ A FIXED ASPECT BOX, AND `contain` RATHER THAN `cover` ██
 *
 * The box holds its shape from the `aspect-ratio` and the `width`/`height` on the tag, so the
 * page's full height is known before a single image byte lands and nothing below jumps while the
 * client is reading. That matters more here than it did with six: these are ~1.5MB screenshots and
 * there are nine of them.
 *
 * `contain` MEANS A PICTURE IS NEVER STRETCHED AND NEVER CROPPED. The nine differ by a few pixels
 * of height; `cover` would tidy that up by cutting the edges off the ones that do not match, and
 * on a page whose entire question is "how does this look to you", cropping the answer is not a
 * trade worth making. The panel fill behind it makes a letterbox read as deliberate. */
.build__frame {
  position: relative;
  aspect-ratio: 2299 / 1179;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  transition: border-color var(--ease), box-shadow var(--ease);
}
.build__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* PICKED: the accent blue as a ring around the picture, not a wash over it. A tint on top of a
 * screenshot would change the thing the client is being asked to judge. */
.build__pick.is-picked .build__frame {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent);
}

/* The tick, drawn in CSS, on the corner of the frame -- the confirmation sits on the thing that
 * was tapped. `aria-pressed` on the button is what actually says it; this is the sighted half. */
.build__tick {
  position: absolute;
  top: var(--gap-2);
  right: var(--gap-2);
  width: 30px;
  height: 30px;
  border-radius: var(--r-pill);
  background: var(--accent);
  opacity: 0;
  transform: scale(0.8);
  transition: opacity var(--ease), transform var(--ease);
}
.build__tick::after {
  content: "";
  position: absolute;
  left: 10px;
  top: 7px;
  width: 7px;
  height: 13px;
  border-right: 2.5px solid var(--on-accent);
  border-bottom: 2.5px solid var(--on-accent);
  transform: rotate(45deg);
}
.build__pick.is-picked .build__tick { opacity: 1; transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  .build__tick { transition: none; }
}

.build__meta { display: block; margin-top: var(--gap-3); }
.build__title {
  display: block;
  font-size: var(--t-lg);
  font-weight: var(--w-semi);
  line-height: var(--lh-snug);
  color: var(--ink);
}
/* THE FEEL LINE IS THE CARD'S OWN SENTENCE and it is what most clients actually react to -- the
 * picture says what it looks like and this says what it FEELS like, which is the question. Quiet
 * but not fine print. */
.build__feel {
  display: block;
  margin-top: var(--gap-1);
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  color: var(--ink-dim);
}

/* The taps only exist on a picked card, and they are indented under it so they read as belonging
 * to the picture rather than to the next one. */
.build__why { margin-top: var(--gap-3); }

/* ---- THE COLOURS OFF THEIR OWN SITE --------------------------------------------------------- */

.swatchblock { display: block; }

.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-3);
  margin-bottom: var(--gap-4);
}

.swatch { text-align: center; }

/* A REAL BLOCK OF THE COLOUR, because a hex string is not a colour to anybody who does not write
 * CSS, and this question asks somebody to agree or disagree with a palette. The border is there so
 * a very pale swatch still has an edge on a white card rather than dissolving into it. */
.swatch__block {
  width: 56px;
  height: 56px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
}
.swatch__hex {
  display: block;
  margin-top: var(--gap-1);
  font-size: var(--t-xxs);
  color: var(--ink-mute);
  /* The one place a monospace figure earns its keep: six hex characters read as a code rather
     than as a word, and they line up under blocks that are all the same width. */
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* ---- A BLOCK THAT ASKS NOTHING --------------------------------------------------------------
 *
 * ██ IT MUST NOT LOOK LIKE A QUESTION, AND THAT IS THE WHOLE REQUIREMENT. ██
 *
 * Three of these are on the page and one sits directly under a question with buttons. A block
 * with the question card's white fill, border and shadow but nothing to press reads as a question
 * that failed to load -- so it is a different shape entirely: no card, a rule down the left, the
 * page ground behind it, and the heading at body weight rather than question weight.
 *
 * THE RULE IS THE BLUE THE DEFERRAL NOTE USES, and that is on purpose: both are the site talking
 * rather than asking, and a client learns one visual idea instead of two. */
.told {
  padding: var(--gap-4) var(--gap-4) var(--gap-4) var(--gap-5);
  background: var(--panel-2);
  border-left: 3px solid var(--brand-blue);
  border-radius: var(--r-sm);
}
.told__head {
  margin: 0 0 var(--gap-2);
  font-size: var(--t-base);
  font-weight: var(--w-bold);
  line-height: var(--lh-snug);
  color: var(--ink);
}
.told__body {
  margin: 0;
  font-size: var(--t-base);
  line-height: var(--lh-normal);
  color: var(--ink-dim);
}
.told__helper {
  margin: var(--gap-2) 0 0;
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  color: var(--ink-mute);
}

/* A statement that carries a drop panel needs room under its words for it. */
.told--drop .dropwrap { margin: var(--gap-4) 0 0; }

/* ---- URL ROWS -------------------------------------------------------------------------------- */

.urlrows { display: block; }
.urlrow + .urlrow {
  margin-top: var(--gap-4);
  padding-top: var(--gap-4);
  border-top: 1px solid var(--line);
}

/* A ONE-LINE BOX. `.q__box` is a 96px textarea; an address and a short note are a line each, and
 * a three-line box invites three lines of an answer that is one. 17px is inherited from `.q__box`
 * and it is load-bearing -- below it iOS zooms the page on focus. */
.q__box--line { min-height: 52px; }
.q__box--note { margin-top: var(--gap-2); }

/* ---- THE DROP BOX ----------------------------------------------------------------------------
 *
 * ALWAYS VISIBLE, DEFERRAL OR NOT (itv-core §123.4). It sits outside the sections so that is true
 * by construction rather than by an exception in the collapse code.
 *
 * THE WHOLE PANEL IS THE LABEL FOR A REAL `<input type="file">`, which is what makes it work on a
 * phone -- there is no dragging there, and a drop zone that is only a drop zone is a decoration
 * on the screen this app is written for first. */
.dropwrap { margin-bottom: var(--gap-6); }

.drop {
  display: block;
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--panel);
  transition: background var(--ease), border-color var(--ease);
}
.drop.is-over { background: var(--brand-orange-wash); border-color: var(--brand-orange-ink); }

.drop__pick {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-6) var(--gap-4);
  text-align: center;
  cursor: pointer;
}
.drop__lead { font-size: var(--t-lg); font-weight: var(--w-semi); color: var(--ink); }
.drop__hint { font-size: var(--t-sm); color: var(--ink-dim); max-width: 34ch; }

/* ---- THE INTERVIEW'S DROP BOX -----------------------------------------------------------
 * 2.1D D.6 offers two doors for a writing sample and NEITHER IS THE RECOMMENDED ONE, so this
 * panel is sized to sit beside the composer as a sibling rather than above it as a feature.
 * Step 4's is the centrepiece of its page and is scaled like one; here the files are one of two
 * equally good answers, and a panel twice the height of the box next to it would be the layout
 * making the recommendation the words are careful not to make.
 *
 * THE LEAD IS A SENTENCE, SO IT IS SET AS ONE. `--t-lg` semibold is a heading size and this is a
 * question with a question mark in it; at heading weight it shouts, and a shouted question is a
 * push. Body size, medium weight, and a measure that stops it running the full width. */
.drop--chat .drop__pick { padding: var(--gap-4); gap: var(--gap-3); }
.drop--chat .drop__lead {
  font-size: var(--t-base);
  font-weight: var(--w-medium);
  line-height: var(--lh-normal);
  max-width: 46ch;
}

/* An upward arrow in a box, drawn in CSS. No icon font, no SVG file, no second request. */
.drop__icon {
  width: 26px;
  height: 26px;
  border: 2px solid var(--accent);
  border-radius: var(--r-sm);
  position: relative;
}
.drop__icon::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 5px;
  width: 8px;
  height: 8px;
  border-left: 2px solid var(--accent);
  border-top: 2px solid var(--accent);
  transform: translateX(-50%) rotate(45deg);
}
.drop__icon::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 6px;
  width: 2px;
  height: 12px;
  background: var(--accent);
  transform: translateX(-50%);
}

/* THE INPUT IS FOCUSABLE, NOT DISPLAY:NONE. `.sr-only` keeps it in the tab order and keeps its
 * label working; `display: none` would take it out of both and leave the panel unreachable by
 * keyboard. The focus ring is drawn on the panel, which is what a sighted keyboard user sees. */
.drop:focus-within { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* ---- THE UPLOAD QUEUE ------------------------------------------------------------------------ */

.upl { display: block; margin-top: var(--gap-3); }

.upl__row {
  padding: var(--gap-3) var(--gap-4);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.upl__row + .upl__row { margin-top: var(--gap-2); }

.upl__head { display: flex; align-items: baseline; gap: var(--gap-3); }
.upl__name {
  flex: 1 1 auto;
  font-size: var(--t-sm);
  font-weight: var(--w-semi);
  /* A filename can be very long and it must not push the page sideways. */
  overflow-wrap: anywhere;
}
.upl__size { flex: 0 0 auto; font-size: var(--t-xxs); color: var(--ink-mute); }

.upl__track {
  height: 6px;
  margin-top: var(--gap-2);
  background: var(--panel-3);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.upl__fill {
  height: 100%;
  width: 0;
  background: var(--brand-blue);
  border-radius: var(--r-pill);
  transition: width var(--ease-slow);
}

/* WHEN THE BROWSER CANNOT SAY HOW FAR IT HAS GOT, the bar says "something is happening" rather
 * than lying about a number. Off under prefers-reduced-motion by the reset at the top of this
 * file, which leaves a static part-filled bar and the sentence underneath still doing the work. */
.upl__track.is-indeterminate .upl__fill {
  width: 40%;
  animation: upl-slide 1.1s ease-in-out infinite;
}
@keyframes upl-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}

.upl__state {
  margin: var(--gap-2) 0 0;
  font-size: var(--t-xxs);
  color: var(--ink-mute);
}

/* DONE IS THE SACRED GREEN AND IT IS EARNED: this file is with us, verified by read-back at the
 * other end. It is the same green a finished step wears, on the same terms -- an outcome, not a
 * state of progress. */
.upl__row.is-done { background: var(--done-bg); border-color: var(--done-edge); }
.upl__row.is-done .upl__state { color: var(--done-ink); font-weight: var(--w-semi); }
.upl__row.is-done .upl__fill { background: var(--done-ink); }

/* AND A FAILURE IS NOT RED, because there is no red in this file and a client whose signal
 * dropped has not done anything wrong (§3 of the header). It is the ordinary panel with a
 * sentence and a way forward -- which is the same treatment the one failure screen in this
 * project gets. */
.upl__row.is-failed { border-color: var(--line-strong); }
.upl__row.is-failed .upl__state { color: var(--ink-dim); }
.upl__retry { margin-top: var(--gap-3); }

.btn--small { min-height: 44px; padding: 0 var(--gap-4); font-size: var(--t-sm); }

/* ██ AND NOTHING REPAINTS A DISABLED COMMITTING BUTTON ANY MORE ██
 *
 * There used to be a rule here giving `.btn--cta:disabled` the quiet palette, written for the
 * pre-flight gate that disabled this button until every picture had a reaction. §29 removed that
 * gate -- and removing it exposed what the rule had ALSO been doing all along.
 *
 * `working()` DISABLES THE BUTTON TOO. So `.btn--cta:disabled` was firing during "Finishing…",
 * where it beat `.btn.is-working` on source order and painted the committing button --panel-2 with
 * --ink-mute text. Measured on the deployed site, mid-press:
 *
 *     SAVE AND FINALIZE   rgb(221, 107, 0)   white
 *     FINISHING…          rgb(241, 244, 249) rgb(90, 102, 117)   <- grey, at the one moment it is
 *                                                                   actually doing the thing
 *
 * A button that goes grey the instant it is pressed reads as a control switching itself off, which
 * is the opposite of what the working state exists to say. The gate is gone, nothing else here
 * disables this button on purpose, and the working state keeps its colour like every other button
 * in the project. */

/* ---- WIDER SCREENS ---------------------------------------------------------------------------
 *
 * Twelve lines, like every other adaptation in this file. THE PICTURES STAY ONE COLUMN -- see the
 * header of this section; the extra width goes into making each one bigger, which is the whole
 * point of having it. */
@media (min-width: 720px) {
  .sect          { margin-bottom: var(--gap-7); }
  .sect__label   { font-size: var(--t-xl); }
  .build + .build { margin-top: var(--gap-7); }
  .swatch__block { width: 72px; height: 72px; }
  .drop__pick    { padding: var(--gap-7) var(--gap-5); }
  .tile          { font-size: var(--t-base); }
}

/* ==========================================================================================
   15  A REFUSED FINALIZE -- the objection, the marks, and the running count
   ==========================================================================================

   Gerek pressed Save and finalize with unfinished sections and nothing appeared to happen. The
   Worker refused correctly and its sentence went into the small grey line in the corner of the
   bar -- the line §16.2 had already established a client does not read, in the session that moved
   save feedback onto the button for exactly that reason.

   EVERYTHING BELOW IS AMBER AND NONE OF IT IS RED. See the --amber block in the token section:
   there is still no red in this file, the sacred green is untouched, and the progress strip stays
   blue because it is measuring progress and not reporting a fault.
   ========================================================================================== */

/* ---- THE MARK ON A CONTROL -------------------------------------------------------------------
 *
 * The line is the same shape as `.q__steer` -- a wash, a rule down the side, and a sentence --
 * because the two are the same KIND of thing: the page telling a client something about the box
 * they are standing in. Different colour, different message, identical grammar. */
.needed {
  margin: var(--gap-3) 0 0;
  padding: var(--gap-2) var(--gap-3);
  background: var(--amber-wash);
  border-left: 4px solid var(--amber);
  border-radius: var(--r-sm);
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  font-weight: var(--w-semi);
  color: var(--amber-ink);       /* 6.97:1 on --amber-wash */
}

/* THE CONTAINER'S EDGE GOES AMBER TOO, so the mark is findable while scrolling past at speed --
 * one line of text inside a tall card is easy to scroll straight over.
 *
 * `inset` box-shadow FOR THE RULE, NOT A THICKER BORDER. Growing `border-left-width` from 1px to
 * 4px moves everything inside the card 3px right at the moment the mark appears, which on a page
 * that has just scrolled itself somewhere is one movement too many. An inset shadow is painted
 * inside the existing box and shifts nothing. */
.is-needed { border-color: var(--amber); }
.q.is-needed { box-shadow: inset 4px 0 0 0 var(--amber), var(--shadow-card); }

/* ⚠ THERE IS NO MARK FOR A BUILD IN THE GALLERY, AND THERE MUST NOT BE.
 *
 * The rule this replaced put an amber edge on a style card's frame, because the old card question
 * required a reaction to every picture and a refused finalize marked the ones that were missed.
 * THE GALLERY CANNOT BE REQUIRED -- zero picks is a recorded answer (itv-core §148) -- so it can
 * never appear in a `missing` list, and a rule that styled a state nothing can reach would be a
 * promise the page cannot keep. `.is-needed` still lands on question cards, which is every block
 * that can actually be owed. */

/* ---- THE BUTTON'S OBJECTION -------------------------------------------------------------------
 *
 * ██ THE REFUSED LABEL DROPS TO THE BODY FACE, AND THAT IS A RULE BEING FOLLOWED ██
 *
 * `--font-display` is documented in the token block as never being used for a sentence -- it is
 * an all-caps condensed face for a word or three. "SAVE AND FINALIZE" is a command; "Almost -- a
 * few things left" is a sentence, and setting it in Bebas Neue at 24px would be both wrong by that
 * rule and too wide for the button at 375px.
 *
 * AND IT IS LEGAL TO DO, WHICH IT WOULD NOT HAVE BEEN ON THE ORANGE. White on --brand-orange is
 * 3.40:1 and clears ONLY the large-text floor, so that fill is locked to 24px display type
 * (§25.2). White on --amber is 4.69:1, which clears the 4.5:1 floor for text at ANY size -- so the
 * amber state is free to be a sentence in the reading face, and the size here is a typographic
 * choice rather than a load-bearing one. check.js measures both and knows the difference. */
.btn.is-refused,
.btn--cta.is-refused {
  background: var(--amber);
  border-color: var(--amber);
  color: var(--on-brand);          /* white -- 4.69:1 on --amber, clears AA at any size */
  font-family: var(--font);
  font-size: var(--t-md);
  font-weight: var(--w-semi);
  letter-spacing: normal;
  text-transform: none;
  opacity: 1;
}
.btn.is-refused:hover,
.btn--cta.is-refused:hover { background: var(--amber); }

/* Entered instantly, for the same reason the green is (§16.2's measured note): with the hover
   transition running, the fill spends 120ms crossing from the orange while the label is already
   the new one, and the client reads a button mid-costume-change. */
.btn.is-refused { transition: none; }

/* ---- THE RUNNING COUNT, ABOVE THE BAR ---------------------------------------------------------
 *
 * It lives INSIDE the fixed bar rather than beside it, so it cannot drift out of alignment with
 * the buttons it is about and needs no second measurement of the bar's height to sit on top of it.
 * The bar is a column on a phone, so "first child" is already "above". */
.bar__left {
  margin: 0;
  padding: var(--gap-2) var(--gap-3);
  background: var(--amber-wash);
  border-left: 4px solid var(--amber);
  border-radius: var(--r-sm);
  font-size: var(--t-xxs);
  line-height: 18px;
  font-weight: var(--w-semi);
  color: var(--amber-ink);         /* 6.97:1 on --amber-wash */
}

/* The bar grows by one line when the count is up, so the page has to give back the same height or
   the last card sits underneath it. Keyed off a class the guard sets on <body>, because the bar is
   fixed and the padding is on a different element entirely. */
body.has-left:has(.bar) .wrap { padding-bottom: 196px; }

@media (min-width: 720px) {
  /* THE BAR IS A ROW UP HERE, so the count needs a line of its own -- `wrap` plus a full-width
     child puts it above the buttons at every width rather than beside them at some of them. */
  .bar        { flex-wrap: wrap; }
  .bar__left  { width: 100%; text-align: right; }
  body.has-left:has(.bar) .wrap { padding-bottom: 168px; }
}

/* ==========================================================================================
   16  A FINISHED STEP, AT A GLANCE
   ==========================================================================================

   The chip said Done and nothing else did -- one small word per card, on the one screen whose
   entire job is answering "where am I up to". Gerek's ruling: the whole card goes green.

   ██ NOT ONE NEW GREEN. ██ Every value below is a token this file already had, and both of them
   came from the family: --done-bg is itv-tokens' opaque green wash and --done-ink is the sacred
   green itself (§25.4). Inventing a lighter green for a card would have put a fourth green in a
   project whose rule is that there is exactly one.

   MEASURED, and the numbers are why the border is not decoration:

       --ink on --done-bg        15.55:1   the step's name, which stays ink and stays readable
       --ink-mute on --done-bg    5.12:1   the chip row beneath it
       white on --done-ink        5.34:1   the tick in the badge
       --done-ink on --done-bg    4.68:1   the border, and the chip's border on the card
       --done-bg vs --bg          1.07:1   ██ THE WASH ALONE IS ALMOST INVISIBLE AGAINST THE PAGE ██

   That last number is the load-bearing one. A soft wash on a light ground is a whisper, so the
   STRONG BORDER is what actually says "finished" -- 5.02:1 against the page. A version of this
   with the wash alone would look fine on a designer's white screen and vanish on a phone held at
   arm's length in daylight.
   ========================================================================================== */

.step-card.is-done {
  background: var(--done-bg);
  /* 1.5px, not 1px: it is carrying the message now, and a hairline in a mid-tone green reads as a
     rendering artefact at the sub-pixel widths a phone actually draws. */
  border: 1.5px solid var(--done-ink);
}

/* The hover keeps the green rather than reverting to the neutral edge -- the ordinary rule sets
   `--line-strong`, which on a green card would look like the card losing its state under the
   mouse. Still a real hover: the lift changes. */
.step-card.is-done:hover { border-color: var(--done-ink); box-shadow: var(--shadow-lift); }

/* THE BADGE: the ordering number becomes a tick, and the quiet grey disc becomes the sacred
   green. White on it is 5.34:1. */
.step-card.is-done .step-num {
  background: var(--done-ink);
  color: var(--on-brand);
  /* The tick is a glyph in a 36px disc and wants to sit optically centred rather than on the
     baseline the digits were using. */
  font-size: var(--t-lg);
  line-height: 1;
}

/* THE NAME STAYS INK. Tinting it green would be the card saying its status twice and reading
   worse for it -- 15.55:1 is the best contrast on this screen and there is no reason to spend it. */

/* ██ THE CHIP WOULD OTHERWISE DISAPPEAR INTO THE CARD ██
 *
 * `.chip--done` is --done-ink on --done-bg, which is exactly what the card now is: the chip would
 * be the same colour as the thing it sits on, with a 1.16:1 edge. So on a finished card it takes
 * the panel as its ground -- a white badge on green, which is more legible than it was on white
 * and keeps the green border that ties it to the card. Text 5.34:1, border 4.68:1. */
.step-card.is-done .chip--done {
  background: var(--panel);
  border-color: var(--done-ink);
}

/* The all-finished sentence. Still the lede's size and rhythm -- it is not a banner and it is not
   a second thank-you; it is the same line saying the last true thing it has to say. */
.lede--done { color: var(--done-ink); font-weight: var(--w-semi); }

/* ==========================================================================================
   THE ALL-DONE MOMENT -- the only decorative motion on this site
   ==========================================================================================
   Two and a half seconds over the sentence that says the engagement is finished, once per
   client, then removed from the DOM. See js/confetti.js for why it is thirty-six divs and a
   keyframe rather than a canvas and a loop.

   ██ pointer-events: none IS LOAD-BEARING, NOT TIDINESS. ██ This layer covers the whole
   checklist for two and a half seconds. Without it, a client who taps their first step card the
   moment the page lands taps confetti instead, and the page does nothing -- which reads as an
   app that has frozen at exactly the moment it is trying to look pleased with itself.

   AND THE JS SKIPS IT ENTIRELY UNDER prefers-reduced-motion. The global reset at the top of this
   file would switch the animation off and leave thirty-six pieces sitting frozen over the page
   for ever, because the cleanup is a timer and the pieces would never move. The reset is the
   second line of defence; the first one is that nothing is created. */
.confetti-host { position: relative; }

.confetti {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;   /* above the cards, below the save bar's 20 and the scrim */
}

.confetti__bit {
  position: absolute;
  top: -24px;
  width: 8px;
  height: 14px;
  border-radius: 1px;
  animation-name: confetti-fall;
  animation-timing-function: cubic-bezier(.25, .6, .5, 1);
  animation-iteration-count: 1;
  animation-fill-mode: both;
}

/* ONE KEYFRAME, AND THE DRIFT IS IN IT RATHER THAN IN JS. A piece that only falls reads as rain;
   the sideways nudge and the tumble are what make it confetti. `translate3d` keeps the whole
   thing on the compositor, which is why thirty-six of them cost nothing on a phone. */
@keyframes confetti-fall {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translate3d(14px, 320px, 0) rotate(540deg); opacity: 0; }
}

/* ==========================================================================================
   17  STEP 2 -- the interview: a calm room, not a chat app
   ==========================================================================================

   Every convention a messaging app has, this page leaves out: no avatars, no timestamps, no read
   receipts, no reactions. What it keeps is the one thing the form factor is genuinely good at --
   you can see what you already said, and the answer arrives while you watch.

   ██ THE CONVERSATION SITS ON WHITE, AND THAT IS A MEASUREMENT RATHER THAN A LOOK ██

   The brief asked for panel-grey interviewer bubbles, and on this page's own ground that is
   `--panel-2` against `--bg`: **1.04:1**. Invisible. Not "subtle" -- the same trap §28.2 found on
   the finished step card, where a wash on a light ground turned out to be doing no work at all.

   So the transcript gets a white panel of its own and the bubbles sit on THAT, where a blue tint
   and a grey tint read as two different things:

       --brand-blue-pale on --panel   1.19:1     the client's own words
       --panel-3         on --panel   1.14:1     the interviewer's
       --ink on either                14:1+      which is what actually has to be readable

   The two tints are deliberately close in weight to each other and distinct in hue. Position does
   the heavy lifting -- theirs right, ours left -- and the colour is the confirmation.
   ========================================================================================== */

.chat {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
  margin-bottom: var(--gap-5);
  padding: var(--gap-4);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
}

.bub-row { display: flex; }
.bub-row--them { justify-content: flex-end; }
.bub-row--us   { justify-content: flex-start; }

.bub {
  max-width: 85%;
  padding: var(--gap-3) var(--gap-4);
  border-radius: var(--r-lg);
  font-size: var(--t-base);
  /* GENEROUS, because this is prose rather than a control. The client-facing 1.55 of the body,
     not the tighter leading a form label wants. */
  line-height: var(--lh-body);
  color: var(--ink);
  /* ██ THE LINE BREAKS, WITHOUT A MARKDOWN RENDERER ██
   *
   * Replies arrive with paragraph breaks in them and the obvious next step is "just render the
   * markdown". That would be a dependency this project does not have, an HTML parser it does not
   * want, and an injection surface on the one page whose content is model output. `pre-wrap` costs
   * one declaration, honours the breaks, and keeps the content text. See renderBubble(). */
  white-space: pre-wrap;
  /* A pasted URL is one unbroken word and must not push the page sideways. */
  overflow-wrap: anywhere;
}

.bub--them { background: var(--brand-blue-pale); }
.bub--us   { background: var(--panel-3); }

/* ---- THE WAIT ---------------------------------------------------------------------------------
 *
 * Three dots between the send and the first token. It is `aria-hidden` in the markup and the live
 * region says "One moment…" instead -- an animation is not information, and a screen reader
 * reading three bullets forever is worse than silence. */
.bub--thinking { display: flex; gap: 5px; align-items: center; min-height: 24px; }
.bub__dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  background: var(--ink-mute);
  opacity: .35;
  animation: bub-pulse 1.2s ease-in-out infinite;
}
.bub__dot:nth-child(2) { animation-delay: .18s; }
.bub__dot:nth-child(3) { animation-delay: .36s; }
@keyframes bub-pulse {
  0%, 60%, 100% { opacity: .28; }
  30%           { opacity: .85; }
}

/* ---- THE COMPOSER -----------------------------------------------------------------------------
 *
 * STICKY, NOT FIXED. It stays in thumb reach while the transcript scrolls behind it, and because
 * it is sticky rather than fixed it cannot cover the last bubble the way a fixed bar would -- the
 * page's own flow reserves its height. */
.composer {
  position: sticky;
  bottom: 0;
  padding: var(--gap-3) 0 var(--gap-2);
  background: var(--bg);
}

.composer__row { display: flex; gap: var(--gap-3); align-items: flex-end; }

.composer__box {
  flex: 1 1 auto;
  min-height: 52px;
  max-height: 200px;
  padding: var(--gap-3) var(--gap-4);
  background: var(--panel);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  font: inherit;
  /* 17px or larger, or iOS zooms the whole page on focus. The same floor every box in this
     project keeps, and on this page it is the box people use most. */
  font-size: var(--t-base);
  line-height: var(--lh-normal);
  color: var(--ink);
  resize: none;
  overflow-y: auto;
}
.composer__box:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* ██ BRAND BLUE, AND NOT THE ONE CALLED --brand-blue ██
 *
 * The brief said brand blue and measurement picked which one: white on `--brand-blue` (#2494e0)
 * is **3.29:1** and fails the 4.5:1 floor at this size. `--accent` is the brand's accent blue --
 * the same one a selected segment and a chosen tile already wear -- and white on it is **6.93:1**.
 *
 * IT IS NOT ORANGE, WHICH IS THE POINT OF THE RULE. Sending a message is not committing a step;
 * the orange belongs to the control that ends something, and on this page that is the confirm
 * inside the finish dialog. */
.btn--send {
  flex: 0 0 auto;
  min-height: 52px;
  padding: 0 var(--gap-5);
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.btn--send:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }

/* WHILE A REPLY IS STREAMING the whole composer goes quiet -- box and button together. Disabling
   only the button leaves a box whose Enter key silently does nothing, which is worse than a box
   that is plainly unavailable. */
.composer.is-busy .composer__box { background: var(--panel-2); color: var(--ink-dim); }
.btn--send:disabled { background: var(--panel-2); border-color: var(--line-strong); color: var(--ink-mute); cursor: default; }

.composer__steer { margin-top: var(--gap-2); }

/* ---- THE WAY OUT ------------------------------------------------------------------------------
 *
 * Quiet, and under the composer. Finishing is not what this page is for -- talking is -- so the
 * control that ends it does not compete with the one that continues it. */
.finish-row { display: flex; justify-content: center; padding-bottom: var(--gap-4); }

.chat-state {
  margin: 0 0 var(--gap-4);
  min-height: 18px;
  text-align: center;
  font-size: var(--t-xxs);
  line-height: 18px;
  color: var(--ink-mute);
}

/* ---- THE PAUSE --------------------------------------------------------------------------------
 *
 * ██ NOT ONE WORD ABOUT COST, EVER. ██ The cap is a number in our settings and the reason for it
 * is our business. What a client needs to know is that the conversation is complete enough and a
 * person is picking it up -- which is true, and is the whole message.
 *
 * AND IT IS NOT AMBER OR RED. Being paused is not a fault and not something they must fix; it is
 * the brand blue rule, the same one the deferral note wears, because both are the page telling
 * somebody calmly where they stand. */
.pause-note {
  margin-bottom: var(--gap-5);
  padding: var(--gap-4);
  background: var(--panel-2);
  border-left: 3px solid var(--brand-blue);
  border-radius: var(--r-sm);
}
.pause-note__lead { margin: 0 0 var(--gap-1); font-size: var(--t-base); font-weight: var(--w-semi); }
.pause-note__body { margin: 0; font-size: var(--t-sm); color: var(--ink-dim); }

/* ---- THE FINISHED INTERVIEW -------------------------------------------------------------------
 * The whole transcript, calmly. Not a summary and not a fold -- it is what they said, and this is
 * the one place they can re-read it. */
.chat--done { margin-top: var(--gap-5); }

@media (min-width: 720px) {
  .chat  { padding: var(--gap-5); gap: var(--gap-4); }
  .bub   { max-width: 78%; }
}
