/* Uskulpt design layer — the blue system, ported from Figma.
 *
 *   file  https://www.figma.com/design/1KIK0PPt2yKu4XC7eryVrk/USkult
 *   page  00 Design System
 *
 * The structure below is read from that file rather than invented here: 32
 * bound colour tokens, a 14-step type ramp, six radii, two shadows and one
 * spacing scale. The cream Clay theme this replaces is gone entirely —
 * including Fraunces, which was a stand-in for a typeface licensed to Clay and
 * has no place in this system.
 *
 * THE PALETTE CHANGED ON 2026-09-20, and this block is the record of it.
 *
 * Three colours were handed over as the new brand: #102240 deep navy,
 * #9DCAE3 sky, #3070B6 primary. The Figma frame `Colours Changes`
 * (node 330:75) is drawn in them, so the frame and the swatches agree and
 * the frame is the authority for how they are used. Two of the three appear
 * in it literally; the deep navy is the one value taken from the swatches
 * rather than the frame, which still carries #0f172a there. They are 1.06
 * apart in luminance, so nothing measurable moved and the swatch wins.
 *
 * Measured rather than assumed, the swap costs almost nothing where it
 * matters most: white on #3070B6 is 5.10:1 where white on #2563eb was
 * 5.17:1, so every place that sat close to the AA floor still sits the same
 * distance from it.
 *
 * One thing it does cost. #3070B6 is lighter than #2563eb, and as TEXT on
 * its own 10% tint it measures 4.48:1 — under the floor, where #2563eb
 * cleared it. So `--brand-blue-ink` is no longer the old #1e40af-shaped
 * "slightly darker brand"; it is #1F4E80, which measures 7.50:1 on the tint
 * and 8.54:1 on white. Every eyebrow, link and label on a tinted ground uses
 * it. The base blue is for fills and borders, which only need 3:1.
 *
 * `--brand-blue-rgb` is new and exists because the old palette could not
 * actually be swapped: twenty-seven rules across nine stylesheets had
 * `rgba(var(--brand-blue-rgb), x)` typed into them, so a token change would have left
 * the shadows, glows and focus rings on the previous brand. They all read
 * the channel token now.
 *
 * `00 Design System` is stale on colour and needs re-binding.
 *
 * Legacy class names (`display-md`, `card-pink`, `btn-secondary` …) are kept at
 * the bottom as aliases onto the new primitives, so every screen that has not
 * been rebuilt yet still wears the new design instead of half of each.
 */

/* ---------------------------------------------------------------- tokens --- */

:root {
  color-scheme: light;

  /* Brand — 01 · Colour. The three handed-over values are marked. */
  --brand-blue: #3070b6;          /* HANDED OVER. White on it: 5.10:1 */
  --brand-blue-hover: #285f9b;    /* white 6.56:1 */
  --brand-blue-600: #285f9b;
  --brand-blue-ink: #1f4e80;      /* text on a tint: 7.50:1. See the note above */
  --brand-blue-900: #102240;      /* HANDED OVER. White on it: 15.85:1 */
  --brand-blue-200: #9dcae3;      /* HANDED OVER. Navy on it: 9.05:1 */
  --brand-blue-100: #d8eaf4;      /* the sky at 40% on white — the panel colour */

  /* The primary as channels, so a rule that needs it at an alpha can reach it
     without typing the value. Every rgba() that used to carry the old brand
     reads this. */
  --brand-blue-rgb: 48, 112, 182;
  --brand-sky-rgb: 157, 202, 227;
  --brand-navy-rgb: 16, 34, 64;

  /* Surface. The ground is the sky at a low alpha over white rather than the
     old blue-tinted white — same job, new hue. */
  --surface-ground: #f2f8fc;
  --surface-card: #ffffff;
  --surface-sunk: #e7edf2;
  --surface-mock: #f5f9fc;
  --surface-tint: #eaf1f8;        /* the primary at 10% on white */
  --surface-deep: #102240;

  /* Text */
  --text-ink: #101828;
  --text-body: #475467;
  --text-muted: #606d83;
  --text-on-blue: #ffffff;
  --text-on-deep: #ffffff;
  --text-deep-body: #9dcae3;      /* 9.05:1 on the deep navy */

  /* Line */
  --border-line: rgba(100, 116, 139, 0.18);
  --border-line-soft: rgba(100, 116, 139, 0.11);

  /* Status */
  --status-green: #0e9f6e;
  --status-green-tint: #e6f6f0;
  --status-amber: #b54708;
  --status-amber-tint: #fef0c7;

  /* Category accents */
  --accent-violet: #7a5af8;
  --accent-violet-tint: #f4f3ff;
  --accent-rose: #e31b54;
  --accent-rose-tint: #fff1f3;
  --accent-teal: #0e9384;
  --accent-teal-tint: #e6f7f5;

  /* Text on an accent tint. The base accents are 3.03-4.20:1 on their own
     tints and fail AA as text; these clear 4.5:1 at the same hue. Borders and
     fills keep the base tokens, which only need 3:1. */
  --accent-violet-ink: #7250f8;
  --accent-rose-ink: #da1a51;
  --accent-teal-ink: #0c7d70;
  --status-green-ink: #0b7e57;

  /* 06 · Radius — six, and no others */
  --r-sm: 8px;    /* small tiles, time pills */
  --r-md: 12px;   /* inputs, inner rows */
  --r-lg: 14px;   /* nested surfaces */
  /* 22px, not the 18px this line used to claim. The legacy block below
     redeclared `--r-xl` in the same `:root` with a different number, and being
     later it won - so every card in the product has been drawn at 22px while
     the design system said 18. The shipped value is the real one; what was
     wrong was the documentation and the second declaration. Declared once now.
     Numerically equal to `--r-2xl` at present; the two are kept apart because
     what they name is different, and the card radius is free to move without
     taking every hero panel with it. */
  --r-xl: 22px;   /* every card */
  --r-2xl: 22px;  /* big panels and heroes */
  --r-pill: 999px;

  /* 06 · Shadow — two, plus the blue glow and the focus ring
     On the brand navy since 2026-09-20. These two were the last literals the
     palette sweep missed: `rgba(16, 24, 40)` is #101828, the slate the old
     blue system shadowed with, and against a #f2f8fc page it reads grey where
     everything else now reads navy. */
  --shadow-card: 0 14px 34px rgba(var(--brand-navy-rgb), 0.07), 0 1px 2px rgba(var(--brand-navy-rgb), 0.05);
  --shadow-lift: 0 24px 52px rgba(var(--brand-navy-rgb), 0.11), 0 2px 4px rgba(var(--brand-navy-rgb), 0.05);
  --shadow-glow: 0 6px 16px rgba(var(--brand-blue-rgb), 0.26);
  --shadow-glow-strong: 0 6px 16px rgba(var(--brand-blue-rgb), 0.34);
  /* 0.9, not the old 0.8. #3070B6 is the lighter brand, so the ring needed the
     alpha back to stay as findable as it was. */
  --focus-ring: 0 0 0 3px rgba(var(--brand-blue-rgb), 0.9);
  /* Button edges only. --border-line (8% ink) is 1.17:1 and cannot identify a
     control; 48% reaches 3.20:1 against a white fill. */
  --border-control: rgba(16, 24, 40, 0.48);

  /* 06 · Spacing — even numbers only; 10 / 14 / 20 / 24 do most of the work */
  --s-10: 10px;
  --s-14: 14px;
  --s-20: 20px;
  --s-24: 24px;
  --s-32: 32px;   /* page padding in the app */
  --s-52: 52px;   /* between sections in the app */
  --s-88: 88px;   /* between sections on the landing */
  --s-120: 120px; /* landing side margin */

  /* Landing atmosphere — 01 Landing only.
     `.lp-*` and `.t-display` appear in public/landing.html and nowhere else,
     so everything keyed off them stops at the public page and never reaches
     the signed-in app. The base is the blue-tinted white the redesign brief
     asks for; #f7faff (--surface-ground) is the app's own ground and stays the
     app's ground.

     The three glows are all under 14% alpha. That is the ceiling at which a
     wash still reads as light rather than as a coloured shape, and it is what
     keeps the background from competing with 800-weight display type. */
  --atmos-base: #ebf4f9;   /* the frame's page wash: the sky at 20% on white */
  --atmos-tint: #ddedf6;

  /* The floating bar's full outer height: 14px + 64px + 14px. The hero reads
     it to pull its background up behind the bar. */
  --nav-h: 92px;

  /* The footer, which is the only dark surface on the public page now that the
     ending is light. #102240 is the handed-over deep navy; it works here
     precisely because the section above it is no longer a navy panel - those
     two measured 1.15 apart and read as one block. The three text tokens
     measure 9.92:1, 7.61:1 and 5.44:1 on it. */
  --surface-footer: #102240;
  --footer-head: #93b4f5;
  --footer-link: #b9cdf7;
  --footer-dim: #8098c4;

  /* Depth for the hero preview.
     Every layer is tinted blue, never ink. Grey shadow on a blue ground is
     what reads as dirt under the card. And all three are kept light: on a
     near-white ground a card only needs enough shadow to prove it is a
     separate surface - past that it turns into a dark halo, which is exactly
     what the first pass did. */
  --shadow-hero: 0 1px 2px rgba(var(--brand-blue-rgb), 0.04),
                 0 8px 18px -6px rgba(var(--brand-blue-rgb), 0.07),
                 0 28px 48px -28px rgba(var(--brand-blue-rgb), 0.16);
  --shadow-float: 0 2px 6px rgba(var(--brand-blue-rgb), 0.05), 0 6px 14px -6px rgba(var(--brand-blue-rgb), 0.07);

  /* Motion — the one group below not read from the Figma file, which specifies
     no timings. These are the nine durations and three curves this stylesheet
     had already invented at the point of use, consolidated into three and two.
     --mo-ease at --mo-enter is roughly where a spring with a slight bounce
     settles, which is the entire vocabulary a page this quiet needs. */
  /* Five durations by purpose, 2026-09-17. Read the purpose, not the number:
       fast      micro feedback - a tap, a hover, a focus ring
       standard  a UI change - a tab, a chip, an accordion, a nav state
       medium    a surface arriving - a modal, a sheet, a drawer
       slow      a section revealing itself as you scroll to it
       story     one beat of a narrative animation, the hero's spine filling in
     The three older names below are kept as aliases so nothing already written
     against them moves. */
  --mo-fast: 150ms;
  --mo-standard: 260ms;
  --mo-medium: 420ms;
  --mo-slow: 640ms;
  --mo-story: 1200ms;
  --mo-quick: var(--mo-fast);
  --mo-enter: var(--mo-medium);
  --mo-hero: 800ms;   /* the landing headline, once per page load */
  --mo-ease: cubic-bezier(.22, 1, .36, 1);
  /* Softened from (.34, 1.56, .64, 1). A 56% overshoot is a bounce; this is a
     settle. Still used for the two Discovery celebrations and the "you are
     here" pill, which are the only places anything should spring at all. */
  --mo-spring: cubic-bezier(.34, 1.3, .64, 1);

  /* 02 · Typography — ONE family since 2026-09-20: Google Sans Flex.
   *
   * The frame `Colours Changes` (330:75) is set in it end to end, every text
   * node, so the two-family split the previous system drew — Manrope for
   * headings, Inter for paragraphs — has nothing left to stand on. Both
   * tokens point at the same face and both names are kept, because 40-odd
   * rules across nine stylesheets say "display" or "body" to mean a role,
   * and that distinction is still real even when one family serves both.
   * It is now carried by weight and optical size rather than by family.
   *
   * It is a variable font: one woff2, `font-weight: 1 1000`, with wdth, GRAD,
   * ROND and opsz axes. The frame pins GRAD 0, ROND 0, wdth 100 on every
   * node, which is the neutral position of all three — so that is what
   * `--font-axes` says, and a rule that wants a different grade sets its own.
   *
   * The weights the frame actually uses are 400 Regular, 500 Medium and 600
   * SemiBold. Nothing in it is 700 or 800. The ramp below follows it.
   */
  --font-display: "Google Sans Flex", "Google Sans", -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-body: "Google Sans Flex", "Google Sans", -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-axes: "GRAD" 0, "ROND" 0, "wdth" 100;

  /* Was Inter's character-variant set. Google Sans Flex has no cv01/cv05/
     cv09/cv11/ss03, so asking for them is asking a font for features it does
     not carry — harmless, and untrue. The one feature worth turning on is
     lining tabular figures wherever numbers are compared in a column; that is
     set at the point of use, not globally. */
  --font-features: normal;

  /* Legacy token names, still referenced by inline styles on screens that have
     not been rebuilt against the components above. They resolve to the blue
     system, not the cream one — a name kept, a value replaced. Nothing new
     should use them, and the list only shrinks: fifteen of the original
     twenty-five went on 2026-09-11 once three sessions had rebuilt the screens
     holding their last references. Verified by searching app.css, every
     template and app.js with comments stripped, so an explanatory mention of a
     token could not be mistaken for a use of it.

     Three radius tokens used to sit in this list and did not belong in it:
     `--r-md`, `--r-lg` and `--r-xl` are current design-system names, not
     legacy ones. Two of the three restated their own values and did nothing;
     the third restated `--r-xl` as a different number and silently beat the
     definition above. */
  --body: var(--text-body);
  --muted: var(--text-muted);
  --hairline-soft: var(--border-line-soft);
  --error: var(--accent-rose);

}

/* ------------------------------------------------------------------ base --- */

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

body {
  margin: 0;
  background: var(--surface-ground);
  color: var(--text-body);
  font-family: var(--font-body);
  font-size: 14.5px;
  line-height: 1.58;
  letter-spacing: -0.05px;
  font-feature-settings: var(--font-features);
  font-variation-settings: var(--font-axes);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::selection { background: var(--brand-blue-100); color: var(--text-ink); }

a { color: inherit; text-decoration: none; }

/* One focus treatment for the whole product: the 3px blue ring. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* ------------------------------------------------------- 02 · type ramp --- */

/* 600, not 800, since the 2026-09-20 recolour.
 *
 * Nothing in frame 330:75 is heavier than SemiBold — its four section heads,
 * its eight card titles and its display headline are all 600, and the frame
 * is set in one family rather than the two this ramp was built for. Manrope
 * 800 was carrying the job of "this is a heading" on its own; Google Sans Flex
 * carries it on size and its much larger cap height instead, and at 800 the
 * same headline reads as shouting.
 *
 * `--font-axes` on body already pins GRAD 0 / ROND 0 / wdth 100. Weight is a
 * separate axis and is set here, so the two do not fight. */
.t-display, .t-h1, .t-h2, .t-h3, .t-h4, .t-h5, .t-eyebrow, .t-value {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--text-ink);
  margin: 0;
}

/* 4.5vw lands at 58px on the 1280 frame, which is what the frame draws, and
   falls to 34px on a phone. Tracking is in em so it stays proportional
   instead of over-tightening the small end, where the frame's -2px would
   close the letters up.
   
   Leading is 1.27, up from 1.06, and it is the same optical result rather
   than a looser one: the frame sets 74px on 58px, and Google Sans Flex has a
   cap height near 0.72em where Manrope sits near 0.72em but with far more
   built-in line gap. Measured off the frame's own render, the two headline
   lines sit 75px apart. */
.t-display { font-size: clamp(34px, 4.5vw, 62px); line-height: 1.16;
  letter-spacing: -0.034em; }
.t-h1      { font-size: 48px;   line-height: 1.1;  letter-spacing: -1.95px; }
.t-h2      { font-size: 30px;   line-height: 1.16; letter-spacing: -1px; }
.t-h3      { font-size: 25px;   line-height: 1.24; letter-spacing: -0.7px; }
.t-h4      { font-size: 20px;   line-height: 1.26; letter-spacing: -0.5px; }
.t-h5      { font-size: 16px;   line-height: 1.28; letter-spacing: -0.4px; }

/* 13px and sentence case, not 11.5px uppercase.
 *
 * The frame's eyebrows are 13px Medium in sentence case inside a tinted pill
 * — "Career + College discovery, built around you" — and the 11-11.5px
 * uppercase eyebrow has been an open accessibility item since 2026-09-12
 * (27 text nodes on the landing, 12 on Home). The recolour is the moment to
 * close it, because every one of them had to be restyled anyway.
 *
 * `text-transform` is left on for the surfaces that still use it as a rail
 * label; `.t-eyebrow-plain` turns it off for the frame's pill form. */
.t-eyebrow {
  font-size: 13px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--brand-blue-ink);
}

/* The frame's eyebrow: sentence case, no tracking, on a tint.
   #3070B6 measures 4.48:1 on its own 10% tint and cannot carry 13px text, so
   this is --brand-blue-ink at 7.50:1. Recorded as a departure from the frame,
   which sets the base blue here. */
.t-eyebrow-plain {
  text-transform: none;
  letter-spacing: -0.2px;
  color: var(--brand-blue-ink);
}

.t-lead, .t-body, .t-small, .t-caption {
  font-family: var(--font-body);
  font-weight: 400;
  color: var(--text-ink);
  margin: 0;
}

/* The frame's paragraph weight is 500, not 400: its 20px hero sub, its 18px
   for-parents copy and its 14px card copy are all Medium. On Google Sans Flex
   at these sizes 400 reads thin against a 600 heading, which is what the
   frame is compensating for. */
.t-lead    { font-size: 18px;   line-height: 1.6;  letter-spacing: -0.2px; font-weight: 500; }
.t-body    { font-size: 14.5px; line-height: 1.58; letter-spacing: -0.05px; }
.t-small   { font-size: 13px;   line-height: 1.5;  letter-spacing: -0.1px; }
.t-caption { font-size: 12px;   line-height: 1.48; letter-spacing: 0; }

/* The hero's own sub, which the frame sets larger than a lead: 20px Medium
   with 38px leading. */
.t-lead-lg { font-size: clamp(16.5px, 1.6vw, 20px); line-height: 1.9;
  letter-spacing: -0.25px; font-weight: 500; }

/* Colour modifiers. Hierarchy comes from size and weight; these only set the
   ink, never the scale. */
.t-body-muted  { color: var(--text-body); }
.t-muted       { color: var(--text-muted); }
.t-on-deep     { color: var(--text-on-deep); }
.t-deep-body   { color: var(--text-deep-body); }
.t-blue        { color: var(--brand-blue); }
.t-blue-ink    { color: var(--brand-blue-ink); }

/* Headlines break on meaning, not on the container edge. */
.t-display, .t-h1, .t-h2 { text-wrap: balance; }

/* --------------------------------------------------------- 03 · buttons --- */

/* The shared shape. Every style below is also listed here so that legacy
   markup writing `class="btn-primary"` on its own still gets a button rather
   than a coloured rectangle. */
.btn,
.btn-primary,
.btn-ghost,
.btn-tint,
.btn-white,
.btn-outline-deep,
.btn-danger,
.btn-secondary,
.btn-on-color {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  padding: 13px 22px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 14px;
  line-height: 1;
  letter-spacing: -0.15px;
  white-space: nowrap;
  cursor: pointer;
  transition: transform var(--mo-quick) var(--mo-ease), background-color var(--mo-quick) var(--mo-ease), box-shadow var(--mo-quick) var(--mo-ease);
}

.btn-lg { padding: 15px 22px; font-size: 15px; }
.btn-sm { padding: 11px 18px; font-size: 13.5px; }

.btn .ico { width: 15px; height: 15px; }
.btn-lg .ico { width: 15px; height: 15px; }
.btn-sm .ico { width: 13.5px; height: 13.5px; }

/* Primary — the only filled one. At most one per screen. */
.btn-primary {
  background: var(--brand-blue);
  color: var(--text-on-blue);
  box-shadow: var(--shadow-glow);
}
.btn-primary:hover { background: var(--brand-blue-hover); transform: translateY(-1px); }
.btn-primary:active { background: var(--brand-blue-600); box-shadow: none; transform: none; }

/* Ghost — white fill, 8% ink border. The default secondary. */
.btn-ghost {
  background: var(--surface-card);
  border-color: var(--border-control);
  color: var(--text-ink);
}
.btn-ghost:hover { border-color: var(--brand-blue-200); transform: translateY(-1px); }

/* White — only on surface/deep. Never brand blue there; it disappears. */
.btn-white {
  background: var(--surface-card);
  color: var(--brand-blue-ink);
}
.btn-white:hover { transform: translateY(-1px); }

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: .4;
  pointer-events: none;
}

.btn-block { width: 100%; }

/* A text action that is not a button. */
.link {
  color: var(--brand-blue);
  font-weight: 600;
}
.link:hover { color: var(--brand-blue-ink); text-decoration: underline; }

/* ------------------------------------- 05 · tags, chips and pills --------- */

/* Three different jobs, so they look different: a tag labels, a chip filters,
   a pill carries a value. */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--brand-blue);
  background: var(--surface-card);
  color: var(--brand-blue);
  border-radius: var(--r-pill);
  padding: 5px 11px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12px;
  line-height: 1;
  letter-spacing: -0.12px;
  white-space: nowrap;
}
.tag .ico { width: 11px; height: 11px; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border-line);
  background: var(--surface-card);
  color: var(--text-body);
  border-radius: var(--r-pill);
  padding: 6px 10px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 11.5px;
  line-height: 1;
  letter-spacing: -0.05px;
  white-space: normal;
  max-width: 100%;
  line-height: 1.3;
  cursor: pointer;
  transition: background-color var(--mo-quick) var(--mo-ease), border-color var(--mo-quick) var(--mo-ease);
}
.chip:hover { border-color: var(--brand-blue-200); }
.chip[aria-pressed="true"], .chip-on {
  background: var(--surface-tint);
  border-color: var(--brand-blue);
  color: var(--brand-blue-ink);
}
.chip .ico { width: 11px; height: 11px; }

/* ---------------------------------------------------------- 04 · cards --- */

/* Flat at rest since 2026-09-20, which is the planner's card and now every
   card. It had a resting `--shadow-card` as well as the border, so a page of
   them read as a pile of floating things; the planner draws the same surface
   with a 1px line and nothing under it, and that is the one Naveen kept.

   The shadow is not gone, it moved to the interactions: `.card-lift:hover`
   still lifts, and every other `--shadow-card` in the build is already a
   hover or a current state. A shadow that is always on says nothing. */
.card {
  background: var(--surface-card);
  border: 1px solid var(--border-line);
  border-radius: var(--r-xl);
  padding: var(--s-24);
}

/* The big panel — heroes, bands, the deep navy call to action. */
.panel {
  border-radius: var(--r-2xl);
  padding: 32px;
}

/* Tailwind compatibility shim.
 *
 * `cdn.tailwindcss.com` used to be loaded in base.html - a ~400KB in-browser
 * JIT compiler that Tailwind documents as development-only, render-blocking on
 * a product whose audience PRODUCT.md describes as "mostly on a phone". It is
 * gone. These are the utilities the templates still use, at Tailwind v3's
 * exact values, so removing the script changed no layout.
 *
 * The first version of this block defined four of them. That was wrong: it
 * came from grepping for four names rather than enumerating every class in
 * every template against every selector in this file, and it left Roadmap,
 * Targets, the agenda, Journey, consent and the error pages laying out with
 * roughly fifty undefined classes. The list below is generated from that
 * audit, plus the handful a parallel session flagged as in flight.
 *
 * This is a shim, not part of the design system. Nothing new should use it -
 * use the tokens and components above. It exists so the templates already
 * written keep working without a sweep through all of them at once, and it
 * should shrink to nothing as those screens are rewritten.
 *
 * Tailwind's preflight is gone with the CDN, so `border-t` carries an explicit
 * style and colour; under the play CDN it inherited both from preflight.
 */

/* Skip link. Off-screen until focused, then a real button at the top left. */
.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  z-index: 200;
  padding: 11px 16px;
  border-radius: var(--r-md);
  background: var(--brand-blue);
  color: var(--text-on-blue);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 13.5px;
  transition: top var(--mo-quick) var(--mo-ease);
}
.skip-link:focus { top: 12px; }

/* Preflight's `[hidden]` rule, restored - and the reason this one carries
 * `!important`.
 *
 * Tailwind's play CDN injected its stylesheet at runtime, which put preflight
 * *after* app.css in the cascade, so its plain `[hidden] { display: none }`
 * beat any class rule of equal specificity. Removing the CDN removed that, and
 * `.ecotab-panel { display: grid }` immediately won against the `hidden`
 * attribute the tab script sets - all five ecosystem panels rendered stacked on
 * the landing page. Six places in app.js toggle `hidden`; a plain rule here
 * would still lose to every one of them that owns a `display`, so this is the
 * one place in the file where `!important` is the correct tool rather than a
 * shortcut. The attribute means "not rendered", and nothing should outrank it.
 */
[hidden] { display: none !important; }

/* display */
.flex         { display: flex; }
.grid         { display: grid; }

/* ----------------------------------------------- 04 · the page header ----- */

/* The top of a page: an optional eyebrow, the title, an optional lead.
 *
 * It exists because the same role was being drawn six ways. The page title
 * rendered at 25, 30, 36 or 46px depending on which screen you were on, through
 * six different classes - one of them a legacy alias living in discovery.css,
 * on a public page. A student moving between Explore, Roadmap and Journey got a
 * different answer to "where does this page begin" each time.
 *
 * The size is fluid for the reason `t-display` is: 46px is right on a desktop
 * and wrong on a 390px phone, where Journey's title was three lines and 152px
 * tall before any content. The floor is 30px rather than something smaller,
 * because a page title that shrinks to body size stops being a page title.
 *
 *   390px   30px
 *   834px   38.7px
 *   1440px  46px
 *
 * Not every heading in the product is one of these. The auth card keeps its own
 * treatment because it sits inside a card rather than at the top of a page, the
 * documentation pages keep theirs because three of them already agree, and Home
 * keeps its greeting, which is a greeting and not a title.
 */
.page-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(30px, 2vw + 22px, 46px);
  line-height: 1.1;
  letter-spacing: -0.03em;
  color: var(--text-ink);
  margin: 0;
  text-wrap: balance;
}

/* Spacing is set on the parts rather than as a gap on the header, so a page
   that puts something else in there - Explore's tabs - keeps its own rhythm
   for that part without having to undo this one. */
.page-head > .page-title { margin-top: var(--s-10); }

/* ---------------------------------------------- 05 · inputs and controls --- */

.input, select.input, textarea.input {
  width: 100%;
  background: var(--surface-card);
  border: 1px solid var(--border-line);
  border-radius: var(--r-md);
  padding: 14px;
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.3;
  color: var(--text-ink);
  transition: border-color var(--mo-quick) var(--mo-ease), box-shadow var(--mo-quick) var(--mo-ease);
}
.input::placeholder { color: var(--text-muted); }
.input:focus {
  outline: none;
  border: 1.5px solid var(--brand-blue);
  box-shadow: var(--focus-ring);
}
select.input {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5' stroke='%23606d83' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  padding-right: 36px;
}

.field-label {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: -0.1px;
  color: var(--text-ink);
  margin-bottom: 9px;
}
.field-help { margin-top: 6px; color: var(--text-muted); }
.field-error { margin-top: 6px; color: var(--accent-rose-ink); }
.input-invalid, .input-invalid:focus { border-color: var(--accent-rose); }

/* An input with an icon sitting inside it. */
.input-wrap { position: relative; }
.input-wrap .ico {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: var(--text-muted);
  pointer-events: none;
}
/* Room for the icon — and ONLY where there is an icon.
   `.input-wrap` is also the wrapper the show/hide control uses, which has no
   icon in it, so this rule was indenting every password field in the product
   by 41px against a label sitting at 16. `:has()` asks the real question
   rather than adding a second class that callers would have to remember. */
.input-wrap:has(> .ico) .input { padding-left: 41px; }

/* Checkbox, radio, toggle and the task tick — drawn, not the browser's. */
.check {
  appearance: none;
  flex: none;
  width: 20px;
  height: 20px;
  border: 1.5px solid var(--border-line);
  border-radius: 6px;
  background: var(--surface-card);
  cursor: pointer;
  display: inline-grid;
  place-content: center;
  transition: background-color var(--mo-quick) var(--mo-ease), border-color var(--mo-quick) var(--mo-ease);
}
.check::after {
  content: "";
  width: 12px;
  height: 12px;
  background: var(--text-on-blue);
  opacity: 0;
}
.check:checked { background: var(--brand-blue); border-color: var(--brand-blue); }
.check:checked::after { opacity: 1; }

.toggle {
  appearance: none;
  position: relative;
  flex: none;
  width: 44px;
  height: 26px;
  border-radius: var(--r-pill);
  background: var(--surface-sunk);
  cursor: pointer;
  transition: background-color var(--mo-quick) var(--mo-ease);
}
.toggle::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: var(--r-pill);
  background: var(--surface-card);
  box-shadow: 0 1px 3px rgba(16, 24, 40, 0.2);
  transition: transform var(--mo-quick) var(--mo-ease);
}
.toggle:checked { background: var(--brand-blue); }
.toggle:checked::after { transform: translateX(18px); }

.tick {
  appearance: none;
  flex: none;
  width: 26px;
  height: 26px;
  border: 1.8px solid var(--brand-blue);
  border-radius: var(--r-pill);
  background: var(--surface-card);
  cursor: pointer;
  display: inline-grid;
  place-content: center;
}
.tick::after {
  content: "";
  width: 13px;
  height: 13px;
  background: var(--text-on-blue);
  opacity: 0;
}
.tick:checked { background: var(--brand-blue); }
.tick:checked::after { opacity: 1; }

/* The tick inside the drawn controls is the exported `check` glyph from
   00 Design System -> 06 -> Icons, used as a mask so a checkbox, a radio and a
   task tick all carry the same geometry as every other tick on the site.
   A pseudo-element cannot reference the sprite, so the path travels inline. */
.check::after, .tick::after, .choice-mark::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2022%2022%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M18.3333%205.5L8.25%2015.5833L3.66667%2011%22%20stroke%3D%22%23000%22%20stroke-width%3D%221.83333%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2022%2022%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M18.3333%205.5L8.25%2015.5833L3.66667%2011%22%20stroke%3D%22%23000%22%20stroke-width%3D%221.83333%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
}

/* --------------------------------------------------------- 05 · progress --- */

.bar {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--surface-sunk);
  overflow: hidden;
}
.bar > span, .bar-fill {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--brand-blue);
  transition: width var(--mo-enter) var(--mo-ease);
}
.bar-amber > span, .bar-amber .bar-fill { background: var(--status-amber); }

/* --------------------------------------------------------------- icons --- */

.ico {
  display: inline-block;
  flex: none;
  width: 18px;
  height: 18px;
  vertical-align: -0.18em;
  color: currentColor;
}

/* ------------------------------------------------- the public shell ------- */

.shell { max-width: 1440px; margin: 0 auto; }
/* The side margin is published as a variable, not just applied, so anything
   inside can bleed to the page edge by exactly the right amount at every
   width. The ecosystem tab rail is the one thing that does; it hardcoded 24px
   before this and hung 4px off both edges below 720px, where the padding is
   actually 20px. */
.wrap {
  --wrap-pad: var(--s-120);
  max-width: 1440px;
  margin: 0 auto;
  padding: 0 var(--wrap-pad);
}
@media (max-width: 1280px) { .wrap { --wrap-pad: 48px; } }
@media (max-width: 720px)  { .wrap { --wrap-pad: 20px; } }

/* The floating bar.
 *
 * Sticky, not fixed. Sticky keeps the header in normal flow, so its height is
 * still reserved and no page has to be given a compensating top padding - the
 * landing, explore, the college list and the auth screens all keep working
 * without a single per-page offset. Fixed would have overlapped every one of
 * them.
 *
 * The 14px band above the pill is transparent at rest, which is what makes the
 * pill read as floating over the hero rather than sitting in a bar. The moment
 * the page moves, content would otherwise smear through that band, so a soft
 * veil fades in behind it. At rest there is nothing above the pill to hide.
 */
/* The bar's own ground is the body's ground, so there is no band at all.
   It was white, which on a page whose body is #f2f8fc drew a hard 11-step
   edge straight across the viewport at y=92 - a white strip sitting above the
   page on explore, the college list and the auth screens. Matching the body
   makes the strip disappear; the pill still reads as floating because of its
   own border and shadow, not because of a plate behind it.
   The landing overrides this to transparent - see .top-nav-clear. */
.top-nav {
  position: sticky;
  top: 0;
  z-index: 60;
  padding: 14px 0;
  /* Transparent on every page, at every scroll position. The bar itself is
     never a plate; the veil below does all the hiding, and only once there is
     something to hide. Two earlier attempts got this wrong in opposite
     directions: white drew a strip across pages whose body is grey, and
     body-coloured made the bar invisible - which is worse, because content
     scrolling up was then chopped off at an edge with nothing there to
     explain it. */
  background: transparent;
}

/* The veil, and why the blur lives on a masked pseudo-element rather than on
 * the bar itself.
 *
 * A `backdrop-filter` does not fade with the background painted over it. Both
 * earlier versions put a tint that faded to transparent and a blur that did
 * not on the same 92px box, so whatever scrolled underneath was blurred right
 * down to the bar's bottom edge and razor sharp one pixel below it. Measured
 * across the full width, that row stepped by 19 levels while the rows either
 * side of it moved by 1 - a hard line under the nav at every scroll position
 * where anything with contrast passed beneath.
 *
 * The mask fades the blur and the tint together, so the effect runs out before
 * the edge does and there is no line left to see.
 */
.top-nav[data-scrolled]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* The veil takes the ground of the page it sits on, so where there is no
     content under it there is nothing to see. */
  background: linear-gradient(to bottom,
    var(--veil) 0%, var(--veil) 70%, transparent 100%);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 84%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 84%, transparent 100%);
}

/* Grey on the app-ground pages, white on the landing, whose hero starts white
   directly under the bar. */
.top-nav { --veil: var(--surface-ground); }

/* Where backdrop-filter is unavailable the glass would fall back to 88% white
   over live content, which is unreadable. Go opaque instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .top-nav[data-scrolled] { background: var(--atmos-base); }
  .top-nav[data-scrolled]::after { display: none; }
}

.brand { display: inline-flex; align-items: center; gap: 9px; }
/* The nav carries the u[skulpt] wordmark the frame draws, and since 2026-09-19
   so does the footer, in a light cut for its dark ground. The sparkle mark and
   the name in type are still the brand in the app's sidebar and on the auth and
   onboarding layouts. */
.brand-wordmark { display: block; width: 100px; height: 28px; }

/* ------------------------------------------------------- the link row --- */

/* No `justify-self` here, and that is load-bearing rather than tidy: on a
   phone the same element becomes a fixed-position sheet with left:0/right:0,
   where a self-alignment of `center` makes an out-of-flow box size to
   fit-content and centre inside those insets instead of stretching. */
.nav-links { display: flex; align-items: center; gap: 8px; }

/* ------------------------------------------------------------- mobile --- */

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--border-line);
  border-radius: var(--r-md);
  background: var(--surface-card);
  color: var(--text-ink);
  cursor: pointer;
}
.nav-toggle .ico { width: 18px; height: 18px; }
.nav-toggle:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.nav-scrim { display: none; }

/* Below this width the link row stops fitting and would drag the page wider
   than the viewport. It becomes a bottom sheet: reachable with a thumb, which
   a menu dropped from the top of a 900px screen is not, and this audience is
   on a phone by default. */
@media (max-width: 1100px) {
  /* 14 + 58 + 14 */
  :root { --nav-h: 86px; }
  

  
  .nav-toggle { display: inline-flex; }

  .nav-links {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 70;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    max-height: 86vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 14px 14px calc(18px + env(safe-area-inset-bottom));
    border-radius: var(--r-2xl) var(--r-2xl) 0 0;
    background: var(--surface-card);
    box-shadow: 0 -18px 44px -20px rgba(var(--brand-navy-rgb), 0.35);
    transform: translateY(101%);
    visibility: hidden;
    transition: transform var(--mo-medium) var(--mo-ease),
                visibility 0s linear var(--mo-medium);
  }
  .nav-links.is-open { transform: translateY(0); visibility: visible; transition-delay: 0s; }

  /* The grab handle. Drawn, not draggable - it says "this came from the
     bottom edge", which is the only thing it needs to say. */
  .nav-links::before {
    content: "";
    align-self: center;
    flex: none;
    width: 38px;
    height: 4px;
    margin-bottom: 12px;
    border-radius: 3px;
    background: var(--surface-sunk);
  }

  

  

  

  .nav-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 65;
    border: 0;
    padding: 0;
    background: rgba(var(--brand-navy-rgb), 0.34);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--mo-medium) var(--mo-ease),
                visibility 0s linear var(--mo-medium);
  }
  .nav-scrim.is-open { opacity: 1; visibility: visible; transition-delay: 0s; }
}

/* The smallest phones. Logo, Start free and the toggle have a min-content
   width of about 343px between them, which dragged a 320px page 19px wider
   than its own viewport - measured, across all six public pages. Everything
   here buys back width and nothing is removed: the brief's three elements all
   stay in the bar. The arrow goes because it is the only part of the button
   that is decoration. */
@media (max-width: 380px) {
  .brand-wordmark { width: 86px; height: 24px; }
  .nav-toggle { width: 40px; }
}

/* Pointer-coarse devices need a 44px target; the desktop sizes stay as drawn. */
@media (pointer: coarse) {
  .btn, .btn-primary, .btn-ghost, .btn-tint, .btn-white,
  .btn-outline-deep, .btn-danger, .btn-secondary, .btn-on-color { min-height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .top-nav-inner, .nav-links, .nav-scrim, .nav-link::after,
  .btn.nav-cta, .btn.nav-cta .ico { transition-duration: 1ms; }
}

.section { padding: var(--s-88) 0; }

/* The dark ending.
 *
 * Two navy glows and one very large blurred form off the right edge. Nothing
 * bright, nothing coloured: "premium dark" is mostly a matter of not doing
 * much. `overflow: hidden` is load-bearing - both the form and the wordmark
 * are sized in vw and would widen the document without it. */
.site-footer {
  position: relative;
  overflow: hidden;
  padding: 76px 0 28px;
  background:
    radial-gradient(46% 44% at 86% 87%, rgba(var(--brand-blue-rgb), 0.22) 0%, rgba(var(--brand-blue-rgb), 0) 100%),
    var(--surface-footer);
}
.site-footer::before {
  content: "";
  position: absolute;
  z-index: 0;
  right: -14%;
  top: -34%;
  width: 56%;
  aspect-ratio: 1;
  border-radius: 46% 54% 38% 62% / 52% 44% 56% 48%;
  background: linear-gradient(140deg, rgba(var(--brand-blue-rgb), 0.16), rgba(var(--brand-navy-rgb), 0));
  filter: blur(72px);
  pointer-events: none;
}
.site-footer > * { position: relative; z-index: 1; }

.footer-col-head {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--footer-head);
}
.footer-link { display: inline-block; color: var(--footer-link); font-size: 12.5px; line-height: 2; }
.footer-link:hover { color: var(--text-on-deep); }
/* A footer entry with no page behind it yet: dimmer than a link, not
   clickable, and still clear of AA. */
.site-footer .t-muted, .site-footer .footer-link.t-muted { color: var(--footer-dim); }
.site-footer .brand-wordmark { width: 118px; height: 33px; }
.site-footer .divider { background: rgba(255, 255, 255, 0.14); }
.site-footer .footer-bottom .t-caption { color: var(--footer-link); }
/* Both of these kept the light-surface body token and measured 2.45:1 the
   first time this footer went dark. */
.site-footer .footer-lang { color: var(--footer-link); }
.site-footer .footer-lang .ico,
.site-footer .footer-verify .ico { color: var(--footer-head); }

.footer-brand-copy { margin: 16px 0 0; max-width: 300px; font-size: 13px;
  line-height: 1.62; color: var(--footer-link); }

/* The wordmark that closes the page.
 *
 * Decorative, and hidden from readers: the brand is already named in the
 * footer's own link and in the nav, so a screen reader hearing "Uskulpt" a
 * third time learns nothing. Set in a light blue rather than the full brand
 * colour - at this size the brand blue stops being a signature and becomes
 * the loudest thing on the page.
 *
 * The negative bottom margin crops the descender against the page edge, which
 * is what makes it read as a mark rather than as a very large heading. */
.footer-wordmark {
  margin: 46px 0 -0.14em;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(58px, 13.2vw, 196px);
  line-height: 0.84;
  letter-spacing: -0.05em;
  color: rgba(255, 255, 255, 0.07);
  user-select: none;
}

/* ---------------------------------------------------- the app shell ------- */

.app { display: flex; min-height: 100vh; background: var(--surface-ground); }

.sidebar {
  width: 252px;
  flex: none;
  background: var(--surface-card);
  border-right: 1px solid var(--border-line);
  padding: 20px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.search {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 400px;
  max-width: 100%;
  height: 46px;
  padding: 0 15px;
  border-radius: var(--r-md);
  background: var(--surface-mock);
  border: 1px solid var(--border-line);
  color: var(--text-muted);
  font-size: 13px;
}
.search .ico { width: 16px; height: 16px; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--r-md);
  border: 1px solid var(--border-line);
  background: var(--surface-card);
  color: var(--text-body);
  cursor: pointer;
}
.icon-btn:hover { border-color: var(--brand-blue-200); color: var(--brand-blue); }
.icon-btn .ico { width: 17px; height: 17px; }

.app-content { padding: 26px var(--s-32) 52px; display: flex; flex-direction: column; gap: 18px; }

/* A banner across the top of a screen — guardian consent, an unverified note. */
.banner {
  display: flex;
  align-items: center;
  gap: 14px;
  border-radius: var(--r-lg);
  padding: 15px 18px;
  background: var(--status-amber-tint);
  color: var(--status-amber);
}

/* --------------------------------------------------------------- misc ----- */

.divider { height: 1px; background: var(--border-line); }

/* ------------------------------------------------------------- motion ----- */

/* Scroll reveal.
 *
 * The hidden state is gated on `js` on <html>, set by an inline script in the
 * head before the first paint. If app.js never runs, that script's own timer
 * takes the class back off and every section renders plainly — a page whose
 * content depends on an IntersectionObserver firing is a page that can go
 * blank, and this one is read by students on unreliable connections.
 *
 * Revealing is one-way: `is-in` is added once and never removed. Re-running the
 * entrance every time a section scrolls back into view reads as a glitch on a
 * phone, where the scroll direction changes constantly.
 *
 * Stagger a group by setting --i on each child: 0, 1, 2 … and the delay ladder
 * follows at 70ms a step.
 */
.js [data-reveal]:not([data-reveal="stagger"]):not([data-reveal="seq"]),
.js [data-reveal="stagger"] > * {
  opacity: 0;
  transform: translateY(var(--mo-rise, 28px)) scale(var(--mo-scale, .985));
  transition:
    opacity var(--mo-slow) var(--mo-ease) calc(var(--i, 0) * 70ms),
    transform var(--mo-slow) var(--mo-ease) calc(var(--i, 0) * 70ms);
}

.js [data-reveal]:not([data-reveal="stagger"]):not([data-reveal="seq"]).is-in,
.js [data-reveal="stagger"].is-in > * {
  opacity: 1;
  transform: none;
}

/* A row of cards built from {% include %} calls cannot carry --i per child, so
   the ladder is drawn here instead: mark the grid, not the cards. Beyond six
   the delay stops growing - a seventh card arriving two thirds of a second
   after the first is waiting, not staggering. */
.js [data-reveal="stagger"] > :nth-child(2) { transition-delay: 70ms; }
.js [data-reveal="stagger"] > :nth-child(3) { transition-delay: 140ms; }
.js [data-reveal="stagger"] > :nth-child(4) { transition-delay: 210ms; }
.js [data-reveal="stagger"] > :nth-child(5) { transition-delay: 280ms; }
.js [data-reveal="stagger"] > :nth-child(n+6) { transition-delay: 350ms; }

/* The landing headline, and nothing else: further to travel and longer to
   settle, so the first thing read arrives last. */
.js [data-reveal="hero"] {
  --mo-rise: 40px;
  transition-duration: var(--mo-hero);
}

/* A phone scrolls faster than a desktop and its sections are taller than its
   screen, so what moves travels less and gets there sooner. */
@media (max-width: 720px) {
  .js [data-reveal]:not([data-reveal="stagger"]):not([data-reveal="seq"]),
  .js [data-reveal="stagger"] > * { --mo-rise: 16px; --mo-scale: 1; transition-duration: var(--mo-medium); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .btn:hover, .card-lift:hover { transform: none; }
  /* No entrance at all rather than an instant one: the content is simply
     there, which is what a reader who asked for less motion meant. */
  .js [data-reveal], .js [data-reveal="stagger"] > * {
    opacity: 1;
    transform: none;
  }
  
}

/* ------------------------------------------------ shell odds and ends ----- */

.search:focus-within { border-color: var(--brand-blue); box-shadow: var(--focus-ring); }

.footer-cols {
  display: grid;
  grid-template-columns: 1.7fr 1fr 1fr 1fr;
  gap: 40px 32px;
}
@media (max-width: 900px) { .footer-cols { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer-cols { grid-template-columns: 1fr; } }

.footer-lang {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 18px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12px;
  color: var(--text-body);
}
.footer-lang .ico { color: var(--text-muted); }

.footer-bottom { display: flex; align-items: flex-start; justify-content: space-between; gap: 40px; }
.footer-verify { display: flex; align-items: flex-start; gap: 8px; max-width: 720px; text-align: right; }
.footer-verify .ico { flex: none; margin-top: 2px; }
@media (max-width: 900px) {
  .footer-bottom { flex-direction: column; gap: 14px; }
  .footer-verify { text-align: left; }
}

/* Two earlier authentication shells stood here and are gone (2026-09-11).
 *
 * The first was a two-column screen - form left, navy promises panel right.
 * The second replaced it with a 600px navy brand panel and a card. The current
 * one is `.authx-*` further down: a quiet page with a single card, no panel at
 * all. Neither predecessor had a single template left using it; between them
 * they defined `.auth` twice with different layouts, so any markup that said
 * `class="auth"` would have picked up whichever came later in the file.
 *
 * Removed with them: `.ask-row`, `.on-deep-tag`, `.on-deep-card`,
 * `.on-deep-tile`, `.track-deep`, `.numbered-step`, `.numbered-rule`,
 * `.promise`, `.promise-mark`, `.steps`, and the whole `.auth-*` family. The
 * three components that were their last nominal users - `_deep_step.html`,
 * `_deep_point.html` and `_promise.html` - were themselves included by nothing
 * and went at the same time.
 */

/* Password strength, three segments. Green only once it is genuinely long. */
.strength { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.strength > span { flex: 1; height: 4px; border-radius: var(--r-pill); background: var(--surface-sunk); }
.strength > span.on { background: var(--status-green); }

/* --- the onboarding wizard ------------------------------------------------ */

.wizard { min-height: 100vh; display: flex; flex-direction: column; background: var(--surface-ground); }

.opt {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 11px 16px;
  border: 1px solid var(--border-line);
  border-radius: var(--r-pill);
  background: var(--surface-card);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 14px;
  letter-spacing: -0.2px;
  color: var(--text-body);
  cursor: pointer;
  transition: border-color var(--mo-quick) var(--mo-ease), background-color var(--mo-quick) var(--mo-ease), color var(--mo-quick) var(--mo-ease);
}
.opt:hover { border-color: var(--brand-blue-200); }
.opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.opt .ico { display: none; width: 13px; height: 13px; }
.opt:has(input:checked) {
  background: var(--brand-blue);
  border-color: var(--brand-blue);
  color: var(--text-on-blue);
  font-weight: 600;
  box-shadow: 0 6px 16px rgba(var(--brand-blue-rgb), 0.28);
}
.opt:has(input:checked) .ico { display: inline-block; }
.opt:has(input:focus-visible) { box-shadow: var(--focus-ring); }

/* Onboarding 2's three choice cards: a whole card is the control. */
.choice {
  display: block;
  position: relative;
  background: var(--surface-card);
  border: 1.5px solid var(--border-line);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-card);
  padding: 22px 24px;
  cursor: pointer;
  transition: border-color var(--mo-quick) var(--mo-ease);
}
.choice:hover { border-color: var(--brand-blue-200); }
.choice input { position: absolute; opacity: 0; width: 0; height: 0; }
.choice:has(input:checked) { border-color: var(--brand-blue); }
.choice:has(input:focus-visible) { box-shadow: var(--focus-ring); }

/* ============================================================ the landing ==
   Figma: USkult -> 01 Landing -> "Draft Version" (node 174:252), 1280 wide.
   Rebuilt against that frame 2026-09-17. The frame is the authority now - see
   SESSION.md, Open decisions 12, which this closes.

   Ten sections in the frame's order: nav, hero copy, product preview, ticker,
   start anywhere, the ecosystem, for parents, FAQ, contact, start free, footer.
   Every rule here is prefixed `lp-` or belongs to a landing-only block, so the
   stylesheet stops at the public page and never reaches the signed-in app.

   Two deliberate departures from the frame, both recorded rather than slipped:
     - colours come from tokens.css, not from the frame's hex values. The frame
       draws #3070B6 since the 2026-09-20 recolour, and so does the product
       of tokens.css.
     - the frame is desktop-only. Everything under 1000px is a judgement call
       made here, with the frame's proportions as the guide.
   ========================================================================== */

/* --- 1 · the hero atmosphere ------------------------------------------- */

/* The copy, and on 330:75 the whole hero: there is nothing under it now but
   the ticker. Centred, 1152 wide, 78px above the pill and 112px below the
   buttons, which is where the frame puts the ticker's top edge. */
.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 38px;
  padding-top: 78px;
  padding-bottom: 112px;
}
@media (max-width: 1000px) { .hero { padding-top: 64px; padding-bottom: 88px; gap: 30px; } }
@media (max-width: 720px)  { .hero { padding-top: 40px; padding-bottom: 64px; gap: 26px; } }

/* 58/74, -2px, on the 1280 frame. The clamp lands on 58 at 1280 and on 34 at
   390. Leading is the frame's own 74px rather than the 1.1 this rule carried
   for Manrope: measured off the frame's render, its two headline lines sit
   75px apart, and Google Sans Flex needs the number to be larger to arrive
   at the same optical gap. */
.hero h1.t-display {
  max-width: 1152px;
  font-size: clamp(34px, 4.55vw, 58px);
  line-height: 1.276;
  letter-spacing: -0.0345em;
}

/* --- 4 · for parents ---------------------------------------------------- */

.parents {
  display: grid;
  grid-template-columns: 572px minmax(0, 1fr);
  gap: 56px;
  align-items: start;
}
@media (max-width: 1180px) { .parents { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 40px; } }
@media (max-width: 900px)  { .parents { grid-template-columns: 1fr; gap: 36px; } }

/* --- 5 · FAQ ------------------------------------------------------------ */

.faq {
  display: grid;
  grid-template-columns: 572px minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}
@media (max-width: 1180px) { .faq { grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); gap: 40px; } }
@media (max-width: 900px)  { .faq { grid-template-columns: 1fr; gap: 40px; } }

/* =================================================== Discovery ============
   The redesign: Discovery as a journey rather than a fifty-one-question form.

   What this replaced, and why each thing went:

     five accent-cycling completion cards   the last of the Clay palette
     the chisel mascot                      same
     confetti + a three-note fanfare        a fifty-one-item instrument does not
                                            need celebrating five times, and the
                                            confetti had no CSS behind it at all
     a live radar during answering          six answers do not make a shape, and
                                            showing one invites the student to
                                            answer toward the shape they want
     four simultaneous progress readouts    "Question 1 of 6 in this chapter",
                                            "Chapter 1 of 5", "51 left overall"
                                            and a chapter minute estimate, all
                                            on one screen, none of them the
                                            answer to "how far through am I"

   One progress statement now: position, a line, and the chapter's name.
   ========================================================================== */

/* --- the flow shell ----------------------------------------------------- */

.dv-stage { position: relative; }

/* --- the question ------------------------------------------------------- */

/* The composition, both ways round: on a phone the question owns the screen; on
   a desktop it stays the same width and the chapter rail takes the space beside
   it rather than the question growing to fill it. A statement set 90 characters
   wide is harder to read, not more impressive. */
.q { display: grid; gap: var(--s-32); }
@media (min-width: 1080px) {
  .q { grid-template-columns: minmax(0, 40rem) 12rem; gap: var(--s-52); align-items: start; }
}

/* The hero. Large, tight, and given room — this is the one thing on the screen
   the student is meant to be thinking about. */
.q-text {
  margin: var(--s-24) 0 0;
  max-width: 30ch;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(26px, 4.4vw, 38px);
  line-height: 1.18;
  letter-spacing: -1.1px;
  color: var(--text-ink);
  text-wrap: balance;
}

/* --- the five options --------------------------------------------------- */

.opt {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 56px;
  padding: 12px 16px;
  border: 1.5px solid var(--border-line);
  border-radius: var(--r-lg);
  background: var(--surface-card);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--mo-quick) var(--mo-ease),
              background-color var(--mo-quick) var(--mo-ease),
              transform var(--mo-quick) var(--mo-ease),
              opacity var(--mo-quick) var(--mo-ease);
}
@media (min-width: 720px) {
  .opt {
    flex-direction: column;
    justify-content: flex-start;
    gap: 10px;
    min-height: 0;
    padding: 18px 12px 16px;
    text-align: center;
  }
}
.opt:hover { border-color: var(--brand-blue-200); background: var(--surface-tint); }
.opt:active { transform: scale(.985); }
.opt:focus-visible { outline: none; border-color: var(--brand-blue); box-shadow: var(--focus-ring); }

/* The recorded answer. One filled mark, one blue edge, and nothing brighter for
   the agreeing end of the scale — a gradient toward "Definitely me" would
   introduce the acquiescence bias the reverse-scored items exist to cancel. */
.opt.is-chosen {
  border-color: var(--brand-blue);
  background: var(--surface-tint);
}

/* Every other option steps back the instant one is tapped, before the POST has
   resolved. The interface answers the finger, not the network. */
.opt.is-dimmed { opacity: .38; }

/* --- under the question ------------------------------------------------- */

@keyframes q-saved {
  0%   { opacity: 0; transform: translateY(3px); }
  14%  { opacity: 1; transform: none; }
  72%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes q-momentum {
  0%   { opacity: 0; transform: translate(-50%, 10px); }
  9%   { opacity: 1; transform: translate(-50%, 0); }
  82%  { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, 6px); visibility: hidden; }
}

/* --- the chapter overview ----------------------------------------------- */

.ch { max-width: 34rem; padding: var(--s-52) 0; }/* --- chapter complete, and the end ------------------------------------- */

/* The one moment in the flow that is allowed to be a moment. The sequence the
   brief specifies — line, glow, tick, headline, next chapter, action — is six
   animation delays adding up to about 1.3s, and it collapses to nothing under
   prefers-reduced-motion. Nothing here waits on the student and nothing traps
   them: every element is in the DOM and usable from the first frame. */
.done { position: relative; max-width: 34rem; padding: var(--s-52) 0; }

/* --- the directional transition between states ------------------------- */

/* Forward moves left, back moves right, and the direction is set by whichever
   control was pressed — see app.js. `swap:190ms` on the HTMX attribute is what
   holds the old state on screen long enough for its exit to be seen. */
#card > * { animation: q-in-fwd 260ms var(--mo-ease) both; }
#card[data-dir="back"] > * { animation-name: q-in-back; }

@keyframes q-in-fwd  { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
@keyframes q-in-back { from { opacity: 0; transform: translateX(-14px); } to { opacity: 1; transform: none; } }

/* Motion is an enhancement here, never the mechanism. Under reduced motion
   every state appears immediately, in full, with no delay and no offset —
   including the six-step completion sequence, which is decoration. */
@media (prefers-reduced-motion: reduce) {
  #card > *,
  #card.htmx-swapping > *,
  .done-tick,
  .done-tick path,
  .done-head,
  .done-sub,
  .done-count,
  .done-next,
  .done-act,
  .gen-list li,
  .gen-ready,
  .q-saved,
  .q-momentum {
    animation: none;
  }
}

/* --- the result --------------------------------------------------------- */

/* Wider since 2026-09-20, but not a dashboard. This page is read, so the
   prose keeps a measure - `.res-head`, `.res-note` and `.res-points` are all
   held to one - while the page itself is wide enough for the readings below to
   sit two-up instead of running down a 46rem ribbon with half the canvas
   empty beside them. */
.res { width: 100%; max-width: 1100px; }

/* The chart supports the sentence. Given a quiet corner rather than the top of
   the page, and never sized as the headline. */

kbd {
  display: inline-block;
  min-width: 18px;
  padding: 2px 5px;
  border: 1px solid var(--border-line);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--surface-mock);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 11.5px;
  color: var(--text-body);
  text-align: center;
}

/* -------------------------------------------------- legacy class aliases --- */

/* Screens not yet rebuilt against the components above still resolve to the
   new system through these. Nothing new should use them. */

.lead    { font-size: 17.5px; line-height: 1.62; letter-spacing: -0.1px; color: var(--text-ink); }

/* ================================================== the sign-up dialog ==== */

/* Centred on a desktop, a bottom sheet on a phone. The phone case is not a
 * style preference: a centred box and the on-screen keyboard fight over the
 * same space, and the field being typed into is the one that loses. Anchored to
 * the bottom, the keyboard pushes the sheet up instead of covering it.
 */
.sheet {
  width: min(560px, calc(100vw - 32px));
  max-height: min(88vh, 900px);
  padding: 24px;
  border: 0;
  border-radius: var(--r-2xl);
  background: var(--surface-card);
  box-shadow: var(--shadow-lift);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.sheet::backdrop {
  background: rgba(0, 28, 101, 0.44);
  backdrop-filter: blur(2px);
}

@media (max-width: 600px) {
  .sheet {
    width: 100vw;
    max-width: none;
    max-height: 92vh;
    margin: 0 0 0 0;
    /* A dialog is centred by `margin: auto` in the UA sheet; pinning it to the
       bottom of the viewport is what turns it into a sheet. */
    position: fixed;
    inset: auto 0 0 0;
    border-radius: var(--r-2xl) var(--r-2xl) 0 0;
    padding: 20px 18px calc(20px + env(safe-area-inset-bottom));
  }
}

/* ==================================================== the motion layer ====
 *
 * Rebuilt 2026-09-17 with the landing, to one brief: the student should feel
 * the product responding to them, not animations happening around them. So
 * every rule here has one of three jobs - to say where you are, to explain a
 * relationship, or to acknowledge that you moved forward - and anything that
 * did none of those was left out.
 *
 * Two clocks:
 *
 *   arrival    `.is-in`, added once by the reveal observer in app.js, plays an
 *              entrance and then rests. Durations from tokens.css:
 *              --mo-fast / --mo-standard / --mo-medium / --mo-slow / --mo-story.
 *   scroll     `--p`, a 0-1 progress written by motion.js from the scroll
 *              position. No duration at all: the scroll is the clock, so it
 *              runs backwards as readily as forwards and stops when the reader
 *              does. The hero's depth and the ecosystem story run on it.
 *
 * Rhythm across the page, deliberately uneven:
 *
 *   hero            rich - the headline lands a word at a time, the counts
 *                   count up, the navigator fills itself in, the depth
 *                   follows the scroll
 *   ticker          one quiet loop that hurries and reverses with the scroll
 *   start anywhere  a plain reveal
 *   the ecosystem   the story - the five parts are dealt onto the grid one
 *                   at a time, each from its own side, as you scroll
 *   for parents     a plain reveal; the photograph settles as it arrives
 *   FAQ             a plain reveal; the answers open at a speed you can follow
 *   start free      calm but present - the band settles, the picture arrives,
 *                   a glow climbs through it on the scroll
 *
 * 2026-09-18: the word-by-word headline, the count-up, the scroll-following
 * ticker, the dimming and the glow were added after two reference sites -
 * klarna.com/us and numa.uprock.pro - to bring their feel here without a
 * new section or a changed layout. Smooth wheel scrolling (Lenis, landing
 * only) is the one library; its four lines of CSS are at the end.
 *
 * `.js` gates every entrance, so a page whose script never ran is complete
 * and still. The reduced-motion block at the end restates the rest state for
 * everything above it, because at equal specificity the later rule wins.
 */

/* --- section heads: label, heading, lead ------------------------------
 *
 * The order the eye should take. The tag wipes in from its left edge - the
 * "small blue indicator growing" - then the heading, then the lead, 90ms
 * apart. `seq` is excluded from the generic reveal so the block is never
 * hidden as one unit; each child carries its own hidden state.
 */
.js [data-reveal="seq"] > * {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--mo-slow) var(--mo-ease), transform var(--mo-slow) var(--mo-ease),
              clip-path var(--mo-slow) var(--mo-ease);
}
/* The tag alone is clipped, and wipes in from its left edge. Nothing else
   carries a clip-path: a rounded inset on a heading takes the corner off its
   first letter. */
.js [data-reveal="seq"] > .tag { transform: none; clip-path: inset(0 100% 0 0 round 999px); }
.js [data-reveal="seq"] > :nth-child(2) { transition-delay: 90ms; }
.js [data-reveal="seq"] > :nth-child(3) { transition-delay: 180ms; }
.js [data-reveal="seq"].is-in > * { opacity: 1; transform: none; }
.js [data-reveal="seq"].is-in > .tag { clip-path: inset(0 0 0 0 round 999px); }

/* --- 1 · the hero -----------------------------------------------------
 *
 * The copy lands top to bottom on a 70ms ladder (--i on each piece, in the
 * markup): eyebrow, first line, second line, lead, actions, then the card.
 * The headline lines travel 40px over 800ms, everything else 28px over 640.
 */

.js .hero-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.45em);
  transition: opacity var(--mo-slow) var(--mo-ease), transform var(--mo-hero) var(--mo-ease);
  transition-delay: calc(var(--w, 0) * 55ms);
}

/* --- buttons, everywhere on the page ----------------------------------
 *
 * The press: 1 -> .98 -> 1, on every button. The lean: primaries marked
 * `data-magnetic` follow a fine pointer by up to 5px through `--mx`/`--my`,
 * written by motion.js; `translate` so it composes with the hover lift.
 */
.btn { translate: var(--mx, 0) var(--my, 0); transition: transform var(--mo-fast) var(--mo-ease), background-color var(--mo-fast) var(--mo-ease), box-shadow var(--mo-fast) var(--mo-ease), translate 260ms var(--mo-ease); }
.btn:active { transform: scale(.98); }
.btn .ico:last-child { transition: transform var(--mo-fast) var(--mo-ease); }
.btn:hover .ico:last-child { transform: translateX(4px); }

/* --- reduced motion ---------------------------------------------------
 *
 * `animation: none` rather than a shortened duration, so nothing is parked
 * on a `from` keyframe at opacity 0: the reader who asked for less motion
 * gets the page whole and still. The story shows complete, the depth stays
 * flat, and the ticker stands.
 */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal="seq"] > *, .js [data-reveal="seq"] > .tag { opacity: 1; transform: none; clip-path: none; transition: none; }
  .js .hero-word { opacity: 1; transform: none; transition: none; }
  .btn:active, .btn:hover .ico:last-child, .lp-contact-btn:hover, .lp-contact-btn:active { transform: none; }
  .btn { translate: none; }
}

/* ============================================= authentication, rebuilt ====
 *
 * Sign up, sign in and the four password-reset screens.
 *
 * What this replaces was a landing page with a form attached: a 600px navy
 * panel carrying five numbered promises, three FAQ links and a pull quote, next
 * to a card holding nine fields, a tinted panel explaining the DPDP Act and two
 * paragraphs of reassurance. A person on these screens has already decided.
 * Selling to them again only stands between them and the product.
 *
 * So: a quiet page, one card, and nothing else with any weight. The card is the
 * only surface that is white, bordered and raised, which is what makes it the
 * subject without a single decorative element being added to point at it.
 *
 * The old `.auth-*` rules above are left where they are. `.on-deep-*` and
 * `.numbered-step` are shared with `_deep_step.html` and `_deep_point.html`,
 * which other pages still include, so this is not a block that can simply be
 * deleted - untangling it is a cleanup of its own.
 */

.authx {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* The landing hero's atmosphere at about a third strength. A single card on
     a flat grey field read as a page that had not finished loading; this gives
     the space around it a reason to be empty. Same three ideas as `.lp-atmos`
     and no fourth: a white light source where the card sits, one brand glow
     low and right, and a body that is not pure white. */
  background:
    radial-gradient(52% 42% at 50% 22%, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0) 70%),
    radial-gradient(48% 40% at 88% 96%, rgba(var(--brand-blue-rgb), 0.10) 0%, rgba(var(--brand-blue-rgb), 0) 72%),
    linear-gradient(174deg, #ffffff 0%, var(--atmos-base) 58%, var(--atmos-tint) 100%);
}

/* Two items, and only two. Every marketing link is gone: from here the only
   places to go are into the account or back out to the site. */

/* The counterpart route, at the foot of the form since 2026-09-20 — see the
   comment in `layouts/auth.html` for why it left the top-right corner.

   The question is set in body type and the answer in display type, so the pair
   reads as one line with one thing in it that is clickable, rather than as two
   links. A rule above it, because at the bottom of a form with no box around
   it there is otherwise nothing to say the form has ended. */
.authx-swap {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 26px 0 0;
  padding-top: 22px;
  border-top: 1px solid var(--border-line-soft);
  font-size: 14px;
  color: var(--text-body);
}
.authx-swap:empty { display: none; }
.authx-swap a {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.2px;
  color: var(--brand-blue-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--brand-blue-200);
  transition: color var(--mo-quick) var(--mo-ease),
              text-decoration-color var(--mo-quick) var(--mo-ease);
}
.authx-swap a:hover { color: var(--brand-blue); text-decoration-color: currentColor; }

/* `center` rather than `flex-start`, with the top bar's height discounted, so
   the card sits on the optical centre of the screen and not below it. */
.authx-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 8px var(--s-24) 56px;
}

/* NO BOX, since 2026-09-20.
 *
 * It was a white card with a hairline and a soft shadow, and that was right
 * when this screen was one object floating on a grey field. It is not that any
 * more: on the split layout the form has a whole pane to itself with a navy
 * panel beside it, so the card was a box drawn inside a box — two rounded
 * rectangles, one inside the other, with nothing between them.
 *
 * What made the card the subject was never the border. It was that everything
 * else had been taken off the screen. That is still true, so the form holds
 * its own without being fenced in.
 *
 * The four password-reset screens have no panel, and they still work: on those
 * the pane's atmosphere is the ground the form sits on, the same way the
 * landing's hero copy sits on its own.
 */
.authx-card {
  width: 100%;
  max-width: 420px;
  padding: 0 0 8px;
}

.authx-head {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 27px;
  line-height: 1.16;
  letter-spacing: -0.9px;
  color: var(--text-ink);
  margin: 0;
}
.authx-lead {
  margin: 9px 0 0;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--text-body);
}

/* One gap value for the whole form, so the rhythm cannot drift the way it does
   when every gap is a `<div style="height:18px">`. */
.authx-fields {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin: 26px 0 20px;
}

/* 54px. The design system's 48px input is right in a dense app screen and too
   small here, where there are three of them and nothing else to look at. */
.authx .input,
.authx select.input {
  padding: 15px 16px;
  font-size: 15px;
  border-radius: var(--r-lg);
}
.authx select.input { padding-right: 38px; }
/* Same correction as the base rule in components.css: room for the icon, and
   only where there is one. Both files carried it, so both were indenting the
   password field 43px against a label sitting at 16. */
.authx .input-wrap:has(> .ico) .input { padding-left: 43px; }
.authx .field-label { font-size: 12.5px; margin-bottom: 8px; }

/* Show / hide. A word, not an eye: there is no `eye-off` in the icon set, and
   a single eye cannot say which of the two states it is currently in. The word
   changes, so it always can. Hidden without JavaScript, because that is what
   drives it. */
.input-reveal { display: none; }
.js .input-reveal {
  display: block;
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  padding: 8px 10px;
  border-radius: var(--r-md);
  background: transparent;
  border: 0;
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: -0.1px;
  color: var(--text-muted);
  transition: color var(--mo-quick) var(--mo-ease), background-color var(--mo-quick) var(--mo-ease);
}
.js .input-reveal:hover { color: var(--text-ink); background: var(--surface-mock); }
.js .input-reveal:focus-visible { outline: none; box-shadow: var(--focus-ring); color: var(--text-ink); }
/* Room for the control, so a long password never runs underneath it. */
.js .input-wrap-reveal .input { padding-right: 68px; }

.authx-check {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  margin-bottom: 20px;
  cursor: pointer;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--text-body);
}
.authx-check .check { margin-top: 1px; }
/* The two policy links inside the agreement label. Underlined, because a
   link inside a consent sentence has to look like one; the box next to it is
   the only other affordance and it must not be confused with the link. */
.authx-check a, .authx-note a { color: var(--brand-blue); text-decoration: underline; text-underline-offset: 2px; }
.authx-check a:hover { color: var(--brand-blue-ink); }

/* --- the two steps of sign-up -------------------------------------------- */

/* No `display: none` here on purpose. With no JavaScript both steps render as
   one long form that still posts correctly - `signupSteps` in app.js is what
   hides step two, using the `hidden` attribute so the fields leave the tab
   order with it. */
/* The three controls that only mean anything once the steps exist. Without
   JavaScript the page is one form, so "Continue" would be a button that does
   nothing, "Back" would go nowhere and "Step 2 of 2" would be counting pages
   there is only one of. They are hidden rather than left there misleading. */
[data-step-next], .authx-backstep, .authx-steps, .authx-steps-label { display: none; }
/* Keyed to `data-signup-ready` - the attribute `setUpSignUp` writes onto the
   form - and deliberately not to `.js`. The two are not the same claim: `.js`
   says a script was expected, this says the script that owns these three
   controls has run and is holding the steps. Gating on `.js` meant a late
   `app.js` could hide step two while the 2500ms failsafe had already hidden
   the Continue button - and the only submit control on the page lives inside
   step two. The form went unsubmittable, silently, for exactly the slow
   connection the failsafe exists to protect. The controls now appear if and
   only if the code that drives them is live. */
[data-signup-ready] [data-step-next] { display: inline-flex; }

/* The pending state on a submit button. The label stays, so the control still
   says what it is doing rather than becoming a spinner with no name; the
   opacity change and the wait cursor carry the state. */
.btn[data-pending] {
  opacity: 0.72;
  cursor: wait;
}
.btn[data-pending] .ico { animation: m-pending 900ms var(--mo-ease) infinite; }
@keyframes m-pending {
  0%, 100% { opacity: 0.35; transform: translateX(0); }
  50%      { opacity: 1;    transform: translateX(2px); }
}
@media (prefers-reduced-motion: reduce) {
  .btn[data-pending] .ico { animation: none; opacity: 1; }
}

/* --- narrower ------------------------------------------------------------ */

@media (max-width: 560px) {
  /* The card gave up its border here first, on the argument that at this size
     the page and the card are the same object. That is now true at every size
     — see the note on `.authx-card` — so all that is left to do is take the
     width. */
  .authx-body { padding: 0 0 40px; justify-content: flex-start; }
  .authx-card { max-width: none; padding: 12px var(--s-24) 28px; }
  .authx-head { font-size: 25px; }
  
}

/* ================================================== the split, 2026-09-20 ===
 *
 * A content panel on the left, the form pane on the right — the shape of the
 * reference Naveen supplied, in Uskulpt's colours. See the comment at the top
 * of `layouts/auth.html` for why a page that argued against a panel now has
 * one, and `accounts/_auth_aside.html` for the rule the panel's copy works to.
 *
 * `.authx` without `.authx-split` is untouched and still renders the single
 * centred card, so the four password-reset screens did not have to change.
 */

.authx-split {
  flex-direction: row;
  align-items: stretch;
  /* The pane carries its own atmosphere; the shell no longer needs to, and
     leaving it on tinted the navy panel's edge. */
  background: var(--surface-card);
}

/* --- the panel ---------------------------------------------------------- */

/* 44% rather than half. The form is the reason the page exists, and an even
   split makes the panel argue with it; at 44 the panel is unmistakably the
   supporting half. It stops growing at 620 so the pills do not stretch into a
   line of text on a wide monitor. */
.authx-aside {
  flex: 0 0 44%;
  max-width: 620px;
  display: flex;
  flex-direction: column;
  gap: 38px;
  padding: 34px 44px 0;
  position: relative;
  overflow: hidden;
  color: var(--text-on-deep);
  /* The deep navy, lit from the top left by the primary. Two stops, not a
     wash of three: the panel has line art sitting on it and every extra
     gradient is something that drawing has to survive. */
  background:
    radial-gradient(72% 56% at 14% 8%, rgba(var(--brand-blue-rgb), 0.55) 0%, rgba(var(--brand-blue-rgb), 0) 68%),
    var(--surface-deep);
}
@media (max-width: 1100px) { .authx-aside { padding: 30px 32px 0; gap: 30px; } }

.authx-aside-brand { display: inline-flex; flex-shrink: 0; }
.authx-aside-brand img { display: block; height: 28px; width: auto; }

/* --- the form pane ------------------------------------------------------ */

/* The single-card layout centres on the viewport. Split, it centres on its own
   column, and the top bar's height is discounted the same way. */
.authx-split .authx-body { padding-inline: var(--s-32); }
.authx-split .authx-card { margin-inline: auto; }

/* --- collapsed ---------------------------------------------------------- */

/* One column below 900. The panel becomes a brand band: the wordmark, the
   heading, and nothing else. The pills and the drawing go rather than stack,
   because the first field belongs above the fold on a phone and five pills
   put it three screens down. */
@media (max-width: 900px) {
  .authx-split { flex-direction: column; }
  .authx-aside {
    flex: 0 0 auto;
    max-width: none;
    gap: 16px;
    padding: 20px var(--s-24) 24px;
  }
}
/* ============================== The app head, one component, 2026-09-20 ===
 *
 * Every signed-in page opens the same way, because Naveen looked at the
 * planner and asked for the rest of the product to read like it:
 *
 *     a strip   the tab's name and what today is, with a live dot
 *     a line    one sentence about THIS student, ending in a full stop
 *     a sub     the sentence under it, optional
 *     tiles     three numbers that are true, each linking to what it counts
 *
 * It was written for My Space on 2026-09-20 and lived in `pages/home.css`
 * under an `.sp-` prefix, shared by exactly two screens. Five more screens
 * take it now, so it is a component with its own file and its own name -
 * `.ah-`, for app head. A class called `sp-tile` on the Roadmap would be the
 * drift the naming is meant to stop.
 *
 * The tiles are optional and deliberately so. They appear where the page has
 * three numbers it can stand behind; Targets is a form and has none, so it
 * gets the strip and the line and no tiles. Three zeroes under a heading is
 * not a dashboard, it is furniture.
 *
 * `components/_app_head.html` renders the strip, the line and the sub for the
 * pages whose head is only those three. Home and My Space write their own
 * header markup with these classes, because theirs carry actions and a
 * disclosure that an include cannot take as children.
 */

.ah-head { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }

/* The dated strip. A live dot, because this line is the only thing on the page
   that changes by itself. */
.ah-strip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--brand-blue-ink);
}

/* A MODIFIER, not a size. The dashboard headline is on the product's
   page-title system - `class="page-title sp-title"` - which owns the family,
   the weight and the clamp; all this adds is the measure.

   It had its own `clamp(28px, 3.6vw, 44px)` for half a day, which was the
   page-title clamp written out again with different numbers. That is exactly
   the drift the system exists to stop, and the test that caught it
   (`test_my_space_is_on_the_page_title_system`) was right to. */
.ah-title { max-width: 20ch; }

.ah-sub {
  margin: 0;
  max-width: 54ch;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-body);
}

/* ======================================================== Home ============
   Home as a navigator: one question, one answer, everything else quieter.

   The screen it replaces drew six cards of the same size — Discovery, Explore,
   "So far", Colleges, Roadmap and a seven-stage rail — which left the student
   to work out the priority themselves. That is Home's only job.

   So there is exactly one filled button on this page, one card with a coloured
   edge, and one measure. The rest are open sections separated by rules rather
   than boxed, because not every group of three sentences is a card.
   ========================================================================== */

/* --- the journey -------------------------------------------------------- */

.hm-stages {
  display: flex;
  gap: 0;
  margin: 0;
  padding: 0 40px 0 0;
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { .hm-stages { scroll-behavior: auto; } }
.hm-stages::-webkit-scrollbar { display: none; }

/* ================================================ the app shell on a phone ==
   The 252px sidebar had no small-screen behaviour at all. It stayed put and
   took its width out of the page, which left every signed-in screen rendering
   into about 140px at 390px wide, with 146px of horizontal overflow behind it.
   Measured, not guessed — see the pixel scan in the session log.

   Below 900px it becomes a drawer over the page instead: same markup, same
   seven destinations, no second copy of the navigation to drift out of step.
   `aria-expanded` on the toggle is the state of record; the class on <html> is
   only for this stylesheet.
   ========================================================================== */

@media (max-width: 899px) {
  .app { display: block; }

  .sidebar {
    position: fixed;
    z-index: 60;
    top: 0;
    left: 0;
    bottom: 0;
    width: min(19rem, 84vw);
    height: auto;
    transform: translateX(-101%);
    transition: transform var(--mo-enter) var(--mo-ease);
    box-shadow: var(--shadow-lift);
  }
  .shell-open .sidebar { transform: none; }

  /* The backdrop. A pseudo-element on <body> rather than a div, so the shell
     needs no extra markup and nothing to clean up when it closes. */
  .shell-open body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgba(var(--brand-navy-rgb), 0.42);
  }

  

  
  .app-content { padding: 20px var(--s-20) 48px; }

  
}

/* Behind the drawer nothing scrolls, so a swipe cannot move the page under an
   open panel. */
.shell-open { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
}

/* On a desktop the question is the hero, so it sits in the middle of the space
   rather than at the top of it with 270px of nothing underneath. Top-aligned on
   a phone, where content flows and there is no space to centre in. */
@media (min-width: 1080px) {
  .dv-stage { display: grid; align-content: center; min-height: calc(100vh - 15rem); }
}

/* ======================================================== Explore ========
 *
 * One destination, three tabs, and a card language shared by both catalogues.
 *
 * Everything below is prefixed `xp-`, so it starts and stops at the Explore
 * templates and cannot reach the landing page or the other app screens. It is
 * built entirely out of the tokens declared at the top of this file — no new
 * colours, no new radii, no new shadows, and no gradients.
 *
 * The rules that shaped it:
 *
 *   Calm over dense.   A three-up grid at 1320px with 20px gutters and 20px
 *                      card padding. Cards state four things and stop; the
 *                      fifth thing is what makes a grid read as a table.
 *   Blue is the verb.  Blue marks the selected tab, the focused field, the
 *                      saved bookmark and the one action. Everything else is
 *                      ink, body, muted and one hairline.
 *   Nothing decorative. No coloured card backgrounds, no tinted panels behind
 *                      content, no pills that carry no value.
 */

/* ------------------------------------------------------------- the card --- */

.xp-bookmark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px;
  border: 1px solid var(--border-line);
  border-radius: var(--r-pill);
  background: var(--surface-card);
  color: var(--text-muted);
  cursor: pointer;
  position: relative;
  z-index: 2;
  flex: none;
  transition: background-color var(--mo-quick) var(--mo-ease),
              border-color var(--mo-quick) var(--mo-ease),
              color var(--mo-quick) var(--mo-ease);
}
.xp-bookmark:hover { border-color: var(--brand-blue-200); color: var(--brand-blue); }
.xp-bookmark:focus-visible { outline: none; box-shadow: var(--focus-ring); }
/* Saved is a filled blue mark, and the fill is what carries the state — not
   colour alone: the outline glyph becomes a solid one. */
.xp-bookmark.is-on {
  background: var(--brand-blue);
  border-color: var(--brand-blue);
  color: var(--text-on-blue);
}
.xp-bookmark.is-on .ico { fill: currentColor; }
.xp-bookmark.is-on:hover { background: var(--brand-blue-hover); border-color: var(--brand-blue-hover); }

/* --------------------------------------------------- the filter drawer --- */

/* A right-side panel on a desktop, a bottom sheet on a phone, and a native
 * <dialog> in both cases — showModal() is where the focus trap, Escape, the
 * backdrop and inerting the page behind it come from.
 *
 * `max-height: 100dvh` rather than `100vh`: on iOS the visual viewport shrinks
 * when the address bar shows, and a 100vh panel puts Apply below the fold with
 * no way to scroll to it.
 */
.xp-drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(420px, 100vw);
  max-width: none;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--r-2xl) 0 0 var(--r-2xl);
  background: var(--surface-card);
  box-shadow: var(--shadow-lift);
  overflow: hidden;
}
.xp-drawer::backdrop {
  background: rgba(var(--brand-navy-rgb), 0.42);
}

@media (max-width: 600px) {
  .xp-drawer {
    inset: auto 0 0 0;
    width: 100vw;
    /* A definite height, not `auto` with a `max-height`. It has to be definite
       or the flex column above it does not resolve: `.xp-drawer-form` is
       `height: 100%`, a percentage against an auto-height parent computes as
       auto, the body never gets a scrollport, and the footer is laid out past
       the dialog's bottom edge where `overflow: hidden` clips it. That put
       Apply filters and Clear all off-screen and unreachable on a phone. */
    height: 88dvh;
    max-height: 88dvh;
    border-radius: var(--r-2xl) var(--r-2xl) 0 0;
  }
}

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

.xp-modal {
  position: relative;
  width: min(880px, calc(100vw - 40px));
  /* Not decoration. The UA stylesheet carries
     `dialog:modal { max-width: calc(100% - 6px - 2em) }`, which clamped the
     phone sheet 35px narrower than the viewport and left a visible strip of
     page down its right edge. `.sheet` above already had to do this. */
  max-width: none;
  max-height: 82dvh;
  padding: 0;
  border: 0;
  border-radius: var(--r-2xl);
  background: var(--surface-card);
  box-shadow: var(--shadow-lift);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Dimmed, not blurred to obliteration: the student should still be able to see
   the grid they came from, because that is what makes this feel like a look
   rather than a departure. */
.xp-modal::backdrop { background: rgba(var(--brand-navy-rgb), 0.46); }

.xp-modal-close { position: absolute; top: 18px; right: 18px; z-index: 3; }

@media (max-width: 720px) {
  /* Near full-screen on a phone, anchored to the bottom so it arrives from
     where the thumb is and the keyboard cannot cover what is being read. */
  .xp-modal {
    position: fixed;
    inset: auto 0 0 0;
    width: 100vw;
    max-height: 92dvh;
    margin: 0;
    border-radius: var(--r-2xl) var(--r-2xl) 0 0;
  }
  .xp-modal-close { top: 14px; right: 14px; }
}

/* ===================== the completion moments, centred ====================
   The question screen is left-aligned and stays that way: fifty-one statements
   read down a left edge. These two screens are centred instead, and the change
   of register is the point — it is how a student can tell at a glance that this
   is not another question. It also puts the moment in the middle of the desktop
   content area rather than in its top-left corner.

   Appended rather than edited in place: another session is working in this tree
   and app.css is shared, so these win on source order instead of on a rewrite.
   ========================================================================== */

.done {
  margin-inline: auto;
  text-align: center;
}

/* The labelled save control. Same state, same blue as the card bookmark —
   these were teal and mint, which gave one state two colours. Replaces a block
   of inline `style=` that could not be themed centrally. */
/* A column so the failure line sits under the button rather than beside it.
   Hidden it generates no box and no gap, so with nothing wrong this is the
   button on its own, exactly as before. */
.xp-save-form {
  flex: none;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.xp-save {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 14px;
  border: 1px solid var(--border-line);
  border-radius: var(--r-pill);
  background: var(--surface-card);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12.5px;
  letter-spacing: -0.15px;
  color: var(--text-body);
  cursor: pointer;
  transition: background-color var(--mo-quick) var(--mo-ease),
              border-color var(--mo-quick) var(--mo-ease),
              color var(--mo-quick) var(--mo-ease);
}
.xp-save:hover { border-color: var(--brand-blue-200); color: var(--brand-blue); }
.xp-save:focus-visible { outline: none; box-shadow: var(--focus-ring); }
/* Saved fills the glyph as well as changing the colour, so the state is not
   carried by hue alone. */
.xp-save.is-on {
  border-color: var(--brand-blue);
  background: var(--surface-tint);
  color: var(--brand-blue-ink);
}
.xp-save.is-on .ico { fill: currentColor; }

/* The touch size every `.btn` already gets from the `pointer: coarse` block in
   layout.css. This control is not a `.btn`, so it was the one Save the rule
   never reached: 32px on a phone, while signed out - where the same slot
   renders `_signup_prompt.html` as `.btn-ghost.btn-sm` - the same action was
   44px. One button, two sizes, depending on whether you had an account.

   Here rather than in that block because this file is later in the cascade and
   the 32px above would win from there. Desktop is untouched at 32px, so Save
   stays the quiet control beside the page's filled CTA and only grows where a
   thumb is doing the work. */
@media (pointer: coarse) {
  .xp-save { min-height: 44px; }
}

/* ================================================ Discovery: focus mode ===
   Discovery is the only place in the product that replaces the app shell.

   The sidebar, the global search, the bookmark and roadmap shortcuts and the
   sign-out control are all gone for its duration. Seven destinations and a
   search field sitting beside a question about whether you like taking apart
   gadgets are seven invitations to stop answering, and the screen this replaced
   spent 252px of a 390px phone on them.

   What is left: a wordmark that is also the way home, an exit, and one line.
   ========================================================================== */

.fx {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: var(--surface-ground);
}

/* The one progress indicator. Persistent, so its width transitions - see
   _progress.html for why this is not an out-of-band swap. */
.fx-progress { flex: none; position: relative; height: 2px; background: var(--brand-blue-100); }

/* Full width, scaled from the left by `--pct` (0-100, set inline and by
   app.js). A transform, so fifty-one updates cost no layout, and it moves
   backwards as smoothly as forwards. */
.fx-progress > span {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--brand-blue);
  transform: scaleX(calc(var(--pct, 0) / 100));
  transform-origin: left center;
  transition: transform var(--mo-medium) var(--mo-ease);
}
@media (prefers-reduced-motion: reduce) {
  .fx-progress > span { transition: none; }
}

/* One column, the same width everywhere. A statement set 90 characters wide is
   harder to read, not more impressive, so the column does not grow with the
   window - the window grows around it. */
.fx-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 42rem;
  margin-inline: auto;
  padding: var(--s-32) var(--s-24) var(--s-52);
}
@media (min-width: 900px) {
  /* Centred in the space rather than sitting at the top of it with a third of
     the viewport empty underneath. */
  .fx-main { justify-content: center; padding-block: var(--s-52); }
}

/* --- the question, single column --------------------------------------- */

.q { display: block; }
.q-text { margin-top: var(--s-14); }

/* --- the chapter overview ---------------------------------------------- */

.ch { max-width: 32rem; padding: 0; }

/* --- the completion moments, without the box -------------------------- */

/* No card around the next chapter. An eyebrow, a title and a line, separated by
   whitespace, say exactly what a border round them said - and a bordered white
   rectangle holding one sentence is the treatment this product had started
   applying to everything by default. */
.done { padding: 0; }

/* ============================= no box by default ==========================
   White rectangle + border + rounded corners + shadow had become the default
   treatment for any group of sentences in this product, which is how a page
   ends up reading as a dashboard. These three were the remaining ones inside
   Discovery and on Home.

   The rule that replaced it: a card has to earn containment. Where the
   hierarchy is already carried by an eyebrow, a heading and whitespace, the
   border was drawing a box around something that did not need one.
   ========================================================================== */

/* ------------------------------------------------------------ roadmap --- */
/* Full width since 2026-09-20. It was `max-width: 46rem`, which put the plan
   and the timeline one under the other with half a 1440px canvas empty beside
   them - a student scrolled past the plan to reach dates that would have fitted
   next to it. */
.rm { width: 100%; max-width: 1320px; }

/* A row, not a card: tick, body, when. */
/* A row, not a card, and since 2026-09-20 not a bordered one either: these sit
   inside `.rm-panel`, and a bordered row inside a bordered panel is a box in a
   box. The tint on hover and the tick are what mark a row now. */
.rm-row {
  display: grid; grid-template-columns: 44px minmax(0, 1fr) auto; gap: 12px; align-items: start;
  padding: 12px 14px 12px 8px; border-radius: var(--r-lg);
  background: transparent; border: 1px solid transparent;
  transition: background-color var(--mo-fast) var(--mo-ease);
}
.rm-row:hover { background: var(--surface-tint); }
.rm-row.is-done { background: transparent; }
.rm-tick-form { display: contents; }
@media (max-width: 560px) {
  .rm-row { grid-template-columns: 44px minmax(0, 1fr); }
}

/* ------------------------------------------------------- consent state --- */
/* One shape for "waiting on a guardian", on Home and the Discovery intro.
   Facts on the left, actions on the right; a single column below 720px. */
.cs {
  display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(16rem, 1fr); gap: var(--s-24);
  padding: var(--s-24); border-radius: var(--r-xl, 18px);
  background: var(--status-amber-tint); border: 1px solid color-mix(in srgb, var(--status-amber) 35%, transparent);
}
@media (max-width: 720px) {
  .cs { grid-template-columns: 1fr; padding: var(--s-20); }
}

/* Digits that line up. */
.num { font-variant-numeric: tabular-nums; }

/* ========================================== Discovery, on the reference ====
 *
 * Naveen supplied a language-lesson screen on 2026-09-20 as the reference for
 * how answering should feel. What is taken from it is the furniture: a close
 * control and a fat rounded progress track in one top bar, a figure with a
 * speech bubble introducing the task, wide choices, and an action bar fixed to
 * the bottom of the viewport.
 *
 * What is NOT taken from it is anything that scores the student. No hearts, no
 * streak, no XP, no accuracy. Discovery has no right answers - that is the
 * first thing the intro says and the reason the instrument works at all - so
 * every one of those would be furniture that lies about what is happening.
 * Each slot they would have occupied carries something true instead, and the
 * comment in the template beside it says what.
 *
 * This file is last in the cascade after `no-box-overrides.css`, so it wins by
 * order over the `.q-*` and `.fx-*` rules in `discovery.css` and
 * `discovery-focus.css` that it supersedes. Those are left in place: the
 * chapter overview, the completion screen, the generating screen and the whole
 * result page still draw on them, and untangling which is a job of its own.
 */

/* --- the shell ---------------------------------------------------------- */

.qz {
  background:
    radial-gradient(60% 40% at 50% 0%, rgba(var(--brand-sky-rgb), 0.22) 0%, rgba(var(--brand-sky-rgb), 0) 70%),
    var(--surface-card);
}

/* One row: the X, the track, the pips. The track takes whatever is left, which
   is what makes it read as the bar's subject rather than as a third item. */
.qz-bar {
  display: flex;
  align-items: center;
  gap: 18px;
  flex: none;
  height: 68px;
  padding: 0 clamp(16px, 4vw, 40px);
  background: transparent;
  border-bottom: 0;
}
@media (max-width: 560px) { .qz-bar { gap: 12px; height: 60px; } }

.qz-exit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 40px;
  height: 40px;
  margin-left: -8px;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color var(--mo-fast) var(--mo-ease),
              color var(--mo-fast) var(--mo-ease);
}
.qz-exit:hover { background: var(--surface-sunk); color: var(--text-ink); }
.qz-exit:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* --- the progress track ------------------------------------------------- */

/* 16px and fully rounded, in the bar rather than under it. `overflow: hidden`
   is what gives the fill its own round cap without a second radius on it. */
.qz-progress {
  flex: 1 1 auto;
  position: relative;
  height: 16px;
  border-radius: var(--r-pill);
  background: var(--surface-sunk);
  overflow: hidden;
}

/* Full width, scaled from the left by `--pct` (0-100, set inline and by
   app.js). A transform, so two hundred updates cost no layout, and it moves
   backwards as smoothly as forwards - which it has to, because Previous moves
   it back. */
.qz-progress > span {
  display: block;
  /* Positioned, so the highlight below anchors to the FILL. Without this it
     resolved against `.qz-progress` and drew a full-width white band across an
     empty track, which read as a bar that was already finished. */
  position: relative;
  height: 100%;
  width: 100%;
  border-radius: inherit;
  background: var(--brand-blue);
  transform-origin: left center;
  transform: scaleX(calc(var(--pct, 0) / 100));
  transition: transform var(--mo-standard) var(--mo-ease);
}
@media (prefers-reduced-motion: reduce) { .qz-progress > span { transition: none; } }

/* The highlight along the top of the fill. One rule, and it is the difference
   between a filled rectangle and something that looks made. */
.qz-progress > span::after {
  content: "";
  position: absolute;
  left: 6px; right: 6px; top: 3px;
  height: 4px;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.34);
  pointer-events: none;
}

/* --- the chapter pips --------------------------------------------------- */

.qz-chapters { display: flex; align-items: center; gap: 6px; flex: none; }
.qz-pip {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--surface-sunk);
  transition: background-color var(--mo-standard) var(--mo-ease),
              box-shadow var(--mo-standard) var(--mo-ease);
}
.qz-pip.is-done { background: var(--brand-blue-200); }
/* The one you are in: filled, and ringed so it is findable at 9px without
   being any larger than its neighbours. */
.qz-pip.is-now {
  background: var(--brand-blue);
  box-shadow: 0 0 0 3px rgba(var(--brand-blue-rgb), 0.22);
}

/* --- the question ------------------------------------------------------- */

.qz-main {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Room for the fixed action bar, so the last choice is never under it. */
  padding: 8px clamp(16px, 4vw, 40px) 116px;
}

/* --- the figure and what it says ---------------------------------------- */

.qz-guide { display: block; width: 84px; height: auto; flex: none; overflow: visible; }
@media (max-width: 560px) { .qz-guide { width: 62px; } }

/* --- the idle -------------------------------------------------------------
 *
 * Three loops on three different, deliberately incommensurate periods - 3.6s,
 * 7s and 5.2s - so they drift against each other and the figure never repeats
 * the same combination of positions. One loop on one period is what makes an
 * animated character read as a looping GIF.
 *
 * The shadow is on the body's period and inverted: it widens as the figure
 * comes down. Without it the body looks like it is sliding up a wall rather
 * than lifting off the floor.
 */
@media (prefers-reduced-motion: no-preference) {
  .qz-guide-body   { animation: guide-bob 3.6s ease-in-out infinite; }
  .qz-guide-shadow { animation: guide-shadow 3.6s ease-in-out infinite; transform-origin: 60px 123px; }
  .qz-guide-spark  { animation: guide-spark 7s ease-in-out infinite; transform-origin: 60px 20px; }
  .qz-guide-eyes   { animation: guide-blink 5.2s steps(1, end) infinite; transform-origin: 60px 66px; }
}

@keyframes guide-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}
@keyframes guide-shadow {
  0%, 100% { transform: scaleX(1);    opacity: .10; }
  50%      { transform: scaleX(.88);  opacity: .07; }
}
@keyframes guide-spark {
  0%, 100% { transform: rotate(0deg)   scale(1); }
  46%      { transform: rotate(-9deg)  scale(1.07); }
  72%      { transform: rotate(7deg)   scale(1); }
}
/* A blink is two frames, not a fade: 120ms shut out of 5.2s. `steps(1, end)`
   is what keeps it from being a slow wink. */
@keyframes guide-blink {
  0%, 96%   { transform: scaleY(1); }
  97%, 99%  { transform: scaleY(.08); }
  100%      { transform: scaleY(1); }
}

/* --- the reaction ---------------------------------------------------------
 *
 * Set by app.js on the tap, not on the response - so the figure answers the
 * finger the same way the progress line does. One nod, 620ms, then the
 * attribute comes off. */
.qz-guide[data-guide-react] .qz-guide-body {
  animation: guide-nod 620ms var(--mo-spring) both;
}
.qz-guide[data-guide-react] .qz-guide-spark {
  animation: guide-spark-pop 620ms var(--mo-spring) both;
}
@keyframes guide-nod {
  0%   { transform: translateY(0)     scale(1); }
  32%  { transform: translateY(-9px)  scale(1.05, .95); }
  64%  { transform: translateY(2px)   scale(.97, 1.03); }
  100% { transform: translateY(0)     scale(1); }
}
@keyframes guide-spark-pop {
  0%   { transform: scale(1)    rotate(0deg); }
  40%  { transform: scale(1.35) rotate(18deg); }
  100% { transform: scale(1)    rotate(0deg); }
}

/* --- the action bar ----------------------------------------------------- */

/* Fixed, full width, and present on every question rather than appearing on
   some: a bar that comes and goes moves the choices under the reader's thumb
   at the moment they are aiming at one. */
.qz-foot {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  border-top: 2px solid var(--border-line-soft);
  background: var(--surface-card);
  transition: background-color var(--mo-standard) var(--mo-ease),
              border-color var(--mo-standard) var(--mo-ease);
}

/* ================================ the Discovery door, rebuilt 2026-09-20 ===
 *
 * `.dv-intro` in discovery.css is a 38rem left-aligned column. That is the
 * right shape for a page of detail and the wrong one for the screen that asks
 * a student for forty minutes, which was Naveen's note. These rules take the
 * three states of the intro and centre them at the width of the flow itself,
 * so the door and the questions behind it are the same object.
 */

/* `.fx-main` is a 42rem column and says why: a statement set 90 characters
   wide is harder to read, not more impressive. That is about the QUESTION
   screens, and it still holds for them. The door is the one screen in focus
   mode that is not a statement to read - it is the shape of what is ahead, and
   the five chapters are the shape - so it is the one allowed past 42rem. The
   prose on it keeps its own measure regardless, so nothing here is set 90
   characters wide. */
.fx-main:has(.dv-door) { max-width: 1120px; }

/* The first signal and the chapter close that carries the career map get the
   same exemption, for the same reason: neither is a statement to read. The
   signal is two cards side by side and six bars, and at 42rem the bar track
   collapses to about 50px, which turns a ranking into six identical stubs. */
.fx-main:has(.sg) { max-width: 1000px; }
.fx-main:has(.cm) { max-width: 820px; }

/* ================================================ motion, on the app screens
 *
 * The landing has its own layer (landing-motion.css). This is the rest of the
 * product, and it is smaller on purpose: the app is where a student does the
 * work, and the brief for it is acknowledgement, not spectacle. Every rule
 * here answers a tap - a dialog brought forward, a save confirmed, a task
 * ticked, an answer opened - and then rests.
 *
 * Last in the cascade so it wins on order over the screen stylesheets whose
 * controls it moves. Durations are the tokens in tokens.css.
 *
 *   surfaces      a dialog opens in --mo-medium and closes in --mo-standard,
 *                 from where it was asked for; a sheet rises from the bottom
 *   saved         the bookmark that was just saved pulses once, 1 -> 1.15 -> 1
 *   ticked        the roadmap tick that was just ticked pops its check in
 *   opened        a <details> opens at a speed you can follow
 *   moved on      a page navigation fades the old page down and the new one up
 *
 * `.is-closing` is set by `closeDialog` in app.js for the length of the exit
 * animation; `data-just-saved` and `data-just-toggled` are set by app.js on
 * the element that came back from the server for the action just taken, so a
 * page that loads with things already saved or ticked stays still.
 */

/* --- surfaces --------------------------------------------------------- */

@keyframes am-modal-in  { from { opacity: 0; transform: translateY(20px) scale(.96); } to { opacity: 1; transform: none; } }
@keyframes am-modal-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(12px) scale(.98); } }
@keyframes am-sheet-in  { from { transform: translateY(100%); opacity: .6; } to { transform: none; opacity: 1; } }
@keyframes am-sheet-out { from { transform: none; opacity: 1; } to { transform: translateY(100%); opacity: .6; } }
@keyframes am-drawer-in  { from { transform: translateX(28px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes am-drawer-out { from { transform: none; opacity: 1; } to { transform: translateX(20px); opacity: 0; } }
@keyframes am-fade-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes am-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* The Explore detail and the sign-up dialog: brought forward into focus, from
   the card that asked for them. `--mo-ox`/`--mo-oy` are set by app.js as the
   clicked element's centre in the dialog's own coordinates; unset, the scale
   is about the middle. The page behind recedes - dimmed and blurred a little,
   never so much that the student loses where they came from. */
.xp-modal, .sheet { transform-origin: var(--mo-ox, 50%) var(--mo-oy, 50%); }
.xp-modal[open], .sheet[open] { animation: am-modal-in var(--mo-medium) var(--mo-ease) both; }
.xp-modal[open]::backdrop, .sheet[open]::backdrop { animation: am-fade-in var(--mo-medium) var(--mo-ease) both; }
.xp-modal[open].is-closing, .sheet[open].is-closing { animation: am-modal-out var(--mo-standard) var(--mo-ease) both; }
.xp-modal[open].is-closing::backdrop, .sheet[open].is-closing::backdrop { animation: am-fade-out var(--mo-standard) var(--mo-ease) both; }
.xp-modal::backdrop { background: rgba(var(--brand-navy-rgb), 0.46); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }
.sheet::backdrop { background: rgba(0, 28, 101, 0.44); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }

/* The filter drawer slides in from its edge. */
.xp-drawer[open] { animation: am-drawer-in var(--mo-medium) var(--mo-ease) both; }
.xp-drawer[open]::backdrop { animation: am-fade-in var(--mo-medium) var(--mo-ease) both; }
.xp-drawer[open].is-closing { animation: am-drawer-out var(--mo-standard) var(--mo-ease) both; }
.xp-drawer[open].is-closing::backdrop { animation: am-fade-out var(--mo-standard) var(--mo-ease) both; }

/* On a phone the detail, the sign-up and the filters are bottom sheets, and a
   sheet comes from the bottom edge: 100% -> 0, and back. The page behind stays
   where it was, because a dialog never moved it. */
@media (max-width: 720px) {
  .xp-modal[open] { animation-name: am-sheet-in; transform-origin: 50% 100%; }
  .xp-modal[open].is-closing { animation-name: am-sheet-out; }
}
@media (max-width: 600px) {
  .sheet[open], .xp-drawer[open] { animation-name: am-sheet-in; transform-origin: 50% 100%; }
  .sheet[open].is-closing, .xp-drawer[open].is-closing { animation-name: am-sheet-out; }
}

/* --- saved ------------------------------------------------------------ */

/* Outline -> filled is the state change the stylesheet already draws. This is
   the acknowledgement on top of it: one pulse, and settle. */
@keyframes am-saved { 0% { transform: scale(1); } 45% { transform: scale(1.15); } 100% { transform: scale(1); } }
.xp-bookmark[data-just-saved], .xp-save[data-just-saved] { animation: am-saved var(--mo-standard) var(--mo-spring) both; }
.xp-bookmark, .xp-save { transition: background-color var(--mo-fast) var(--mo-ease), border-color var(--mo-fast) var(--mo-ease), color var(--mo-fast) var(--mo-ease), transform var(--mo-fast) var(--mo-ease); }
.xp-bookmark:active, .xp-save:active { transform: scale(.94); }

/* --- ticked ----------------------------------------------------------- */

.rm-row[data-just-toggled] { animation: am-row-settle var(--mo-medium) var(--mo-ease) both; }
@keyframes am-row-settle { from { background: var(--surface-tint); } to { background: transparent; } }

.bar > span, .bar-fill { transition: width var(--mo-medium) var(--mo-ease); }

/* --- moved on --------------------------------------------------------- */

/* A page navigation: the old page fades down a little, the new one fades up
   into place, 200 and 320ms. The bar and the sidebar are named so the browser
   carries them across as the same object rather than fading them with the
   page. Cross-document view transitions are progressive: where unsupported,
   pages simply replace each other as they always have. */
@view-transition { navigation: auto; }
.top-nav { view-transition-name: top-nav; }
.sidebar { view-transition-name: sidebar; }
@keyframes am-page-out { to { opacity: 0; transform: translateY(-6px); } }
@keyframes am-page-in  { from { opacity: 0; transform: translateY(10px); } }
::view-transition-old(root) { animation: am-page-out 200ms var(--mo-ease) both; }
::view-transition-new(root) { animation: am-page-in 320ms var(--mo-ease) both; }

/* --- reduced motion ---------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .xp-modal[open], .xp-modal[open]::backdrop, .sheet[open], .sheet[open]::backdrop,
  .xp-drawer[open], .xp-drawer[open]::backdrop,
  .xp-modal[open].is-closing, .xp-modal[open].is-closing::backdrop,
  .sheet[open].is-closing, .sheet[open].is-closing::backdrop,
  .xp-drawer[open].is-closing, .xp-drawer[open].is-closing::backdrop,
  dialog.dv-leave[open].is-closing, dialog.dv-leave[open].is-closing::backdrop,
  .xp-bookmark[data-just-saved], .xp-save[data-just-saved],
  .rm-row[data-just-toggled], .rm-row[data-just-toggled] .rm-tick .ico,
  .js .xp-empty-art { animation: none; }
  .xp-modal::backdrop, .sheet::backdrop { backdrop-filter: none; -webkit-backdrop-filter: none; }
  .xp-bookmark:active, .xp-save:active, .rm-tick:active { transform: none; }
  @view-transition { navigation: none; }
}
