/* ============================================================================
 * MANDALA BRAND TOKENS — the one file, for EVERY Mandala surface.
 * ----------------------------------------------------------------------------
 * ★ BRAND v2, 2026-09-14. Source: the real per-product brand books, which now
 *   exist — "Mandala AI", "Mandala Core" and "Mandala Hub" Brand Guidelines
 *   v1.0, plus MandalaButler-Brandguide v1.0. This REPLACES the interim system
 *   (Butler's book stood in for every app from 2026-09-04 because nothing else
 *   existed yet), so this is the planned swap, not a change of mind.
 *
 *   WHAT MOVED, and what Ryan decided where the books and the estate disagreed:
 *     navy      #16273D -> #0A3B6E   (+ Deep Navy #062544)
 *     neutrals  Slate #5A6B7D · Mist #DDE3EA · Paper #F5F7F9, from the books
 *     UI face   Montserrat -> Jost   (the books' web stand-in for Futura)
 *     gold      KEPT. The AI/Core/Hub books all say "No accent colour"; Ryan
 *               overrode that on 2026-09-14 so the estate stays one system.
 *               Gold therefore lives on the NEW navy, where it measures
 *               4.67:1 — down from 6.25:1 but still AA. See §1.
 *     Spectral  KEPT for display, likewise against the books, which specify a
 *               single face. Ryan's call, same date.
 *     Butler    also moves to the new navy. ⚠ Its lockups that BAKE IN navy
 *               (-gold-navy, -navy) still carry #16273D and will read as the
 *               old colour — use the transparent -gold / -white variants on a
 *               token-driven ground instead.
 *
 * ARBITRATED BY CORE, 2026-09-05, closing HANDOFF_all_brand_consolidate_tokens.md.
 *
 * Three files existed for one job:
 *     core/brand/tokens.css          50 tokens   (QC, seeded 09-04)
 *     core/brand/brand-tokens.css    43 tokens   (HK, 09-05)
 *     core/brand/mandala-brand.css   69 tokens   (Hub, 09-05)
 *
 * ⚠ THEY DID NOT SPLIT BY QUALITY. THEY SPLIT BY GROUND — and that is why
 *   nobody merged them, and why a naive merge would have been a disaster:
 *
 *     HK  writes navy ink on a WHITE page   --ink: #16273D
 *     Hub writes white ink on a NAVY page   --ink: rgba(255,255,255,.92)
 *
 *   Same token name. Opposite value. Measured, crossing them gives 1.00:1 and
 *   1.16:1 — not "low contrast", INVISIBLE TEXT. Same for --line and
 *   --line-strong. Each file is correct on its own ground (HK 15.09, Hub 12.96)
 *   and neither thread could see the collision from inside its own app.
 *
 * THE FIX IS STRUCTURAL, NOT A VOTE. Both value sets survive verbatim, scoped
 * by ground. :root is the LIGHT ground because three of the five surfaces are
 * light (HK, HK-maint, QC). [data-ground="navy"] redefines the same semantic
 * names for the two navy surfaces (Butler desk, Hub). So --ink always means
 * "the reading colour on the ground you are actually on", and an app opts in
 * with ONE attribute on <html> instead of maintaining a second palette.
 *
 * Nobody's work was thrown away. Hub's dark system is here in full — every one
 * of its contrast claims was re-measured and CONFIRMED at the time (ink 12.96 vs claimed
 * 13.0, soft 6.98 vs 7.0, faint 4.85 vs 4.9, warn 5.39 vs 5.4, alert-text 5.24
 * vs 5.2). HK's light system is here in full. QC's structure — deriving every
 * grey from the one navy instead of inventing 28 of them — is the principle the
 * whole light half is built on.
 *
 * ── WHAT IS NOT HERE, DELIBERATELY ─────────────────────────────────────────
 * · STATUS COLOUR lives in status-palette.css (arbitrated separately, same day).
 *   QC's --s-* block and Hub's §3 --ok/--warn/--alert/--info are SUPERSEDED —
 *   do not carry them forward. One meaning per colour needs one file.
 * · @font-face lives in each app. The families and stacks are agreed HERE, but
 *   the src paths cannot be: HK loads 'montserrat-300.woff2' from its own
 *   directory, Hub from /fonts/. A shared @font-face would 404 in four of five
 *   apps. Declare the faces locally; take the family NAMES from here.
 *
 * ── HOW THIS REACHES THE APPS ──────────────────────────────────────────────
 * These apps boot OFFLINE, so each PWA ships a COPY of this file and lists it
 * in its service-worker SHELL. This file is the agreement; the copies are the
 * delivery. Hub is a separate origin and copies it at build (package.json
 * "prebuild"). ⚠ When you re-copy, add it to SHELL and bump the cache version
 * in the SAME commit — a stylesheet missing from SHELL works on the desk and
 * fails in a corridor.
 *
 * ⚠ THIS IS THE ONLY PLACE A HEX LITERAL IS ALLOWED. Five apps had five navies
 * before this — #0F2D4A, #0c2340, #1D3653 — and not one of them was the brand.
 * ==========================================================================*/


/* ══ 1 · THE THREE COLOURS. There is no fourth. ═════════════════════════════
 * Balance navy 66% · white 26% · gold 8%. Ground-independent: these are the
 * constants every other token is derived from or measured against.
 *
 * Names are --brand-* because this file is copied into five apps and a bare
 * --navy or --white collides with anything an app already defines. (QC had
 * --m-*, Hub had bare --navy, HK had --brand-*; all three agreed on the VALUES,
 * which is the one thing nobody got wrong.) */
:root {
  --brand-navy:       #0A3B6E;   /* Mandala Navy  — PMS 2955 C */
  --brand-navy-deep:  #062544;   /* Deep Navy     — depth behind navy */
  --brand-gold:  #C4A265;
  --brand-white: #FFFFFF;

  /* Gold, varied. ACCENTS AND LABELS ONLY — never a paragraph, never a link you
   * have to read at length.
   * ⚠ RE-MEASURED 2026-09-14 on the NEW navy: gold on white is still 2.41:1,
   * below AA at any size; on navy it is 4.67:1 — it NARROWED from 6.25:1 and
   * still passes AA, but the margin is thin. On the lifted navy surfaces it
   * runs 5.44 (raise) and 6.42 (panel). --brand-gold-bright is 5.91:1 on navy
   * if a label ever needs more headroom.
   * So a gold label is a NAVY-GROUND device. On a light surface keep the case
   * and the tracking and take var(--ink-2). HK found this and it binds all five. */
  --brand-gold-bright: #D6B87F;
  --brand-gold-deep:   #8E7343;


  /* ══ 2 · TYPE ════════════════════════════════════════════════════════════
   * Jost sets everything. (Was 'Jost for UI, Spectral for display' earlier the
   * same day — see §2 below for why Spectral went.)
   * Jost is the books' web stand-in for Futura (which we do not licence for
   * web). It ships as ONE variable file, wght 100-900, so 300/400/500 all come
   * from a single 26KB download — smaller than the two static Montserrat cuts
   * it replaces. Spectral is RETAINED for display against the books, which
   * specify a single face; Ryan kept the serif deliberately.
   * Stacks are real fallbacks: a tablet that fails to load a font must still be
   * readable. Roboto is in the stack because these are ANDROID tablets and it is
   * the system face — the most likely fallback to actually resolve on the metal.
   * (Union of the three stacks; Hub's omitted Roboto, QC's omitted
   * BlinkMacSystemFont.) */
  --font-ui:      'Jost', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  /* ★ 2026-09-14, second pass. ONE FAMILY, and the books are explicit:
   *   Core / AI  "Futura Light and Book SET EVERYTHING ... use Jost"
   *   Hub        "Jost sets everything else ... the typeface for ALL copy —
   *               headlines, body, labels"
   * There is no serif in any of the three books.
   *
   * ⚠ Spectral was never a Mandala face. It came in with the interim system,
   *   which was derived from MandalaButler-Brandguide v1.0 — and that guide
   *   says, in as many words, "TYPEFACE TO BE CONFIRMED. The scale, weights and
   *   case rules below hold whichever family is chosen." So Montserrat and
   *   Spectral were PLACEHOLDERS for an unconfirmed family. The family is now
   *   confirmed, and it is Jost.
   *
   * The display ROLE survives; it is just no longer carried by a second family.
   * The books carry it by WEIGHT — headlines are Light 300 at -1% tracking,
   * which --t-display-w / --t-heading-w and the -track tokens already encode.
   * Keeping the token name means no app had to change a reference.
   *
   * ⚠ BUTLER IS THE ONE EXCEPTION and overrides this locally. Its own book is
   *   still "to be confirmed", so it is not covered by the Core-family books,
   *   and its guest UI has a deliberate serif voice. That divergence is a
   *   decision waiting on Ryan, not an oversight. */
  --font-display: 'Jost', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* ⚠ JOST CARRIES NO ARROWS, STARS OR WARNING SIGNS. Montserrat did, so the
   * font swap silently took them out everywhere: → ← ↑ ↓ ★ ⚠ ✦ have been
   * rendering from whatever the OS offered since 2026-09-14. Nothing errored,
   * nothing 404'd, and a fallback arrow still looks like an arrow — which is
   * exactly why it survived a screenshot review. Hub found it; measured on the
   * live origin, → ★ ⚠ had advance widths IDENTICAL to the no-font fallback
   * while ‹ › and letters differed, so the test is sound.
   *
   * This does not add a download. It names the fallback instead of leaving it
   * to an OS lottery: the SAME family name, restricted by unicode-range to the
   * ranges Jost lacks, so a → in Sydney and a → on an Android tablet come from
   * a deliberate stack rather than chance. Letters are untouched.
   *
   * ⚠ Do NOT add ranges Jost DOES cover — ‹ › • · — – " " … are all present,
   * and claiming them here would override real Jost glyphs with a system face.
   * If pixel-identical symbols ever matter more than a zero-byte fix, the
   * upgrade is a tiny subset font, not a wider range here. */
  /* (declared after the app's own @font-face, which sets the letterforms) */

  /* Two weights per family. Every extra weight is a file every tablet
   * downloads, and the guide asks for two per layout. */
  --w-light: 300;
  --w-reg:   400;

  /* ── THE TABLET SCALE. This is the default because four of the five surfaces
   * are tablets. The guide's RATIOS are kept and the pixel values lifted for a
   * screen held at arm's length, exactly as the brief instructs.
   *
   * ⚠ HK's values won over QC's (34/26/20/19/16/13 against 40/28/21/19/16/14 —
   * they agreed on body at 19px). Not because bigger or smaller is better, but
   * because HK and QC RUN ON THE SAME PHYSICAL TABLET: a housekeeper finishes a
   * room in HK and does the QC check in QC, and two type scales on one device
   * read as two products. HK's is the safer of the two to standardise on
   * because it was proved against the densest real screen in the estate — the
   * 60-room Albury board, where they hit actual overflow (a 25-hour elapsed made
   * one tile taller than its row) and fixed it. A scale that survives that
   * survives a checklist; the reverse is not guaranteed.
   *
   * ⚠ BODY IS REGULAR 400, NOT THE GUIDE'S LIGHT 300 — HK's call, adopted for
   * all five. Light at 19px, held at arm's length in corridor glare by someone
   * wearing gloves, is genuinely hard to read, and legibility outranks
   * refinement on a work tool. Light 300 is kept for headings and display,
   * where size carries it. The per-role weights below encode that. */
  --t-display:  34px;  --t-display-lh: 38px;  --t-display-w: 300;  --t-display-track: -.02em;
  --t-heading:  26px;  --t-heading-lh: 32px;  --t-heading-w: 300;  --t-heading-track: -.01em;
  --t-subhead:  20px;  --t-subhead-lh: 27px;  --t-subhead-w: 400;
  --t-body:     19px;  --t-body-lh:    1.55;  --t-body-w:    400;
  --t-small:    16px;  --t-small-lh:   1.45;
  --t-label:    13px;  --t-label-track: .24em;   /* ALL CAPS, one per section */


  /* ══ 3 · SHAPE AND RHYTHM ════════════════════════════════════════════════
   * One radius scale and one spacing step, so a card in HK really is the same
   * card as a card in Hub. (Hub's file already claimed this in a comment while
   * setting --r-lg to 18px against the others' 16 — the claim is now true.) */
  --r-sm:  8px;
  --r-md:  12px;
  --r-lg:  16px;
  --r-pill: 999px;

  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px;
  --sp-4: 16px; --sp-5: 24px; --sp-6: 32px; --sp-7: 48px;

  /* ⚠ 48, not the brief's floor of 44. HK measured their shipped targets at
   * 49-56px and set 48 because these are used with gloves and cleaning cloths.
   * Hub's file cited the same reason and then set 44. Raising a tap target
   * never hurt anyone. */
  --tap: 48px;

  --logo-min: 120px;        /* never smaller, per the guide */


  /* ══ 4 · LIGHT GROUND — the default ══════════════════════════════════════
   * HK, HK-maintenance and QC. Every grey is the ONE navy, quieter — that is
   * QC's principle and it is why there is no neutral palette in this file.
   * Hand-tuned rather than derived with color-mix(): these are the values that
   * shipped and were verified in a browser, and a token five apps depend on is
   * the wrong place to swap verified constants for computed ones. */
  --ink:         #0A3B6E;   /* reading text.  11.27:1 on --surface, 10.49 on -2 */
  --ink-2:       #5A6B7D;   /* Slate, from the book. 5.48 white / 5.10 Paper  */
  /* ⚠ THIS TOKEN HAS NOW BEEN THE BUG TWICE. It carries CAPTIONS, which are
   * type, so it must clear 4.5:1 — and it is the first thing a lighter navy
   * breaks. HK shipped #7C8899 (3.60:1). The 09-05 arbitration fixed it to
   * #687381 (4.82:1). Re-deriving it the same way from the NEW navy — the one
   * navy, quieter, at 65% — gives #6080A1, which is 4.12:1 and reintroduces
   * exactly the same fault.
   * So it is solved against the WORST ground it meets, not the best: --ink-3
   * sits on Paper as often as on white, and Paper is darker. At navy 71% it is
   * 4.55:1 on Paper and 4.88:1 on white. Both clear AA; the white-only figure
   * would have hidden a failure on the page ground. Measure against Paper. */
  --ink-3:       #517498;   /* captions, disabled.  4.88 white / 4.55 Paper   */
  --line:        #DDE3EA;   /* Mist, from the book                            */
  --line-strong: #B6C4D4;
  --surface:     #FFFFFF;   /* a card                                         */
  --surface-2:   #F5F7F9;   /* Paper, from the book — the page behind cards   */
  --surface-3:   #EBEFF3;   /* selected / among-navy fills                    */
  --fill:        rgba(10,59,110,0.05);
  --fill-raise:  rgba(10,59,110,0.10);
  --scrim:       rgba(10,59,110,0.55);
  --shadow:      0 6px 20px rgba(10,59,110,0.10);

  /* The navy bar that sits inside a light app (an app bar, a pair screen). */
  --on-navy:    #FFFFFF;
  --on-navy-2:  #C3CEDC;
}


/* ══ 4b · NAVY GROUND ═══════════════════════════════════════════════════════
 * Butler desk and Hub. Opt in with ONE attribute on <html>:
 *
 *     <html data-ground="navy">
 *
 * The SAME semantic names are redefined, so a component written against --ink
 * and --surface is correct on either ground with no per-app palette. This is
 * the whole mechanism that resolves the collision — do not reintroduce a
 * separate dark token set.
 *
 * All of Hub's values, carried verbatim. Re-measured and confirmed:
 * ⚠ RE-DERIVED 2026-09-14 for the new navy, and the ladder had to be REBUILT,
 * not just recoloured. #0A3B6E is far lighter than #16273D, so the obvious move
 * — make the brand navy the panel — measured out at gold 2.80:1 on the hover
 * surface and ink-3 2.96:1. Both FAIL. The ladder is therefore anchored DARK
 * and rises TO the brand navy rather than starting from it: Deep Navy is the
 * panel, and --surface-4 is #0A3B6E, the lightest it ever gets.
 * Measured on the lightest surface (--surface-4), the worst case:
 * ink 9.75:1 · ink-2 5.56 · ink-3 4.55 · gold 4.67 — all AA. On the panel they
 * run 13.29 / 7.09 / 5.59 / 6.42. ink-3 went from .50 to .55 alpha to hold AA
 * on the lifted surfaces. ink-ghost = DECORATION, never words. */
[data-ground="navy"] {
  --ink:         rgba(255,255,255,0.92);
  --ink-2:       rgba(255,255,255,0.64);
  --ink-3:       rgba(255,255,255,0.55);
  --ink-ghost:   rgba(255,255,255,0.16);   /* a rule, a disabled glyph. NOT type. */
  --line:        rgba(255,255,255,0.10);
  --line-strong: rgba(255,255,255,0.20);

  /* Navy, deepened and lifted. Surfaces only — never type. */
  --surface:     #062544;   /* a panel on the ground = Deep Navy, per the book */
  --surface-2:   #051E36;   /* the ground behind it                           */
  --surface-3:   #08315B;   /* a card lifted off it                           */
  --surface-4:   #0A3B6E;   /* hover/pressed = the brand navy itself          */

  --fill-faint:  rgba(255,255,255,0.04);
  --fill:        rgba(255,255,255,0.06);
  --fill-raise:  rgba(255,255,255,0.12);

  /* Gold as a surface rather than as ink. */
  --gold-soft:   rgba(196,162,101,0.16);
  --gold-fill:   rgba(196,162,101,0.28);
  --gold-edge:   rgba(196,162,101,0.34);

  /* Atmosphere — the soft washes behind a splash or lock screen. Here rather
   * than as one-off rgba() in an app sheet, which is how five apps drift. */
  --glow-navy:   rgba(10,59,110,0.90);
  --glow-gold:   rgba(196,162,101,0.05);
  --scrim:       rgba(5,30,54,0.86);
  --scrim-solid: rgba(5,30,54,0.97);
  --shadow:      0 8px 28px rgba(0,0,0,0.28);

  --on-navy:     #FFFFFF;
  --on-navy-2:   #C3CEDC;
}


/* ══ 5 · DESKTOP SCALE ══════════════════════════════════════════════════════
 * Hub's pod-PC screen is not a tablet at arm's length, so it takes the guide's
 * own desktop values. Opt in alongside the ground:
 *
 *     <html data-ground="navy" data-scale="desktop">
 *
 * These are the guide's published sizes and are NOT to be re-derived per app —
 * that is how three scales appeared in the first place. Body stays Regular 400
 * for the same legibility reason as the tablet scale. */
[data-scale="desktop"] {
  --t-display: 64px;  --t-display-lh: 68px;
  --t-heading: 40px;  --t-heading-lh: 46px;
  --t-subhead: 24px;  --t-subhead-lh: 32px;
  --t-body:    17px;  --t-body-lh:    1.6;
  --t-small:   15px;
  --t-label:   12px;
}


/* ══ 6 · TYPE ROLES ═════════════════════════════════════════════════════════
 * Optional helpers. An app may style its own elements from the tokens instead,
 * but using these is what makes five apps read as one product. They resolve
 * correctly on either ground because they only reference semantic tokens. */
.t-display { font-family: var(--font-display); font-size: var(--t-display);
             line-height: var(--t-display-lh); font-weight: var(--t-display-w);
             letter-spacing: var(--t-display-track); }
.t-heading { font-size: var(--t-heading); line-height: var(--t-heading-lh);
             font-weight: var(--t-heading-w); letter-spacing: var(--t-heading-track); }
.t-subhead { font-size: var(--t-subhead); line-height: var(--t-subhead-lh);
             font-weight: var(--t-subhead-w); }
.t-body    { font-size: var(--t-body); line-height: var(--t-body-lh);
             font-weight: var(--t-body-w); }
.t-small   { font-size: var(--t-small); line-height: var(--t-small-lh); }

/* ⚠ GOLD LABEL, NAVY GROUND ONLY. On a light surface .t-label keeps its case
 * and tracking and takes --ink-2, because gold on white is 2.41:1. The rule is
 * expressed here so no app has to rediscover it. */
.t-label   { font-size: var(--t-label); text-transform: uppercase;
             letter-spacing: var(--t-label-track); font-weight: var(--w-reg);
             color: var(--ink-2); }
[data-ground="navy"] .t-label { color: var(--brand-gold); }

/* ══ 6b · THE PRODUCT LOCKUP — "MandalaCore Operations" ═══════════════════
 * Ryan, 2026-09-14: every Core-family surface names itself the same way — the
 * real MandalaCore lockup, then the product word beside it. Operations,
 * Housekeeping, Maintenance, Quality Control. One pattern, so a manager moving
 * between the web app and a tablet sees the same masthead.
 *
 * ⚠ THE SUFFIX IS UI TEXT, THE LOCKUP IS ARTWORK. Never set "MandalaCore" in a
 *   typeface to fake this — the C is drawn from the aperture and cannot be
 *   typed. The suffix is a descriptor and is not part of the logotype, so it
 *   takes --font-ui at the label spec.
 *
 * ── THE ALIGNMENT, and why it is a constant and not a guess ───────────────
 * Ryan asked for the suffix to sit on the bottom of the logo. "The bottom" is
 * ambiguous in this artwork and the two readings differ by 12% of its height:
 *
 *     file height          173 units (the landscape lockup, any size)
 *     MARK bottom          166   ← the aperture is a circle and OVERSHOOTS
 *     WORDMARK baseline    145   ← what the eye reads as the logo's bottom line
 *
 * Aligning to the mark would drop the suffix 21 units below the wordmark and
 * look wrong against the very thing it sits next to. So it aligns to the
 * WORDMARK BASELINE, which is round-overshoot-correct and is what typesetting
 * a word beside a logo means.
 *
 * Measured off the real artwork in a browser (canvas alpha bounds), not eyeballed.
 * 1 - 145/173 = 0.1618, so the wordmark baseline sits that fraction of the
 * rendered height ABOVE the bottom edge of the <img> box.
 *
 * ⚠ HOW THIS IS DONE, because the obvious way silently does nothing. A flex
 *   container with align-items:baseline lines an <img> up by a SYNTHESIZED
 *   baseline, and the spec synthesizes it from the element's BORDER BOX — so a
 *   negative margin-bottom does not move it. Measured: the margin computed to
 *   -4.2068px and the suffix still sat exactly 4.21px low, i.e. the margin had
 *   no effect on alignment at all. Do not reach for margin here.
 *   Instead the SUFFIX is shifted up by the same fraction with relative
 *   positioning, which moves nothing else and leaves the lockup — the dominant
 *   element — optically where the layout put it.
 * Change --lockup-h and the alignment follows; there is nothing else to tune. */
.product-lockup {
  --lockup-h: 26px;
  display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0;
}
.product-lockup__img {
  height: var(--lockup-h); width: auto; display: block; flex: none;
}
.product-lockup__suffix {
  font-family: var(--font-ui);
  font-size: var(--t-label); font-weight: var(--w-reg);
  letter-spacing: var(--t-label-track); text-transform: uppercase;
  white-space: nowrap; opacity: .62;
  position: relative; top: calc(var(--lockup-h) * -0.1618);
}
/* ⚠ THE SUFFIX HIDES ON A NARROW SCREEN, and this lives here because three
 * apps had each worked it out separately (QC raised it, 2026-09-14, rather than
 * editing the synced file — right call).
 *
 * The lockup CANNOT shrink to fit: the image is flex:none and the suffix is
 * nowrap, so the pair has a hard minimum width. QC measured its home header at
 * 229 + 12 + 172 = 413px against 343px of usable width at 375 — 54px of
 * sideways scroll on the whole page. And --lockup-h cannot rescue it: fitting
 * 320px would need a 15px lockup, far under the book's own floor.
 *
 * So the suffix is what gives way, not the artwork. "Quality Control" is the
 * longest of the four product words, which is why QC hit this first and worst.
 * An app needing a different breakpoint overrides it — the Core SPA does, at
 * 900px, because its bar also carries the scope switcher and property picker. */
@media (max-width: 600px) {
  .product-lockup__suffix { display: none; }
}

/* Centred and stacked — a sign-in or a splash, where the lockup IS the
 * masthead rather than an item in a bar. The suffix sits BELOW the lockup, so
 * it is centred rather than baseline-aligned: the baseline offset is reset to
 * zero here, or the word would float 16% of the lockup height too high. Size it
 * with --lockup-w, since a stacked lockup is measured by width. */
.product-lockup--stacked {
  flex-direction: column; align-items: center; text-align: center;
  gap: var(--sp-3);
}
.product-lockup--stacked .product-lockup__img {
  height: auto; width: var(--lockup-w, 200px); max-width: 60%;
}
.product-lockup--stacked .product-lockup__suffix {
  top: 0; padding-left: 0; border-left: 0;
}

/* A hairline between the lockup and the word, so the suffix reads as a
 * descriptor rather than as part of the name. */
.product-lockup__suffix--ruled {
  padding-left: var(--sp-3);
  border-left: 1px solid currentColor;
  border-left-color: color-mix(in srgb, currentColor 22%, transparent);
}

/* Logo clear space = the cap height of the M, ~1/3 of the lockup height. */
.brand-lockup { display: block; min-width: var(--logo-min); padding: 33% 0; }


/* ══ 7 · MIGRATION ALIASES — DELETE THIS BLOCK ══════════════════════════════
 * Every name the three files disagreed on, mapped to the arbitrated one, so a
 * spoke can copy this file in TODAY and rename at its own pace instead of
 * needing one flag-day commit across five apps.
 *
 * ⚠ This block is temporary by design. Aliases ARE the drift this file exists
 * to remove — leaving them permanently just relocates the problem. Delete your
 * app's lines once you have renamed, and tell Core. The block goes when the
 * last app reports in. */
:root {
  /* QC */
  --m-navy: var(--brand-navy);  --m-gold: var(--brand-gold);  --m-white: var(--brand-white);
  --f-ui: var(--font-ui);       --f-display: var(--font-display);
  --w-regular: var(--w-reg);
  /* QC's tint ladder, kept VERBATIM rather than aliased to the semantic names.
   * The two ladders are parallel but not identical, so mapping them would have
   * silently restyled QC — measured, --m-navy-45 is 2.71:1 where the nearest
   * semantic token (--line-strong) is 1.80:1. Verbatim means QC can adopt this
   * file today with ZERO visual change and rename at its own pace.
   * ⚠ These are SURFACES AND BORDERS ONLY, never type — -45/-22/-10/-05
   * measure 2.71 / 1.56 / 1.21 / 1.10 on white. QC's own file says so; the
   * numbers agree. Only -90 (11.08) and -70 (5.67) are safe for text. */
  --m-navy-90: color-mix(in srgb, var(--brand-navy) 90%, white);
  --m-navy-70: color-mix(in srgb, var(--brand-navy) 70%, white);
  --m-navy-45: color-mix(in srgb, var(--brand-navy) 45%, white);
  --m-navy-22: color-mix(in srgb, var(--brand-navy) 22%, white);
  --m-navy-10: color-mix(in srgb, var(--brand-navy) 10%, white);
  --m-navy-05: color-mix(in srgb, var(--brand-navy)  5%, white);
  /* Hub */
  --navy: var(--brand-navy);    --gold: var(--brand-gold);    --white: var(--brand-white);
  --r: var(--r-md);
  --lh-display: var(--t-display-lh);  --lh-heading: var(--t-heading-lh);
  --lh-subhead: var(--t-subhead-lh);  --lh-body:    var(--t-body-lh);
  --tr-display: var(--t-display-track); --tr-heading: var(--t-heading-track);
  --tr-label:   var(--t-label-track);
  --ink-soft:  var(--ink-2);    --ink-faint: var(--ink-3);
  --navy-deep: var(--surface-2); --navy-card: var(--surface);
  --gold-bright: var(--brand-gold-bright);  --gold-deep: var(--brand-gold-deep);
  /* HK */
  --navy-tint: var(--surface-3);

  /* ── CORE WEB SPA (core/frontend) ─────────────────────────────────────────
   * Added 2026-09-14. The SPA was never in the 09-05 arbitration at all — it
   * carried a THIRD palette from the Module 0 brief (--color-navy #1D3653,
   * --color-gold #A2885E, DM Sans/Nunito), which matched neither the old brand
   * nor the new one. These 22 names have ~1400 call sites across the app, so
   * they are aliased rather than renamed: one import and the whole SPA is on
   * brand, with no component churn and nothing to miss.
   *
   * ⚠ STATUS COLOURS ARE DELIBERATELY NOT MAPPED. --color-danger/-info/
   *   -success/-warn and their -soft pairs are semantic, not brand; the tablet
   *   ladder in status-palette.css is a different ladder and crossing them
   *   would silently restyle every alert in the SPA. Left exactly as they are.
   *
   * Three of these MOVE contrast, all upward or safely:
   *   --color-text        #1A1A1A 17.40:1 -> navy 11.27:1  (the book asks for
   *                       navy body copy on white; still far above AA)
   *   --color-text-muted  #6B7280  4.83:1 -> Slate  5.48:1
   *   --color-text-faint  #9CA3AF  2.54:1 -> ink-3  4.88:1  ← was FAILING AA
   *                       at 76 call sites. This fixes it. */
  --color-navy:        var(--brand-navy);
  --color-navy-deep:   var(--brand-navy-deep);
  --color-navy-mid:    #91A7BE;              /* navy@45% — matches the old 2.47:1 */
  --color-navy-light:  var(--surface-3);
  --color-gold:        var(--brand-gold);
  --color-gold-light:  var(--brand-gold-bright);
  --color-white:       var(--brand-white);
  --color-light-grey:  var(--surface-2);
  --color-bg:          var(--surface-2);
  --color-border:      var(--line);
  --color-border-soft: #EFF2F6;              /* one step lighter than --line */
  --color-text:        var(--ink);
  --color-text-muted:  var(--ink-2);
  --color-text-faint:  var(--ink-3);
  /* Ryan 2026-09-14: Jost for UI, Spectral kept for display. --font-heading
   * carries the SPA H1s, so it takes the display face and the web app finally
   * reads like the tablets. It was 'Tenor Sans', which is the LOGOTYPE face —
   * and the logotypes ship as outlines, so nothing needed it as live type. */
  --font-primary:      var(--font-ui);
  --font-secondary:    var(--font-ui);
  --font-heading:      var(--font-display);

  /* Four names the SPA referenced but never defined. They were not broken —
   * every call site carries an inline fallback, e.g.
   *     background: var(--color-surface-alt, #fafafa);
   * which is worse than broken: it is an off-brand grey hard-coded at the call
   * site that no brand change can ever reach. The fallbacks were #fff, #fafafa,
   * #F5F6F8 and #F7F8FA — four shades of the same idea. Defining the tokens
   * puts them on Paper and white, so they move with the brand from now on. */
  --color-surface:      var(--surface);
  --color-surface-alt:  var(--surface-2);
  --color-border-light: var(--color-border-soft);
  /* A literal, not an alias: --shadow-card is defined in the SPA's own
   * index.css, and this file is shared with Hub and the four tablets, where
   * that name does not exist. An alias would have shipped a dangling var() to
   * five other apps. */
  --shadow-sm:          0 1px 3px rgba(10,59,110,0.06);
}
[data-ground="navy"] {
  --navy-raise: var(--surface-3);  --navy-edge: var(--surface-4);
}
