/* ============================================================
   SKILLS NATION — WEB LAYER
   Loaded AFTER css/tokens.css, which is the brand kit's
   tokens/skills-nation.css copied byte-for-byte. That file is
   never edited here: when the kit changes, it is re-copied.

   This file holds (in order):
     0. the self-hosted font
     1. WEB LAYER TOKENS — the fluid type scale, layout metrics
        and the four documented derivations (docs/05_DESIGN_SYSTEM)
     2. base elements
     3. components  (Phase 4)

   The kit rule still stands: no hex value and no font size
   outside a token block. If you need a new one, declare it in
   section 1 with a comment saying where it came from.
   ============================================================ */

/* ---------- 0. FONT ----------------------------------------
   Red Hat Display, variable, weights 300-900, self-hosted from
   the kit (SIL OFL — see fonts/RedHatDisplay-LICENSE.txt).
   Never loaded from Google Fonts: one less origin, and the CSP
   stays font-src 'self' (docs/05 section 3, docs/15).

   Deviation from the kit red-hat-display.css: that file sets
   font-display: block because social exports must never render
   in a fallback face. On the web that is a blank-text penalty,
   so this uses swap, which docs/05 section 3 specifies.
   ------------------------------------------------------------ */

@font-face {
  font-family: "Red Hat Display";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("../fonts/RedHatDisplay-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Red Hat Display";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("../fonts/RedHatDisplay-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ============================================================
   1. WEB LAYER TOKENS
   ============================================================ */

:root {
  /* ---------- 1a. DERIVED COLOUR --------------------------
     Four values the kit does not carry. Each is derived from a
     kit colour, never invented, and each was signed off in the
     token grill on 2026-08-26 (docs/00, Token grill).         */

  /* Primary-button hover. White on --sn-blue-900 is 4.2:1, so the
     hover must not go lighter. This is blue-900 darkened ~15%
     (color-mix(in srgb, #3976E9 85%, black)) and measures ~5.6:1
     with white — better than the resting fill. Supersedes
     --sn-blue-700 as *button* hover only; blue-700 keeps its kit
     job as a secondary fill / shape colour.                   */
  --sn-blue-hover: #3064C6;

  /* The logo-legal light blue, read off both official
     blue-on-light-blue Do tiles (docs/05 section 2). Use THIS for
     any surface carrying the blue logo. The ramp --sn-blue-100
     (#BCD3FF) stays a tint value and is not logo-legal.       */
  --sn-blue-tile: #D0E7FF;

  /* Semantic states. The kit defines none, and an error has to
     read as an error, so both are derived (docs/05 section 7).
     Error: the site red pop #ED1C24 measures 4.38:1 on white and
     fails AA, darkened until it clears — #C0201F is ~6.0:1.
     Raw #ED1C24 is never a text colour.
     Success: the kit HBCU 900, 6.2:1 on white, no derivation.  */
  /* Link text. --sn-blue-900 measures 4.25:1 on white, which fails AA for
     anything under 24px — so the blue that is correct for a BUTTON FILL is
     wrong for a 17px link. This is the same already-approved derivation as
     the button hover (5.58:1 on white, 5.05:1 on off-white), reused under
     its own name so its job is obvious at the call site. No new colour.
     Measured in Phase 5; docs/12's claim that --sn-grey-500 is 4.6:1 on
     white is likewise wrong — it is 4.25:1, so it cannot carry small text
     either, and every small muted label below uses --sn-ink instead. */
  --sn-link: var(--sn-blue-hover);

  --sn-error:   #C0201F;
  --sn-success: var(--sn-p-hbcu-900);

  /* Field washes for the two states — mixes of the above, no new
     hues enter the system. */
  --sn-error-wash:   color-mix(in srgb, var(--sn-error) 8%, var(--sn-white));
  --sn-success-wash: color-mix(in srgb, var(--sn-success) 10%, var(--sn-white));

  /* ---------- 1b. FLUID TYPE SCALE ------------------------
     The kit --sn-fs-* values are px for a fixed 1080x1350
     artboard. The web scale is the kit own rule (display / ~1.6)
     expressed as clamp(). Table and roles: docs/05 section 3.  */
  /* Calibration B, chosen by Malena 2026-08-26 after seeing all three on
     /scale-check. The first pass ran every token at its maximum at once —
     100px hero AND 40px radii AND 30px card text — so nothing read as the
     signature moment. docs/02 puts the references at 48-72px sentence case
     and asks for "exactly one signature moment per page, standard furniture
     elsewhere". The hero is still that moment; everything else stepped back. */
  --fs-hero-xl: clamp(2.5rem, 1.2rem + 4.4vw, 4.5rem);     /*  72 -> 40 · homepage h1 ONLY */
  --fs-hero:    clamp(2.25rem, 1.3rem + 3vw, 3.75rem);     /*  60 -> 36 · h1 everywhere else */
  --fs-h2:      clamp(1.75rem, 1.4rem + 1.4vw, 2.5rem);    /*  40 -> 28 */
  --fs-h3:      clamp(1.125rem, 1.05rem + 0.5vw, 1.375rem);/*  22 -> 18 */
  --fs-lead:    clamp(1.0625rem, 1rem + 0.35vw, 1.1875rem);/*  19 -> 17 */
  --fs-stat:    clamp(2.5rem, 1.8rem + 3.2vw, 4rem);       /*  64 -> 40 */

  /* Program-card titles get their OWN size, and it is not --fs-h3.
     They sit on the coloured tile, where the contrast law applies: orange is
     3.0:1 and teal 3.09:1, legal for display text and illegal for small text.
     "Display" means 24px, or 18.66px at weight 700 — so this floors at 19px
     and is always bold, which keeps a card title legal on every ground at
     every width. Sizing it from --fs-h3 would have dropped it to 18px on a
     phone and quietly broken the orange and teal cards. */
  --fs-card-title: clamp(1.1875rem, 1.05rem + 0.6vw, 1.375rem);  /* 22 -> 19 */

  /* Shinta's display voice, measured: 64px at weight 700, tracking -0.04em,
     sentence case. This OVERRIDES the kit's "Light 300 heroes, never bold" —
     a rule that is in the brand kit, the Linear ticket and docs/05. Changed on
     Malena's explicit instruction 2026-08-26; recorded in docs/00 and docs/05
     so nobody later "restores" it thinking it was a slip. */
  --w-display:  var(--sn-w-bold);   /* 700 */
  --ls-display-tight: -0.04em;
  --fs-body:    1.0625rem;  /* 17 */
  --fs-eyebrow: 0.8125rem;  /* 13 */
  --fs-caption: 0.8125rem;  /* 13 */

  /* Primary blue buttons carry white on --sn-blue-900 (4.2:1),
     which is AA only at large-text size. The label is therefore
     locked at 19px/700 — the threshold for bold large text.
     Never render a primary blue button with a smaller label.  */
  --fs-btn: 1.1875rem;      /* 19 */

  /* Line heights the kit social scale does not cover. */
  --lh-hero-xl: 0.98;
  --lh-hero:    1;
  --lh-h2:      1.05;
  --lh-h3:      1.25;
  --lh-body:    1.6;
  --lh-stat:    0.9;

  /* Campaign-caps variant: Light, ALL CAPS, open tracking.
     Reserved for exactly one moment per site — the pre-footer
     CTA band headline (docs/04, Type voice).                  */
  --ls-campaign: 0.04em;

  /* ---------- 1b-bis. SHINTA DIRECTION ---------------------
     Malena redirected the design to Shinta as the primary reference
     (2026-08-26). Shinta's colour architecture is NOT "neutral page plus an
     accent" — measured off the live site, the saturated colour is the CANVAS
     behind everything (37M px² of #FFA8F2), with a neutral slab floating on
     it (#F4F4F3), white cards inside that, and small candy accents.

     Skills Nation has no legal pink, so the master brand blue takes the
     canvas role and the kit's off-white takes the slab. The candy accents are
     already ours: the six program palettes.                                  */
  --canvas: var(--sn-blue-900);     /* what Shinta's pink does */
  --slab:   var(--sn-off-white);    /* what Shinta's #F4F4F3 does */
  --card:   var(--sn-white);

  /* Depth. Reverses token-grill decision #18 ("hairlines and rings, NO drop
     shadows") at Malena's explicit request — the floating-slab feel is a large
     part of why Shinta reads as a premium product. Multi-layer and very soft,
     the way Shinta's are: a single hard shadow reads as 2010s Material. */
  --shadow-card: 0 1px 2px rgba(25, 70, 80, .04),
                 0 4px 12px rgba(25, 70, 80, .06),
                 0 12px 32px rgba(25, 70, 80, .05);
  --shadow-slab: 0 2px 8px rgba(25, 70, 80, .06),
                 0 18px 48px rgba(25, 70, 80, .10);

  /* ---------- 1c. LAYOUT ----------------------------------- */
  --slab-radius: clamp(24px, 2.5vw, 44px);
  --slab-inset:  clamp(8px, 1.2vw, 20px);
  --header-h:    64px;   /* matches .site-header__inner min-height */
  --maxw:        1280px;              /* 1200 -> 1440 -> 1280, Malena 2026-08-26 */
  --gutter:      var(--sn-space-3);   /* 24px */
  --measure:     68ch;                /* running text cap */
  --section-pad: clamp(48px, 3vw + 32px, 88px);   /* tightened with calibration B */

  /* THE EYEBROW-TO-HEADING GAP, standardised (Malena, 2026-08-27).
     It was two different things: .band__head set 10px, and the heroes set
     nothing at all — headings reset to margin 0, so an eyebrow above an <h1>
     sat flush against it. Doubled to 20px and stated once, so the pattern is
     the same wherever it appears. */
  --eyebrow-gap: 20px;

  /* The gap under a heading, before its descriptive line. Several pages carried
     this as an inline margin-top of one space step; stated once here instead. */
  --lead-gap: 20px;

  /* ---------- 1d. SHAPE ------------------------------------
     Buttons and chips stay --sn-radius-pill (kit rule, not
     negotiable). Cards and media got rounder on 2026-08-26.   */
  /* Shinta runs a RANGE (12/16/24/32/40), not one value — small things get
     small radii. Ours: cards 24, media 16, chips/pills unchanged. */
  --radius-card:  24px;
  --radius-media: 16px;

  /* Depth is hairlines and outline rings — no drop shadows
     anywhere on this site (docs/05 section 4).                */
  --hairline: 1px solid var(--sn-grey-200);
  --ring:     0 0 0 1px var(--sn-grey-200);

  /* ---------- 1e. FOCUS ------------------------------------ */
  --focus-width:  2px;
  --focus-offset: 2px;
  --focus-color:  var(--sn-blue-900);

  /* ---------- 1f. MOTION (names and values from docs/06) ---- */
  --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);   /* entrances */
  --ease-tween: cubic-bezier(0.6, 0, 0.4, 1);    /* reveals, hovers */
  --dur-fast: 0.2s;
  /* Card hovers only. --dur-fast at 0.2s with --ease-tween reads as a snap on
     anything that lifts — Malena's words: "parece que se levanta de una y muy
     rápido". These are slower, and the curve starts gently instead of leaving
     immediately. Leaving is slower still: a lift that settles back unhurriedly
     feels calm, while a fast return feels like the card is being dropped. */
  --dur-lift:     0.42s;
  --dur-lift-out: 0.58s;
  --ease-lift: cubic-bezier(0.32, 0.08, 0.24, 1);
  --dur-med:  0.5s;
  --dur-hero: 0.8s;
  --rise:     16px;   /* standard entrance offset */

  /* ---------- 1h. BREAKPOINTS (docs/09) --------------------
     Declared for reference only — @media cannot read a custom
     property, so the queries below carry the literal px with the
     token name in a comment. Changing one means changing both.  */
  --bp-sm:  480px;
  --bp-md:  768px;
  --bp-lg: 1024px;
  --bp-xl: 1280px;
}

@media (max-width: 30rem) {   /* 480px */
  :root { --gutter: 20px; }
}

/* ---------- 1g. GROUND FALLBACKS ----------------------------
   The kit tokens eight grounds. Four pages in this site sit on
   plain white or off-white (home, partners, contact, 404 —
   docs/03), so those two are expressed here as grounds too.
   Both are built entirely from existing kit tokens: no new
   colour enters the system.
   ------------------------------------------------------------ */

[data-ground="white"] {
  --g-bg: var(--sn-white);     --g-ink: var(--sn-ink); --g-pop: var(--sn-blue-100);
}
[data-ground="off-white"] {
  --g-bg: var(--sn-off-white); --g-ink: var(--sn-ink); --g-pop: var(--sn-blue-100);
}

/* A page or section with no data-ground still resolves --g-*. */
:root {
  --g-bg: var(--sn-white); --g-ink: var(--sn-ink); --g-pop: var(--sn-blue-100);
}

/* The kit program fallback is scoped to .sn-canvas, which is the
   social artboard class. The site equivalent is <body>. */
body:not([data-program]) {
  --p-900:    var(--sn-blue-900);
  --p-700:    var(--sn-blue-700);
  --p-400:    var(--sn-blue-400);
  --p-100:    var(--sn-blue-100);
  --p-accent: var(--sn-blue-900);
  --p-on-900: var(--sn-white);
  --p-wash:   color-mix(in srgb, var(--p-100) 50%, var(--sn-white));
  --p-name:   "Skills Nation";
}

/* ============================================================
   2. BASE
   ============================================================ */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--sn-white);
  color: var(--sn-black);
  font-family: var(--sn-font);
  font-size: var(--fs-body);
  font-weight: var(--sn-w-regular);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  margin: 0;
  color: var(--sn-ink);
  font-weight: var(--sn-w-medium);
  text-wrap: balance;
}

h1 {
  font-size: var(--fs-hero);
  font-weight: var(--sn-w-light);
  line-height: var(--lh-hero);
  letter-spacing: var(--sn-ls-display);
}
h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); letter-spacing: var(--sn-ls-display); }
h3 { font-size: var(--fs-h3); line-height: var(--lh-h3); }

p { margin: 0; max-width: var(--measure); }

a { color: inherit; }

/* Links inside running copy are underlined — colour is never the
   only signal (docs/05 section 7, WCAG 1.4.1). */
p a, li a, dd a { text-decoration: underline; text-underline-offset: 0.18em; }

img, svg, video { max-width: 100%; height: auto; display: block; }

:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* On dark or saturated grounds the focus ring flips to white. */
[data-ground="navy"] :focus-visible,
[data-ground="blue"] :focus-visible,
[data-ground="yap-blue"] :focus-visible,
[data-ground="teal"] :focus-visible { outline-color: var(--sn-white); }

/* ---------- Typographic primitives -------------------------- */

/* Eyebrow: Black 900, ALWAYS uppercase, open tracking. Half of the
   brand signature hero pattern — the other half is the Light 300
   display below it. A bold hero is a brand violation. */
.eyebrow {
  font-size: var(--fs-eyebrow);
  font-weight: var(--sn-w-black);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  line-height: var(--sn-lh-normal);
}

/* Keyed on the adjacent-sibling PATTERN, not on a container class, so it covers
   every place the eyebrow-then-heading structure appears — the seven heroes, the
   fifty-seven band heads, and anything added later — without each of them having
   to remember to set it. Beats the h1..h4 { margin: 0 } reset on specificity. */
.eyebrow + h1,
.eyebrow + h2,
.eyebrow + h3 { margin-top: var(--eyebrow-gap); }

/* And the other half of the same pattern: the descriptive line under a heading.
   Headings reset to margin 0 and `p` has none either, so a .lead straight after
   an <h2> was touching it — visible in the programme form sections, which are
   the newest place the pattern appears. Structural like the rule above, so it
   covers every heading-then-lead pair rather than each section remembering an
   inline margin. */
h1 + .lead,
h2 + .lead,
h3 + .lead { margin-top: var(--lead-gap); }

.lead  { font-size: var(--fs-lead); line-height: var(--sn-lh-body); }
.muted { color: var(--sn-ink); }
.caption { font-size: var(--fs-caption); color: var(--sn-ink); }

.u-container {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* ============================================================
   3. COMPONENTS (docs/05 section 6)

   Mobile-first. Breakpoints handle structure only — the fluid
   tokens in section 1b already handle size (docs/09).
   Every hover effect is guarded by (hover: hover) so touch
   devices never get a stuck hover state, and nothing that only
   appears on hover ever carries information.
   ============================================================ */

/* ---------- 3.1 SKIP LINK ---------------------------------- */

.skip-link {
  position: absolute;
  left: var(--gutter);
  top: 0;
  z-index: 100;
  transform: translateY(-120%);
  background: var(--sn-blue-900);
  color: var(--sn-white);
  font-size: var(--fs-body);
  font-weight: var(--sn-w-bold);
  padding: 12px 22px;
  border-radius: 0 0 var(--sn-radius-sm) var(--sn-radius-sm);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-tween);
}
.skip-link:focus-visible { transform: translateY(0); }

/* ---------- 3.2 BUTTONS ------------------------------------
   Pill radius is a kit rule. The primary label is locked at
   19px/700 because white on --sn-blue-900 is 4.2:1 — AA only
   at large-bold size (docs/05 section 1).
   ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 44px;                 /* touch target (docs/09) */
  padding: 13px 30px;
  border: 1.5px solid transparent;
  border-radius: var(--sn-radius-pill);
  font-family: inherit;
  font-size: var(--fs-btn);
  font-weight: var(--sn-w-bold);
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-tween),
              border-color var(--dur-fast) var(--ease-tween),
              color var(--dur-fast) var(--ease-tween);
}

.btn__arrow { transition: transform var(--dur-fast) var(--ease-tween); }

.btn--primary { background: var(--sn-blue-900); color: var(--sn-white); }
.btn--secondary { background: transparent; color: var(--sn-ink); border-color: var(--sn-blue-900); }
/* Ink. It used to dress the form submits; they went to the logo blue on
   2026-08-27 ("todos los botones de los formularios deben ser del azul del
   logo, sin excepción"), so nothing uses this today. Kept because it is a
   system variant and the rule costs one line. White on ink is 10.33:1. */
.btn--ink { background: var(--sn-ink); color: var(--sn-white); }

/* SIN EXCEPCIÓN, and enforced rather than left to each page remembering the
   right modifier. (0,2,0) beats every single-class .btn--* variant no matter
   which one a form ends up carrying or what order the sheet is in, so a submit
   button cannot come out any colour but the logo blue. Delete this pair if a
   form ever legitimately needs a different one — do not fight it per page. */
.btn.form__submit { background: var(--sn-blue-900); color: var(--sn-white); }

/* On dark or saturated grounds the primary inverts to a white
   fill with an ink label, and the secondary outlines in white. */
.btn--inverse { background: var(--sn-white); color: var(--sn-ink); }
.btn--ghost   { background: transparent; color: var(--sn-white); border-color: var(--sn-white); }

@media (hover: hover) and (pointer: fine) {
  /* Hover goes DARKER, not lighter: --sn-blue-hover is 5.6:1
     with white against the resting fill's 4.2:1. The kit's
     blue-700 would have reduced contrast (docs/05 section 1). */
  .btn--primary:hover { background: var(--sn-blue-hover); }
  .btn.form__submit:hover { background: var(--sn-blue-hover); }   /* darker, 5.6:1 */
  .btn--secondary:hover { background: var(--sn-blue-900); color: var(--sn-white); }
  .btn--inverse:hover { background: var(--sn-blue-tile); }
  .btn--ghost:hover { background: var(--sn-white); color: var(--sn-ink); }
  .btn:hover .btn__arrow { transform: translateX(4px); }
}

/* ---------- 3.3 BAND SYSTEM --------------------------------
   Bands run full-bleed; the content column inside is capped at
   --maxw. A band paints itself from --g-bg / --g-ink, so giving
   it a data-ground is the only thing needed to reskin it.
   ------------------------------------------------------------ */

.band {
  background: var(--g-bg);
  color: var(--g-ink);
  padding-block: var(--section-pad);
}
.band > * { position: relative; }          /* above any pop shape */

/* Default page rhythm: white, then off-white, alternating —
   never a wall of pure white sections (docs/04). */
.band--offwhite { --g-bg: var(--sn-off-white); --g-ink: var(--sn-ink); --g-pop: var(--sn-blue-100); }
.band--tight { padding-block: calc(var(--section-pad) * 0.6); }
.band--flush-top { padding-top: 0; }

/* Headings on a band inherit the ground's ink rather than the
   global --sn-ink, so one attribute really does reskin it. */
.band h2, .band h3 { color: inherit; }

.band__head { margin-bottom: var(--sn-space-5); }
.band__head .eyebrow { color: var(--sn-ink); }
[data-ground="navy"] .band__head .eyebrow,
[data-ground="blue"] .band__head .eyebrow,
[data-ground="yap-blue"] .band__head .eyebrow,
[data-ground="teal"] .band__head .eyebrow { color: inherit; }

/* ---------- 3.4 HERO ---------------------------------------
   .hero--ground is the signature: full-bleed ground colour with
   one oversized pop circle cropped by the edge. On orange and
   teal the ground carries display type ONLY — the contrast law
   is enforced by putting body copy in the band below, never here.
   ------------------------------------------------------------ */

.hero {
  padding-block: calc(var(--section-pad) * 0.9) var(--section-pad);
  background: var(--g-bg);
  color: var(--g-ink);
}
.hero--ground { position: relative; overflow: hidden; }

/* Full-viewport hero — the homepage's signature moment.

   Three things this has to get right:

   1. MIN-height, never height. At 200% zoom, on a landscape phone, or with a
      long headline at 320px the content must push the hero taller instead of
      being clipped. A fixed height is how full-screen heroes swallow their own
      buttons (docs/12 requires 200% zoom to lose nothing).
   2. svh, not vh. On iOS Safari 100vh means "viewport with the browser chrome
      hidden", so a 100vh hero is taller than the screen on load and the CTAs
      sit under the address bar. 100svh is the small-viewport height — the one
      that is always visible. The vh line stays first as the fallback.
   3. Minus the header. The header is sticky at the top, so a full 100svh hero
      plus a 72px header means the page scrolls before anything has happened. */
.hero--full {
  /* The bar floats over the hero now, so the hero owns the whole viewport
     and clears the bar with its own top padding instead of subtracting it. */
  display: grid;
  align-items: center;
}

/* No "collapse on short screens" breakpoint here on purpose. min-height cannot
   clip: where the content is taller than the viewport — a landscape phone, a
   320px screen, 200% zoom — the content simply wins and the hero grows. Adding
   a media query for it would be a rule that never does anything. Verified at
   375x812, 320x568, 812x375 and 640x800: every hero element inside the box. */

.hero__inner { position: relative; }
.hero .eyebrow { color: var(--sn-ink); }
.hero--ground .eyebrow { color: inherit; }

/* The hero look is a class, not an element selector, so the heading LEVEL
   stays the page's decision: real pages put it on their single <h1>, and a
   gallery page can show two heroes without shipping two <h1>s (docs/12). */
.hero__title {
  color: inherit;
  max-width: 18ch;
  font-size: var(--fs-hero);
  font-weight: var(--sn-w-light);
  line-height: var(--lh-hero);
  letter-spacing: var(--sn-ls-display);
}
.hero--xl .hero__title { font-size: var(--fs-hero-xl); line-height: var(--lh-hero-xl); }
.hero__lead { margin-top: var(--sn-space-3); font-size: var(--fs-lead); max-width: 56ch; }
.hero__ctas { display: flex; flex-wrap: wrap; gap: 14px; margin-top: var(--sn-space-4); }
.hero__ctas .btn { width: 100%; }

/* The pop circle is decoration: aria-hidden in the markup, and
   it never sits between the reader and the words. */
.hero__pop {
  position: absolute;
  border-radius: 999px;
  background: var(--g-pop);
  pointer-events: none;
  width: 260px; aspect-ratio: 1;
  right: -90px; bottom: -110px;
}

@media (min-width: 30rem) {   /* --bp-sm 480 */
  .hero__ctas .btn { width: auto; }
}
@media (min-width: 48rem) {   /* --bp-md 768 */
  .hero__pop { width: 380px; right: -110px; bottom: -140px; }
}
@media (min-width: 64rem) {   /* --bp-lg 1024 */
  .hero__pop { width: 520px; right: -120px; bottom: -180px; }
}

/* On the tall hero the circle would otherwise drift to the far bottom corner,
   away from the type it is meant to be composed with. Pull it up to sit beside
   the headline instead. */
@media (min-width: 64rem) {
  .hero--full .hero__pop { bottom: auto; top: 50%; transform: translateY(-40%); }
}

/* ---------- 3.5 HEADER -------------------------------------
   Sticky, white, gains a hairline once the page scrolls. The
   .scrolled class is set by an IntersectionObserver on a
   zero-height sentinel — never a scroll listener (docs/08).
   ------------------------------------------------------------ */

.header-sentinel { position: absolute; top: 0; height: 1px; width: 1px; }

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--sn-white);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-fast) var(--ease-tween);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sn-space-3);
  min-height: 72px;
}

/* The blue logo is legal here because the header is always white
   (docs/05 section 2). If a header variant ever goes dark, it
   must swap to skills-nation-logo-white.svg. */
.site-header__logo { display: block; flex: 0 0 auto; }
.site-header__logo img { width: 104px; }

/* --- nav, mobile first: everything lives in an overlay ------ */

.nav-toggle {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  padding: 8px 12px;
  margin-right: -12px;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: var(--fs-eyebrow);
  font-weight: var(--sn-w-black);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--sn-ink);
  cursor: pointer;
}
.nav-toggle__bars { position: relative; width: 22px; height: 2px; background: currentColor; }
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: ""; position: absolute; left: 0; width: 22px; height: 2px; background: currentColor;
  transition: transform var(--dur-fast) var(--ease-tween), opacity var(--dur-fast) var(--ease-tween);
}
.nav-toggle__bars::before { top: -7px; }
.nav-toggle__bars::after  { top: 7px; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after  { transform: translateY(-7px) rotate(-45deg); }

.nav {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  background: var(--sn-white);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 88px var(--gutter) var(--sn-space-7);
}
.nav[data-open="true"] { display: block; }
body[data-nav-open="true"] { overflow: hidden; }

.nav__list { list-style: none; margin: 0; padding: 0; }
.nav__list > li { border-bottom: var(--hairline); }

.nav__link {
  display: flex;
  align-items: center;
  min-height: 56px;
  font-size: var(--fs-h3);
  font-weight: var(--sn-w-medium);
  color: var(--sn-ink);
  text-decoration: none;
}
.nav__link[aria-current="page"] { color: var(--sn-link); }

/* Programs is a real disclosure — a button, opened by click or
   Enter/Space, closed by Escape. The live site's hover-only
   dropdown was a keyboard and touch failure (docs/03). */
.nav__disclosure {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  min-height: 56px;
  padding: 0;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: var(--fs-h3);
  font-weight: var(--sn-w-medium);
  color: var(--sn-ink);
  text-align: left;
  cursor: pointer;
}
.nav__caret {
  flex: 0 0 auto;
  width: 11px; height: 11px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-3px) rotate(45deg);
  transition: transform var(--dur-fast) var(--ease-tween);
}
.nav__disclosure[aria-expanded="true"] .nav__caret { transform: translateY(2px) rotate(-135deg); }

.nav__submenu { list-style: none; margin: 0; padding: 0 0 var(--sn-space-2); }
.nav__submenu[hidden] { display: none; }
.nav__submenu a {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding-left: var(--sn-space-2);
  font-size: var(--fs-body);
  color: var(--sn-ink);
  text-decoration: none;
}
.nav__submenu a[aria-current="page"] { color: var(--sn-link); font-weight: var(--sn-w-bold); }

.nav__cta { margin-top: var(--sn-space-4); }
.nav__cta .btn { width: 100%; }

/* --- desktop nav: a real bar with a floating dropdown ------- */

@media (min-width: 64rem) {   /* --bp-lg 1024 */
  .nav-toggle { display: none; }

  .nav {
    position: static;
    display: block;
    padding: 0;
    background: none;
    overflow: visible;
  }
  .nav__list { display: flex; align-items: center; gap: var(--sn-space-1); }
  .nav__list > li { border-bottom: 0; position: relative; }

  .nav__link,
  .nav__disclosure {
    min-height: 44px;
    padding-inline: 14px;
    font-size: var(--fs-body);
    font-weight: var(--sn-w-medium);
    width: auto;
    border-radius: var(--sn-radius-pill);
  }
  .nav__link[aria-current="page"] { background: var(--p-wash); }

  .nav__submenu {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 60;
    /* Wide enough for the longest programme name on ONE line — the panel sized
       to 268px and wrapped "College Fellowship Program" and "Pathways to Public
       Service" onto two, which made the list read as ragged blocks rather than
       rows. max-content asks the longest row how wide it needs to be, and the
       nowrap below is what makes that measurement mean one line. */
    width: max-content;
    min-width: 268px;
    max-width: min(360px, calc(100vw - var(--gutter) * 2));
    padding: 10px;
    background: var(--sn-white);
    border-radius: var(--radius-media);
    box-shadow: var(--ring);
    border: var(--hairline);
  }
  .nav__submenu a { min-height: 44px; padding-inline: 14px; border-radius: var(--sn-radius-sm); white-space: nowrap; }

  .nav__cta { margin: 0 0 0 var(--sn-space-2); }
  .nav__cta .btn { width: auto; padding-inline: 26px; }
}

@media (hover: hover) and (pointer: fine) {
  .nav__link:hover, .nav__disclosure:hover, .nav__submenu a:hover { color: var(--sn-link); }
  .nav__submenu a:hover { background: var(--sn-off-white); }
}

/* ---------- 3.6 PROGRAM CARD -------------------------------
   The card IS the ground system: data-ground paints it, and the
   accessible name is the program (never nine "Learn more"s).
   Everything is visible at rest — hover only lifts.
   ------------------------------------------------------------ */

.card-grid {
  display: grid;
  gap: var(--sn-space-2);
  grid-template-columns: 1fr;
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (min-width: 48rem) { .card-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .card-grid { grid-template-columns: repeat(3, 1fr); } }

.card-program {
  display: flex;
  flex-direction: column;
  gap: 10px;
  text-decoration: none;
  color: var(--sn-ink);
}

/* The coloured half. Only the TITLE lives in here, at display size — which is
   what makes the card legal on orange (3.0:1) and teal (3.1:1) as well as on
   the light grounds. The meta line is small text, so it sits underneath on the
   page background instead. Same structural enforcement as the hero lead:
   the contrast law is expressed in the markup, not left to whoever writes the
   next card (docs/05). */
.card-program__tile {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 208px;
  padding: var(--sn-space-3);
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--g-bg);
  color: var(--g-ink);
  transition: transform var(--dur-fast) var(--ease-tween);
}
.card-program__pop {
  position: absolute;
  right: -40px; top: -46px;
  width: 156px; aspect-ratio: 1;
  border-radius: 999px;
  background: var(--g-pop);
  pointer-events: none;
  transition: transform var(--dur-fast) var(--ease-tween);
}
.card-program__title {
  position: relative;
  font-size: var(--fs-card-title);
  font-weight: var(--sn-w-bold);   /* required: see --fs-card-title */
  line-height: var(--lh-h3);
}
.card-program__meta {
  padding-inline: 4px;
  font-size: var(--fs-caption);
  color: var(--sn-ink);
}

@media (hover: hover) and (pointer: fine) {
  .card-program:hover .card-program__tile { transform: translateY(-4px); }
  .card-program:hover .card-program__pop { transform: scale(1.06); }
}

/* ---------- 3.6b PLAYFUL CARD HOVER (trial) -----------------
   Asked for 2026-08-27: the card lifts and tilts, a white card fans out behind
   it the other way, and sparkles go off. Trialled on the homepage grid, then
   approved and propagated the same day to every .card-grid on the site — the
   two other public ones (/404, /job-matching) and the three internal harness
   pages, so those keep testing what actually ships.

   NO EXTRA MARKUP, and the grid is why. .card-program becomes a two-row grid
   and BOTH pseudo-elements become grid items sharing row 1 with the tile, so
   the ghost card is automatically the tile's exact size — no duplicated
   aspect-ratios to keep in sync with .card-program__tile--art, which declares a
   different one per breakpoint.

   PAINT ORDER IS LOAD-BEARING HERE:
     ::before  ghost  — not positioned, so it paints under everything
     tile             — position: relative already, so it paints above the ghost
     ::after   sparks — position: relative, later in DOM order, so above the tile
   Give the ghost a z-index and this stops working the moment the tile picks up
   a transform (a stacking context), which is exactly how the calendar arrow and
   the journey numbers broke earlier in this build. DOM order does it for free.
   ------------------------------------------------------------ */

.card-grid--playful .card-program {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto;
}
.card-grid--playful .card-program::before,
.card-grid--playful .card-program__tile { grid-area: 1 / 1; }
.card-grid--playful .card-program::after { grid-area: 1 / 1; }
/* NO margin-top HERE. .card-program already sets `gap: 10px`, which a flex
   container and a grid container both honour, so adding a margin stacked on top
   of it: the card grew 264px -> 274px and the tile-to-caption gap doubled to
   20px. Caught by measuring scale-check before and after propagating. */
.card-grid--playful .card-program__meta  { grid-area: 2 / 1; }

/* The ghost. Sits exactly under the tile until the hover fans it out. */
.card-grid--playful .card-program::before {
  content: "";
  border-radius: var(--radius-card);
  background: var(--sn-white);
  box-shadow: 0 18px 38px rgb(25 70 80 / 0.18);
  opacity: 0;
  rotate: 0deg;
  scale: 0.97;
  transition: opacity var(--dur-fast) var(--ease-tween),
              rotate var(--dur-base, 320ms) var(--ease-tween),
              scale var(--dur-base, 320ms) var(--ease-tween),
              translate var(--dur-base, 320ms) var(--ease-tween);
}

/* The sparkles: five four-point stars as background layers on ONE element.
   background-size is a comma list and CSS interpolates it per layer, so each
   star can twinkle on its own schedule without five separate nodes. */
.card-grid--playful .card-program::after {
  content: "";
  position: relative;          /* joins the positioned layer: above the tile */
  pointer-events: none;
  opacity: 0;
  background-repeat: no-repeat;
  background-position: 8% 14%, 88% 8%, 94% 62%, 16% 84%, 62% 4%;
  background-size: 0 0, 0 0, 0 0, 0 0, 0 0;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c1 8 4 11 12 12-8 1-11 4-12 12-1-8-4-11-12-12 8-1 11-4 12-12z' fill='%23FFFFFF'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c1 8 4 11 12 12-8 1-11 4-12 12-1-8-4-11-12-12 8-1 11-4 12-12z' fill='%233976E9'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c1 8 4 11 12 12-8 1-11 4-12 12-1-8-4-11-12-12 8-1 11-4 12-12z' fill='%23FFFFFF'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c1 8 4 11 12 12-8 1-11 4-12 12-1-8-4-11-12-12 8-1 11-4 12-12z' fill='%23E8A70F'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c1 8 4 11 12 12-8 1-11 4-12 12-1-8-4-11-12-12 8-1 11-4 12-12z' fill='%23FFFFFF'/%3E%3C/svg%3E");
}

@keyframes card-sparkle {
  0%   { opacity: 0; background-size:  0    0,    0    0,    0    0,    0    0,    0    0; }
  30%  { opacity: 1; background-size: 20px 20px, 11px 11px, 15px 15px,  8px  8px, 13px 13px; }
  55%  { opacity: 1; background-size: 12px 12px, 17px 17px,  9px  9px, 15px 15px,  7px  7px; }
  80%  { opacity: 1; background-size: 17px 17px,  9px  9px, 14px 14px, 10px 10px, 15px 15px; }
  100% { opacity: 0; background-size:  7px  7px, 14px 14px, 19px 19px,  7px  7px, 11px 11px; }
}

@media (hover: hover) and (pointer: fine) {
  /* Beats the plain .card-program:hover .card-program__tile lift above (0,3,0
     against 0,2,0), so the two do not add up into a double translate. */
  .card-grid--playful .card-program:hover .card-program__tile,
  .card-grid--playful .card-program:focus-visible .card-program__tile {
    transform: translateY(-14px) rotate(-3deg);
  }

  .card-grid--playful .card-program:hover::before,
  .card-grid--playful .card-program:focus-visible::before {
    /* SIZED BY MEASUREMENT, not by eye. At the first values (tile -3deg, ghost
       4.5deg/1.015) the ghost's rotated bounding box came out NARROWER than the
       tile's, so the white card sat entirely hidden behind it and the effect
       did not exist.

       Softened twice on review, 2026-08-27: tile 4deg -> 3deg, ghost 7deg ->
       5deg -> 3deg. The two now turn by the SAME angle in opposite directions,
       6deg apart, and the side clearance is down to ~6px.
       THE SCALE IS WHAT KEEPS IT ALIVE at these gentler angles: 1.03 is the
       reason the ghost still clears the tile by ~10px a side once both are
       barely turned. Drop the scale back and the two boxes converge again and
       the white card disappears — that is the failure this comment exists for,
       so soften the angles further only after re-measuring the clearance. */
    opacity: 1;
    rotate: 3deg;
    scale: 1.03;
    translate: 0 10px;
  }

  .card-grid--playful .card-program:hover::after,
  .card-grid--playful .card-program:focus-visible::after {
    animation: card-sparkle 1.5s var(--ease-tween) infinite;
  }

  /* A tilted card has to sit above its neighbours, or the next card's tile
     covers the corner this one just tilted into. */
  .card-grid--playful .card-program:hover,
  .card-grid--playful .card-program:focus-visible { position: relative; z-index: 2; }
}

/* The whole point of this is motion, so under reduce there is nothing to
   soften — the ghost and the sparkles simply never appear, and the card keeps
   the plain lift it has everywhere else on the site. */
@media (prefers-reduced-motion: reduce) {
  .card-grid--playful .card-program::before,
  .card-grid--playful .card-program::after { display: none; }
  .card-grid--playful .card-program:hover .card-program__tile { transform: translateY(-4px); }
}

/* ---------- 3.6c THE ABOUT PAGE ----------------------------
   Built 2026-08-27 to the Shinta hero Malena pointed at: pill eyebrow, the
   headline owning the left column alone, the supporting sentence dropped to
   the right at body size, and the photograph full width underneath as the
   actual subject.

   The note sits at the BOTTOM of its column (align-items: end), which is what
   makes the two columns read as one block rather than two stacked starts —
   it is the whole trick in that reference.
   ------------------------------------------------------------ */

/* Shinta's eyebrow is a pill. --sn-blue-100 is the kit's tinted-background
   blue and the logo mark is legal on it, so it is the one tint on the palette
   this can be without inventing a colour. Ink on it measures 6.84:1. */
.eyebrow--pill {
  display: inline-block;
  padding: 6px 14px 5px;
  border-radius: 999px;
  background: var(--sn-blue-100);
  color: var(--sn-ink);
}

.about-hero__head { display: grid; gap: var(--sn-space-4); }
.about-hero__title { max-width: 15ch; }

/* Body size, NOT --fs-lead. In the reference this paragraph is deliberately
   quieter than the headline it sits beside; at lead size the two columns
   compete and the hero stops having a subject. */
.about-hero__note {
  font-size: var(--fs-body);
  line-height: var(--sn-lh-body);
  max-width: 46ch;
}

.about-hero__figure {
  margin: var(--sn-space-6) 0 0;
  border-radius: var(--radius-card);
  overflow: hidden;
}
.about-hero__figure img {
  display: block;
  width: 100%;
  /* The file is 3:2. Cropping to a wider band stops one photograph from
     swallowing a whole viewport on a laptop, and keeps the faces centred. */
  aspect-ratio: 16 / 8;
  object-fit: cover;
  object-position: 50% 42%;
}

.about-info { display: grid; gap: var(--sn-space-5); align-items: center; }
.about-info__copy .lead { margin-top: var(--lead-gap); }
.about-info__copy p + p { margin-top: var(--sn-space-3); }
.about-info__ctas { display: flex; flex-wrap: wrap; gap: 14px; margin-top: var(--sn-space-4); }

/* A brand wave, not a cropped corner disc. */
.about-info__art {
  min-height: 220px;
  background: url("/images/wave-gold.svg") center / contain no-repeat;
}

@media (min-width: 56rem) {
  .about-hero__head {
    grid-template-columns: 1.15fr 0.85fr;
    gap: var(--sn-space-6);
    align-items: end;
  }
  .about-hero__figure { margin-top: var(--sn-space-7, 72px); }
  .about-info { grid-template-columns: 1.1fr 0.9fr; gap: var(--sn-space-6); }
  .about-info__art { min-height: 300px; }
}

/* ---------- 3.6d FIGURE ROW (about) ------------------------
   A row of big audited numbers. Deliberately NOT .stat-grid: those were cards,
   and Malena had them taken off the homepage. Here the number IS the object —
   no box, no border, just a rule above each one to group it with its label.

   The value is the logo blue on off-white, which measures 3.90:1 — under the
   4.5 for normal text and comfortably over the 3.0 for large. It is legal
   because the number is 56-80px, and the rule below enforces that: the size
   and the colour are set in the same place so one cannot be changed without
   meeting the other.
   ------------------------------------------------------------ */

.figure-row {
  display: grid;
  gap: var(--sn-space-4);
  margin: var(--sn-space-6) 0 0;
  padding: 0;
  list-style: none;
}

.figure {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: var(--sn-space-3);
  border-top: 2px solid var(--sn-blue-100);
}

.figure__value {
  font-family: var(--font-ui);
  /* Large-text territory at every width — see the contrast note above. */
  font-size: clamp(3.5rem, 2.6rem + 3.2vw, 5rem);
  font-weight: var(--sn-w-bold);
  line-height: 1;
  color: var(--sn-blue-900);
  font-variant-numeric: tabular-nums;
}

.figure__label {
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  font-weight: var(--sn-w-bold);
  color: var(--sn-ink);
}

.figure__note {
  font-size: var(--fs-caption);
  line-height: var(--sn-lh-body);
  color: var(--sn-ink);
  max-width: 34ch;
}

@media (min-width: 40rem) { .figure-row { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .figure-row { grid-template-columns: repeat(4, 1fr); gap: var(--sn-space-5); } }

/* ---------- 3.7 CTA BAND -----------------------------------
   The one dark moment on the page (docs/04). Its headline is the
   only place the campaign-caps variant is allowed to appear.
   ------------------------------------------------------------ */

.cta-band { text-align: center; }
.cta-band h2 {
  font-weight: var(--sn-w-light);
  text-transform: uppercase;
  letter-spacing: var(--ls-campaign);
  margin-inline: auto;
  max-width: 20ch;
}
.cta-band p { margin: var(--sn-space-2) auto 0; }
.cta-band__ctas {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 14px; margin-top: var(--sn-space-4);
}

/* ---------- 3.8 FOOTER -------------------------------------
   Off-white, not navy: the navy CTA band sits directly above it,
   and two dark blocks in a row read as one mass. Fixes carried
   from the audit: the Consortium link goes straight to
   dcworkforceconsortium.com instead of 301-chaining, the privacy
   href is repaired, "Powered by BuildWithin" is on every page,
   and the year is generated rather than frozen at 2024.
   ------------------------------------------------------------ */

.site-footer {
  background: var(--sn-off-white);
  color: var(--sn-ink);
  padding-block: var(--section-pad) var(--sn-space-4);
  overflow: hidden;
}

.site-footer__cols {
  display: grid;
  gap: var(--sn-space-5);
  /* TWO COLUMNS EVEN ON A PHONE. Stacked in one column the footer measured
     1731px at 375 — the four blocks are 214/380/284/378 and they simply queue
     up. Two columns measured 1264px on the same screen, a 467px saving, and it
     is the link lists that pay for it: the labels wrap, but two wrapped columns
     are still far shorter than two full-height ones stacked.

     The 44px tap target is NOT what was traded away to get there — that is a
     docs/09 accessibility floor and it stays at every width. */
  grid-template-columns: repeat(2, 1fr);
}
/* 35% for the brand column and the remaining 65% split three ways (Malena).
   Written as 105fr : 65fr : 65fr : 65fr rather than as percentages: fr divides
   the space that is LEFT after the gaps, so the proportions hold exactly,
   whereas four percentages plus three gaps add up to more than the row and the
   last column gets squeezed. 105/300 = 35%, 65/300 = 21.67%. */
@media (min-width: 64rem) {
  .site-footer__cols { grid-template-columns: 105fr repeat(3, 65fr); }
}

.site-footer h2 {
  font-size: calc(var(--fs-eyebrow) * var(--footer-scale));
  font-weight: var(--sn-w-black);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--sn-ink);
  margin-bottom: var(--sn-space-2);
}
.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li + li { margin-top: 4px; }
/* 44px on touch (docs/09) — footer link lists are the densest tap targets
   on the site. A precise pointer gets the tighter 32px rhythm back, which
   keeps the four-column desktop footer from turning into a tower. */
.site-footer a {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: calc(var(--fs-body) * var(--footer-scale)); text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .site-footer a { min-height: 32px; }
  .site-footer__social a { min-height: 44px; }
}
/* THE CONTACT LIST. The browser's own `dd { margin-inline-start: 40px }` was
   what made this look broken: every number hung off a 40px indent under its
   own label, so four entries read as eight ragged lines.

   One row per entry now. From 64rem the footer is four columns wide enough to
   put the label and the value on the same line — "Phone" plus a US number
   measures about 171px inside a 290px column — so each entry really is on its
   own line. Below that the footer is two columns of roughly 147px, where that
   would wrap mid-number, so label sits above value with no indent. */
.site-footer address { font-style: normal; font-size: calc(var(--fs-body) * var(--footer-scale)); }

.site-footer address dl {
  display: grid;
  gap: 2px;
  margin: 0;
}

.site-footer dd { margin: 0; }

.site-footer dt {
  color: var(--sn-white);
  font-size: calc(var(--fs-caption) * var(--footer-scale));
  opacity: .78;              /* white at 78% on navy is 7.27:1 — past AA */
}

/* THE ICON SLOT IS GONE, and leaving it behind is what Malena just caught.
   This block reserved 1.55em on every label so the two that had an icon and the
   two that did not would still line up. The icons were then removed from the
   markup — but this rule survived the edit, so the slot stayed as pure
   indentation with nothing in it, and the four labels sat 21px right of the
   "Contact" heading above them. Measured: heading ink at 1038, label ink at
   1059. With no icons there is nothing to reserve room for: the labels start
   where the heading starts. */

@media (min-width: 64rem) {
  .site-footer address dl {
    grid-template-columns: auto 1fr;
    gap: 6px var(--sn-space-2);
    align-items: baseline;
  }
}


/* THE 4px THAT MISALIGNED THE SOCIAL ICONS.
   `.site-footer li + li { margin-top: 4px }` above is written for the vertical
   link columns, but it matches ANY footer li that follows another — including
   this horizontal row, so the second circle sat exactly 4px lower. Measured:
   li tops 5787 and 5791, margin-top 0px and 4px.

   Two fixes, deliberately both: the margin is cancelled where it does not
   belong, and align-items: center makes the row robust against any future cause
   of a height difference rather than only against this one. */
/* DOUBLE CLASS, AND IT IS NOT DECORATION. `.site-footer ul { margin: 0 }` above
   is (0,1,1); a bare `.site-footer__social` is (0,1,0), so the reset won on
   specificity — not on order — and silently zeroed this margin. The change
   looked applied in the file and measured 0px in the browser.

   Third time a broad `.site-footer X` rule has quietly beaten a specific one in
   this footer: the li+li margin that staggered the icons, the a:hover underline,
   and now this. Anything scoped to a footer component needs to outrank the
   element-level resets. */
.site-footer .site-footer__social {
  display: flex;
  align-items: center;
  gap: 10px;
  /* 32px, up from 16. The buttons are a different kind of thing from the phone
     and email rows above them and need to read as their own group — and 16px
     barely cleared the 13px of padding each circle carries inside itself, which
     is the proximity rule this build keeps relearning: a gap has to beat the
     internal padding of what sits next to it or the eye groups them together. */
  margin-top: var(--sn-space-4);
}

.site-footer__social li + li { margin-top: 0; }
.site-footer__social a {
  width: 44px; height: 44px;
  display: inline-grid; place-items: center;
  border-radius: 999px;
  border: var(--hairline);
}
.site-footer__social svg { width: 18px; height: 18px; fill: currentColor; }

.site-footer__legal {
  display: flex; flex-wrap: wrap; gap: var(--sn-space-1) var(--sn-space-3);
  align-items: center;
  margin-top: var(--section-pad);
  padding-top: var(--sn-space-3);
  border-top: var(--hairline);
  font-size: calc(var(--fs-caption) * var(--footer-scale));
  color: var(--sn-ink);
}
.site-footer__legal a { font-size: calc(var(--fs-caption) * var(--footer-scale)); }

/* Decorative wordmark, aria-hidden in the markup. Clipped by the
   footer's overflow so it reads as a watermark, not a headline. */
.site-footer__wordmark {
  display: none;
  margin-top: var(--sn-space-5);
  font-size: clamp(5rem, 15vw, 13rem);
  font-weight: var(--sn-w-light);
  line-height: .8;
  letter-spacing: var(--sn-ls-display);
  color: var(--sn-grey-200);
  user-select: none;
}
@media (min-width: 64rem) { .site-footer__wordmark { display: block; } }

@media (hover: hover) and (pointer: fine) {
  /* The underline Malena disliked lived here, from the pale-footer era — along
     with --sn-link, which measures 1.93:1 on the new dark ground and would have
     been an invisible hover. The hover is a colour lift only; see §6.x. */
}


/* ---------- 3.9 PROSE --------------------------------------
   Running copy inside a band. Capped at --measure so a 1280px
   container never produces 200-character lines.
   ------------------------------------------------------------ */

.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--sn-space-2); }
.prose ul, .prose ol { margin: 0; padding-left: 1.25em; }
.prose li + li { margin-top: 6px; }
.prose a { color: var(--sn-link); }

.lead { max-width: 56ch; }

/* ---------- 3.10 FACTS STRIP -------------------------------
   The <dl> under every program hero. A fact the audit does not
   state renders no row at all — never a placeholder or a dash.
   This block also feeds the EducationalOccupationalProgram
   JSON-LD in Phase 10.
   ------------------------------------------------------------ */

.facts {
  display: grid;
  gap: var(--sn-space-3);
  margin: 0;
  grid-template-columns: 1fr;
}
@media (min-width: 48rem) { .facts { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .facts { grid-template-columns: repeat(4, 1fr); } }

.facts__row { border-top: var(--hairline); padding-top: var(--sn-space-2); }
.facts dt {
  font-size: var(--fs-eyebrow);
  font-weight: var(--sn-w-black);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--sn-ink);
}
.facts dd { margin: 8px 0 0; font-size: var(--fs-body); }

/* ---------- 3.11 AUDIENCE / TRACK CARDS --------------------- */

ol.audience-grid { margin: 0; padding: 0; list-style: none; }

.audience-grid {
  display: grid;
  gap: var(--sn-space-2);
  grid-template-columns: 1fr;
}
/* THE COLUMN COUNT FOLLOWS THE CARD COUNT. Hardcoded at three, a two-card grid
   left a third of the row empty and made both cards narrower than they needed
   to be — which is what happened to "Who benefits" on /pre-apprenticeship.

   :has(> :nth-child(N):last-child) reads as "the grid whose Nth child is also
   its last", i.e. exactly N cards. Two and four both lay out in two columns;
   three and five keep three. Fixing it here rather than with a modifier on one
   page means the next two-card grid cannot inherit the same hole.

   Without :has() support this falls back to three columns, which is today's
   behaviour — a worse layout, never a broken one. */
@media (min-width: 64rem) {
  .audience-grid { grid-template-columns: repeat(3, 1fr); }
  .audience-grid:has(> :nth-child(2):last-child),
  .audience-grid:has(> :nth-child(4):last-child) { grid-template-columns: repeat(2, 1fr); }
}

/* The pastel-tint version of these cards was rejected. This is the Consortium's
   "Key strategies include:" treatment instead, in the Pathways programme blue:
   white card, a big numeral that FILLS from the bottom as you arrive, and a
   border that draws itself while the card lifts.

   THE FILL IS A GRADIENT WINDOW, not a colour swap. The numeral is painted with
   a two-stop gradient twice the height of its own box, clipped to the glyph;
   moving background-position from 0% to 100% slides the hard boundary up
   through the digits, so they fill progressively rather than changing colour
   all at once. A plain `color` transition cross-fades and reads as a fade, not
   a fill — which is the whole difference Malena is asking for. */

/* One attribute carries the programme colour, so putting these cards on another
   programme page is a single value change rather than a new block. --aud is the
   Pathways hero ground; --aud-pale is the kit's tinted blue. */
.audience-grid[data-accent="pathways"]   { --aud: #153E6E; --aud-pale: #BCD3FF; }
/* Out-of-School. TWO GOLDS, and the split is what lets the vivid one be used.

   --aud is #E8A70F exactly: the programme's hero ground, the same yellow the
   form section below is painted in. Malena asked for that one specifically, and
   it measures 2.11:1 on white — which would be illegal for text.

   IT IS NOT TEXT. Every numeral carries aria-hidden, so none of them is exposed
   to assistive tech, and WCAG's contrast rules exempt pure decoration. Checked
   per section rather than assumed: What to expect is an <ol>, so its sequence
   lives in the markup and the digits only restate it; Tracks is a <div> of
   three PARALLEL tracks where 01/02/03 never carried order in the first place.
   Either way the digits inform nothing.

   Drop an aria-hidden and that stops being true, at which point this has to go
   back to a darker gold: #C18B0D is the 3:1 minimum for text this size,
   #9A6F0A the 4.5:1 one.

   --aud-ink carries the 12px label, which IS informative text and does have to
   pass — hence a second, darker value rather than one shared gold.

   The pale step is the kit's own outofschool-100, not a lightened gold: mixing
   this hue toward white turns it muddy, and the kit already ships a tint that
   does not. */
/* The audience cards read --aud; the occ/doc components read --acc. Rather than
   rename either, the grid maps one onto the other for the programmes that use
   both on the same page. */
.audience-grid[data-accent="preapp"] { --aud: #FF5E57; --aud-ink: #CF453E; --aud-pale: var(--sn-p-preapp-100); }

.audience-grid[data-accent="outofschool"] {
  --aud: #E8A70F;        /* the form section's yellow — decorative numeral + border */
  --aud-ink: #8A6309;    /* 5.43:1 — the 12px label, which is real text */
  --aud-pale: var(--sn-p-outofschool-100);
}

.audience {
  position: relative;
  padding: var(--sn-space-4) var(--sn-space-3) var(--sn-space-3);
  border-radius: var(--radius-card);
  box-shadow: 0 2px 6px rgb(25 70 80 / 0.05);
}

/* BOTH OF THESE LIVE ON [data-tone] BECAUSE OF ONE RULE ELSEWHERE IN THIS
   SHEET. `.benefit, .audience, .contact-list li, .logo-wall li, .quality-grid
   li, .link-card, .form-success { background: var(--sn-white); border: 0 }`
   matches .audience at the same (0,1,0) weight and sits later, so anything
   `background` or `border` set on the bare class is silently discarded — it ate
   the tint first and then the border. (0,2,0) settles it. Check that rule before
   adding any box property to .audience.

   The border exists AT REST and is transparent, so hovering changes a colour
   rather than adding a box: adding one on hover shifts every glyph inside by
   1.5px, which reads as the text jumping. */
.audience[data-tone] {
  background: var(--sn-white);
  border: 1.5px solid transparent;
}

/* PORTED FROM Consortium/public/css/styles.css .card__index, read rather than
   reinvented. My first attempt had the right mechanism and the wrong details,
   and one of them is what actually broke it:

   1. width: max-content — THE ONE THAT MATTERED. Without it the gradient is
      sized to the whole block, so the 50% boundary sits somewhere off past the
      end of the digits and the sweep looks misplaced instead of filling them.
      Their comment calls it imprescindible; it is.
   2. -webkit-text-fill-color: transparent alongside color: transparent, or
      WebKit keeps painting the solid colour over the clipped gradient.
   3. The clip lives INSIDE the hover media query, and the resting colour is
      the solid accent. On a touch device there is no hover to complete the
      fill, so a numeral left in the pale tone would just look washed out
      forever. Touch gets the solid number and no effect.

   Timings are theirs too: 550ms for the fill, 350ms for the lift. */
.audience__num {
  display: block;
  width: max-content;
  font-family: var(--font-ui);
  font-size: clamp(2.5rem, 1.97rem + 2.25vw, 4rem);
  font-weight: var(--sn-w-bold);
  line-height: 1;
  letter-spacing: -0.02em;
  margin-bottom: var(--sn-space-3);
  color: var(--aud);
}

/* ---- TRACKS: NO NUMERALS ----------------------------------
   The filling numeral came off this section first, then the numerals went
   entirely (Malena, 2026-08-27), and the second step followed from the first:
   once a number is not worth animating it is usually not worth printing. LEAP,
   PATH and PACE are three PARALLEL tracks — 01/02/03 implied a sequence that
   does not exist and took the top of every card to say nothing. What to expect
   keeps its numerals and its fill, because those really are steps in order.

   THE EXPANSION MOVED, for a different reason than the styling. On Pathways the
   small label is an AUDIENCE ("Prospective apprentices") — a category, which
   belongs above the heading it categorises. Here it is the expansion of an
   acronym: it defines LEAP rather than filing it, so it reads under the name.

   And it loses the uppercase and the letter-spacing entirely. That treatment is
   built for two or three words; at five it wrapped to two tracked-out lines and
   outweighed the acronym it was supposed to explain — the "espaciado sin
   sentido". */

.audience__expand {
  margin-top: 4px;
  /* 15px, not --fs-caption's 13. Caption size is for asides you can skip; this
     line is the acronym's meaning, which is the second thing anyone reads on
     the card. Deliberately between caption and --fs-body's 17 so it still sits
     below the paragraph in the ranking rather than tying with it. */
  font-size: 0.9375rem;
  font-weight: var(--sn-w-regular);
  line-height: var(--sn-lh-body);
  color: var(--aud-ink, var(--sn-ink));
}
.audience__expand + .audience__kicker { margin-top: var(--sn-space-3); }

@media (hover: hover) and (pointer: fine) {
  @supports ((-webkit-background-clip: text) or (background-clip: text)) {
    .audience__num {
      background-image: linear-gradient(to top, var(--aud) 50%, var(--aud-pale) 50%);
      background-size: 100% 200%;
      background-position: 0 0%;
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
      color: transparent;
      transition: background-position 550ms var(--ease-tween);
    }
    .audience:hover .audience__num { background-position: 0 100%; }
  }

  .audience:hover {
    /* `transform`, NOT `translate` — and this is the opposite of the choice the
       Consortium made, for the opposite reason. There `.reveal.in` sets
       `transform: none`, so transform lost and translate was safe. Here
       `.js [data-reveal].in` sets `translate: 0 0`, so translate is the one
       that would be overwritten and transform is the safe property. Same trap,
       mirrored — check which one the reveal owns before picking. */
    transform: translateY(-6px);
    border-color: var(--aud);
    /* Same fix as .occ__card: this was Pathways' navy written literally, so
       Out-of-School and Pre-Apprenticeship glowed navy. */
    box-shadow: 0 18px 38px rgb(25 70 80 / 0.16);
    box-shadow: 0 18px 38px color-mix(in srgb, var(--aud) 20%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .audience:hover { transform: none; }
}

/* THREE LEVELS, AND THEY HAVE TO LOOK LIKE THREE. Before this the audience
   label, the heading and the kicker were all near the same weight and the
   spacing ran the wrong way round — the label was jammed under the heading and
   the kicker floated 16px below, so the eye read four loose lines instead of a
   ranked block.

   Now: the label is a small tag ABOVE the heading (it names who the card is
   for, which is the card's actual subject), the heading is the one heavy thing,
   and the kicker is plain body text.

   MEASURED, not intended: 600 / 700 / 400 at 12px / 22px / 17px. The label's
   600 is not mine — `.eyebrow, .facts dt, .site-footer h2, .audience__who`
   further down sets the UI font at --w-title and wins on source order. That
   rule is RIGHT: it is the eyebrow treatment every small caps label on this
   site uses, and Open Sauce at 600 sits heavier than Red Hat at 600 would. So
   no font-weight is declared here at all rather than one that never applies.
   (Fifth time this sheet's grouped component rules have quietly outranked a
   local declaration — check for one before adding any type property.)

   The three levels separate by SIZE and COLOUR as much as weight, which is what
   survives a reader who is not comparing them side by side. */
.audience__who {
  margin: 0 0 4px;
  font-size: 0.75rem;
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--aud-ink, var(--aud));
}

.audience h3 {
  font-size: var(--fs-h3);
  font-weight: var(--sn-w-bold);
  line-height: var(--lh-h3);
  color: var(--sn-ink);
}

.audience__kicker {
  margin-top: 12px;
  font-weight: var(--sn-w-regular);
  line-height: var(--sn-lh-body);
  color: var(--sn-ink);
}

/* ---------- 3.11b DETAIL CARDS -----------------------------
   Replaced the two-row accordion on /pathways 2026-08-27. An accordion asks a
   reader to guess which row holds what they need; these two things — do I
   qualify, and what do I have to send — are the whole reason someone is on that
   part of the page, so both are open and side by side.

   Two columns, but the grid takes a third without any change if the missing
   "Program Components" copy ever arrives.
   ------------------------------------------------------------ */

.detail-cards {
  display: grid;
  gap: var(--sn-space-3);
  margin-top: var(--sn-space-5);
}
@media (min-width: 56rem) { .detail-cards { grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); } }

.detail-card {
  position: relative;
  padding: var(--sn-space-4) var(--sn-space-3) var(--sn-space-3);
  border-radius: var(--radius-card);
  background: var(--sn-white);
  border: var(--hairline);
  /* A RESTING SHADOW, AND IT IS THE POINT. Going from `none` to a 34px shadow
     is what actually read as a pop — the card was not lifting, a shadow was
     appearing. Starting from a faint one means the hover DEEPENS a shadow that
     is already there, which is what a lift looks like. */
  box-shadow: 0 2px 6px rgb(25 70 80 / 0.05);
  transition: transform var(--dur-lift-out) var(--ease-lift),
              box-shadow var(--dur-lift-out) var(--ease-lift),
              border-color var(--dur-lift-out) var(--ease-lift);
}

.detail-card__icon {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background: var(--sn-blue-100);
  color: var(--sn-ink);
  margin-bottom: var(--sn-space-2);
}
.detail-card__icon .icon { width: 20px; height: 20px; }

.detail-card__title { font-size: var(--fs-h3); color: var(--sn-ink); }
.detail-card__title + p, .detail-card__title + ul { margin-top: var(--sn-space-2); }

.detail-card__list { margin: 0; padding: 0; list-style: none; }
.detail-card__list li {
  position: relative;
  padding-left: 26px;
  line-height: var(--sn-lh-body);
}
.detail-card__list li + li { margin-top: 10px; }
/* A tick per row, drawn in CSS so the list needs no extra markup. */
.detail-card__list li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 0.55em;
  width: 9px;
  height: 5px;
  border-left: 2px solid var(--sn-blue-900);
  border-bottom: 2px solid var(--sn-blue-900);
  transform: translateY(-50%) rotate(-45deg);
}

@media (hover: hover) and (pointer: fine) {
  .detail-card:hover {
    /* Enter faster than it leaves — see the token note. */
    transition-duration: var(--dur-lift);
    transform: translateY(-5px);
    box-shadow: 0 18px 38px rgb(25 70 80 / 0.13);
    border-color: var(--sn-blue-400);
  }
  .detail-card:hover .detail-card__icon { background: var(--sn-blue-900); color: var(--sn-white); }
  .detail-card__icon { transition: background var(--dur-lift-out) var(--ease-lift), color var(--dur-lift-out) var(--ease-lift); }
  .detail-card:hover .detail-card__icon { transition-duration: var(--dur-lift); }
}

@media (prefers-reduced-motion: reduce) {
  .detail-card { transition: none; }
  .detail-card:hover { transform: none; }
}

/* ---------- HOVER TRANSITIONS THAT SURVIVE [data-reveal] ----
   THIS IS WHY THE HOVER SNAPPED. Both card types carry [data-reveal], and
   `.js [data-reveal]` declares `transition: opacity, translate, scale` — one
   `transition` property, so it REPLACES the list rather than adding to it. That
   rule is (0,2,0) and sits later in the sheet than the components, so it won it
   and the cards had no transform or box-shadow transition at all: the lift was
   instantaneous. Malena read it exactly right — "se levanta de una y muy
   rápido" — and no amount of tuning the duration on the losing rule could have
   changed it.

   So the list is declared ONCE here, at (0,3,0), covering both jobs: the
   reveal's three properties on the reveal's timing, and the hover's three on
   the slower lift timing. Leaving is slower than entering, which is what makes
   a lift feel like it settles rather than drops.

   ADD NEW HOVER-LIFTING COMPONENTS TO THE SELECTOR LIST BELOW. Nowhere else.
   This warning was already written here on 2026-08-27 and then walked past the
   same day: .occ__card and .doc were given their own `transition` in their own
   block, lost to [data-reveal] exactly as described, and shipped with a hover
   that snapped from 0 to 100 — the identical bug, reported again by Malena.

   The tell is always the same: the hover state IS correct, the lift just has no
   easing. If that is what you are looking at, the transition is being
   overwritten; do not tune durations on the losing rule.
   ------------------------------------------------------------ */
.js .audience[data-reveal],
.js .detail-card[data-reveal],
.js .occ__card[data-reveal],
.js .doc[data-reveal] {
  transition:
    opacity   var(--dur-med)      var(--ease-out),
    translate var(--dur-med)      var(--ease-out),
    scale     var(--dur-med)      var(--ease-out),
    transform  350ms var(--ease-tween),
    box-shadow 350ms var(--ease-tween),
    border-color var(--dur-fast) var(--ease-tween);
}
.js .audience[data-reveal]:hover,
.js .detail-card[data-reveal]:hover,
.js .occ__card[data-reveal]:hover,
.js .doc[data-reveal]:hover { transition-duration: var(--dur-med), var(--dur-med), var(--dur-med), 350ms, 350ms, 350ms; }

/* ---------- ELIGIBILITY SPLIT (out-of-school) ---------------
   "Who we serve" and "Eligibility" were two text-only bands that overlapped;
   merged 2026-08-27. Two columns because the halves are read differently: the
   left is prose you read once to see if this is for you, the right is a list
   you scan for your own situation. Stacked, nine statutory bullets buried the
   summary above them.

   The statutory list keeps its own panel with a tinted ground, so it reads as a
   reference block rather than as more body copy — which is what stops a
   text-only section from being a wall.
   ------------------------------------------------------------ */

.elig { display: grid; gap: var(--sn-space-5); align-items: start; }
@media (min-width: 56rem) {
  .elig { grid-template-columns: 1fr 1.05fr; gap: var(--sn-space-6); }

  /* THE LEFT COLUMN TRAVELS. The statutory list on the right runs to nine
     bullets and roughly 700px; the summary beside it is four short lines, so
     the column simply ran out and left a hole for most of the section's height.

     Sticky rather than align-items: center — centring only splits the same
     void in two, and this way the plain-English summary stays on screen while
     you scan the legal list it is summarising, which is the one moment the two
     halves need to be read together.

     The offset clears the floating bar. Nothing here sets overflow, which is
     what sticky needs — see the note in the How-it-works section about
     `overflow: hidden` silently killing it. */
  .elig__intro {
    position: sticky;
    top: calc(var(--bar-space) + var(--sn-space-3));
    align-self: start;
  }
}

.elig__intro .lead { margin-top: var(--lead-gap); }
/* Was a bar in the programme colour down the left. Removed 2026-08-27 with the
   others — see the note by .checks__note. It is an aside, so it reads as one by
   being smaller and set apart, not by being flagged. */
.elig__note {
  margin-top: var(--sn-space-3);
  font-size: var(--fs-caption);
  color: var(--sn-ink);
}

.elig__panel {
  padding: var(--sn-space-4);
  border-radius: var(--radius-card);
  /* Pure white, asked for 2026-08-27. The section itself is transparent over
     the off-white page, so white still separates the panel from the column
     beside it — but only by 10 luminance points, which is the same near-invisible
     boundary that made a card read as part of the text under it earlier in this
     build. Hence the hairline: the edge is drawn, not left to the fill. */
  background: var(--sn-white);
  border: var(--hairline);
}
/* .checks is two columns on its own; inside a half-width .elig column that
   leaves ~4 words a line. One column in here. */
.elig .checks { grid-template-columns: 1fr; margin-top: var(--sn-space-3); }

/* White, not the purple wash — asked for 2026-08-27. On the off-white section
   that is only ~10 luminance points of separation, so the panel is drawn with a
   hairline instead of relying on the fill, the same way the statutory panel on
   /out-of-school-program is. */
.elig--it .elig__panel { background: var(--sn-white); border: var(--hairline); }

/* NO COLOURED BAR beside the note, and no bold. It had both, which made a
   secondary aside the heaviest thing in the panel — louder than the two
   criteria above it that the reader is actually checking themselves against.
   A rule ACROSS the top separates it without competing; the label names it. */
.elig--it .checks__note {
  margin-top: var(--sn-space-4);
  padding: var(--sn-space-3) 0 0;
  border-left: 0;
  border-top: var(--hairline);
  font-size: 0.9375rem;
  font-weight: var(--sn-w-regular);
  color: var(--sn-ink);
}
.checks__note-label {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.75rem;
  font-weight: var(--w-title);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--acc-ink, var(--sn-ink));
  margin-bottom: 4px;
}

.elig__title {
  font-size: var(--fs-h3);
  font-weight: var(--sn-w-bold);
  color: var(--sn-ink);
  margin-bottom: var(--sn-space-2);
}
/* Scoped to the statutory lead-in ("An OSY is an individual who is:") on
   /out-of-school-program, which is an UNCLASSED <p>. :first-of-type was the
   first attempt and was wrong for a subtle reason: in the /it-accelerator panel
   the schedule note is the only <p> there is, so it matched and the aside came
   out bolder than the criteria above it — the exact problem the scoping was
   meant to fix. `p:not([class])` keys on what actually separates the two. */
.elig__panel > p:not([class]) { font-weight: var(--sn-w-bold); }
.elig__panel ul { margin: var(--sn-space-2) 0 0; padding-left: 1.1em; }
.elig__panel li { line-height: var(--sn-lh-body); }
.elig__panel li + li { margin-top: 8px; }
.elig__panel ul ul { margin-top: 8px; }

/* ---------- 3.11c OCCUPATIONS / CHECKS / DOC (youth) --------
   Three sections on /youth-apprenticeship that were prose and lists.

   ONE VALUE DOES BOTH JOBS NOW, and that is the point of the new palette. This
   pair used to be #ED7133 for icons and a darkened #CA4F12 for small text,
   because the orange was 3.00:1 on white — legal for large text and UI,
   illegal for anything small. DOES replaced the palette on 2026-09-08 and
   #215E99 measures 6.72:1, so --acc and --acc-ink are the same colour and
   there is nothing to keep in sync. If a future palette drops below 4.5:1 on
   white, split them again.

   --acc-pop is the third colour, #C00001 at 6.48:1 on white. It is NOT
   interchangeable with the other two: on the 900 blue it measures 1.04:1, so
   it belongs on white and light grounds only.
   ------------------------------------------------------------ */

[data-accent="youth"] {
  --acc: #215E99;
  --acc-ink: #215E99;
  --acc-wash: var(--sn-p-youth-100);
  --acc-pop: var(--sn-p-youth-pop);
}


/* Pre-Apprenticeship. Its ground is #FF6A63 at 2.80:1 on white — under the 3:1
   a hover border wants, so --acc is nudged to #FF5E57 (3.00), visually the same
   coral. --acc-ink is NOT the same hue darkened: straight darkening reaches 4.5
   at #ED0B00, a fire-engine red that leaves the coral family entirely. Dropping
   saturation 40% as well lands on #CF453E at 4.59 — deep coral, same family. */
[data-accent="preapp"] { --acc: #FF5E57; --acc-ink: #CF453E; --acc-wash: var(--sn-p-preapp-100); }

/* IT Accelerator is the ONLY programme here that needs one value rather than
   two: #7765C0 measures 4.77:1 on white, so the same purple is legal for the
   12px label and for the icons. The kit ships no purple tint, so the wash is
   that purple at 12% on white, written out rather than mixed at runtime. */
[data-accent="it"] { --acc: #7765C0; --acc-ink: #7765C0; --acc-wash: #EFEDF8; }

/* Pharmacy. Its ground #6C92FF is 2.92:1 on white — just under the 3:1 a border
   wants and well under 4.5, so this needs the two-value split like the others.
   The kit ships no tint in this blue either, so the wash is written out. */
[data-accent="pharmacy"] { --acc: #688FFF; --acc-ink: #366AFF; --acc-wash: #EDF2FF; }

/* Job Matching. Like IT, one value does both jobs: #1D77B8 is 4.79:1 on white. */
[data-accent="jobs"] { --acc: #1D77B8; --acc-ink: #1D77B8; --acc-wash: #E8F2F9; }
[data-accent="college"] { --acc: #3BB99C; --acc-ink: #157A63; --acc-wash: var(--sn-p-college-100); }
[data-accent="pathways"] { --acc: #153E6E; --acc-ink: #153E6E; --acc-wash: var(--sn-blue-100); }
[data-accent="outofschool"] { --acc: #E8A70F; --acc-ink: #8A6309; --acc-wash: var(--sn-p-outofschool-100); }
[data-ground="jobs"] { --g-bg: #1D77B8; --g-ink: #FFFFFF; --g-pop: #BCD3FF; }

/* ---- occupations ---- */
.occ { display: grid; gap: var(--sn-space-3); margin-top: var(--sn-space-5); }
@media (min-width: 48rem) { .occ { grid-template-columns: repeat(2, 1fr); gap: var(--sn-space-4); } }
/* Six cards rather than two: a slightly tighter gap keeps the pairs reading as
   rows instead of drifting apart down the page. */
@media (min-width: 48rem) { .occ--two { gap: var(--sn-space-3); } }
/* AN ODD LAST CARD SPANS BOTH COLUMNS instead of sitting alone beside a hole.
   Pre-Apprenticeship's quality elements went from six to seven when DOES's own
   document replaced the Department of Labor's generic list, and the seventh —
   employer and industry partnerships — reads perfectly well as the wide closing
   card. Count-aware rather than a modifier class so the layout follows the
   content: :last-child AND :nth-child(odd) is only true when the total is odd,
   so the four-card Pharmacy grid and the two-card grids are untouched.
   Scoped to --two: .occ--three does its own thing with three. */
@media (min-width: 48rem) {
  .occ--two > .occ__card:nth-child(odd):last-child { grid-column: 1 / -1; }
}
/* Explicit rather than counting children like .audience-grid does: .occ already
   carries a two-card grid (Youth) and a six-card one (Pre-Apprenticeship) that
   are both meant to be two columns, so counting would change layouts already
   approved. */
@media (min-width: 64rem) { .occ--three { grid-template-columns: repeat(3, 1fr); } }
.occ__body { margin-top: var(--sn-space-2); line-height: var(--sn-lh-body); color: var(--sn-ink); }

.occ__card {
  padding: var(--sn-space-4) var(--sn-space-3);
  border-radius: var(--radius-card);
  background: var(--sn-white);
  border: 1.5px solid transparent;
  box-shadow: 0 2px 6px rgb(25 70 80 / 0.05);
}
.occ__icon {
  display: grid; place-items: center;
  width: 48px; height: 48px;
  border-radius: 999px;
  background: var(--acc-wash);
  color: var(--acc-ink);
  margin-bottom: var(--sn-space-3);
}
.occ__icon .icon { width: 22px; height: 22px; }
.occ__title { font-size: var(--fs-h3); font-weight: var(--sn-w-bold); color: var(--sn-ink); }
.occ__lead {
  margin-top: 4px;
  font-size: 0.9375rem;
  color: var(--acc-ink);
}

/* The chips carry the colour; the text inside them stays ink, so the tint never
   has to hold small text on its own. */
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: var(--sn-space-2) 0 0; padding: 0; list-style: none; }
/* SOFTER FILL, asked for 2026-08-27. --acc-wash straight (#FFE4D6) plus a
   full-strength --acc outline made eight small pills the loudest thing in the
   section — louder than the two headings above them, when they are examples.
   Both are mixed back toward white: the chip keeps its identity as orange
   without competing with the card it sits in.

   The fallbacks matter here rather than being ceremony: without color-mix these
   fall back to the ORIGINAL values, which is the loud version, not a broken
   one. color-mix is already used elsewhere in this sheet (the application
   calendar), so this is not new ground. */
.chips li {
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--acc-wash);
  background: color-mix(in srgb, var(--acc-wash) 45%, var(--sn-white));
  border: 1px solid var(--acc);
  border-color: color-mix(in srgb, var(--acc) 34%, var(--sn-white));
  font-size: 0.9375rem;
  color: var(--sn-ink);
}

.occ__more { margin-top: var(--sn-space-4); font-size: var(--fs-body); color: var(--sn-ink); }

/* THE TWO PARAGRAPHS IN THIS SECTION RUN WIDER THAN THE SITE MEASURE.
   Malena, 2026-09-11: "Que llenen el 85% del espacio disponible a lo ancho" —
   the occupations lead and the apprenticeship.gov note.

   WHAT WAS HOLDING THEM IN, measured at a 1440 viewport where the container
   gives 1232px of usable width:

     .occ__more   765.3px (62.1%)  <- p { max-width: var(--measure) }, 68ch
     .lead        704.4px (57.2%)  <- .lead { max-width: 56ch }

   Both now land on 1047.2px. Note they were capped by DIFFERENT rules at
   different specificities, which is why the selectors below are not symmetrical:
   .occ__more only has to beat the bare `p` (0,0,1), but the lead has to beat
   .lead (0,1,0), so it is qualified down through .band__head.

   ONLY FROM 64rem UP, following the download cards, which took 80% on desktop
   and 100% below. Percentages cut the wrong way on a phone — 85% of a 335px
   column is a 50px strip of nothing down one side. The crossover where 85%
   stops being wider than the natural cap is around a 948px viewport for
   .occ__more and 876px for the lead, so 64rem clears both.

   TWO SELECTORS OF SCOPE, and both were earned by getting it wrong first.

   :has(.occ) alone is not the occupations section — it is FOUR of them. IT
   Accelerator, Pharmacy Technician and Pre-Apprenticeship carry the same block,
   and the first version of this rule silently widened a lead on Pre-Apprentice-
   ship that nobody asked about. So it is qualified by [data-program="youth"],
   the marker build-pages.js puts on <body>.

   And .band__head .lead alone is not this lead — "Who can apply" further down
   the same page has one too. Hence the .band:has(.occ) in the middle, which
   picks the one section on the one page that was actually asked for.

   If the other three occupations sections should match, drop the
   [data-program] qualifier and they all follow. */
@media (min-width: 64rem) {
  [data-program="youth"] .band:has(.occ) .occ__more,
  [data-program="youth"] .band:has(.occ) .band__head .lead { max-width: 85%; }
}

/* display: inline-block IS THE FIX, and it is the site-wide reset that makes it
   necessary: `img, svg, video { display: block }` at the top of this sheet turns
   an inline icon into a block, which forces a line break before it. Same reset
   that put the alert triangle on its own line above every form error. Setting
   width, height and vertical-align without setting display looks like it should
   work and does not.

   nowrap on the wrapper keeps the icon glued to the word it belongs to, so the
   link never breaks as "…apprenticeship.gov" / "[icon]" across two lines. */
.occ__more .icon,
.doc__meta .icon {
  display: inline-block;
  width: 0.9em;
  height: 0.9em;
  vertical-align: -0.12em;
  margin-left: 3px;
}
.u-nowrap { white-space: nowrap; }

/* ---- eligibility checklist ---- */
.checks {
  display: grid;
  gap: var(--sn-space-3);
  margin: var(--sn-space-5) 0 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 48rem) { .checks { grid-template-columns: repeat(2, 1fr); gap: var(--sn-space-3) var(--sn-space-5); } }

/* THE TEXT MUST BE ONE ELEMENT. This is a grid with two columns, and a grid
   turns each contiguous run of bare text into its own ANONYMOUS grid item. So
   the moment an inline tag appeared in the copy — a <strong> on the condition —
   the line split into three items: mark, <strong>, and the rest. Three items in
   two columns wrapped the remainder into the 28px column, and it came out one
   word per line.

   .checks__text makes the count fixed at two whatever the copy contains. Do not
   put text directly in .checks__item. */
.checks__item {
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: 12px;
  align-items: start;
  line-height: var(--sn-lh-body);
  color: var(--sn-ink);
}
.checks__text { min-width: 0; }
/* The tick is drawn, not an icon font or an image: two borders rotated. */
.checks__mark {
  position: relative;
  width: 28px; height: 28px;
  border-radius: 999px;
  background: var(--acc-wash);
}
.checks__mark::after {
  content: "";
  position: absolute;
  left: 9px; top: 12px;
  width: 10px; height: 5px;
  border-left: 2px solid var(--acc-ink);
  border-bottom: 2px solid var(--acc-ink);
  transform: rotate(-45deg);
}

/* NO COLOURED LEFT RULE — a habit Malena has now asked to break four times
   (the handbook card, this note, the /it-accelerator schedule note, and the
   .approach panel on /job-matching). A vertical accent bar is the reflex reach
   for "this bit is different", and it is not part of this design language.

   What separates a secondary block here instead: size, weight, a hairline
   ACROSS the top, or a container. Not a stripe down the side. */
.checks__note {
  margin-top: var(--sn-space-4);
  padding-top: var(--sn-space-3);
  border-top: var(--hairline);
  font-size: var(--fs-body);
  color: var(--sn-ink);
}

/* ---- document pack (pharmacy WIOA) ------------------------
   The eight WIOA proofs. Panelled rather than left loose on the band because
   the two lists on this page do different jobs: the admission criteria above
   are conditions you either meet or do not, read once; these are a pack you
   physically go and assemble before turning up. A container says "collect
   these" in a way a bare list does not — and it keeps the two from reading as
   one long undifferentiated set of ticks.

   Two columns at width: eight single-line items stacked make a column of ticks
   taller than the section around it. */
.pack {
  margin-top: var(--sn-space-5);
  padding: var(--sn-space-4);
  border-radius: var(--radius-card);
  background: var(--sn-white);
  border: var(--hairline);
}
.pack__lead {
  font-weight: var(--sn-w-bold);
  color: var(--sn-ink);
  max-width: var(--measure);
}
.checks--pack { margin-top: var(--sn-space-4); }
@media (min-width: 48rem) { .checks--pack { grid-template-columns: repeat(2, 1fr); } }

/* ---- the handbook ----
   Third version. A row was too slight; a drawn page with a folded corner was
   rejected. This one adds no illustration at all: an accent rule down the left
   edge, the title, one factual line, and the site's own .btn. The previous two
   looked invented because they were — every part of this is a component the
   rest of the site already uses.

   Row on wide, stacked on narrow, and the button never shrinks below its label. */
.doc {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sn-space-4);
  /* WIDTH BY SHARE, NOT BY MEASURE (Malena, 2026-09-07): "que las cards de
     descarga de documentos utilicen el 80% de la pantalla en desktop y en
     menores tamanos un 100%". The old cap was 46rem, a reading measure, which
     is the wrong instinct here — nothing in these cards is a paragraph. They
     are a label, one line and a button, and at 46rem they read as a narrow
     strip against a 1280 container.

     80% of the CONTAINER, not of the viewport: the container is already capped
     at --maxw and inset by the page gutter, so 80% of the viewport would push
     these two cards wider than every other block on the page and out past the
     gutter. Full width below the breakpoint, where there is no room to give
     away. */
  width: 100%;
  margin-top: var(--sn-space-5);
  padding: var(--sn-space-4);
  border-radius: var(--radius-card);
  background: var(--sn-white);
  /* The orange rule down the left edge came off 2026-08-27. The book icon is
     what marks this block now — a shape says "document" faster than a stripe,
     and the stripe was competing with the accent already in the icon. */
  border: 1.5px solid transparent;
  box-shadow: 0 2px 6px rgb(25 70 80 / 0.05);
}

@media (min-width: 64rem) {
  .doc { width: 80%; }
}

.doc__icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-media, 14px);
  background: var(--acc-wash);
  color: var(--acc-ink);
}
.doc__icon .icon { width: 26px; height: 26px; }

.doc__body { display: flex; flex-direction: column; gap: 4px; min-width: 12rem; flex: 1 1 16rem; }
.doc__eyebrow {
  font-family: var(--font-ui);
  font-size: 0.75rem;
  font-weight: var(--w-title);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--acc-ink);
}
/* An <h2>: it is the only heading its section has, so it is that section's
   heading and not a sub-level inside one. */
.doc .doc__title { font-size: var(--fs-h3); font-weight: var(--w-title); color: var(--sn-ink); }
.doc__meta { font-size: var(--fs-caption); color: var(--sn-ink); }
.doc .btn { flex: 0 0 auto; }

@media (hover: hover) and (pointer: fine) {
  /* NO `transition` HERE — it is declared with the other lifting cards, where it
     outranks [data-reveal]. See the note by that rule. */
  /* THE SHADOW TAKES THE PROGRAMME COLOUR. It was hardcoded rgb(237 113 51) —
     Youth's orange — because this component was built on /youth-apprenticeship
     and the value was written literally. Every other page that reuses .occ__card
     (Pre-Apprenticeship, IT, Pharmacy) was then throwing an orange glow that
     belongs to a different programme. Derived from --acc, it cannot drift again.
     The fallback is a neutral ink shadow, never another programme's colour. */
  .occ__card:hover, .doc:hover {
    transform: translateY(-4px);
    border-color: var(--acc);
    box-shadow: 0 18px 38px rgb(25 70 80 / 0.16);
    box-shadow: 0 18px 38px color-mix(in srgb, var(--acc) 20%, transparent);
  }
}
@media (prefers-reduced-motion: reduce) {
  .occ__card:hover, .doc:hover { transform: none; }
}

/* ---------- 3.11d APPROACH PANEL + LOUD CTA (job matching) --
   ------------------------------------------------------------ */

.approach {
  display: grid;
  grid-template-columns: 56px 1fr;
  gap: var(--sn-space-3);
  align-items: start;
  max-width: 52rem;
  margin-top: var(--sn-space-5);
  padding: var(--sn-space-4);
  border-radius: var(--radius-card);
  /* NO COLOURED LEFT RULE. The tint and the icon already mark this as a pulled-out
     statement; the bar was a third marker doing the same job. */
  background: var(--acc-wash);
}
.approach__icon {
  display: grid; place-items: center;
  width: 56px; height: 56px;
  border-radius: 999px;
  background: var(--sn-white);
  color: var(--acc-ink);
}
.approach__icon .icon { width: 24px; height: 24px; }
.approach__text {
  font-size: var(--fs-lead);
  line-height: var(--sn-lh-body);
  color: var(--sn-ink);
  /* The text is ONE element on purpose — .approach is a grid, and a bare text
     run would become an anonymous grid item and land in the 56px column. Same
     trap as .checks__item; see the note there. */
}

/* ---- the loud CTA ----
   The section that replaced the Openings list. Full-bleed programme blue, one
   large line, the action. The wave is the brand decoration used elsewhere on
   the site — not a cropped corner disc, which is not part of this design
   language. */
.cta-loud {
  position: relative;
  overflow: hidden;
  text-align: center;
  padding-block: calc(var(--section-pad) * 1.25);
}
.cta-loud__wave {
  position: absolute;
  right: -8%;
  bottom: -22%;
  width: min(620px, 62%);
  aspect-ratio: 426 / 232;
  background: url("/images/wave-blue-pink.svg") right bottom / contain no-repeat;
  opacity: .28;
  pointer-events: none;
}
.cta-loud__inner { position: relative; z-index: 1; }
/* No margin-top: the eyebrow above this was removed, so the section's own
   padding sets the space. Leaving --eyebrow-gap here would push the heading
   down against nothing. */
.cta-loud__title {
  font-size: var(--fs-hero);
  font-weight: var(--w-display);
  line-height: var(--lh-hero);
  color: var(--sn-white);
  max-width: 18ch;
  margin-inline: auto;
  text-wrap: balance;
}
.cta-loud__lead {
  margin: var(--lead-gap) auto 0;
  max-width: 54ch;
  font-size: var(--fs-lead);
  line-height: var(--sn-lh-body);
  color: var(--sn-white);
}
.cta-loud__ctas {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 14px;
  margin-top: var(--sn-space-5);
}

/* ---- THE QUESTION ROUTE, UNDER THE APPLY BUTTON ------------
   Malena, 2026-08-27: the contact form is gone from every program page and the
   people who sat beside it moved here — "abajo del CTA en pequeno, para
   contacto si tienen preguntas". Applying is the goal; asking is the fallback,
   so it is one small line rather than a section.

   WHY IT IS A WHITE PILL WHEN THE HEADING ABOVE IT IS BARE WHITE TEXT.
   The heading and lead are large text, which WCAG exempts at 3:1, and every
   program ground is an exception Malena signed off on deliberately (see §12).
   This line is SMALL text and needs 4.5:1 — and five of the eight grounds
   cannot carry white small text at all: out-of-school 2.11, college 2.44,
   pre-apprenticeship 2.80, pharmacy 2.92 (Youth was the fifth until its
   repalette took it to 6.72). Ink on white is legible
   on all eight, and it is the arrangement `.contact-list li` already uses.

   Not a 999px radius: this wraps to two lines on a narrow screen with two
   named contacts, and a pill radius on a two-line box reads as a mistake. */
.apply-ask {
  margin: var(--sn-space-5) auto 0;
  width: fit-content;
  max-width: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px 14px;
  padding: 10px 18px;
  border-radius: 14px;
  background: var(--sn-white);
  color: var(--sn-ink);
  font-family: var(--font-ui);
  font-size: 0.875rem;              /* 14 */
  line-height: 1.45;
  text-align: left;
}
/* flex: 0 0 auto is the load-bearing part — the envelope must not be squeezed
   when the row wraps. display is NOT: .apply-ask is a flex container, so it
   blockifies its children and the site's `svg { display: block }` reset cannot
   push the icon onto its own line here the way it does in a text flow. Set
   anyway so the rule still reads correctly if this ever stops being flex. */
.apply-ask .icon {
  display: inline-block;
  flex: 0 0 auto;
  width: 15px; height: 15px;
  color: var(--sn-link);
}
.apply-ask__label { font-weight: var(--w-title); }
.apply-ask__who {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
}
.apply-ask__who b { font-weight: var(--w-title); }
.apply-ask__who a { color: var(--sn-link); }

/* Stacked and left-aligned once the row cannot hold the label and a contact
   side by side — centred text that wraps mid-name is worse than a short list. */
@media (max-width: 40rem) {
  .apply-ask {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    border-radius: 12px;
  }
}

/* ---- THE TEAM, ITS OWN SECTION ----------------------------
   Malena, 2026-09-07. This started as a white panel inside the apply CTA and
   moved out to a section of its own; the panel styles went with it, because a
   roster on the page's own white ground needs no plate to be legible and
   nothing else was using them.

   Three explicit breakpoints rather than auto-fit: five people land 1 / 2 / 3
   per row, so the last row holds two and reads as a team rather than as a
   remainder. auto-fit would have decided that for itself and changed answer
   with the gutter.
   ------------------------------------------------------------ */
.team-grid {
  list-style: none;
  margin: var(--sn-space-5) 0 0;
  padding: 0;
  display: grid;
  gap: var(--sn-space-3);
  grid-template-columns: 1fr;
}
@media (min-width: 48rem) { .team-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .team-grid { grid-template-columns: repeat(3, 1fr); } }

.team-card {
  padding: var(--sn-space-3);
  border-radius: var(--radius-card);
  background: var(--sn-white);
  border: var(--hairline);
}
.team-card__name {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  font-weight: var(--w-title);
  color: var(--sn-ink);
}
/* The role sits under the name at eyebrow scale: it labels the person rather
   than competing with them, and it is the only line three of the five carry. */
.team-card__role {
  margin: 2px 0 0;
  font-family: var(--font-ui);
  font-size: 0.8125rem;             /* 13 */
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--acc-ink, var(--sn-link));
}
.team-card__link {
  margin: 8px 0 0;
  font-size: 0.875rem;              /* 14 */
  line-height: 1.4;
}
.team-card__link a { color: var(--sn-link); }
.team-card__link + .team-card__link { margin-top: 2px; }

/* ---- THE GRADUATE TESTIMONY --------------------------------
   Tavon Williams, supplied by Tiffany Harkless (DOES) 2026-09-15.

   NOT .quote-split, which is the home page's pull quote. That component's art
   column is calibrated to one specific file — the shares 0.7105 / 0.2581 /
   0.0314 are measured off the alpha channel of Dr. Morris Hughes' 1019x1023
   cutout — so pointing it at a photograph would lay the image out against a
   transparent lead that does not exist.

   THE DISPLAY LINE IS HIS, THE PARAGRAPH IS NOT. What DOES supplied is a
   third-person summary of an interview with short direct quotations inside it,
   so the paragraph cannot go in a blockquote attributed to him. Only the words
   actually in quotation marks are his, and one of them is lifted as the display
   line — which is why that line is aria-hidden: it repeats text that is already
   in the paragraph below, and a screen reader should hear it once.

   The art column is optional. .testimony:has() means the grid only splits in
   two when a portrait is actually present, so the section reads correctly in
   the window between the words arriving and the photograph arriving.
   ------------------------------------------------------------ */
.testimony { display: grid; gap: var(--sn-space-4); align-items: center; }

/* ---- the oversized quotation mark ----
   Malena, 2026-09-15: "pon algun diseño de quote, como unas comillas que sean
   grandes y que queden detras de la seccion".

   BEHIND, and that is the whole constraint. Anything sitting under body copy
   changes what that copy is measured against, so the mark is painted in
   --acc-wash — Youth's #E0EDF8 — which is pale enough that every line over it
   still clears AA with room to spare. A mid-blue at this size would have looked
   better and put the 22px pull quote under 4.5:1, which is not a trade worth
   making on a government programme page.

   It is a text glyph rather than an SVG so it scales with the type and inherits
   the family; aria-hidden and unselectable because it is punctuation the
   paragraph below already contains. clamp() keeps it from swallowing the column
   on a phone. */
.testimony__body { position: relative; }
.testimony__body > *:not(.testimony__mark) { position: relative; z-index: 1; }

.testimony__mark {
  position: absolute;
  /* TOP-RIGHT, not top-left where a quote mark instinctively goes. Three
     placements were rendered against the real text before this one was picked:
     at the top left it lands exactly under the eyebrow and the pull quote and
     is completely hidden — only its tail shows, reading as a smudge — and at
     470px behind the paragraph the glyph's shape is lost and it stops looking
     like punctuation at all. Top right is the one piece of genuinely empty
     space in this composition, so the mark reads whole and still sits behind.

     Anchored to .testimony__body rather than .testimony so it works at both
     breakpoints: on desktop the body IS the right column, so this is the
     section's top right; stacked on a phone the body is full width and it lands
     beside the eyebrow, which is empty there too.

     right: 0, deliberately not a negative bleed — .u-container does not clip,
     so a mark hanging past its right edge would push the document wider than
     the viewport. That bug already cost a fix once, on the marquee rows. */
  top: -0.34em;
  right: 0;
  z-index: 0;
  /* 340px at the 1280 cap, floor 96px on a phone. 24vw is what puts it at 340
     on a 1440 viewport before the max takes over. */
  font-size: clamp(6rem, 24vw, 21.25rem);
  line-height: 1;
  font-weight: var(--sn-w-bold, 700);
  color: var(--acc-wash, var(--sn-grey-200));
  pointer-events: none;
  user-select: none;
}

@media (min-width: 56rem) {
  .testimony:has(.testimony__art) { grid-template-columns: 300px minmax(0, 1fr); column-gap: var(--sn-space-6); }
}

/* A CUTOUT, NOT A PHOTOGRAPH, since 2026-09-15. Everything this rule used to do
   is wrong for one: the rounded corners clipped nothing (the file's own corners
   are transparent), overflow:hidden had nothing to hide, the grey placeholder
   showed THROUGH the 18.1% of the file that is transparent, and aspect-ratio
   4/5 with object-fit:cover cropped a square 2929x2914 composition. It renders
   at its own ratio now and sits straight on the band, like the programme
   artwork it matches. */
.testimony__art { max-width: 300px; }
.testimony__art img { display: block; width: 100%; height: auto; }

.testimony__pull {
  margin: 0 0 var(--sn-space-3);
  font-size: var(--fs-h3);
  line-height: var(--sn-lh-tight);
  font-weight: var(--sn-w-medium);
  letter-spacing: var(--ls-display-tight);
  /* Same fallback trap as .testimony__who b below: --sn-blue-900 is #3976E9,
     which is 3.90:1 on the off-white band, and 22px at weight 500 is not large
     text so it needs 4.5. --acc-ink carries the page accent (#215E99 here,
     6.16:1) and --sn-ink is 9.47:1 if a page ever uses this with no accent. */
  color: var(--acc-ink, var(--sn-ink));
}
.testimony__pull p { margin: 0; max-width: none; }

.testimony__who {
  margin-top: var(--sn-space-3);
  font-size: 0.875rem;              /* 14 */
  line-height: 1.5;
  color: var(--sn-ink);
}
/* --acc-ink, NOT --sn-blue-900. The first version used the latter on the
   assumption it was a dark navy; it is #3976E9, which put a real person's name
   at 3.90:1 on the off-white band and failed AA. On this page --acc-ink is
   Youth's #215E99 and measures 6.16:1; the --sn-ink fallback is 9.47:1, so the
   rule is safe on a band with no accent set. */
.testimony__who b { display: block; font-weight: var(--sn-w-medium); color: var(--acc-ink, var(--sn-ink)); }

/* ---- THE PHOTO MARQUEE -------------------------------------
   Jen asked for "Today's Apprentices, Tomorrow's Leaders" and a slideshow;
   Malena asked for the shape (2026-09-11): "2 lineas de fotografias pero que
   vayan pasando una a la izquierda y otra la derecha en loop real, para que se
   vean mas fotos, no solo 8 fotos". Twenty photographs, ten per row, the rows
   travelling in opposite directions.

   THIS IS THE ONE LOOPING THING ON THE SITE, and it is a deliberate exception
   to the standing no-tickers rule, so it carries the control that rule exists
   to protect: WCAG 2.2.2 asks for a way to pause motion that starts by itself
   and runs past five seconds, and the button below is it. The earlier tickers
   were removed precisely because they had none. So the rule holds — no
   unpausable loops — rather than being waived here.

   NO `gap` BETWEEN TILES, margin-inline-end instead, and that is load-bearing.
   The track holds the set twice and animates to translateX(-50%). With `gap`,
   a track of 2N tiles is 2N*T + (2N-1)*G wide, so half of it lands G/2 short of
   the second copy and the loop visibly jumps every cycle. With a margin on
   every tile the track is exactly 2N*(T+G) and -50% is exactly one copy.

   SPEED IS CONSTANT ACROSS BREAKPOINTS, not the duration. A row is ten tiles,
   so the travel is 10*(200+16)=2160px on desktop and 10*(150+12)=1620px below
   56rem. Holding 45px/s gives 48s and 36s — a single duration would have made
   the phone crawl.
   ------------------------------------------------------------ */
.photo-marquee {
  --tile: 150px;
  --tile-gap: 12px;
  --marquee-dur: 36s;
  margin-top: var(--sn-space-5);
}
@media (min-width: 56rem) {
  .photo-marquee { --tile: 200px; --tile-gap: 16px; --marquee-dur: 48s; }
}

/* Full-bleed, and it gets there by NOT being in the container rather than by
   breaking out of one. The first version used the usual `width: 100vw;
   margin-inline: calc(50% - 50vw)` trick and it was wrong: 100vw counts the
   scrollbar, so on a desktop with a 15px one the row came out 966px wide
   inside a 951px content box and hung ~7.5px past each edge, which gave the
   whole document 8px of horizontal scroll. Measured, not theorised.

   So the rows sit directly inside .band, which has padding-block only and no
   horizontal padding — they already span the section, with no vw anywhere and
   nothing to overflow. Only the heading and the pause button stay in
   .u-container. Keep it that way: putting these back inside the container
   brings the bug back with them. */
.photo-marquee__row { overflow: hidden; }
.photo-marquee__row + .photo-marquee__row { margin-top: var(--tile-gap); }

.photo-marquee__track {
  display: flex;
  width: max-content;
  animation: photo-marquee var(--marquee-dur) linear infinite;
}
.photo-marquee__row:nth-child(2) .photo-marquee__track { animation-direction: reverse; }

@keyframes photo-marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

.photo-marquee__set {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
}
.photo-marquee__set li {
  margin: 0 var(--tile-gap) 0 0;
  flex: 0 0 auto;
  border-radius: var(--radius-card);
  overflow: hidden;                 /* the radius has to clip the photo itself */
  background: var(--sn-grey-200);   /* what shows for the instant before decode */
}
.photo-marquee__set img {
  display: block;
  width: var(--tile);
  height: var(--tile);
  object-fit: cover;
}

/* REDUCED MOTION. Not merely slowed — no animation at all, and the row becomes
   something you scroll yourself, so every photograph is still reachable.

   THIS IS NOW THE ONLY WAY TO STOP THE ROWS. There was a pause button here
   until Malena removed it (2026-09-11), which also removed the thing that made
   the loop satisfy WCAG 2.2.2 — a visitor who has not set the OS-level reduced
   motion preference has no way to stop the motion. Flagged, her call. If it
   ever needs to come back, it was one button plus initMarquees in
   public/js/app.js.

   The duplicate set is aria-hidden either way, so scrolling past the seam
   repeats the pictures but announces nothing twice. */
@media (prefers-reduced-motion: reduce) {
  .photo-marquee__track { animation: none; }
  .photo-marquee__row { overflow-x: auto; }
}

/* ---- THE TWO DOES MARKS (Youth page only) ------------------
   Jen, CSM-1086: "Add YAP and Apprenticeship DC Logos at the Header and Footer
   of the landing page." Scoped to Youth by Malena.

   THE FOOTER NEEDS A WHITE PLATE AND THE HEADER DOES NOT, and that is measured
   rather than stylistic: both marks are drawn in red and the brand navy, and
   the footer's ground IS that navy (#153E6E). Dropped straight onto it, every
   navy stroke in both logos disappears — the YAP wordmark loses its Y and P and
   "Apprenticeship DC" loses two of its three words. The header bar is white, so
   there they sit directly on it.

   Worth flagging rather than burying: a DOES/Consortium logo plaque used to sit
   in this footer and Malena had it removed on 2026-08-27. This is a different
   plaque, asked for by the client for one page, but if she does not want plaques
   in the footer at all, this is the block to delete.
   ------------------------------------------------------------ */

/* IN THE HERO. The marks were tried in the header bar first and moved here on
   2026-09-07. Worth recording why the bar did not work: it centres its nav with
   equal 1fr columns on either side, so two marks beside the logo became a third
   flex item and pushed the nav 72px off the page centre against 4px everywhere
   else. Fixing that meant restructuring a partial shared by sixteen pages to
   serve one. The hero has room and belongs to the page.

   Below the CTAs on purpose: Apply is what the hero is for, and these sit under
   it as provenance rather than competing with it. */
.yap-marks--hero { margin-top: var(--sn-space-5); }

/* THE LABEL LIVES INSIDE THE PLATE, and that is a contrast fix rather than a
   composition choice. It sat on the hero ground first, in white at 85% — which
   MEASURED 2.57:1 against Youth's orange at 13px, where small text needs 4.5.
   The 85% made it worse than plain white would have been (3.00), and Youth's
   ground carries white only under the large-text exemption, which 13px does
   not get. Ink on the white plate measures 10.3:1 and works on all eight
   grounds, so this holds if the lockup is ever wanted on another programme. */
.yap-marks__plate {
  display: inline-block;
  padding: 10px 14px 12px;
  border-radius: 12px;
  background: var(--sn-white);
}
.yap-marks__label {
  margin: 0 0 8px;
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  font-weight: var(--w-title);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--sn-ink);
}
.yap-marks__row { display: flex; align-items: center; gap: 14px; }
.yap-marks__row img { display: block; height: 38px; width: auto; }

/* ---------- SCROLL-DRAWN LINE -------------------------------
   Shinta's signature decoration, ported 2026-08-27. Their DOM shows the
   standard SVG stroke-draw: stroke-dasharray set to the path's own length and
   the dashoffset animated from that length to zero.

   pathLength="1" IS WHY THIS NEEDS NO SCRIPT. It renormalises the path so its
   length is exactly 1 regardless of the curve, so the dash values are the
   constants 1 and 0. Framer measures getTotalLength() in JS and writes the
   number into an inline style; the geometry can change here and nothing else
   has to be recalculated.

   FULLY DRAWN IS THE RESTING STATE. Without scroll-timeline support, or under
   reduced motion, the line is simply there — decoration that fails to a
   complete picture rather than to an invisible one.
   ------------------------------------------------------------ */

/* IT SPANS SECTIONS, and that is the whole point of the second version.

   The first one was `top: 0; height: 100%` — the line lived inside one section
   and was cut at its edges. That solved "do not start in mid-air" by trading it
   for a worse problem: the cut landed on an invisible section boundary, so the
   line looked severed. Malena's words: "aunque yo di la instrucción de que
   salga desde un lugar donde no parezca cortado, eso no significa que
   visualmente se resuelva solo cortando entre las secciones".

   Now the box is 210% of its host and starts 55% above it, so it reaches into
   the sections above and below. Nothing clips it: the host is position:
   relative but has no overflow, and an absolutely positioned child is only
   clipped by an ancestor that does.

   PERCENTAGES, NOT PIXELS. The strip could be pinned to measured offsets, but
   those are a snapshot of today's copy — the first edit to a paragraph moves
   the line off its clear column. Tied to the host's own height it follows the
   layout instead.

   THE ONLY EDGE THAT MAY CUT THE STROKE IS THE RIGHT ONE, which sits at the
   screen edge and reads as "continues off-screen". The path's ends both exit
   there; its vertical extent stays inside the box, so no section boundary ever
   crosses the stroke. */
.drawline {
  position: absolute;
  top: var(--drawline-top, -55%);
  /* FULL WIDTH, AND BEHIND THE CONTENT. Confined to a clear right-hand column
     the line could never loop or cross the page — the whole reason Shinta's can
     is that they are a background layer with the content painted on top, so a
     line that runs under a paragraph or vanishes behind a card is the effect
     rather than a collision.

     z-index 0 here, 1 on .u-container (below), so every block of copy and every
     card occludes it. Opacity is what keeps it decoration: at full strength a
     programme colour behind body text is noise, and this site carries statutory
     copy that has to stay readable. */
  left: 0;
  right: 0;
  height: var(--drawline-h, 210%);
  z-index: 0;
  pointer-events: none;
  display: none;              /* only where the empty column exists — see below */
}
/* hidden, so the ends are CUT at the box edge instead of hanging in space, and
   so nothing can push the page wider than the viewport. Both vertical edges
   qualify now: the box spans the full width, so a line may enter left and leave
   right — the two edges that coincide with the screen. Top and bottom still
   never cut the stroke, because those fall mid-page. */
.drawline svg { width: 100%; height: 100%; overflow: hidden; }
.drawline__path {
  fill: none;
  stroke: var(--acc, var(--sn-blue-900));
  stroke-width: 7;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;       /* drawn */
  opacity: .38;
}

/* 75rem is where the section still has a clear column to the right of its
   content. Below that the line would cross the text. */
@media (min-width: 75rem) {
  .drawline-host { position: relative; }
  .drawline-host > .u-container { position: relative; z-index: 1; }
  .drawline { display: block; }
}

@keyframes drawline-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) and (min-width: 75rem) {
    /* THE SUBJECT IS THE LINE, NOT THE SECTION. It was on .drawline-host and the
       line was permanently drawn: the box sits ~90% down a tall host and runs
       past its bottom, so by the time the line entered the viewport the host had
       long since passed `cover 65%` and the animation had already finished.
       Nothing was broken — it had simply played off-screen.

       Since the line's geometry was decoupled from its host, the line is the
       only box whose position matches what the reader sees. */
    .drawline { view-timeline-name: --drawline; }
    .drawline__path {
      animation: drawline-draw linear both;
      animation-timeline: --drawline;
      animation-range: entry 25% cover 65%;
    }
  }
}

/* ---------- 3.12 STEPS -------------------------------------- */

/* Mobile is a vertical stack with a left number rail; tablet and up go
   horizontal (docs/09). The column count is data, not a guess: --steps on the
   <ol> says how many there are, so a 3-step and a 4-step journey each lay out
   correctly without a second class. */
.steps {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--sn-space-3);
}

.step {
  display: grid;
  grid-template-columns: 3rem 1fr;
  column-gap: var(--sn-space-2);
  align-items: start;
  padding-top: var(--sn-space-2);
  border-top: 2px solid var(--p-900);
}
.step__num {
  grid-row: 1 / span 3;
  font-size: var(--fs-h3);
  font-weight: var(--sn-w-light);
  color: var(--sn-ink);
}
.step h3, .step p { grid-column: 2; }
.step p { margin-top: 6px; }

@media (min-width: 48rem) {   /* --bp-md 768 — the rail folds away */
  .steps { grid-template-columns: repeat(2, 1fr); }
  .step { grid-template-columns: 1fr; }
  .step__num { grid-row: auto; display: block; }
  .step h3, .step p { grid-column: auto; }
  .step h3 { margin-top: 6px; }
}
@media (min-width: 64rem) {   /* --bp-lg 1024 */
  .steps { grid-template-columns: repeat(var(--steps, 3), 1fr); }
}

/* ---------- 3.13 QUALITY GRID (chips) ----------------------- */

.quality-grid {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 10px;
}
.quality-grid li {
  padding: 11px 20px;
  border: var(--hairline);
  border-radius: var(--sn-radius-pill);
  background: var(--sn-white);
  color: var(--sn-ink);
}

/* ---------- 3.14 ACCORDION ---------------------------------
   Native <details>: keyboard-operable for free, and it still
   opens with JS disabled. interpolate-size animates the height
   where supported and simply jumps where it is not.
   ------------------------------------------------------------ */

@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .accordion details::details-content {
    height: 0; overflow: hidden;
    transition: height var(--dur-fast) var(--ease-tween), content-visibility var(--dur-fast) allow-discrete;
  }
  .accordion details[open]::details-content { height: auto; }
}

.accordion { max-width: var(--measure); }
.accordion details { border-top: var(--hairline); }
.accordion details:last-of-type { border-bottom: var(--hairline); }
.accordion summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sn-space-2);
  min-height: 64px; padding: var(--sn-space-2) 0;
  font-size: var(--fs-h3); font-weight: var(--sn-w-medium); color: var(--sn-ink);
  cursor: pointer; list-style: none;
}
.accordion summary::-webkit-details-marker { display: none; }
.accordion summary::after {
  content: ""; flex: 0 0 auto; width: 11px; height: 11px;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: translateY(-3px) rotate(45deg);
  transition: transform var(--dur-fast) var(--ease-tween);
}
.accordion details[open] summary::after { transform: translateY(2px) rotate(-135deg); }
.accordion .prose { padding-bottom: var(--sn-space-3); }

/* ---------- 3.15 LINK CARD (external documents) -------------
   Used for the Issuu handbook: a styled cover link out, not an
   iframe — which is why the CSP has no Issuu entry (decision #9).
   ------------------------------------------------------------ */

.link-card {
  display: flex; align-items: center; gap: var(--sn-space-2); flex-wrap: wrap;
  max-width: var(--measure);
  padding: var(--sn-space-3);
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--p-wash);
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-tween);
}
.link-card__label {
  flex: 1 1 auto;
  font-size: var(--fs-h3); font-weight: var(--sn-w-medium); color: var(--sn-ink);
}
.link-card__meta { flex: 1 1 100%; order: 3; font-size: var(--fs-caption); color: var(--sn-ink); }
@media (hover: hover) and (pointer: fine) {
  .link-card:hover { border-color: var(--sn-blue-900); }
  .link-card:hover .btn__arrow { transform: translateX(4px); }
}

/* ---------- 3.16 STATUS NOTE -------------------------------
   Evergreen application status. Deliberately neutral: it is not
   an error and not a success, so it borrows neither palette.
   ------------------------------------------------------------ */

.status-note {
  display: flex; align-items: flex-start; gap: 12px;
  max-width: var(--measure);
  padding: var(--sn-space-2) var(--sn-space-3);
  border: var(--hairline);
  border-radius: var(--radius-media);
  background: var(--sn-off-white);
  color: var(--sn-ink);
}
.status-note__dot {
  flex: 0 0 auto; width: 10px; height: 10px; margin-top: 9px;
  border-radius: 999px; background: var(--p-900);
}

/* ---------- 3.17 CONTACT LIST ------------------------------- */

.contact-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sn-space-2); }
@media (min-width: 48rem) { .contact-list { grid-template-columns: repeat(2, 1fr); } }
.contact-list li {
  padding: var(--sn-space-3);
  border: var(--hairline);
  border-radius: var(--radius-card);
}
.contact-list__name { display: block; font-weight: var(--sn-w-medium); color: var(--sn-ink); }
.contact-list a {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--sn-link);
}

/* ---------- 3.18 BANNER LINE -------------------------------- */

.banner-line {
  /* 24ch broke this 87-character line over three. Widened, then MEASURED by
     sweeping the width: it sets two lines anywhere from 820px to 1240px and
     falls to three at 800. 46ch was the first attempt and computed to 1247px —
     wider than the 1232px container, so it constrained nothing and the two
     lines were the container's doing, not the rule's.

     56rem (896px) sits inside that window with 76px of headroom, and it is a
     better measure than running the full column width. In rem rather than ch
     because ch scales with the font-size clamp, which would slide the limit
     around underneath the window it is supposed to stay inside.

     balance evens the two lines instead of leaving a short second one. */
  max-width: min(56rem, 100%);
  text-wrap: balance;
  /* It had NO bottom margin, so a 40px display line sat flush against the video
     poster underneath it. This is the standard section rhythm, not a nudge. */
  margin-bottom: var(--sn-space-6);
  font-size: var(--fs-h2);
  font-weight: var(--sn-w-light);
  line-height: var(--lh-h2);
  letter-spacing: var(--sn-ls-display);
  color: var(--sn-ink);
}

/* ---------- 3.19 BENEFIT CARDS ------------------------------ */

.benefit-grid {
  display: grid; gap: var(--sn-space-2); grid-template-columns: 1fr;
  list-style: none; margin: 0; padding: 0;
}
@media (min-width: 64rem) { .benefit-grid { grid-template-columns: repeat(3, 1fr); } }

.benefit {
  padding: var(--sn-space-4) var(--sn-space-3);
  border: var(--hairline);
  border-radius: var(--radius-card);
  background: var(--sn-white);
}
.benefit__mark {
  width: 44px; height: 44px; margin-bottom: var(--sn-space-2);
  border-radius: 999px; background: var(--p-100);
}
.benefit p { font-size: var(--fs-h3); font-weight: var(--sn-w-medium); color: var(--sn-ink);
             line-height: var(--lh-h3); max-width: none; }

/* ---------- 3.20 STATS -------------------------------------
   Numbers count up when they scroll into view (Phase 7). The
   markup carries the final value in data-count so a no-JS or
   reduced-motion visitor sees the real figure immediately.
   ------------------------------------------------------------ */

.stat-grid {
  display: grid; gap: var(--sn-space-4) var(--sn-space-3);
  grid-template-columns: repeat(2, 1fr);
  list-style: none; margin: 0; padding: 0;
}
@media (min-width: 64rem) { .stat-grid { grid-template-columns: repeat(4, 1fr); } }

.stat__value {
  display: block;
  font-size: var(--fs-stat);
  font-weight: var(--sn-w-light);
  line-height: var(--lh-stat);
  letter-spacing: var(--sn-ls-display);
  color: inherit;
}
.stat__label { display: block; margin-top: var(--sn-space-2); font-size: var(--fs-body); }

/* ---------- 3.21 QUOTE -------------------------------------- */

.quote { max-width: 46ch; margin: 0; }
.quote blockquote {
  margin: 0;
  font-size: var(--fs-h2);
  font-weight: var(--sn-w-light);
  line-height: var(--lh-h2);
  letter-spacing: var(--sn-ls-display);
  color: var(--sn-ink);
}
.quote figcaption { margin-top: var(--sn-space-3); font-size: var(--fs-body); }
.quote figcaption b { display: block; font-weight: var(--sn-w-medium); color: var(--sn-ink); }

/* ---------- 3.22 MARQUEE — DELETED -------------------------
   Malena, 2026-08-27: "Quita todas las animaciones de tickers de todas las
   secciones ... y que estan en loop."

   This was the partner logo strip on /partners, a 46s linear infinite
   translate. It paused on hover and on focus-within and stopped entirely under
   prefers-reduced-motion, which is more than the SVG ribbons did — but it was
   still motion that starts on its own and runs for the life of the page, and
   it was the last ticker on the site.

   /partners now uses .logo-wall (3.24) for the same three logos. Everything
   this block solved — the exact -50% translate, the seam in the gap, the
   measured group width — was solving problems a static row of three does not
   have.

   Deleted rather than commented out. If a genuinely long list of partners ever
   needs a strip, write it then, with a visible pause control.
   ------------------------------------------------------------ */

/* ---------- 3.23 FORM --------------------------------------
   Plain, working HTML first: it posts to /api/submit with JS
   off. Phase 9 upgrades it to fetch + inline errors + a success
   panel. 16px minimum on inputs stops iOS zooming the page on
   focus (docs/09).
   ------------------------------------------------------------ */

/* .form-layout was the old two-column shell here — form left, supporting
   blocks right. It is gone: /contact now puts the copy and the form side by
   side in the hero (§10) and the supporting blocks in their own band, so no
   page carries the class any more. Removed rather than left as dead weight. */

.form { display: grid; gap: var(--sn-space-3); max-width: 640px; }

.form-field { display: grid; gap: 7px; }
.form-field label { font-size: var(--fs-body); font-weight: var(--sn-w-medium); color: var(--sn-ink); }
.form-field .req { color: var(--sn-error); }

.form-field input,
.form-field select,
.form-field textarea {
  width: 100%;
  font-family: inherit;
  font-size: var(--fs-body);   /* 17px — above the 16px iOS zoom threshold */
  color: var(--sn-black);
  background: var(--sn-white);
  border: 1.5px solid var(--sn-grey-200);
  border-radius: var(--sn-radius-md);
  padding: 13px 16px;
  min-height: 48px;
}
.form-field textarea { min-height: 148px; resize: vertical; }
.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible { border-color: var(--sn-blue-900); }

.form-field[data-invalid="true"] input,
.form-field[data-invalid="true"] select,
.form-field[data-invalid="true"] textarea {
  border-color: var(--sn-error);
  background: var(--sn-error-wash);
}
/* ONE LINE: icon, then message. The site-wide reset at the top of this sheet
   sets `img, svg, video { display: block }`, which is right almost everywhere
   and wrong here — it pushed the alert triangle onto its own line above the
   text. Flex overrides that for the icon (a flex item is blockified either way)
   and gives real control of the space between the two, so no stray space
   character is needed in the JS that fills this. */
.form-field__error {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-caption);
  font-weight: var(--sn-w-bold);
  color: var(--sn-error);
}
/* The icon must never squash when a long message wraps beside it. */
.form-field__error .icon { flex: 0 0 auto; }
.form-field__error:empty { display: none; }

/* Honeypot: off-screen, unfocusable, and hidden from AT so a
   screen-reader user never meets it (docs/16). */
.form__trap {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden;
}

.form__submit { justify-self: start; }

.form-success {
  border-left: 4px solid var(--sn-success);
  background: var(--sn-success-wash);
  border-radius: var(--radius-media);
  padding: var(--sn-space-3);
}
.form-success h2 { color: var(--sn-success); font-size: var(--fs-h3); }
.form-success p { margin-top: 8px; }

/* .form-aside and .form-aside--row were removed 2026-08-27 with the three-column
   band on /contact that was their only user. Grep confirmed zero references in
   public/, templates/ and scripts/ before deleting. */

/* ---------- 3.24 LOGO WALL ---------------------------------
   Static grid of partner logos. Every logo here carries a real
   alt — the live site ships all nine with alt="", which is one
   of the defects this rebuild exists to fix, so a logo we
   cannot name does not go in (docs/12).
   ------------------------------------------------------------ */

.logo-wall {
  display: grid; gap: var(--sn-space-2);
  grid-template-columns: repeat(2, 1fr);
  list-style: none; margin: 0; padding: 0;
}
@media (min-width: 48rem) { .logo-wall { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 64rem) { .logo-wall { grid-template-columns: repeat(4, 1fr); } }

/* THE COLUMN COUNT FOLLOWS THE ITEM COUNT. The four-column rule above was
   written for the nine-logo wall the audit described; /partners now shows the
   three partners we can actually name, and three in a four-column grid leaves
   a visible hole on the right. Same technique .audience-grid uses above:
   :has(> :nth-child(3):last-child) is true only when the third child is also
   the last, so a wall that grows past three goes back to four columns on its
   own. */
@media (min-width: 64rem) {
  .logo-wall:has(> :nth-child(3):last-child) { grid-template-columns: repeat(3, 1fr); }
  .logo-wall:has(> :nth-child(2):last-child) { grid-template-columns: repeat(2, 1fr); }
}

.logo-wall li {
  display: grid; place-items: center;
  min-height: 128px;
  padding: var(--sn-space-3);
  background: var(--sn-white);
  border: var(--hairline);
  border-radius: var(--radius-media);
}
.logo-wall a { display: grid; place-items: center; width: 100%; min-height: 44px; }
.logo-wall img { max-height: 56px; width: auto; }

@media (hover: hover) and (pointer: fine) {
  .logo-wall li:has(a:hover) { border-color: var(--sn-blue-900); }
}

/* Phone and email links inside an address block are real tap targets and were
   inheriting the default inline height. 44px on touch (docs/09); a precise
   pointer keeps the tighter definition-list rhythm. */
address a { display: inline-flex; align-items: center; min-height: 44px; }
@media (hover: hover) and (pointer: fine) {
  address a { min-height: 0; }
}

/* ============================================================
   4. MOTION (docs/06_MOTION_SYSTEM.md)

   Two systems, deliberately separate:
     - the hero intro is a GSAP timeline, once per page load
     - everything else is IntersectionObserver + these classes

   ScrollTrigger is NOT used for reveals. The ticket rules it out
   ("GSAP batch has been unreliable on our other sites") and an
   IO + CSS pair is ~20 lines and cannot desync from layout.
   ============================================================ */

/* The reveal hook in the markup is `data-reveal`, which does nothing on its
   own. app.js puts `js` on <html>, and only then does the hidden state apply.
   That ordering is the whole point: with JavaScript off, or if app.js fails to
   load, every one of these elements is simply visible. Hiding content by
   default and relying on a script to bring it back is how sites end up blank. */
.js [data-reveal] {
  opacity: 0;
  translate: 0 var(--rise);
  transition: opacity var(--dur-med) var(--ease-tween),
              translate var(--dur-med) var(--ease-tween);
}

/* Directional variants for two-column sections: text in from one side, media
   from the other. Same end state, so they share the .in rule. */
.js [data-reveal="l"] { translate: -24px var(--rise); }
.js [data-reveal="r"] { translate: 24px var(--rise); }

.js [data-reveal].in { opacity: 1; translate: 0 0; }

/* Grid children stagger off an inline --i. Capped at 5 steps so a long row
   never leaves the last card crawling in seconds late. */
.js [data-reveal][style*="--i"] {
  transition-delay: calc(min(var(--i), 5) * 60ms);
}

/* Reduced motion beats everything, at every width. No transitions, no
   timeline, no marquee, no count-up — content simply is where it belongs.
   This is a media query rather than a JS branch so it holds even in the
   moment before app.js runs. */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal],
  .js [data-reveal="l"],
  .js [data-reveal="r"] {
    opacity: 1;
    translate: none;
    transition: none;
  }
  .card-program__tile,
  .card-program__pop,
  .btn,
  .btn__arrow,
  .skip-link,
  .nav__caret,
  .accordion summary::after,
  .site-header { transition: none !important; }
}

/* The hero words are wrapped by JS at boot. inline-block is what makes them
   transformable; overflow: clip on the line keeps the pre-animation offset
   from widening the page. */
.hero__title .w {
  display: inline-block;
  will-change: transform, opacity;
}

/* ============================================================
   5. MEDIA (docs/10_MEDIA_OPTIMIZATION.md)
   ============================================================ */

.media {
  border-radius: var(--radius-media);
  overflow: hidden;
  background: var(--sn-off-white);
}
.media img, .media video { width: 100%; height: auto; display: block; }

/* The consortium clip was shot on a phone: 404x720, portrait. Full-bleed it
   would be absurd, so it sits in a capped column with the page beside it. */
.media--portrait { max-width: 340px; aspect-ratio: 404 / 720; }
.media--portrait video { height: 100%; object-fit: cover; }

.media-split { display: grid; gap: var(--sn-space-4); align-items: center; }
@media (min-width: 64rem) { .media-split { grid-template-columns: 340px 1fr; gap: var(--sn-space-6); } }

/* ---------- 5.1 YOUTUBE FACADE -----------------------------
   Nothing from YouTube loads until the visitor asks for it: the
   poster is self-hosted, so the page makes ZERO third-party
   requests up to the click. That is why the Worker's CSP has no
   img-src entry for i.ytimg.com — only frame-src, and only for
   the iframe that appears after the click.
   ------------------------------------------------------------ */

.yt {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  padding: 0;
  border-radius: var(--radius-media);
  overflow: hidden;
  background: var(--sn-ink);
  cursor: pointer;
}
.yt img { width: 100%; height: 100%; object-fit: cover; display: block; }

.yt__play {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  pointer-events: none;
}
.yt__play span {
  display: grid; place-items: center;
  width: 76px; height: 76px;
  border-radius: 999px;
  background: var(--sn-blue-900);
  color: var(--sn-white);
  font-size: 26px; line-height: 1;
  padding-left: 5px;      /* optical centring of the triangle */
  transition: background var(--dur-fast) var(--ease-tween),
              transform var(--dur-fast) var(--ease-tween);
}
.yt__label {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: var(--sn-space-3);
  font-size: var(--fs-body);
  font-weight: var(--sn-w-bold);
  color: var(--sn-white);
  text-align: left;
  /* A scrim, not a tint: white on an unknown video still is a gamble. */
  background: linear-gradient(transparent, rgba(0, 0, 0, .72));
}
@media (hover: hover) and (pointer: fine) {
  .yt:hover .yt__play span { background: var(--sn-blue-hover); transform: scale(1.06); }
}
.yt iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.media-caption { margin-top: 10px; font-size: var(--fs-caption); color: var(--sn-ink); }

/* A send failure always leaves a way to reach a human. role="alert", so a
   screen reader hears it without having to go looking. */
.form-error {
  display: flex; gap: 10px;
  padding: var(--sn-space-2) var(--sn-space-3);
  border-left: 4px solid var(--sn-error);
  border-radius: var(--radius-media);
  background: var(--sn-error-wash);
  color: var(--sn-ink);
  font-size: var(--fs-body);
  max-width: 640px;
}
.form-success:focus-visible { outline: var(--focus-width) solid var(--focus-color); outline-offset: 4px; }

/* ============================================================
   6. SHINTA DIRECTION — rebuilt 2026-08-26

   The first attempt put a blue frame around the page and called it
   a redirection. It was wrong, and measuring Shinta properly says
   why:

   - Its pink is NOT a frame. The neutral slab covers 20,092 of
     21,061px — 95% of the page. Pink appears behind the hero and
     behind the footer, and nowhere else. Two coloured bookends,
     not a border.
   - Its cards have NO shadows. All eight measured come back flat:
     solid fill, 16-32px radius, 24px padding. docs/02's "soft
     multi-layer shadows" is not what the site does.
   - Its pink #FFA8F2 is a LIGHT tint carrying DARK text. So the
     honest analogue here is the brand blue at tint strength, not
     saturated blue with white on it — which also happens to make
     every string legal without special cases.

   The grammar, then: flat saturated blocks, generous radii, one
   accent per block, big numbers, dark cards for problems and
   coloured cards for answers.
   ============================================================ */

/* ---------- 6.1 SURFACES -----------------------------------
   Off-white is the page. Two tinted bookends: the hero and the
   footer. No frame, no floating slab.
   ------------------------------------------------------------ */

body { background: var(--sn-off-white); }

main { background: transparent; }

/* The tinted bookend. --sn-blue-100 against ink measures ~8:1, so
   unlike saturated blue it carries body copy, eyebrows and links
   with no exceptions to remember. */
.band--tint,
.hero--tint {
  --g-bg: var(--sn-blue-100);
  --g-ink: var(--sn-ink);
  --g-pop: var(--sn-blue-tile);
  background: var(--g-bg);
  color: var(--g-ink);
}

.site-header {
  background: var(--sn-blue-100);
  border-bottom: 0;
}
.site-header__inner { min-height: 64px; }

/* ---- 6.x THE FOOTER, REDESIGNED (Malena, 2026-08-27) -------
   Her brief: too big, too pale, no charm, no decorative elements.

   Ground is NAVY (--sn-p-hbcu is not it: the value is the kit's own
   [data-ground="navy"], #153E6E), at Malena's request — it was --sn-ink first.
   Every accent is measured against the new ground, not carried over:
     white       10.79   body, links, headings
     blue-100     7.14   the accent rules, the social ring, the hover
     grey-200     8.06   available
     sn-link      1.93   NEVER here — it was the old hover colour and it would
                         have been invisible
   The closing CTA band above is also navy, so the 3px blue-100 rule at the top
   edge is now the only thing drawing the seam between them. It earns its keep. */
.site-footer {
  /* ALL FOOTER TEXT AT 85% (Malena, 2026-08-27). One multiplier rather than six
     hand-reduced numbers: every size below is `calc(token * --footer-scale)`, so
     the footer keeps tracking the type scale if a token ever changes, and the
     next adjustment is one value here instead of a hunt through the file.

     Declared here and not as an override of --fs-body itself: redefining a token
     in terms of its own value is circular and computes to nothing. */
  --footer-scale: 0.85;

  /* The tagline and the credit line have no font-size of their own — they
     inherited 17px from the body — so the scale has to land on the container for
     them to follow. */
  font-size: calc(var(--fs-body) * var(--footer-scale));

  position: relative;
  background: #153E6E;
  color: var(--sn-white);
  /* Was --section-pad top (up to 88px) plus another --section-pad before the
     legal row. Roughly half now, which is most of "demasiado grande". */
  padding-block: var(--sn-space-5) var(--sn-space-3);
  border-top: 3px solid var(--sn-blue-100);
}

/* THE BRAND WAVE, large and faint (Malena, 2026-08-27) — it replaces the pop
   circle she asked to remove.

   A background-image, not an <img>: it is purely decorative, so it should not be
   a DOM node an assistive tree has to skip past, and `img-src 'self'` already
   covers a same-origin background. The file was copied out of brand/waves/ into
   public/images/ because brand/ is not deployed — public/ is the whole root.

   The gradient is the brand blue #3976E9 into #FFDEDD, and at 14% it reads as a
   tint of the navy rather than as a second graphic competing with the content.
   Anchored bottom-right and cropped by the footer's overflow, as the circle was. */
.site-footer__wave {
  position: absolute;
  right: -4%;
  bottom: -14%;
  width: min(920px, 78%);
  aspect-ratio: 426 / 232;      /* the file's own viewBox */
  background: url("/images/wave-blue-pink.svg") right bottom / contain no-repeat;
  opacity: .14;
  pointer-events: none;
}

.site-footer .u-container { position: relative; }   /* above the circle */

.site-footer h2,
.site-footer dt { color: var(--sn-white); }

/* The accent that makes the link columns feel deliberate rather than dumped:
   a short rule under each heading, in the one tint that is legal here. */
.site-footer h2 {
  padding-bottom: 8px;
  border-bottom: 2px solid var(--sn-blue-100);
  display: inline-block;
}

.site-footer a { color: var(--sn-white); }

@media (hover: hover) and (pointer: fine) {
  /* Colour only. The old rule underlined on hover, which Malena disliked, and it
     is also the weaker signal here: blue-100 on navy is a 7.14:1 lift the eye
     catches without the line. */
  .site-footer a:hover { color: var(--sn-blue-100); text-decoration: none; }
}

/* A LINK INSIDE A SENTENCE IS NOT A LIST ITEM. `.site-footer a` makes every
   footer link an inline-flex box with a 44px (32px on a fine pointer) minimum
   height, which is right for the column lists — they are stacked tap targets —
   and wrong inside running text: it turned each line of the credit line into a
   32px box, so those two lines sat far further apart than the tagline above
   them at the same 23.12px line-height. That was the "alto de línea más grande"
   Malena spotted; the line-height was never the difference.

   Dropping the target here is correct rather than a compromise: an inline link
   in a sentence cannot be given a 44px box without breaking the text flow, and
   that is exactly the case target-size rules exempt. */
.site-footer p a {
  display: inline;
  min-height: 0;
  align-items: initial;
}

/* The partner plaque lived here until 2026-08-27. Removed with the markup at
   Malena's request — the logos it existed to carry are gone from the footer, so
   the white surface they needed goes with them. The pop circle and the wordmark
   still carry the decoration. */

/* The social buttons were hairline rings on a light ground. On the dark one the
   ring is the accent tint, and it fills on hover. */
.site-footer__social a {
  border-color: var(--sn-blue-100);
  color: var(--sn-white);
}

@media (hover: hover) and (pointer: fine) {
  .site-footer__social a:hover {
    background: var(--sn-blue-100);
    color: var(--sn-ink);
  }
}

/* The decorative "SkillsNation" watermark was removed 2026-08-27 at Malena's
   request; the wave carries the decoration on its own now. */

/* The legal row: the old --section-pad gap above it was one of the two biggest
   contributors to the height. It sits above the watermark. */
.site-footer__legal {
  position: relative;
  margin-top: var(--sn-space-3);
  border-top-color: rgba(255, 255, 255, .18);
  color: var(--sn-white);
}

/* Inside the page, a band with no ground is the off-white paper and
   the "offwhite" modifier is the white panel. */
.band:not([data-ground]) { background: transparent; }
.band--offwhite { --g-bg: var(--sn-white); }

/* ---------- 6.2 DISPLAY VOICE ------------------------------
   Sentence case, weight 700, tracking -0.04em (Shinta, measured).
   Replaces the kit's Light 300 hero on Malena's instruction.
   ------------------------------------------------------------ */

.hero__title,
h2,
.stat__value,
.banner-line { font-weight: var(--w-display); letter-spacing: var(--ls-display-tight); }
.cta-band h2 { text-transform: none; }
.quote blockquote { font-weight: var(--sn-w-medium); letter-spacing: var(--ls-display-tight); }
.band__head .eyebrow { color: var(--sn-link); }

/* THE RED, WITH ONE JOB. It labels the sections on /youth-apprenticeship and
   nothing else.

   IT LIVES HERE, AFTER THE RULE ABOVE, AND THAT IS NOT TIDINESS. It was first
   written beside the other Youth accents in 3.11c, where it is (0,2,0) — the
   same specificity as `.band__head .eyebrow` on the line above, which sits
   later in the file and therefore won. The eyebrows measured #3064C6, the site
   link blue, and the red never appeared. Same trap as the hero aside gap: equal
   specificity, later source. Moving it beats raising specificity, because the
   two rules are peers and the file already reads top-to-bottom as later-wins.

   Why the eyebrows got the red, out of everything on the page: a third colour
   with no assignment ends up sprinkled at random. The other candidates were
   worse — a red tick in the eligibility list reads as "no", a red label on the
   two document cards reads as a warning, and on the hero ground the same red
   measures 1.04:1 and simply disappears. Section eyebrows are small uppercase
   labels on white, repeated down the page, carrying no state: red is decoration
   there rather than a signal, it passes at 6.48:1, and it echoes the two DOES
   logos in the hero, which are themselves navy and red. */
[data-program="youth"] .band__head .eyebrow { color: var(--acc-pop, #C00001); }
/* ...except on a coloured ground, where that red is 1.04:1 against the blue.
   Defensive: no Youth eyebrow sits on a ground today, and one added later
   should inherit the ground's ink rather than vanish. */
[data-program="youth"] [data-ground] .band__head .eyebrow,
[data-program="youth"] [data-hero-bg] .band__head .eyebrow { color: inherit; }

/* ---------- 6.3 FLAT BLOCKS --------------------------------
   Shinta's card: a solid fill, a generous radius, 24px of padding,
   and no shadow at all. Depth comes from colour against colour.
   ------------------------------------------------------------ */

.benefit,
.audience,
.contact-list li,
.logo-wall li,
.quality-grid li,
.link-card,
.form-success {
  background: var(--sn-white);
  border: 0;
  box-shadow: none;
}
.form-field input,
.form-field select,
.form-field textarea {
  background: var(--sn-white);
  border-color: transparent;
  box-shadow: none;
}
.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible { border-color: var(--sn-blue-900); }
.card-program__tile { box-shadow: none; }

/* Numbered blocks — Shinta's process cards: a big ghost number, a
   coloured fill, the text under it. */
.step {
  padding: var(--sn-space-3);
  border-top: 0;
  border-radius: var(--radius-card);
  background: var(--sn-white);
}
.step__num {
  font-size: clamp(2.25rem, 1.6rem + 2.6vw, 3.25rem);
  font-weight: var(--w-display);
  letter-spacing: var(--ls-display-tight);
  line-height: 1;
  color: var(--sn-blue-100);
}
.steps > .step:nth-child(4n+1) { background: var(--sn-p-college-100); }
.steps > .step:nth-child(4n+2) { background: var(--sn-p-preapp-100); }
.steps > .step:nth-child(4n+3) { background: var(--sn-p-outofschool-100); }
.steps > .step:nth-child(4n+4) { background: var(--sn-blue-100); }
/* Solid, not a ghost. A 32% ink number on a pale tint measures about 2:1 —
   and "01" is content, not decoration: it tells someone which step they are
   reading. Shinta's own process numbers are solid too. */
.steps > .step .step__num { color: var(--sn-ink); }

/* Metric-led stats — the number IS the headline (Shinta, Ordina). */
.stat {
  padding: var(--sn-space-3);
  border-radius: var(--radius-card);
  background: var(--sn-white);
}
.stat__label { color: var(--sn-ink); }

/* Benefit cards get a number chip, the way Shinta labels blocks. */
.benefit { position: relative; padding: var(--sn-space-3); }
.benefit__mark {
  display: grid; place-items: center;
  width: 38px; height: 38px; margin-bottom: var(--sn-space-2);
  border-radius: 999px;
  background: var(--sn-blue-100);
  color: var(--sn-ink);
  font-size: var(--fs-caption); font-weight: var(--sn-w-black);
}
.benefit p { font-size: var(--fs-h3); font-weight: var(--sn-w-medium); }

/* ---------- 6.4 DIAGONAL ENTRANCES -------------------------
   Shinta's motion signature: elements arrive from alternating
   corners with a slight scale, staggered — not a uniform fade-up.

   NOTE ON SPECIFICITY, because getting this wrong shipped cards
   sitting on top of each other: the resting rules below are all
   written at the SAME weight as the .in rule, and .in comes last,
   so it always wins. A `.grid > [data-reveal]:nth-child(odd)`
   selector outweighs `[data-reveal].in` and freezes the element
   in its offset forever. Never raise the specificity here.
   ------------------------------------------------------------ */

.js [data-reveal] {
  opacity: 0;
  translate: 0 var(--rise);
  scale: 0.96;
  transition: opacity var(--dur-med) var(--ease-out),
              translate var(--dur-med) var(--ease-out),
              scale var(--dur-med) var(--ease-out);
}
.js [data-reveal="l"] { translate: -30px 22px; }
.js [data-reveal="r"] { translate: 30px 22px; }

/* .in is last and equal-weight, so it wins for every variant. */
.js [data-reveal].in { opacity: 1; translate: 0 0; scale: 1; }

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: 1; translate: none; scale: 1; transition: none; }
}

/* ---------- 6.5 THE BAR (Shinta's structure, white) ---------
   A white pill FLOATING over the page: mark left, links centred
   against the page, one CTA hard right. White rather than
   Shinta's near-black because the blue wordmark is only legal on
   white, off-white and #D0E7FF.

   The geometry is derived, not eyeballed. --bar-gap is the space
   left above and below the Apply button inside the pill, and the
   pill's right padding is set to exactly that — so the button's
   distance from the right edge equals its distance from the top
   and bottom. Change the bar height or the button height and the
   inset follows on its own.
   ------------------------------------------------------------ */

:root {
  --bar-h:      60px;
  --bar-btn-h:  42px;
  --bar-gap:    calc((var(--bar-h) - var(--bar-btn-h)) / 2);   /* 9px */
  /* Total vertical space the floating bar occupies, for anything that has
     to clear it. */
  --bar-space:  calc(var(--bar-h) + (var(--gutter) * 2));
}

/* Fixed, not sticky. Sticky kept the bar in the flow, which pushed the hero
   down and left the page's own off-white showing above it — the "grey box
   behind the menu". Fixed lifts it out so the hero colour runs to the very
   top of the viewport and the bar floats over it. */
.site-header {
  position: fixed;
  top: var(--gutter);
  left: 0;
  right: 0;
  z-index: 50;
  width: calc(100% - (var(--gutter) * 2));
  max-width: var(--maxw);
  margin-inline: auto;
  background: var(--sn-white);
  border: var(--hairline);
  border-radius: var(--sn-radius-pill);
}
/* Wider and fainter than the 1px hairline shadow it replaces, which sat so
   tight to the bar that it read as a second border. Two layers: a close one
   that grounds the pill, a wide one that lets it float. Both very low alpha —
   the bar is white on off-white, so it needs separation, not weight.
   (This is a deliberate exception to decision #27's "no drop shadows": that
   was about CARDS, where measuring Shinta showed flat fills. A floating bar
   over scrolling content is the one place depth carries information.) */
.site-header {
  box-shadow: 0 2px 10px rgba(25, 70, 80, .035),
              0 10px 34px rgba(25, 70, 80, .05);
}
.site-header.scrolled {
  border-color: var(--sn-grey-200);
  box-shadow: 0 3px 12px rgba(25, 70, 80, .05),
              0 14px 44px rgba(25, 70, 80, .07);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--sn-space-2);
  min-height: var(--bar-h);
  /* Left: 32% more than the base gutter step (24 -> 28.8 -> 31.7px, two
     rounds of Malena asking for more air), so the wordmark clears the pill's
     curve. Right: exactly --bar-gap, matching the button's vertical
     breathing room. */
  padding-left: calc(var(--sn-space-3) * 1.32);
  padding-right: var(--bar-gap);
}

.site-header__logo img { width: 83px; }   /* 20% down from 104 */

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--sn-space-1);
  margin-left: auto;
  flex: 0 0 auto;
}

/* The compact bar button. It keeps a 19px/700 label because that is not a
   style choice: white on --sn-blue-900 is 4.25:1, AA only at large-bold
   size. Only the padding shrinks. */
.btn--compact { padding: 10px 22px; min-height: var(--bar-btn-h); }

/* Links go black, hover goes blue — but --sn-link (#3064C6, 5.58:1), not
   --sn-blue-900 (4.25:1), which fails AA on a 17px nav label. */
.nav__link,
.nav__disclosure { color: var(--sn-black); }

@media (hover: hover) and (pointer: fine) {
  .nav__link:hover,
  .nav__disclosure:hover { color: var(--sn-link); }
}
.nav__link[aria-current="page"] { color: var(--sn-link); background: transparent; }

/* The hero now starts at y=0 and has to clear the floating bar itself. */
.hero { padding-top: max(calc(var(--section-pad) * 0.9), var(--bar-space)); }
.hero--full { min-height: 100vh; min-height: 100svh; }

@media (min-width: 64rem) {   /* --bp-lg — links centre against the page */
  .site-header__logo { flex: 1 1 0; }
  .nav { flex: 0 0 auto; }
  .site-header__actions { flex: 1 1 0; justify-content: flex-end; margin-left: 0; }
  .nav__list { gap: var(--sn-space-2); }
  .nav__link, .nav__disclosure { padding-inline: 12px; }

  .nav__submenu {
    top: calc(100% + 14px);
    border: var(--hairline);
    box-shadow: none;
    background: var(--sn-white);
  }
}

/* The mobile overlay opens below the pill rather than over it. */
@media (max-width: 63.9375rem) {
  .nav { padding-top: var(--bar-space); }
}

/* On a narrow phone the bar carries logo + Apply + toggle, and with the word
   "Menu" spelled out they need 331px inside a 279px pill. The label becomes
   visually hidden rather than display:none, so the button keeps its accessible
   name — a bare hamburger with no name is a screen-reader dead end. */
@media (max-width: 30rem) {
  .nav-toggle__label {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
  }
  .nav-toggle { padding-inline: 10px; margin-right: -6px; }
  .btn--compact { padding: 10px 16px; }
  .site-header__inner { padding-inline: var(--sn-space-2); gap: 8px; }
}

/* ---------- 6.6 ICONS --------------------------------------
   Lucide, self-hosted as a sprite (scripts/build-icons-sprite.py).
   17 symbols, ~3.6KB, one request, cached — and no new CSP origin,
   which a CDN would have cost.

   The stroke presentation lives here rather than in the symbols so
   one symbol serves every size and colour: an icon inherits the
   text colour of whatever it sits in, and scales with the type
   because it is sized in em.
   ------------------------------------------------------------ */

.icon {
  width: 1em;
  height: 1em;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Optical alignment with the cap height of the text beside it. */
  vertical-align: -0.125em;
}
.icon--lg { width: 1.35em; height: 1.35em; stroke-width: 1.75; }

/* Button arrows are icons now, not a "→" character. The character rendered
   at whatever weight and baseline the font gave it; this matches the stroke
   of every other icon on the page. */
.btn .icon { transition: transform var(--dur-fast) var(--ease-tween); }
@media (hover: hover) and (pointer: fine) {
  .btn:hover .icon--arrow { transform: translateX(4px); }
}

/* The nav caret and accordion chevron were CSS-rotated borders. Same shape,
   but the sprite version matches the icon set's joins and weight. */
.nav__caret { width: 1em; height: 1em; border: 0; transform: none; }
.nav__disclosure[aria-expanded="true"] .nav__caret { transform: rotate(180deg); }

/* The hamburger becomes menu / x rather than three transformed bars. */
.nav-toggle__bars { display: none; }
.nav-toggle .icon { width: 22px; height: 22px; }
.nav-toggle[aria-expanded="true"] .icon--menu,
.nav-toggle[aria-expanded="false"] .icon--close { display: none; }

/* An icon inside a link or heading must never be the only carrier of
   meaning — every one of these is aria-hidden with real text beside it. */
.icon-row { display: inline-flex; align-items: center; gap: .5em; }

/* ============================================================
   7. OPEN SAUCE ONE (Malena, approved 2026-08-26)

   Shinta's typeface, measured off the live site — docs/02 wrongly
   credits Geist, which only appears on Framer's template badge.

   APPROVED SCOPE: buttons, the menu, and every large number.
   These are the "UI voice": things you act on and figures you
   read at a glance.

   STILL ON TRIAL: the hero headline (section 7b). Malena has not
   decided whether titles adopt it, so that stays one selector and
   is removable on its own.

   NOT in scope and staying Red Hat Display: body copy, section
   headings, eyebrows, facts, prose, labels.

   Two weights ship, and only two:
     600 — titles, uppercase labels, menu links
     700 — buttons, large numbers, card titles
   The 500 was dropped on 2026-08-26: it existed only for the menu,
   and moving the menu to 600 removed a 24KB file from every page
   for a 100-weight difference in the nav links.
   The kit's 900 would have been a third 24KB file for the single
   word "Menu" on the toggle, which is hidden below 480px anyway,
   so the toggle label uses 700 instead. If a real 900 need turns
   up, fetch it then.

   This is the second departure from "Red Hat Display is the ONLY
   family" (the brief, the kit, docs/05); the first was the bold
   hero. Both are Malena's calls, recorded in docs/00.
   ============================================================ */

@font-face {
  font-family: "Open Sauce One";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/OpenSauceOne-latin-600.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Open Sauce One";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/OpenSauceOne-latin-700.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* Red Hat Display stays the fallback everywhere, so a failed font load
     lands on brand rather than on Arial. */
  --font-ui: "Open Sauce One", "Red Hat Display", "Segoe UI", system-ui, sans-serif;

  /* 600 is not one of the kit's five weights (300/400/500/700/900). It exists
     because Malena took the titles down 100 from 700 after seeing them, and
     because it matches Shinta's own eyebrow weight. Declared here rather than
     written at each call site. */
  --w-title: 600;
}

/* ---- buttons ---- */
.btn { font-family: var(--font-ui); }

/* ---- the menu ---- */
.nav__link,
.nav__disclosure,
.nav__submenu a,
.nav-toggle { font-family: var(--font-ui); font-weight: var(--w-title); }

/* 700, not the kit's 900: see the weights note above. At 13px uppercase with
   open tracking it still reads as a label. */
.nav-toggle { font-weight: var(--sn-w-bold); }

/* ---- large numbers ----
   Every figure that exists to be read big: the stat wall, the numbered process
   steps, and the benefit cards' 01/02/03 chips. Count-ups are covered because
   they live in .stat__value.

   WEIGHTS ARE CONSTRAINED HERE and the constraint is invisible if ignored:
   only 600 and 700 of Open Sauce One are bundled. Declaring anything else does
   not fail loudly — CSS font matching silently substitutes the nearest face —
   so a number in this rule that declares 300 or 900 renders at 600 or 700
   while the stylesheet claims otherwise. Both cases existed before this note:
   .stat__value asked for 300 and .benefit__mark for 900. Fixed below rather
   than bundling two more faces for two rules. */
.stat__value,
.step__num,
.benefit__mark,
[data-count] { font-family: var(--font-ui); }

/* 700, not the kit's 900. 900 is not bundled, so this already rendered at 700
   — the declaration was the only thing that disagreed. At 13px the chip is
   small text needing 4.5:1, and ink on blue-100 measures 6.83:1, so the
   lighter weight keeps AA with room spare. */
.benefit__mark { font-weight: var(--sn-w-bold); }

/* 600, not the kit's 300. Same story: the stat wall was designed light against
   Red Hat Display, which has a 300; Open Sauce One here does not, so 300 has
   been resolving to 600 since the family changed. Declaring 600 changes
   nothing on screen and stops the CSS describing a face that cannot load. */
.stat__value { font-weight: var(--w-title); }

/* ---- titles (approved 2026-08-26) ----
   Open Sauce One at 600 — a step lighter than the 700 the hero was trialled
   at, which Malena called for after seeing it.

   NOTE what is deliberately NOT included: .card-program__title. It sits on the
   coloured tile at 19-22px, where orange (3.0:1) and teal (3.09:1) are legal
   only for "large" text, and large means 24px OR 18.66px at weight 700. At 600
   the exemption stops applying and both cards fail. It stays 700. */
.hero__title,
h2 {
  font-family: var(--font-ui);
  font-weight: var(--w-title);
}

/* ---- small uppercase labels (approved 2026-08-26) ----
   The eyebrows and column headings: "Welcome to SkillsNation", "What you get",
   "How it works", "Programs", "Partners". Open Sauce One, well down from the
   kit's 900, keeping the open tracking that made the combination work.
   Shinta's own eyebrows measure 12px/600 uppercase, so this lands in the same
   place from the other direction. */
.eyebrow,
.facts dt,
.site-footer h2,
.audience__who {
  font-family: var(--font-ui);
  font-weight: var(--w-title);
}

/* The footer's column headings are h2s and would otherwise inherit the title
   size from the rule above. */
.site-footer h2 { font-size: calc(var(--fs-eyebrow) * var(--footer-scale)); }

/* The pull quote (Malena, 2026-08-27). 600, not the 500 it had: 500 is not one
   of the two bundled faces, so declaring it would have resolved to 600 anyway
   — see the two-weight note above. At 28-40px on off-white the ink is far
   above AA either way, so the weight is a purely typographic choice here. */
.quote blockquote {
  font-family: var(--font-ui);
  font-weight: var(--w-title);
}

/* Card titles take the family — leaving them in Red Hat Display beside Open
   Sauce numbers on the same card read as an oversight — but the WEIGHT IS
   PINNED AT 700 and must stay there. They sit on the coloured tile at 19-22px,
   where orange (3.0:1) and teal (3.09:1) are legal only for "large" text, and
   large means 24px OR 18.66px at weight 700. Take this to 600 to match the
   other titles and both cards fail AA. */
.card-program__title {
  font-family: var(--font-ui);
  font-weight: var(--sn-w-bold);
}

/* ============================================================
   8. THE SPLIT HERO (Shinta's hero, rebuilt 2026-08-26)

   Three columns: headline + marker rows on the left, a tall media
   card in the middle, a spotlight card and the lead + CTA on the
   right. A curved ribbon of moving text passes behind the card.

   The hero sits on the page's own off-white, NOT on a tint — in
   Shinta the light grey is the page and the pink is only the
   ribbon. Getting that the wrong way round is what made the
   earlier attempt read as a coloured box.
   ============================================================ */

.hero--split {
  position: relative;
  overflow: hidden;
  --g-bg: transparent;
  --g-ink: var(--sn-ink);
}

.hero__grid {
  position: relative;
  z-index: 1;                 /* over the drawn lines below */
  display: grid;
  gap: var(--sn-space-5);
  align-items: center;
}
/* 75rem (1200), NOT the site's usual 64rem: measured at 1055 the three-column
   grid produced 294px side columns — the headline wrapped one word per line
   and the card column was strangled. Three columns need ~1200px to breathe;
   below that the two-column tablet layout carries on. */
@media (min-width: 75rem) {
  .hero__grid {
    /* Narrower than before: the new artwork is 0.64:1 where the old photo
       was 0.8:1, so the side columns gain the difference. */
    grid-template-columns: 1fr minmax(260px, 340px) 1fr;
    gap: var(--sn-space-4);
    /* stretch, not center: each column then chooses its own alignment, which
       is what puts the headline top-left and splits the right column between
       a card at the top and the CTA at the bottom (Shinta's composition). */
    align-items: stretch;
  }
  /* Both side columns start 20px lower (Malena, 2026-08-27). The media column is
     deliberately NOT moved — she asked for the artwork left where it is, so the
     offset goes on the two text columns only and the photograph keeps its
     relationship to the band. */
  .hero__lede  { align-self: start; margin-top: 20px; }
  .hero__aside { margin-top: 20px; }
  .hero__media { align-self: center; }

  /* ONE GROUP AT THE FOOT OF THE COLUMN (Malena, 2026-09-02): "mueve el
     program featured y el texto que esta debajo, justo arriba del boton de
     apply, todo ordenado abajo a la derecha del Hero."

     This replaces space-between, which held the card at the top of the column
     and the lead and button at the bottom — three blocks spread across 480px
     of column with the relationship between them left to the reader. flex-end
     stacks all three at the bottom, so the eye goes card -> what the portal is
     -> Apply in one move.

     The card's -16px optical nudge went with it. That existed to line the
     card's cap height up with the h1's, which is only a relationship worth
     having while the two sit level; at the foot of the column it aligned
     against nothing. */
  .hero__aside {
    align-self: stretch;
    justify-content: flex-end;
    /* 27, NOT 36, and the 9px difference is the point. The three blocks should
       look evenly spaced, and the aside-top gap above is 36px — but a flex gap
       separates LAYOUT boxes, and .cta-pair carries 9px of its own padding
       above its visible pill. 36 here measured 36px card-to-lead and 59px
       lead-to-button: even by the boxes, visibly uneven on screen.
       36 - 9 = 27 was the first guess and measured 30px against the card's 36,
       because the paragraph's box also carries about 6px of descender space
       below its last visible baseline. 33 measures 36 and 36. Both numbers
       here came off the screen, not off the box model. */
    --aside-stack-gap: 33px;
    /* stretch, not flex-start: the card and lead fill the column all the way
       to the container's right edge — the same boundary the header bar marks —
       instead of stopping at a 380px cap and leaving a dead strip. */
    align-items: stretch;
  }
  /* ONE SOURCE FOR THE HUECO. .hero__ctas carries margin-top: --sn-space-4
     from the plain stacked hero (line 564), which ADDED to the flex gap: 24 +
     32 = 56px of layout for a hueco the gap was supposed to own alone. Zeroed
     here at (0,2,0) so it wins wherever line 564 sits relative to this block. */
  .hero__aside > .hero__ctas { margin-top: 0; }
}

/* ---- left: headline + markers ---- */

/* No width cap. The 16ch cap (≈368px) manufactured ragged dead space at every
   width where the column was wider — the grid column is the real constraint. */

.hero__markers {
  list-style: none;
  margin: var(--sn-space-4) 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
}
.hero__markers li {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  font-weight: var(--w-title);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--sn-ink);
}
.hero__markers .icon { color: var(--sn-blue-900); width: 1.15em; height: 1.15em; }

/* ---- centre: the media card ---- */

/* No background and no radius here: the artwork brings its own rounded peach
   panel (rx 60 on a 720-unit viewBox), so a card shape underneath would either
   double the corners or clip them. Ratio matches the artwork exactly, which
   keeps CLS at zero without object-fit cropping anything. */
.hero__media {
  margin: 0;
  aspect-ratio: 720 / 1133;
}
.hero__media img { width: 100%; height: 100%; display: block; }

/* ---- right: spotlight card + lead + CTA ---- */

/* Flex, not grid — and defined ONCE. There used to be a `display: grid` base
   rule sitting after the desktop `display: flex` rule in source order, so grid
   won: its rows stretched and the spotlight card came out 153px tall against
   the 105px its content needs, which is where the dead space under the text
   came from. In a flex column the card keeps its content height. */
.hero__aside {
  display: flex;
  flex-direction: column;
  /* Through a custom property because THIS RULE SITS LATER IN THE FILE than
     the desktop .hero__aside rule that needs to change it. Same specificity,
     later source, so a plain `gap` here silently beat the media query's —
     which is exactly what happened on 2026-09-02: justify-content: flex-end
     applied and the gap beside it did nothing. The property is not declared
     here, so the media query's value survives. */
  gap: var(--aside-stack-gap, var(--sn-space-3));
}

/* The lead sits immediately under the card. 60% off the standard step, as a
   calc off the token rather than a new number, so it still moves with the
   spacing scale. 24px -> 9.6px.

   Both children share --aside-w. Without it the paragraph took the full column
   (414px at 1440) while the card capped at 380, so the text ran 34px past the
   card's right edge — which, at a 10px gap, reads as the paragraph escaping
   from underneath the card rather than as a stacked pair. One token so the two
   cannot drift apart again. */
.hero__aside-top {
  display: flex;
  flex-direction: column;
  /* THE RULE THIS GOT WRONG THREE TIMES: for two stacked blocks to read as
     separate, the gap between them must be LARGER than the padding inside
     either one. Box-overlap tests pass happily while this is violated, which
     is why measuring rectangles kept saying "fine" while the paragraph plainly
     read as sitting on the card.

     So the gap is DERIVED from the card's padding rather than chosen: it can
     never be set to a value that breaks the rule again. The card's padding
     comes down at the same time, which keeps the spacing tight — the point of
     the original request — and makes the card more compact, which was also
     asked for. 24px -> 13px is a 46% reduction, and it clears the padding plus
     its 1px border by 4px. */
  --card-pad: 8px;
  /* THE GAP MUST BEAT THE SHADOW, not just the padding. The card's shadow
     blurs 18px downward; a 16-18px gap is therefore entirely filled by the
     halo, and the paragraph sits flush against it — zero perceived
     whitespace, which is what kept being reported as "the text is on the
     card" while every geometric test passed. 36px = the 18px blur fully
     cleared, plus 18px of actual visible page background. */
  gap: 36px;
  /* 380 -> 320 on the 2026-08-27 review ("mas pequeno"). It moves as ONE
     token because the card and the paragraph below it MUST stay the same
     width — a narrower card with a 380px paragraph under it is exactly the
     "text escaping from under the card" bug this block was written to kill.
     The shorter lead absorbs the loss without gaining a line. */
  --aside-w: 320px;
}
.hero__aside-top > .spotlight {
  padding: var(--card-pad);
  /* Not --sn-space-3 (24px) any more: the arrow chip is 24px wide sitting 9px
     from the corner, so anything under 36px lets a wrapped title run beneath
     it. 46px = cleared, plus 10px of breathing room. */
  padding-right: 46px;
}
.hero__aside-top > .spotlight,
.hero__aside-top > .hero__lead { width: 100%; max-width: var(--aside-w); }
/* .hero__lead carries a 24px top margin from the plain stacked hero, which was
   adding to the flex gap and making the measured space 34px instead of 9.6.
   The group owns the spacing here. The ch cap is dropped too: the shared
   pixel width is the constraint now, and two competing limits is how the
   widths drifted apart in the first place. */
.hero__aside-top > .hero__lead { margin-top: 0; max-width: var(--aside-w); }

/* The spotlight card. Rebuilt 2026-08-26 — the first version was a colour
   block, an eyebrow and a title in a cramped row, and it read as a placeholder.
   Changes: a real programme seal instead of the block, the arrow moved to the
   top-right corner the way Shinta's is, a third line of real information, and
   room to breathe. */
.spotlight {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 12px;
  width: 100%;
  /* Shrunk 2026-08-27 on review: "el featured program del Hero debe estar mas
     pequeno", then widened back to the column 2026-09-02. 64 -> 44 seal, h3 -> 16px title, 24 -> 18
     radius. The radius moves with the box on purpose — a 24px corner on a
     300px card reads as a lozenge rather than a card. */
  max-width: 320px;
  padding: 10px;
  /* Right side clears the arrow chip (26px + 10px offset) so a two-line
     title never runs under it. */
  padding-right: 46px;
  border-radius: 18px;
  background: var(--sn-white);
  /* The card needs an UNMISTAKABLE edge, and a hairline was not enough.
     Shinta's cards are flat, and they can be: they sit on saturated pink, so
     the boundary is obvious. Ours sits on off-white (#F5F5F5) while being
     white (#FFFFFF) — ten points of luminance apart. At a glance that edge
     disappears, and the paragraph underneath reads as being inside the card's
     white area. That is the "text going over the card" that four rounds of
     measuring rectangles could never find, because geometrically nothing was
     ever wrong: the boundary was the problem, not the distance.

     So: hairline AND a soft shadow. Same reasoning as the floating header —
     depth here carries information rather than decoration, which is the
     exception decision #27 allows for. */
  border: var(--hairline);
  box-shadow: 0 1px 3px rgba(25, 70, 80, .05),
              0 6px 18px rgba(25, 70, 80, .06);
  /* Clips the sheen sweep below. Safe here — nothing inside is sticky and
     nothing inside uses a view() timeline, which are the two things an
     overflow container silently kills (see the note at §scroll). */
  overflow: hidden;
  text-decoration: none;
  /* translate/scale, NOT transform: GSAP's hero timeline leaves an inline
     `transform` on every [data-hero] element when it finishes, and an inline
     style beats a stylesheet rule. The individual properties compose on top
     of that transform instead of losing to it. */
  transition: translate var(--dur-fast) var(--ease-tween),
              scale var(--dur-fast) var(--ease-tween),
              border-color var(--dur-fast) var(--ease-tween),
              box-shadow var(--dur-fast) var(--ease-tween);
}

.spotlight__thumb {
  width: 44px; height: 44px;
  border-radius: 12px;
  overflow: hidden;
  background: var(--sn-blue-100);
  flex: 0 0 auto;
}
.spotlight__thumb img { width: 100%; height: 100%; object-fit: contain; }

.spotlight__body { min-width: 0; }

.spotlight__eyebrow {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.625rem;              /* 10 — below a band eyebrow on purpose */
  font-weight: var(--w-title);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
  color: var(--sn-link);
}
.spotlight__title {
  display: block;
  margin-top: 2px;
  font-family: var(--font-ui);
  font-size: 1rem;
  font-weight: var(--w-title);
  line-height: 1.2;
  letter-spacing: var(--ls-display-tight);
  color: var(--sn-ink);
}

/* Top-right corner, as a chip — Shinta puts its arrow there rather than
   vertically centred, which is what stops the row looking like a list item. */
.spotlight__arrow {
  position: absolute;
  top: 10px;
  right: 10px;
  display: grid; place-items: center;
  width: 26px; height: 26px;
  border-radius: 999px;
  background: var(--sn-off-white);
  color: var(--sn-ink);
  transition: background var(--dur-fast) var(--ease-tween),
              color var(--dur-fast) var(--ease-tween),
              translate var(--dur-fast) var(--ease-tween);
}
.spotlight__arrow .icon { width: 13px; height: 13px; }

/* ---- the card's own attention loop ----
   She asked for a smaller card with a BETTER animation, and hover cannot be
   the answer: on the hero this card competes with a headline and a photo, and
   most visitors never point at it. So it moves by itself, in two beats that
   cost nothing to composite (opacity/translate/scale only, no layout, no
   paint of the card body):

     - a sheen crosses the card once every 5s (1.1s of travel, 3.9s of rest).
       A wash of the brand blue rather than white, because white on a white
       card is invisible;
     - the arrow chip pings a ring outward every 2.8s, which is what draws the
       eye to the fact that the whole card is a link.

   The ring stops at 1.5x deliberately: the chip sits 10px from the corner
   and `overflow: hidden` above would slice anything wider. */
@media (prefers-reduced-motion: no-preference) {
  .spotlight::after {
    content: "";
    position: absolute;
    inset: -25% -45%;
    background: linear-gradient(100deg,
                transparent 42%,
                color-mix(in srgb, var(--sn-blue-900) 15%, transparent) 50%,
                transparent 58%);
    translate: -120% 0;
    animation: spotlight-sheen 5s var(--ease-tween) 1.4s infinite;
    pointer-events: none;
  }
  .spotlight__arrow::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    border: 2px solid var(--sn-blue-900);
    animation: spotlight-ping 2.8s var(--ease-out) 1.4s infinite;
    pointer-events: none;
  }
}
@keyframes spotlight-sheen {
  0%   { translate: -120% 0; }
  22%  { translate:  120% 0; }
  100% { translate:  120% 0; }
}
@keyframes spotlight-ping {
  0%   { opacity: .5; scale: 1; }
  55%  { opacity: 0;  scale: 1.5; }
  100% { opacity: 0;  scale: 1.5; }
}

@media (hover: hover) and (pointer: fine) {
  .spotlight:hover {
    translate: 0 -4px;
    scale: 1.02;
    border-color: color-mix(in srgb, var(--sn-blue-900) 45%, transparent);
    box-shadow: 0 2px 6px rgba(25, 70, 80, .06),
                0 16px 34px rgba(57, 118, 233, .22);
  }
  .spotlight:hover .spotlight__arrow {
    background: var(--sn-blue-900);
    color: var(--sn-white);
    translate: 2px -2px;
  }
  /* The ring has done its job once the pointer is on the card. */
  .spotlight:hover .spotlight__arrow::before { animation-play-state: paused; opacity: 0; }
}

.hero__lead { max-width: 38ch; font-size: var(--fs-body); }
.hero__ctas { display: flex; align-items: center; gap: 10px; }

/* Shinta's primary is near-black, not the brand colour. Ink at 19px/700 on
   white is 10.3:1, so this is the safest button on the site. */
/* Shinta's primary is near-black, not the brand colour. Ink at 19px/700 on
   white is 10.3:1, so this is the safest button on the site. */
.btn--dark { background: var(--sn-ink); color: var(--sn-white); }

/* ---- the paired CTA ----
   One anchor, two visual parts, and a single surface that appears behind both
   on hover so they read as one control. Previously two separate links, which
   left an unlabelled circle beside the real button. */
.cta-pair {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px;
  margin: -6px;                 /* the hover surface grows outward, not the layout */
  border-radius: 999px;
  text-decoration: none;
}

/* The surface. Sits under both halves, scales up from the middle so it reads
   as the two parts being wrapped rather than a third box appearing. */
.cta-pair::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--sn-white);
  opacity: 0;
  transform: scaleX(.92);
  transition: opacity var(--dur-fast) var(--ease-tween),
              transform var(--dur-fast) var(--ease-tween);
}

.cta-pair__label,
.cta-pair__arrow { position: relative; }   /* above the surface */

.cta-pair__label {
  display: inline-flex;
  align-items: center;
  min-height: var(--bar-btn-h);
  padding: 12px 30px;
  border-radius: 999px;
  background: #153E6E;
  color: var(--sn-white);
  font-family: var(--font-ui);
  font-size: var(--fs-btn);
  font-weight: var(--sn-w-bold);
  line-height: 1.2;
}

.cta-pair__arrow {
  display: grid;
  place-items: center;
  width: 52px; height: 52px;
  border-radius: 999px;
  background: var(--sn-blue-100);
  color: var(--sn-ink);
  flex: 0 0 auto;
  transition: background var(--dur-fast) var(--ease-tween);
}
.cta-pair__arrow .icon {
  width: 20px; height: 20px;
  transition: transform var(--dur-fast) var(--ease-tween);
}

@media (hover: hover) and (pointer: fine) {
  .cta-pair:hover::before { opacity: 1; transform: scaleX(1); }
  .cta-pair:hover .cta-pair__label { background: var(--sn-black); }
  .cta-pair:hover .cta-pair__arrow { background: var(--sn-blue-tile); }
  .cta-pair:hover .cta-pair__arrow .icon { transform: translateX(4px); }
}

/* The focus ring belongs to the whole control, not to half of it. */
.cta-pair:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .cta-pair::before,
  .cta-pair__arrow .icon { transition: none; }
  .cta-pair::before { transform: none; }
}

/* ---- THE TWO DRAWN LINES ARE DELETED ----
   Added 2026-09-02 to fill the gap the ticker left, removed the same day:
   "Mejor quita las curvas del Hero".

   Worth keeping the finding rather than the code. The programme pages'
   .drawline uses opacity .38 because those lines run under body copy; these
   were routed through measured holes and crossed text 0% of their length, and
   at .38 the brand navy #153E6E composited over the off-white ground to
   rgb(160,176,194) — a grey-blue that did not read as blue at all. If a brand
   colour ever has to read as itself at low opacity, check the composite
   against the actual ground first; .82 was needed here.
   ------------------------------------------------------------ */

/* ---- THE HERO RIBBON IS DELETED ----
   Malena, 2026-08-27, the same call that removed the section ribbons: the
   ticker goes, and is not to be used again.

   It was a thick stroke along an SVG curve with real text running the same
   curve, looping via <animate> on startOffset. The typography and the
   collision-measured curve were the careful parts, and neither survives being
   the wrong component: motion that starts by itself and never stops is what
   WCAG 2.2.2 asks you to provide a pause for, and there was none.

   Its four phrases — FREE TRAINING, PAID EXPERIENCE, CERTIFICATIONS, JOB
   MATCHING — are the hero's marker list, which states them once and is IN the
   accessibility tree. The band was a decorative repetition of copy already on
   the page, so the page lost nothing.
   ------------------------------------------------------------ */

/* Below the three-column breakpoint the hero stacks — and stacking in DOM
   order buried the Apply button ~900px down, under the photograph, on a
   1310px-tall hero. On a portal someone reads on a phone to find out whether
   they qualify, the action cannot sit below the decoration.

   Reordered to: headline and markers -> lead -> Apply -> photo -> spotlight.
   The photograph is the one piece here that is decoration, so it goes last,
   and it is capped smaller to stop the hero running to two viewports.
   Runs to 1199 — the hero goes three-column at 75rem, not the site's 64rem. */
@media (max-width: 74.9375rem) {
  .hero__grid { gap: var(--sn-space-4); }

  .hero__lede  { order: 1; }
  .hero__aside { order: 2; justify-items: stretch; }
  .hero__media { order: 3; margin-inline: auto; }

  /* inside the aside: lead, then the CTA, then the spotlight card */
  .hero__lead  { order: 1; max-width: none; }
  .hero__ctas  { order: 2; }
  .spotlight   { order: 3; max-width: none; }

  /* Stacked, the desktop caps just manufacture dead space on the right —
     measured: card and lead stopped at 380px and the title at 16ch while the
     column kept growing with the viewport. Malena: "todos los textos deben
     cubrir la mayoría del espacio a lo ancho". The column owns the width now;
     the art keeps a cap because it is decoration, not text. */
  .hero__title { max-width: none; }
  .hero__aside-top { --aside-w: 100%; }
  .hero__media { max-width: min(400px, 100%); }
}

/* Tablet, 768–1023: one stacked column left the whole right half dead
   (measured at 896: right gaps of 345–453px beside the text). Two columns —
   text left, artwork right — spend that space instead. Explicit rows, because
   `order` cannot place one item BESIDE two others in a grid. */
@media (min-width: 48rem) and (max-width: 74.9375rem) {
  .hero__grid {
    grid-template-columns: 1fr minmax(240px, 320px);
    column-gap: var(--sn-space-5);
    align-items: center;
  }
  .hero__lede  { grid-column: 1; grid-row: 1; align-self: end; }
  .hero__aside { grid-column: 1; grid-row: 2; align-self: start; }
  .hero__media { grid-column: 2; grid-row: 1 / span 2; max-width: none; margin: 0; }
}

/* Desktop (three-column) width release. Appended AFTER the base .hero__aside-top
   rule on purpose: it has to win the source-order tiebreak against the 380px
   base value. The card and lead fill the right column to the container edge —
   the boundary the header bar marks — instead of capping at 380px. */
@media (min-width: 75rem) {
  .hero__aside-top { --aside-w: 100%; }
  /* THE CARD SHARES THE COLUMN AGAIN (2026-09-02): "haz la card de feature
     program un poco mas grande ... para que use un poco mas del espacio
     disponible". It had been capped at 320px in a 407px column, so it sat
     visibly narrower than the paragraph beneath it and left the right end of
     the column empty.

     Wider is not back to where it started. The first version was too big
     because of its INTERNALS — a 64px seal, an --fs-h3 title and 16px of
     padding — and those stay compact (44 / 16px / 12px). Same footprint as
     the original, noticeably lighter inside it. */
  .hero__aside-top > .spotlight { max-width: none; }
}

/* ============================================================
   9. THE QUOTE, PAIRED WITH A PORTRAIT  (Malena, 2026-08-27)

   Text left, the composed portrait graphic right. The graphic
   carries its own pink panel and transparent surround, so it
   needs no frame, no radius and no background from us — adding
   any would double the panel it already has.
   ============================================================ */

.quote-split { display: grid; gap: var(--sn-space-4); }

/* THE LAYOUT BOX IS THE PANEL, NOT THE FILE.
   ------------------------------------------------------------
   Measured from the alpha channel of the 1019x1023 master: the solid pink
   panel runs x 263..987, so it is 71.05% of the file, with a 25.81%
   transparent lead down the left and a 3.14% trail on the right.

   Sizing the column to the whole file therefore booked a QUARTER of it as
   layout width for transparency — 134px of nothing at desktop — which stacked
   on top of the grid gap and the text's unused measure to read as an enormous
   hole between the quote and the portrait. That was Malena's report, and the
   file, not the CSS, was where the space came from.

   That lead cannot be cropped: the pale peach arc crosses all 263px of it at
   alpha 87..133 (34-52% opaque). It is a designed element, not a residue.

   So the column is the PANEL's width and the image is laid out relative to it:
   width 100%/panel-share makes the panel exactly fill the column, and the two
   negative margins let the transparent bands — and the arc riding in the left
   one — hang outside it. The three shares add back to 100%, so nothing drifts.

   Percentage margins resolve against the containing block, which here IS the
   column, which is why this works without a single hard-coded pixel. */
.quote-split__art {
  --panel-share: 0.7105;
  --lead-share: 0.2581;
  --trail-share: 0.0314;
}

.quote-split__art img {
  display: block;
  width: calc(100% / var(--panel-share));
  max-width: none;
  margin-left: calc((-100% * var(--lead-share)) / var(--panel-share));
  margin-right: calc((-100% * var(--trail-share)) / var(--panel-share));
  height: auto;
}

@media (max-width: 55.9375rem) {
  /* Stacked. The width is the PANEL's, so it centres on the panel the eye
     sees rather than on the file, whose centre sits 12.9% to the right. */
  .quote-split__art { width: 225px; margin-inline: auto; }
}

/* 56rem (896), not the site's usual 48rem: at 768 the split left the text a
   329px column carrying 33px type — four or five words a line, which reads as
   a broken column rather than a pull quote. 896 is where the text column
   clears ~460px, and below it the section simply stacks. */
@media (min-width: 56rem) {
  .quote-split {
    /* THE GAP IS THE ARTWORK'S OWN LEAD. Set to the width the arc overhangs
       by, the arc EXACTLY fills the space between text and panel: nothing is
       empty and nothing overlaps the type. Any smaller and the arc would run
       under the words; any larger and the hole comes back. */
    grid-template-columns: minmax(0, 1fr) 280px;
    column-gap: 102px;   /* 394 rendered x 0.2581 lead */
    align-items: center;
  }
  /* A ceiling, not a measure: the column is the real constraint now that the
     dead quarter is gone. 68ch only stops the line running away on a very wide
     screen — it was 46ch, which is what left the text stopping 394px short of
     the portrait and pooling the gap Malena flagged. */
  .quote-split > .quote { max-width: 68ch; }
}

@media (min-width: 64rem) {
  /* 20% larger than the previous treatment (Malena, 2026-08-27), which the new
     high-resolution master finally allows: the panel goes 307 -> 370, and at
     521px rendered the 1019px file still has better than 1.9x behind it. */
  .quote-split { grid-template-columns: minmax(0, 1fr) 370px; column-gap: 134px; }
}

/* ---- the scroll-written quote ------------------------------
   Ported from the Consortium build (public/js/main.js §scrollWrite), same
   effect, same framing constants. Words light up as the band travels through
   the viewport, three at a time, and stay lit on the way back up.

   OPACITY, NOT A MASK — for the reason the Consortium notes: the phrase has
   descenders and quote marks, and a mask cropped to the line box eats them.

   ONE CUSTOM PROPERTY IS WRITTEN PER FRAME (--sw-p, on the parent). Each word
   derives its own opacity from --sw-p and its own index, so the driver never
   touches 48 inline styles.

   EVERY NAME IS PREFIXED --sw-. The reveal system already uses a bare --i for
   its stagger and it inherits; a word index called --i would have collided
   with it inside any revealed ancestor.

   Degradation, in order:
     · app.js only splits the text when motion is allowed, so with no JS or
       reduced motion there is no .sw-w at all and the quote is simply there.
     · --sw-p defaults to 1, so even if the driver never runs, every word
       resolves to full opacity rather than staying dark.
   ------------------------------------------------------------ */

.quote blockquote {
  --sw-p: 1;
  /* Unlit words sit at 16% rather than 0: at zero the line reflows visibly as
     each word arrives, and the paragraph's shape should be stable from the
     start. */
  --sw-off: 0.16;
  --sw-ramp: 0.1;
  --sw-step: 0.02;
}

.sw-w {
  display: inline-block;
  opacity: clamp(
    var(--sw-off),
    calc(var(--sw-off) + ((var(--sw-p) - var(--sw-i) * var(--sw-step)) / var(--sw-ramp)) * (1 - var(--sw-off))),
    1
  );
}

@media (prefers-reduced-motion: reduce) {
  .sw-w { opacity: 1; }
}

/* ============================================================
   10. THE CONTACT FORM, SHINTA'S COMPOSITION  (Malena, 2026-08-27)

   Shinta's contact section is a split: supporting copy on the
   left, the form inside a saturated rounded panel on the right,
   and inside THAT a white card holding the fields. The panel is
   a frame, not a background — its colour only shows as a border
   and behind the title.

   Translated to brand rather than copied: Shinta's magenta
   becomes --sn-blue-100, the tint this site already uses for
   number chips and step cards, and its black submit pill becomes
   --sn-ink, which is what the hero's Apply pill already is.
   ============================================================ */

/* Copy left, form right — Shinta's composition, and it applies EVERYWHERE the
   form appears, not just /contact. It was hero-only at first, which is why the
   homepage and the seven program pages still showed a stacked form with no blue
   frame: the panel wrapper only existed on one page. Renamed off "hero-" and
   applied to all nine.

   On /contact the supporting blocks moved to their own band below rather than
   competing with the form for the right-hand column. */
.form-split { display: grid; gap: var(--sn-space-5); }

@media (min-width: 56rem) {
  .form-split {
    grid-template-columns: 1fr 1.05fr;
    gap: var(--sn-space-6);
    align-items: start;
  }
  /* The copy's bottom margin was for a stacked heading above a form. Side by
     side it just pushes the column out of alignment with the panel. */
  .form-split > .band__head { margin-bottom: 0; }
}

/* The three supporting blocks read as a row below the fold, not a tall stack:
   they are peers, and stacked they pushed the page down for no reason. */
@media (min-width: 56rem) {
}

/* THE FRAME IS SATURATED, and that is the whole point.
   ------------------------------------------------------------
   It was --sn-blue-100 first, and it measured 1.39:1 against the page's
   off-white — a frame nobody could see, which is why Malena asked for "algún
   diseño en azul... con borde o similar". Shinta's magenta reads instantly
   because it is saturated against its light grey page; a pale tint cannot do
   that job. --sn-blue-900 measures 3.90:1 against off-white, so the frame is
   unmistakable, and it is the brand's primary blue rather than an invention.

   It sits above .hero__pop so the decorative circle passes behind the panel
   instead of colliding with its corner. */
.form-panel {
  position: relative;
  z-index: 1;
  background: var(--sn-blue-900);
  border-radius: var(--sn-radius-lg);
  padding: var(--sn-space-2);
}

/* Become a Partner wears the footer's navy instead of the logo blue, asked for
   2026-08-27 ("el mismo del fondo del footer"). Same value the footer computes
   to and the same one Pathways uses as its hero ground, so it is a colour the
   site already owns rather than a fourth dark blue.

   It is also the safer of the two for this panel: the white title measures
   10.79:1 on #153E6E against 4.25:1 on the logo blue, so here the size of the
   title is a design choice rather than the thing holding the contrast up. */
.form-panel--navy { background: #153E6E; }

/* The heading was .visually-hidden. Shinta shows its form title, and there is
   no reason to hide ours — the copy is unchanged, it just became visible.

   --fs-h2, NOT --fs-h3, and the size is doing accessibility work as well as
   design work: white on blue-900 is 4.25:1, which fails the 4.5:1 rule for
   normal text but clears the 3:1 large-text threshold. Large means 24px, and
   --fs-h3 tops out at 22. --fs-h2 starts at 28. Shinta's own form title is
   large too, so the reference and the contrast rule want the same thing. */
/* DOUBLED SELECTOR, AND IT HAS TO BE. `.band h2, .band h3` sets
   `color: inherit` at (0,1,1) so a band's ground can reskin its headings, and
   that beat the single-class (0,1,0) here — the title took the band's dark ink
   and rendered at 1.04:1 on the panel, i.e. invisible. It only ever looked
   right on /contact because that panel sits in a .hero, which the rule does not
   match. .form-panel .form-panel__title is (0,2,0) and wins in both places.
   Same collision family as .site-footer ul and .band:not([data-ground]) — when
   a component lands inside a themed container, one class is not enough. */
.form-panel .form-panel__title {
  font-size: var(--fs-h2);
  /* Stated, not inherited. This class lands on an <h2> at a page top and on an
     <h3> mid-page, and those two carry different default weights — which is how
     the homepage panel ended up 100 lighter than /contact's while every other
     value matched. The look is the class's job; the level is the page's. */
  font-weight: var(--w-title);
  color: var(--sn-white);
  padding: var(--sn-space-2) var(--sn-space-3) var(--sn-space-3);
}

.form-panel .form {
  /* The panel is the measure now. 640px was for a form standing in an open
     column; here it would leave the card floating inside its own frame. */
  max-width: none;
  gap: 0;
}

.form__card {
  display: grid;
  gap: var(--sn-space-3);
  background: var(--sn-white);
  border-radius: var(--sn-radius-md);
  padding: var(--sn-space-4);
}

/* SCOPED TO .form__card, NOT .form-panel. The form partial is stamped on nine
   pages — /contact plus the homepage and every program page — while the tinted
   panel wraps it only on /contact. Hanging the field styling off the card means
   the form looks the same everywhere it appears; the panel adds only its frame
   and the visible title. Wrap any other section in .form-panel to get those too.

   Labels take the eyebrow treatment: uppercase, Open Sauce One 600, opened up.
   Same voice as every other small label on the site. */
.form__card .form-field label {
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  font-weight: var(--w-title);
  /* No tracking. These took the eyebrow treatment wholesale, open letter-spacing
     included, and on a form label it read as a gap between letters rather than
     as a label. The uppercase and the weight carry the label voice on their
     own here — an eyebrow is set once per section, a field label eight times
     down a column, and the tracking that flatters one crowds the other. */
  letter-spacing: normal;
  text-transform: uppercase;
}

/* Filled fields, as in Shinta — BUT THE BORDER STAYS.
   Shinta's inputs are borderless because its card is saturated pink, so a grey
   fill reads instantly. Ours sit on white, where off-white is ten luminance
   points away: exactly the invisible boundary that made the homepage spotlight
   card look like it was swallowing the paragraph below it. Fill for the shape,
   hairline for the edge. */
.form__card .form-field input,
.form__card .form-field select,
.form__card .form-field textarea {
  background: var(--sn-off-white);
  border-color: var(--sn-grey-200);
  border-radius: var(--sn-radius-sm);
  padding: 15px 18px;
  min-height: 54px;
}

/* ---- the programme select's chevron ----
   The native one is drawn by the platform against the field's right edge and
   cannot be repositioned, which is exactly what looked wrong. appearance: none
   drops it; the sprite draws the replacement, so it is the same icon set as
   every other glyph on the site rather than a one-off. */
.select-wrap { position: relative; display: block; }

/* Scoped through .form-field to outrank the field rule that sets `padding:
   15px 18px` — a bare `.select-wrap select` is (0,1,1) against its (0,3,1), so
   the right padding was ignored and the option text could run under the icon.
   Measured 18px when it should have been 52. Fifth specificity collision of
   this kind in this stylesheet; the tell is always a value that reads correctly
   in the file and computes to something else. */
.form-field .select-wrap select {
  appearance: none;
  -webkit-appearance: none;
  /* Room for the icon plus its own gutter, so the glyph never sits on the edge
     the way the native one did. */
  padding-right: 52px;
}

.select-wrap__chevron {
  position: absolute;
  right: 18px;
  top: 50%;
  translate: 0 -50%;
  width: 18px;
  height: 18px;
  color: var(--sn-ink);
  pointer-events: none;   /* clicks belong to the select underneath */
}

/* Focus lifts the field to white as well as colouring the edge, so the active
   row is obvious without relying on the 1.5px border alone. */
.form__card .form-field input:focus-visible,
.form__card .form-field select:focus-visible,
.form__card .form-field textarea:focus-visible {
  background: var(--sn-white);
}

/* 73% ink is the LOWEST mix that clears 4.5:1 on the off-white fill (4.57:1);
   --sn-grey-500 measures 3.85:1 there and fails. Placeholders are supplementary
   — every field keeps its visible label — but there is no reason to ship text
   that cannot be read. */
.form__card .form-field ::placeholder {
  color: color-mix(in srgb, var(--sn-ink) 73%, var(--sn-off-white));
  opacity: 1;                        /* Firefox dims placeholders by default */
}

/* Shinta's footer row: consent left, submit right, on one line when there is
   room for it. */
.form__foot {
  display: grid;
  gap: var(--sn-space-3);
  align-items: center;
  margin-top: var(--sn-space-2);
}

@media (min-width: 30rem) {
  .form__foot { grid-template-columns: 1fr auto; }
}

.form__consent {
  font-size: var(--fs-caption);
  /* Ink, not grey-500: grey-500 on white is 4.20:1 and this is 13px text.
     Shinta's consent line is dark too, so brand and legibility agree. */
  color: var(--sn-ink);
}

.form__consent a { color: var(--sn-link); font-weight: var(--sn-w-bold); }

.form__card .form__submit { justify-self: stretch; }
@media (min-width: 30rem) {
  .form__card .form__submit { justify-self: end; }
}

/* ============================================================
   11. PROGRAMME CARDS WITH ARTWORK  (Malena, 2026-08-27)

   Six of the eight cards now carry a finished artwork instead of
   a coloured tile. The artwork is a whole card: the programme
   name is set into the pixels, along with the arrow and the
   rounded corners. So the markup drops .card-program__title and
   .card-program__pop for those six — leaving them would print
   the name twice — and the <img> carries the name in its alt,
   which for a screen reader is the only place it exists.

   Art direction switches at 30rem, NOT at the grid's own 48rem:
   the grid is still one column up to 48rem, so a portrait image
   there would render about 719x934 on a wide phone. Landscape
   from 480px up, portrait below it.
   ============================================================ */

.card-program__tile--art {
  display: block;
  padding: 0;
  min-height: 0;
  background: none;
}

.card-program__tile--art img {
  display: block;
  width: 100%;
  height: auto;
  /* THE RATIO IS DECLARED PER BREAKPOINT, and it has to be. The width/height
     attributes describe the landscape file — they match the fallback src — but
     below 30rem a PORTRAIT file loads, so the reserved box and the delivered
     image disagreed. Measured mid-build: tiles came out 201px or 449px tall in
     the same grid depending purely on which variant had finished loading, which
     is a layout shift on every card. CSS aspect-ratio wins over the
     attribute-derived ratio, so the box is correct before any byte arrives. */
  aspect-ratio: 896 / 1168;
}

@media (min-width: 30rem) {
  .card-program__tile--art img { aspect-ratio: 1839 / 1072; }
}

/* IT Accelerator and Job Matching have no artwork yet, so they keep the
   coloured tile — but from 48rem, where the grid is two and three across and
   rows have to line up, they take the artwork's aspect ratio. Below that the
   grid is one column, nothing needs to align, and a 1.7:1 colour block with
   only a title in it would just be a lot of empty paint. */
@media (min-width: 48rem) {
  .card-program__tile:not(.card-program__tile--art) {
    aspect-ratio: 1839 / 1072;
    min-height: 0;
  }
}

/* ============================================================
   12. PROGRAMME HEROES  (Malena, 2026-08-27)

   Full viewport, the programme's own solid ground, copy on the
   left and the cutout photograph on the right.

   The viewport height and the header clearance are NOT redeclared
   here — the hero carries .hero--full, whose rule already reasons
   about min-height vs height, svh vs vh, and the floating bar.
   ============================================================ */

/* MALENA'S PALETTE, and it lives here on purpose.
   ------------------------------------------------------------
   tokens.css is the brand kit byte-for-byte and is never edited, and these are
   keyed on the page slug rather than on [data-ground] so the kit's eight grounds
   keep serving every card, band and CTA on the site unchanged.

   Three of the eight are byte-identical to the kit ground the page already used
   (navy, orange, rose) and keep the kit's own ink pairing. Five depart from it,
   so their ink is whichever of ink-or-white measures higher — the ratio on each
   line is --g-ink against --g-bg, measured, not assumed.

   DISPLAY ONLY marks the two that cannot carry 13px or 20px text at any ink
   colour. Those heroes hold the headline and the buttons; their paragraph is
   emitted into a white band underneath instead, and build-pages.js derives that
   from the same numbers (HERO_GROUND) rather than from a list of names. */
[data-hero-bg="pathways-to-public-service"]    { --g-bg: #153E6E; --g-ink: #FFFFFF; }  /* 10.79 */
/* AA EXCEPTION, DELIBERATE — Malena, 2026-08-27, reaffirmed after seeing the
   numbers: "no me interesa que no sean AA". White on #E8A70F measures 2.11:1,
   which fails 4.5:1 for small text and also 3:1 for large — so the 60px title
   fails here too, not only the paragraph. Black on the same ground measures
   9.97:1 and is the one-value revert. Phase 11 axe/Lighthouse WILL flag this
   hero: that is expected, do not quietly "fix" it. */
[data-hero-bg="out-of-school-program"]         { --g-bg: #E8A70F; --g-ink: #FFFFFF; }  /* AA EXCEPTION, DELIBERATE — Malena, 2026-08-27: "no importa que no sea AA".
   White on #3BB99C measures 2.44:1, under both 4.5:1 (small text) and 3:1
   (large), so the 60px title fails here too. Black would be 8.61:1 and is
   the one-value revert. Phase 11 axe/Lighthouse WILL flag this hero. */
[data-hero-bg="college-fellowship-program"]    { --g-bg: #3BB99C; --g-ink: #FFFFFF; }  /* 2.44 — exception */
/* AA EXCEPTION, DELIBERATE — Malena, 2026-08-27, reaffirmed after seeing the
   numbers: "no me interesa que no sean AA". White on #FF6A63 measures 2.80:1,
   which fails 4.5:1 for small text and also 3:1 for large — so the 60px title
   fails here too, not only the paragraph. Black on the same ground measures
   7.49:1 and is the one-value revert. Phase 11 axe/Lighthouse WILL flag this
   hero: that is expected, do not quietly "fix" it. */
[data-hero-bg="pre-apprenticeship"]            { --g-bg: #FF6A63; --g-ink: #FFFFFF; }  /* AA EXCEPTION, DELIBERATE — Malena, 2026-08-27, the third of three. White on
   #6C92FF measures 2.92:1, failing 4.5:1 for small text and 3:1 for large, so
   the 60px title fails here too. Black would be 7.19:1 and is the one-value
   revert. Phase 11 axe/Lighthouse WILL flag this hero: expected, not a bug. */
[data-hero-bg="pharmacy-technician-program"]  { --g-bg: #6C92FF; --g-ink: #FFFFFF; }  /* 2.92 — exception */
[data-hero-bg="it-accelerator"]                { --g-bg: #7765C0; --g-ink: #FFFFFF; }  /* AA EXCEPTION, DELIBERATE — Malena, 2026-08-27: "no importa que no sea AA".
   White on #7765C0 measures 4.43:1: fine for large text, under the 4.5 a
   paragraph needs. Phase 11 axe/Lighthouse WILL flag this hero. */
/* YOUTH IS NO LONGER AN EXCEPTION. It was the exact-boundary case on the old
   orange — 2.9954:1, which rounds to 3.00 and still failed the large-text
   floor by four ten-thousandths. DOES's new blue is 6.72:1, so white passes
   here at any size, including the small print. One fewer signed-off exception
   on the site. */
[data-hero-bg="youth-apprenticeship"]          { --g-bg: #215E99; --g-ink: #FFFFFF; }  /*  6.72 */
[data-hero-bg="job-matching"]                  { --g-bg: #1D77B8; --g-ink: #FFFFFF; }  /*  4.79 */

/* Copy left, cutout right — and THE CUTOUT SITS ON THE HERO'S BOTTOM EDGE.
   ------------------------------------------------------------
   Malena: "están pensadas" that way, and the files prove it — every one of the
   eight has ink in its last pixel row, 0% transparent padding below the figure
   (measured on the alpha channel). So the figure is cropped at the frame and is
   meant to stand on the edge; centring it left the gap under it that she marked.

   Three moves, because the hero has bottom padding the artwork must escape:
     1. the section stretches its inner instead of centring it, so the split
        grid is as tall as the hero's content box;
     2. the art aligns to the end of that box while the copy stays centred;
     3. a negative bottom margin of exactly --section-pad carries the art past
        the hero's own bottom padding to the section's true bottom edge.

   .hero--ground already sets overflow: hidden, so an overshoot would be clipped
   rather than spilling into the next band — but the margin is exact, so there is
   nothing to clip. */
.hero__split { display: grid; gap: var(--sn-space-4); align-items: center; }

.hero--program { align-items: stretch; }

.hero--program .hero__split {
  align-items: end;
  align-content: stretch;
}

.hero--program .hero__copy { align-self: center; }

.hero--program .hero__art {
  align-self: end;
  margin-bottom: calc(var(--section-pad) * -1);
}

.hero__art { min-width: 0; }

.hero__art img {
  display: block;
  width: 100%;
  height: auto;
  /* The cutouts are 1112x1072, and the attribute pair already says so — this is
     belt and braces against a re-export at a different size silently changing
     the reserved box in a hero that is the page's LCP. */
  aspect-ratio: 1112 / 1072;
}

@media (max-width: 55.9375rem) {
  /* Stacked, the artwork is decoration under the copy and the buttons, so it is
     capped: at full width on a phone it would push the CTAs off a 100svh hero,
     which is the one thing .hero--full exists to prevent. It still sits on the
     bottom edge — the negative margin above is not breakpoint-scoped. */
  .hero__art img { max-width: 368px; margin-inline: auto; }   /* 320 +15% */
}

@media (min-width: 56rem) {
  .hero__split {
    /* 496, down from 560. At the 1280 container that leaves the copy column
       680px — where Malena drew the line — while keeping the full 56px gutter
       between text and artwork. Closing the gap instead was the other option
       and it is not safe: the cutouts carry between 2.5% and 11.2% of
       transparent margin on their left (measured), so Pharmacy would have given
       the text only 13px of real clearance. */
    /* THE ARTWORK IS 15% LARGER (Malena) — 496 -> 570 — AND SOMETHING HAD TO
       GIVE. At the 1232px container, 680px of copy plus 570px of artwork plus
       any gap does not fit: 680 + 570 = 1250 on its own. Bleeding the artwork
       into the right gutter was the other option and it is not safe — College's
       cutout keeps only 4.5% of transparent margin on its right (26px at this
       size), so a viewport at exactly --maxw would clip her arm.

       So the gutter between them pays for it: 56 -> 32, leaving 630px of copy.
       That is 50px short of the line Malena drew and still 202px wider than the
       428px the measure used to allow. The real clearance is larger than 32
       anyway: every cutout carries 2.5% to 11.2% of transparent margin on its
       left, so the narrowest of them (Pharmacy) still shows 46px of air. */
    /* min(), not a flat cap. A fixed 570px column starved the copy on the
       narrower desktops the two-column layout also covers: measured at a 900px
       viewport the artwork took 473px and left the text 332px. 44vw keeps the
       ratio sane all the way down and the 570px ceiling is where the +15%
       actually lands, from about 1300px up. */
    grid-template-columns: minmax(0, 1fr) minmax(0, min(570px, 44vw));
    gap: var(--sn-space-4);
  }
}

/* THE MEASURE WAS THE REAL LIMIT, not the column. `p { max-width: var(--measure) }`
   is 68ch, which against the lead's own 17px resolved to 428px — so the
   paragraph stopped 188px short inside a 616px column and read as a narrow
   ribbon beside a large photograph.

   Released only inside the programme hero, where the container cap already
   bounds the line: at --maxw the copy column tops out at 680px, so nothing runs
   away on a wide screen. It IS a long measure for continuous prose; these are
   two- and three-line hero leads, which is the case the rule is least about. */
.hero--program .hero__lead,
.hero--program .hero__title { max-width: none; }

/* THE LEAD IS LARGE TEXT, and that is load-bearing rather than decorative.
   ------------------------------------------------------------
   Malena: every programme hero must carry its description. At --fs-lead (17px)
   that is small text needing 4.5:1, which Youth (3.45), Job (3.42) and IT (4.43)
   cannot give — so two of them had their paragraph exiled to a band below.

   WCAG's large-text exemption is 24px at regular weight, and the threshold drops
   to 3:1. Every ground clears that with its measured ink: Pathways 10.79,
   Out-of-School 6.00, College 5.49, Pre-Apprenticeship 5.49, Pharmacy 4.92,
   IT 4.43, Job 3.42, Youth 6.72 since the repalette. So the lead can live in
   the hero on all eight.

   THE CLAMP MINIMUM IS EXACTLY 1.5rem AND MUST NOT GO BELOW IT. The exemption
   is per rendered size, so a lead that shrinks to 22px on a small screen stops
   being large text there and three of these grounds become illegal again. */
.hero--program .hero__lead {
  /* 25% SMALLER (Malena): 24-28px -> 18-21px.
     This is only legal because the grounds moved. The 24px floor existed to buy
     the WCAG large-text exemption at 3:1, which was the only way three of the
     old pastel grounds could carry a paragraph at all. Every ground now measures
     white at 4.75:1 or better — past the 4.5:1 that small text needs — so the
     exemption is no longer load-bearing and the size is free to be a design
     choice again. Shrinking this while the pastels were still in place would
     have made three heroes illegal. */
  font-size: clamp(1.125rem, 1.0125rem + 0.41vw, 1.3125rem);
  line-height: 1.5;
}

/* ---- 12b. THE PROGRAMME HERO CTA (Malena, 2026-08-27) ------
   Pure black pill, white label, celeste arrow disc, white surface on hover.

   The hover surface is not added here — .cta-pair already draws one white plate
   under BOTH halves and scales it up from the middle, which is the behaviour
   Malena approved on the homepage. This block only recolours the parts.

   Measured: white on #000000 is 21.00:1, and the arrow glyph goes black on the
   celeste disc at 13.90:1 rather than ink at 6.84 — the black matches the pill
   and there is no reason to take the weaker of two passing options. */
.hero--program .cta-pair__label {
  background: var(--sn-black);
  color: var(--sn-white);
}

.hero--program .cta-pair__arrow {
  background: var(--sn-blue-100);
  color: var(--sn-black);
}

/* ============================================================
   13. THE PROGRAMME CLOSING SECTION  (Malena, 2026-08-27)

   Every programme page ends on this: copy and direct contacts
   on the left, the preselected form on the right, on the
   programme's own hero colour.

   The eight ground rules in §12 are keyed on [data-hero-bg]
   rather than on .hero--program precisely so this section can
   reuse them — the page opens and closes on the same colour
   without the value being written twice.
   ============================================================ */

/* THREE SELECTORS DEEP, AND EVERY ONE EARNS ITS PLACE. `.band:not([data-ground])`
   forces a transparent background and computes to (0,2,0) — a bare
   `.program-close` is (0,1,0), so the ground colour was overridden and the
   section rendered transparent while the rule looked correct in the file.

   Fourth specificity collision of this kind in this stylesheet (the footer's
   li+li margin, its a:hover underline, its ul margin reset, now this). The
   pattern is always the same: a broad structural rule quietly outranking a
   component rule, and it only ever shows up when the computed value is
   measured rather than read. */
.band.program-close[data-hero-bg] {
  position: relative;
  overflow: hidden;                 /* crops the ribbon at the section edges */
  background: var(--g-bg);
  color: var(--g-ink);
}

/* The form's blue frame is dropped here. On /contact it separates a white card
   from an off-white page; on a saturated programme colour the section already
   does that job, and a blue frame between the coral and the card was a third
   colour arguing with two. White card straight onto the ground — which is the
   Shinta arrangement the frame was imitating in the first place. */
.program-close .form-panel {
  background: transparent;
  padding: 0;
}

.program-close .band__head .eyebrow,
.program-close .band__head h2,
.program-close .lead { color: inherit; }

/* The direct contacts, stacked under the copy rather than in the two-column
   grid .contact-list uses elsewhere — this column is already one of two. */
.contact-list--stack {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sn-space-2);
  margin-top: var(--sn-space-4);
  list-style: none;
  padding: 0;
}

.contact-list--stack .contact-list__name {
  display: block;
  font-weight: var(--sn-w-bold);
  /* 4px -> 2.4px, the 40% Malena asked for, kept as a fraction rather than
     rounded so the reduction is exactly what was requested. */
  margin-bottom: 2.4px;
}

/* WHERE THE WHITESPACE WAS COMING FROM, measured on Youth: a 119px card made of
   24px padding all round, a 22px name, and an email link whose ink is 22px tall
   inside a box forced to 44px. That left 36px of empty card below the address —
   24 of padding plus the link's own unused 22, half of it under the text.

   The 44px is a tap target (docs/09) and is NOT given up: it stays wherever the
   pointer is coarse. On a fine pointer there is nothing to hit, so the link
   collapses to its line box and the card loses the padding it was hiding. Same
   split the footer link lists already use.

   Result: 119px -> about 76px, with the address still a 44px target on touch. */
/* THE CARD'S OWN GEOMETRY (Malena, 2026-08-27), and the three values are tied
   to each other on purpose:

     radius   --radius-card is 24px; 20% less is 19.2px.
     padding  her rule is "the radius that remains, plus 2px" — so the sides get
              21.2px, derived from the radius rather than typed separately, which
              means changing one changes both and they cannot drift apart.
     width    80% of the column, capped so the card never fills its side.

   Vertical padding stays at one space step: she specified the sides only. */
.contact-list--stack li {
  --card-radius: calc(var(--radius-card) * 0.8);
  padding: var(--sn-space-2) calc(var(--card-radius) + 2px);
  border-radius: var(--card-radius);
  max-width: 80%;
}

.contact-list--stack a { min-height: 44px; }

@media (hover: hover) and (pointer: fine) {
  .contact-list--stack a { min-height: 0; }
}

.contact-list--stack { gap: 12px; }

/* ---- the section ribbon ------------------------------------
   WHITE BAND, BLACK TEXT (Malena). The hero's ribbon is a pale blue band on
   off-white; this one has to read as a bright stripe laid across a saturated
   colour, so the band is white and the type is black on it — 21:1, and the same
   pair on all eight grounds, which is why it does not need a per-colour rule.

   It sits ABOVE the two columns, in the section's own top padding, and is
   cropped by the section edges. Nothing to dodge, unlike the hero's, so the
   curve is a gentle S rather than a measured route.
   ------------------------------------------------------------ */

/* THE SECTION RIBBON IS DELETED. Malena, 2026-08-27: "I generally do not like
   the ticker across our websites ... it makes our accessibility score go
   down". She is right, and the accessibility reason is the concrete one: the
   loop was an SVG <animate> on startOffset that ran forever with no way to
   stop it, and WCAG 2.2.2 asks for a pause on any motion past five seconds.
   aria-hidden kept it out of the accessibility tree, not out of the viewport.

   Gone with it: .section-ribbon, __band and __text here, ribbon()/ribbonPhrase()
   in scripts/build-pages.js, buildRibbons() in public/js/app.js, and the hero's
   own .hero__ribbon* rules (section 8). All deleted, not disabled — reaching for a
   ticker again should mean writing one from scratch on purpose.

   NOTHING WAS LOST. Each ribbon read "<PROGRAM NAME> • APPLY NOW", and the
   section it crossed is headed "Start your application" with an Apply button
   in it. */

.program-close > .u-container { position: relative; z-index: 1; }

/* Room for the band to cross without touching the heading. */
/* The extra 34px of top padding was room for the band when it crossed above the
   columns. It sits at 70% now, so the section goes back to the standard rhythm. */

/* ============================================================
   14. ABOUT THE PROGRAM  (Malena, 2026-08-27)

   Copy left, application window right. Two states, both built:
   an open window drawn as a calendar in the programme's own
   colour, and a closed one in grey with a way to be told first.

   Every programme shows the closed state today because no
   application dates are audited — see applicationWindow() in
   build-pages.js. Adding dates to programs.json flips a page to
   the calendar with no CSS change.
   ============================================================ */

.about-program__grid { display: grid; gap: var(--sn-space-5); align-items: center; }

@media (min-width: 56rem) {
  .about-program__grid { grid-template-columns: 1fr minmax(0, 420px); gap: var(--sn-space-6); }
}

.appwin {
  padding: var(--sn-space-4);
  border-radius: var(--radius-card);
}

.appwin__status {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  font-weight: var(--w-title);
  text-transform: uppercase;
}

.appwin__dot {
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: currentColor;
  flex: 0 0 auto;
}

/* ---- open: rebuilt to Malena's reference, 2026-08-27 ----
   The first version was a saturated panel with two small leaves. Hers is a pale
   tint panel, a headline with a badge, two tall cards with a pill label riding
   their top edge, a linked arrow between them, and a footer note with the apply
   button. Rebuilt to that.

   TWO DERIVED COLOURS, both from the programme's own --g-bg, so nothing is typed
   twice and any programme keeps its identity:

     --p-tint    22% of the colour over white — the panel. 12% first, which was
                 so washed out the tie to the programme barely read; 22% makes
                 the hue obvious and still leaves ink at 6.96:1 on the worst of
                 the eight (Pathways). Measured across all eight before picking:
                 18% gives 7.47, 28% gives 6.14.
     --p-accent-safe
                 45% of the colour over ink. The colour ALONE fails on white for
                 five of the eight (gold is 2.11:1, College 2.44) so the month
                 name and the year could not simply be the brand colour. Pulled
                 55% toward ink, the worst of the eight measures 4.97:1 and the
                 hue is still plainly the programme's.
   ------------------------------------------------------------ */
.appwin--open {
  --p-tint: color-mix(in srgb, var(--g-bg) 22%, var(--sn-white));
  --p-accent-safe: color-mix(in srgb, var(--g-bg) 45%, var(--sn-ink));

  position: relative;
  overflow: hidden;
  background: var(--p-tint);
  color: var(--sn-ink);
  padding: var(--sn-space-4);
}

/* The decorative disc that sat in the top-right corner was removed at Malena's
   request 2026-08-27. The panel's own tint now carries the programme's colour,
   so the disc was a second statement of the same thing. */

.appwin__head {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sn-space-2);
  align-items: start;
}

.appwin__badge {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background: var(--g-bg);
  color: var(--sn-white);
  flex: 0 0 auto;
}

.appwin__badge .icon { width: 22px; height: 22px; }

.appwin__title {
  font-family: var(--font-ui);
  font-size: var(--fs-h3);
  font-weight: var(--sn-w-bold);
  text-transform: uppercase;
  letter-spacing: .01em;
  color: var(--sn-ink);
}

.appwin__sub {
  margin-top: 6px;
  font-size: var(--fs-caption);   /* 17px -> 13px (Malena) */
  line-height: 1.45;
  color: var(--sn-ink);
}

/* ---- the two cards ---- */
.cal {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sn-space-2);
  /* Room above for the pills, which ride the cards' top edge. */
  margin-top: calc(var(--sn-space-5) + 6px);
}

.cal__card {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: var(--sn-space-4) var(--sn-space-2) var(--sn-space-3);
  border-radius: var(--radius-card);
  background: var(--sn-white);
  box-shadow: 0 1px 3px rgba(25, 70, 80, .06), 0 10px 26px rgba(25, 70, 80, .07);
}

/* The label sits ON the card's edge, half in and half out. */
.cal__pill {
  position: absolute;
  top: 0;
  left: 50%;
  translate: -50% -50%;
  white-space: nowrap;
  padding: 6px 14px;
  border-radius: 999px;
  /* ink, not the programme colour: white on the colour fails on five of the
     eight grounds and this is 11px uppercase. White on ink is 21:1. */
  background: var(--sn-ink);
  color: var(--sn-white);
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: var(--w-title);
  letter-spacing: var(--sn-ls-eyebrow);
  text-transform: uppercase;
}

/* THE PILL HAS TO FIT ITS CARD, and the room runs the other way from usual.
   Stacked on a phone the cards are 271px wide and the full label sits inside
   comfortably; side by side in the 420px column they are 142px, where the same
   pill measured 173-181px and hung 39px past the card on both sides.

   So the tighter setting is the DESKTOP one: from 40rem, where the two cards
   share the row, the label loses a point of size and most of its tracking. */
@media (min-width: 40rem) {
  .cal__pill {
    font-size: 10px;
    letter-spacing: .03em;
    padding: 5px 10px;
  }
}

.cal__month {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-ui);
  font-size: var(--fs-caption);
  font-weight: var(--sn-w-bold);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--p-accent-safe);
}

.cal__month .icon { width: 16px; height: 16px; }

.cal__day {
  font-family: var(--font-ui);
  font-size: clamp(2.75rem, 2rem + 2.4vw, 4rem);
  font-weight: var(--sn-w-bold);
  line-height: 1;
  color: var(--sn-ink);
}

/* The year, ruled on both sides the way the reference has it. */
.cal__year {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin-top: 4px;
  font-family: var(--font-ui);
  font-size: var(--fs-caption);
  font-weight: var(--w-title);
  color: var(--p-accent-safe);
}

.cal__year::before,
.cal__year::after {
  content: "";
  height: 1px;
  background: currentColor;
  opacity: .35;
}

/* The arrow between them, on its own disc over a dashed connector. */
.cal__link {
  position: relative;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  background: var(--sn-white);
  color: var(--p-accent-safe);
  box-shadow: 0 1px 3px rgba(25, 70, 80, .10);
}

/* THE CONNECTOR IS DRAWN BY .cal, NOT BY THE DISC.
   It used to be the disc's own ::before at z-index -1, which worked until the
   disc started rotating on narrow screens: `rotate` creates a stacking context,
   so the -1 could no longer reach behind the disc's own background and the
   dashed line painted straight across the arrow.

   Drawn on the container instead, it is simply an earlier layer: the cards and
   the disc are opaque and later in paint order, so they cover it and it shows
   only in the gap. No z-index anywhere, and nothing to break the next time an
   element in here gets a transform. */
.cal::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  border-top: 2px dashed var(--g-bg);
  opacity: .5;
}

/* ---- narrow: the window stacks ----------------------------
   Side by side, each card gets about 105px at 375 while the pill riding its top
   edge is 173px wide — the label was running out past the card and the panel.
   Below 40rem the two cards stack and the link disc turns a quarter turn, which
   takes its dashed connector vertical with it: one rotation, no second rule for
   the line.

   40rem, not the grid's own 56rem: between 40 and 56 the panel is the full
   container and there is room for the row. */
@media (max-width: 39.9375rem) {
  .cal { grid-template-columns: 1fr; gap: var(--sn-space-4); }
  .cal__link { justify-self: center; rotate: 90deg; }
  /* and the connector turns with it */
  .cal::before {
    left: 50%;
    right: auto;
    top: 0;
    bottom: 0;
    border-top: 0;
    border-left: 2px dashed var(--g-bg);
  }
  /* The pills are centred on cards that are now full width, so they have room
     for the long label again. */
  .cal__card { padding-top: var(--sn-space-5); }
}

/* ---- the footer note and the apply button ---- */
/* Button only (Malena, 2026-08-27) — the note beside it is gone. With nothing
   to sit next to, the two-column split and its 1fr text column go too: the row
   is just the button, aligned right where there is room. Leaving the grid in
   place would have left an empty first column pushing the button around. */
.appwin__foot {
  display: flex;
  justify-content: center;
  margin-top: var(--sn-space-4);
  padding-top: var(--sn-space-3);
  border-top: 1px solid color-mix(in srgb, var(--g-bg) 30%, transparent);
}

.appwin--open .cta-pair__label { background: var(--sn-black); color: var(--sn-white); }
.appwin--open .cta-pair__arrow { background: var(--g-bg); color: var(--sn-white); }

/* THE DISC KEEPS THE PROGRAMME COLOUR ON HOVER. The global
   `.cta-pair:hover .cta-pair__arrow { background: var(--sn-blue-tile) }` is
   (0,3,0) and was repainting it pale blue — the rest state was right and only
   the hover went wrong, which is why it looked like a bug rather than a choice.
   Matched at (0,4,0) so the programme's colour survives the pointer. */
@media (hover: hover) and (pointer: fine) {
  .appwin--open .cta-pair:hover .cta-pair__arrow { background: var(--g-bg); }
}

/* ---- closed: grey, and the CTA is the point ---- */
.appwin--closed {
  background: var(--sn-off-white);
  color: var(--sn-ink);
  border: var(--hairline);
}

/* --sn-ink, not --sn-grey-500. Grey-500 on off-white measures 3.85:1 and this
   line is 13px uppercase — under the 4.5:1 small text needs. Ink is 9.47:1. The
   muted look came from the dot and the grey ground, not from dimming the words. */
.appwin--closed .appwin__status { color: var(--sn-ink); }
.appwin--closed .appwin__dot { background: var(--sn-grey-500); }

.appwin__note { margin-top: var(--sn-space-2); font-size: var(--fs-body); }

.appwin--closed .cta-pair { margin-top: var(--sn-space-3); }

/* ---- motion ----------------------------------------------
   Inside the existing system, not beside it: the card arrives on the reveal
   observer like every other block (data-reveal="r"), and the only addition is
   the status dot, which breathes so the state reads as live rather than
   printed. Two seconds, tiny amplitude — a pulse you notice only if you look.
   ------------------------------------------------------------ */
@keyframes appwin-pulse {
  0%, 100% { transform: scale(1);   opacity: 1; }
  50%      { transform: scale(1.5); opacity: .55; }
}

.js .appwin__dot { animation: appwin-pulse 2.4s var(--ease-tween) infinite; }

@media (prefers-reduced-motion: reduce) {
  .js .appwin__dot { animation: none; }
}

/* The calendar leaves arrive a beat after their card, left then right, so the
   window reads as a range being laid down rather than a block appearing. */
.js [data-reveal].in .cal__leaf { animation: cal-drop var(--dur-med) var(--ease-out) backwards; }
.js [data-reveal].in .cal__leaf:nth-child(1) { animation-delay: .12s; }
.js [data-reveal].in .cal__leaf:nth-child(3) { animation-delay: .22s; }

@keyframes cal-drop {
  from { opacity: 0; transform: translateY(-10px) rotate(-2deg); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal].in .cal__leaf { animation: none; }
}


/* ============================================================
   15. HOW IT WORKS, REDESIGNED  (Malena, 2026-08-27)

   Her brief: the old one was sad and boring — pale tints on
   off-white, four flat cards, nothing happening. So: a dark
   ground, the four programme tints turned up, real decoration
   from brand/, and the numbers made the feature they always
   should have been.

   GROUND: the logo blue, asked for 2026-08-27. It is not a hex I
   picked — it is [data-ground="blue"] straight out of tokens.css,
   the kit's own "Skills Nation master" ground (#3976E9, the exact
   fill in the logo SVG). Using the token rather than hardcoding
   means the day the kit moves, this section moves with it.

   CONTRAST, measured: white on #3976E9 is 4.25:1.
     h2 (28-40px) ....... large text, needs 3:1 .... passes
     .lead (17-19px) .... needs 4.5:1 ............... 4.25, short
     .eyebrow (13px) .... needs 4.5:1 ............... 4.25, short
   The kit itself annotates this ground "4.2:1 — 24px+", so the two
   small sizes falling short is the kit's own documented behaviour
   for this colour, not a regression introduced here. Malena has
   ruled on this class of trade-off before ("no me interesa que no
   sean AA") and chose the colour knowing it. If it ever needs to
   tick the box, #3271E8 — the same hue, 3% darker, indistinguishable
   side by side — measures 4.50:1.

   The ink ground below is kept because the token is cheap and the
   section may want it back; nothing references it right now.
   ============================================================ */

[data-ground="ink"] { --g-bg: var(--sn-ink); --g-ink: var(--sn-white); }

.howto {
  position: relative;
  /* The clip is back here. It lived on .howto__stage while this section was
     pinned, because `overflow: hidden` on an ancestor stops a sticky descendant
     from sticking. There is no sticky descendant any more, so it returns to the
     element that actually needs to crop the wave. */
  overflow: hidden;
  background: var(--g-bg);
  color: var(--g-ink);
}


.howto .band__head .eyebrow,
.howto h2,
.howto .lead { color: var(--sn-white); }

.howto .lead { margin-top: var(--lead-gap); }

/* ---- the two decorations, both from brand/ ---- */
.howto__wave {
  position: absolute;
  left: -6%;
  bottom: -18%;
  width: min(880px, 76%);
  aspect-ratio: 426 / 232;
  background: url("/images/wave-blue-pink.svg") left bottom / contain no-repeat;
  /* Raised from .30 when the ground went from near-black to mid blue: the wave's
     own blues now sit much closer to the ground behind them, so the old value
     read as a smudge. */
  opacity: .42;
  pointer-events: none;
}

/* No corner disc. One lived here and was removed at Malena's request — the
   third time she has asked for the same thing on this build. Cropped decorative
   circles in a corner are not part of this design language; the wave and the
   programme colours carry the decoration. */

.howto .u-container { position: relative; z-index: 1; }

/* ---- THE ROUTE ------------------------------------------
   Four identical cards in a row was the design Malena rejected twice; the second
   attempt only recoloured it. This is a different object: a line across the
   section with the four steps planted on it, alternating above and below, and
   the number promoted from a label to the marker sitting ON the route.

   It also matches what the copy claims. "Career growth doesn't follow one path
   ... you can start anywhere on this journey" was being illustrated by four
   equal boxes, which says the opposite: one path, four identical stops.
   ------------------------------------------------------------ */
.journey {
  position: relative;
  display: grid;
  gap: var(--sn-space-4);
  margin-top: var(--sn-space-6);
  padding: 0;
  list-style: none;
}

.journey__marker {
  /* POSITIONED, AND THAT IS THE WHOLE POINT. The connector is an absolutely
     positioned ::before, so it paints in the positioned layer — above every
     non-positioned sibling, however late they come in the DOM. A marker with a
     background but no `position` therefore sat UNDER the line and the dashes ran
     straight across the numbers.

     Same failure as the calendar's arrow disc a few changes ago, from the same
     family: a positioned decoration painting over content that never joined the
     positioned layer. Anything a positioned line has to pass behind needs its
     own `position`. */
  position: relative;
  display: grid;
  place-items: center;
  width: 68px;
  height: 68px;
  border-radius: 999px;
  font-family: var(--font-ui);
  font-size: 1.5rem;
  font-weight: var(--sn-w-bold);
  color: var(--sn-ink);
  /* A ring in the section's own ground, so the marker reads as planted ON the
     line: the line passes behind the ring instead of needing a gap cut in it
     that would have to be kept in sync with the marker's position. */
  box-shadow: 0 0 0 8px var(--g-bg);
}

.journey__step:nth-child(4n+1) .journey__marker { background: var(--sn-p-college-400); }
.journey__step:nth-child(4n+2) .journey__marker { background: var(--sn-p-outofschool-400); }
.journey__step:nth-child(4n+3) .journey__marker { background: var(--sn-p-youth-400); }
.journey__step:nth-child(4n+4) .journey__marker { background: var(--sn-p-hbcu-400); }

.journey__card h3 {
  font-family: var(--font-ui);
  font-size: var(--fs-h3);
  font-weight: var(--w-title);
  color: var(--sn-white);
}

.journey__card p { margin-top: 6px; font-size: var(--fs-body); color: var(--sn-white); }

/* ---- narrow: the route stands up ---- */
@media (max-width: 55.9375rem) {
  .journey { gap: var(--sn-space-5); }

  .journey__step {
    display: grid;
    grid-template-columns: 68px 1fr;
    /* HALVED 2026-08-27 (24px -> 12px), asked for directly. This is the gap
       between the number and its text; see the desktop rules for the vertical
       half of the same change, which is the one that takes height out. */
    gap: 12px;
    align-items: start;
  }

  .journey::before {
    content: "";
    position: absolute;
    left: 33px;
    top: 34px;
    bottom: 34px;
    width: 2px;
    background: repeating-linear-gradient(to bottom,
      var(--sn-blue-100) 0 8px, transparent 8px 16px);
    opacity: .75;   /* was .55 — same reason as the wave */
  }

  .journey__card { padding-top: 7px; }   /* halved with the gap above */
}

/* ---- wide: the route runs across and the steps alternate ---- */
@media (min-width: 56rem) {
  .journey {
    grid-template-columns: repeat(4, 1fr);
    /* HALVED 2026-08-27: the number-to-text distance was row-gap 32 + card
       padding 24 = 56px, and it is now 16 + 12 = 28px. Exactly half, and it
       comes straight off the section's height, which was the point.
       Column gap is untouched — that separates the four steps from each other,
       not a number from its own text. */
    row-gap: 16px;
    column-gap: var(--sn-space-4);
    /* Card space above, the line's own band, card space below — so a step can
       hang either side without the two rows collapsing into each other. */
    grid-template-rows: 1fr 68px 1fr;
  }

  .journey::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 2px;
    translate: 0 -1px;
    background: repeating-linear-gradient(to right,
      var(--sn-blue-100) 0 8px, transparent 8px 16px);
    opacity: .75;   /* was .55 — same reason as the wave */
  }

  /* display: contents lets the marker and the card place themselves on the
     section grid independently, which is what makes the alternation possible
     without wrapping each step in its own sub-grid. */
  .journey__step { display: contents; }

  .journey__marker { grid-row: 2; justify-self: center; }
  .journey__card { text-align: center; }

  .journey__step:nth-child(odd)  .journey__card { grid-row: 1; align-self: end;   padding-bottom: 12px; }
  .journey__step:nth-child(even) .journey__card { grid-row: 3; align-self: start; padding-top: 12px; }

  .journey__step:nth-child(1) .journey__marker,
  .journey__step:nth-child(1) .journey__card { grid-column: 1; }
  .journey__step:nth-child(2) .journey__marker,
  .journey__step:nth-child(2) .journey__card { grid-column: 2; }
  .journey__step:nth-child(3) .journey__marker,
  .journey__step:nth-child(3) .journey__card { grid-column: 3; }
  .journey__step:nth-child(4) .journey__marker,
  .journey__step:nth-child(4) .journey__card { grid-column: 4; }
}

/* ---- THE ENTRANCE, NOT A SCRUB --------------------------
   Rewritten 2026-08-27 on review: "que ya no tenga animación de que salgan los
   números por scroll, que sólo salgan con animación suave construyéndose sin
   tener que darle scroll para que pase la animación."

   So the scroll timeline is gone, and the 300vh track and the sticky stage went
   with it. They existed only to give the scrub somewhere to happen; keeping a
   pin for an animation that no longer follows the scroll would have made the
   section three screens tall for nothing.

   What replaces it: one CSS animation per element, started by the existing
   [data-reveal] observer when the section first comes into view, and then it
   simply plays. Nobody has to keep scrolling to see it finish.

   THE OBSERVER TRIGGERS IT, THE ANIMATION IS NOT THE OBSERVER'S. .in only
   switches the animation on — the timing lives here. That matters because
   [data-reveal]'s own transition is opacity/translate/scale, and stacking a
   fourth property on it is what broke the card hovers twice; an animation is a
   separate mechanism and cannot collide with it.
   ------------------------------------------------------------ */

@keyframes journey-draw-x   { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes journey-draw-y   { from { transform: scaleY(0); } to { transform: scaleY(1); } }

/* The overshoot is what makes a number read as LANDING on the route rather
   than fading in over it. */
@keyframes journey-marker-land {
  from { opacity: 0; scale: .35; }
  72%  { opacity: 1; scale: 1.09; }
  to   { opacity: 1; scale: 1; }
}

@keyframes journey-card-up   { from { opacity: 0; translate: 0 26px;  } to { opacity: 1; translate: 0 0; } }
@keyframes journey-card-down { from { opacity: 0; translate: 0 -26px; } to { opacity: 1; translate: 0 0; } }

@media (prefers-reduced-motion: no-preference) {
  /* Before the section is reached, the pieces are hidden. .js gates this so a
     reader without JavaScript never meets an empty section — there the observer
     never fires, so nothing would ever switch the animation on. */
  .js .howto .journey::before,
  .js .howto .journey__marker,
  .js .howto .journey__card { opacity: 0; }

  /* The route draws first, then the numbers land along it, then the cards
     arrive — 1.5s end to end, which is a build rather than a performance. */
  .js .howto .band__head.in ~ .journey::before,
  .js .howto .journey.in::before {
    opacity: 1;
    animation: journey-draw-y 700ms var(--ease-out) both;
    transform-origin: top center;
  }
  .js .howto .journey.in .journey__marker {
    animation: journey-marker-land 520ms var(--ease-out) both;
    animation-delay: calc(320ms + var(--i, 0) * 130ms);
  }
  .js .howto .journey.in .journey__card {
    animation: journey-card-up 480ms var(--ease-out) both;
    animation-delay: calc(430ms + var(--i, 0) * 130ms);
  }

  @media (min-width: 56rem) {
    .js .howto .journey.in::before { animation-name: journey-draw-x; transform-origin: left center; }
    /* Cards still fall in from whichever side of the line they hang on. */
    .js .howto .journey.in .journey__step:nth-child(odd) .journey__card { animation-name: journey-card-down; }
  }
}

/* The observer never runs under reduce, and it does not need to: everything is
   already in its final state, which is the correct behaviour. */

/* The four-cards-in-a-row layout lived here and is gone: it is the design
   Malena rejected, and .journey above replaces it entirely. Its colour choices
   survive in the markers. */
