/* ═══ CCD DESIGN TOKENS — single source of truth ═══
   Loaded by index.html and both blog templates (templates/*.html).
   Three blocks, three jobs: PRIMITIVE holds the raw values, SEMANTIC is the
   block you change to rebrand, and MOCK is deliberately outside the swap so a
   palette change does not repaint the device illustrations.
   To rebrand: edit the SEMANTIC block, run `node tools/build.mjs`,
   then `node tools/make-og.mjs`, then deploy.

   "Nothing else to touch" used to be written here and it was never true.
   CSS custom properties cannot reach these, so a palette change must also:
     - retint assets/logo.svg by hand. It is the favicon (rel="icon"), and a
       favicon is a file request — it cannot read a page variable.
     - retint assets/logoname.svg by hand. loader.js loads it as img.src,
       which likewise cannot read page variables.
     - re-derive the five *-ondark hexes below. They are contrast-tuned
       against --brand-900, not aliases of it, so they do NOT follow an ink
       change on their own. See the note on that block.
   Two surfaces that USED to need hand-work and no longer do: the loader
   ground now reads var(--bg), and assets/og.png is rendered from
   design/og-card.html, which links this file.
   Canonical breakpoints (media queries can't read vars — keep in sync):
   560 niches / 640 phone / 900 tablet+nav (renders at zoom 1) /
   960 layout (folding into 900) / 1080 collage / 1200 sticker guard */

/* ─── HOW THIS FILE IS ORGANISED ───────────────────────────────────────
   Three blocks, in this order. Read them as three different jobs:

     BLOCK 1 — PRIMITIVE : the raw material. Hexes, pixels, curves, faces.
                           Names describe MAGNITUDE, never purpose.
     BLOCK 2 — SEMANTIC  : the knobs. Names describe PURPOSE, and every one
                           of them is an alias onto a primitive. This is the
                           block you edit to change how the site looks.
     BLOCK 3 — MOCK      : the device illustrations. Held outside the brand
                           swap on purpose — see that block's header.

   Nothing was retimed, recoloured or resized when this structure went in.
   Every token in block 2 resolves to exactly the value its consumers were
   already getting; the block is vocabulary, not a redesign. Components are
   still on the older aliases in places, and moving them across is a
   separate job — an alias here costs nothing while it waits. */

:root {
    /* ══════════════════════════════════════════════════════════════════
       BLOCK 1 — PRIMITIVE
       Raw values. A primitive knows how big or how dark it is and nothing
       else, which is why a component should not read one directly: --sp-8
       cannot tell you whether it is the gap between cards or the space
       under a heading. Reach for block 2 instead.
       ══════════════════════════════════════════════════════════════════ */

    /* ══ BRAND PRIMITIVES — the ramp itself, not the rebrand knobs ══
       The knobs are in BLOCK 2. A rebrand normally edits BOTH: the ramp
       here, and the aliases there if the mapping changes.
       Two blues carry the whole site: #1D4E89 is the primary (links,
       buttons, arrows, bars) and #22344C is the ink (headings, dark
       bands, dark tiles). The ground stays warm paper.
         This is an INK-SLATE ramp, hue ~213 held down the whole scale.
       It replaced a brighter cobalt (#094FBF / #001A47) in Aug 2026: a
       client read that blue as "Microsoft", and they were right — it was
       the SaaS default. Saturation is the thing that was pulled out, not
       lightness. If a future tweak re-saturates the 400–600 rungs, the
       Microsoft read comes straight back. */
    --brand-25:  #F6F8FB;   /* wash — open accordion panels, subtle card fills */
    --brand-50:  #EDF1F6;   /* washes, soft fills — lightest blue tint */
    --brand-100: #D8E1EB;   /* hairline rules + dividers */
    /* DELIBERATELY NON-MONOTONIC: -150 is DARKER than -200 (L82.0 vs L84.9).
       The ramp is not a straight lightness run here, so any generator that
       derives rungs by interpolating lightness will silently swap these two.
       -150 has no consumer today; it is kept because it is a rung the ramp
       has to be able to emit. Do not "fix" the ordering. */
    --brand-150: #C4D0DE;   /* inactive underlines, muted chips */
    --brand-200: #CBD8E6;   /* chips, quiet fills */
    --brand-300: #9DB8D6;   /* bright glow lines — must read on the dark ink */
    --brand-400: #4A7BAF;   /* mid blue — quiet accents on light ground */
    --brand-500: #1D4E89;   /* PRIMARY — buttons, links, arrows, bars */
    --brand-600: #163C6B;   /* hover / deep accent */
    --brand-700: #1C3A5E;   /* hero field's mid-ground stop */
    --brand-900: #22344C;   /* brand ink — headings, dark bands, dark tiles */
    --brand-950: #16222F;   /* deepest ink */
    --brand-975: #0E1720;   /* hero field's darkest stop — off-canvas right */

    /* neutral primitives */
    --white: #FFFFFF;
    --neutral-bg: #FDFCFA;      /* warm page ground */
    --neutral-soft: #F4F1EC;    /* soft section band — warm, matches the neutral wash */
    --neutral-ink: #1E2323;     /* body text */
    --neutral-muted: #5C6664;   /* secondary text */

    /* status primitives (not part of the brand swap) */
    --success: #28A06B;
    --success-deep: #1B7A43;
    --success-soft: #E2F5E9;
    /* warning and error states — reserved for internal tools and plugins.
       These are not part of the brand swap and do not appear on the marketing site.
       First consumer: the ccd-slice-namer Figma plugin (2026-09-02). */
    --warning: #B45309; --warning-soft: #FFF4E5;
    --error: #B9463F; --error-soft: #FBEDEC;

    /* secondary + tertiary accent slots — unused, full stop. The comment
       here used to say they were "wired up by the palette overrides in
       assets/palettes/*.css"; that directory has never existed. Nothing
       references var(--accent-2) or var(--accent-3) anywhere in the repo. */
    --accent-2: #E8A24B;                 --on-accent-2: var(--neutral-ink);
    /* The single loud accent. Used sparingly — THREE places as of Aug 2026,
       and this comment is the count of record:
         1. the Month 2 offset bands in #process   (.month-card--pop)
         2. the mark on the about photo            (.about-photo::after)
         3. the FAQ media wipe                     (.faq-media picture::after)
       This block used to say "SPENT — it appears exactly once", and a second
       comment in index.html separately claimed the about-photo mark was the
       only use. Both were wrong, and they contradicted each other. Two of the
       three are standing marks; the wipe is transient, on screen for under a
       second and then off-frame for good, which is why it was judged not to
       spend the budget a third time. If a fourth wants it, take one away. */
    --pop-pink: #FE43C3;
    --accent-3: #8A3D66;                 --on-accent-3: var(--white);
    /* conversion-CTA blue — unified with the primary; the Gmail mock keeps its
       own --gmail-blue because it imitates Gmail, not CCD */
    --cta-blue: #1D4E89;
    --cta-blue-deep: #163C6B;

    /* ── non-color scales ── */
    /* radius — a 2px softening on every edge; pill/circle survive for the
       device-mock internals (Gmail Send button, search field) and true circles */
    --r-xs: 2px; --r-sm: 2px; --r-md: 2px; --r-lg: 2px; --r-xl: 2px;
    --r-pill: 999px; --r-circle: 50%;
    --r-chip: 2px;              /* brand chips: same 2px as every other block */
    --radius: 2px;              /* 2px: the editorial language, softened just off-square */
    /* rule weight — how THICK a line is. The colour of a line is a separate
       axis and lives in block 2 as --border-subtle, --border-strong and
       --border-accent; these three do NOT pair off with those by name, and
       the shared words are the trap. --rule-strong is worn with
       --border-subtle (the site's own divider, at its heavier weight), never
       with --border-strong, which is the card edge colour and is 1px
       everywhere it appears. Likewise --rule-hairline has nothing to do with
       the block-2 colour --hairline, which in practice only dresses the
       device mocks.
         Counted off the page, not invented:
           1px  the ordinary hairline. 22 declarations across index.html,
                site.css and opinions.css, on every line colour the site owns.
           2px  the heavier rule: the services list rows, the FAQ accordion
                rows, and the pull-quote's top rule in opinions.css.
           3px  the accent rule that closes the FAQ accordion. One site.
         This is a WEIGHT scale, not a component scale. An earlier reading had
       2px meaning "an interactive row", which is false outside index.html:
       opinions.css puts 2px on a static pull-quote, and the collapsed mobile
       nav card carries a 2px edge of its own. Widths that are not rules stay
       literal and must not be dragged in here — site.css keeps --nav-bw (the
       collapsed nav card, whose 2px is also the arithmetic behind the pop-out
       panel's offsets), the transparent 2px on the nav backdrop
       pseudo-elements, the 3px inset that insets the scrollbar thumb, and the
       2px :focus-visible outline, which is an outline and not a border. The
       illustration namespaces BLOCK 3 covers are excluded for the usual
       reason: their 0.5px, 1.5px and 4.5px edges are Gmail's and iOS's, not
       ours. These sit in BLOCK 1 because block 2 forbids pixel literals; if a
       semantic layer is ever wanted they alias upward the way --track-body
       became --tracking-body. */
    --rule-hairline: 1px;       /* the ordinary divider */
    --rule-strong: 2px;         /* the heavier divider: svc rows, FAQ rows, pull-quote */
    --rule-accent: 3px;         /* the rule that closes the FAQ accordion */
    /* type ramp (display sizes stay as clamps in page CSS) */
    --fs-2xs: 0.62rem; --fs-xs: 0.72rem; --fs-sm: 0.84rem; --fs-base: 1.0625rem;
    --fs-md: 1.05rem; --fs-lg: 1.25rem; --fs-xl: 1.55rem;
    --fs-lede: 1.3125rem;       /* standfirst / section intros — 21px */
    /* spacing — the rung number is the value / 4.
       The ramp as first written was a geometric guess: it ran 24 → 32 → 40 → 48
       with holes at rungs 7, 9 and 11, and it had no concept of anything off the
       ×4 grid at all. The four rungs added below were DERIVED FROM THIS PAGE, by
       counting index.html's own declarations — not by extending a series. Counts
       are margin/padding/gap/row-gap/column-gap declarations inside index.html's
       <style> block, brand selectors only: the illustration namespaces that
       BLOCK 3 covers (.gmail* / .gm-*, .email-card/.email-track, .bcard* / .bviz
       and its vizzes, .pv*) are excluded, because a Gmail mock's padding is
       Gmail's rhythm and not ours. Keep the spaces around those two slashes.
       Closed up, a star immediately before a slash is a comment terminator: it
       ends this comment early, feeds the rest of the prose to the parser as
       garbage, and the declaration below is eaten with it. That is not
       hypothetical — it is how --sp-1 went missing for a while.
       Every addition is a NEW name. No existing rung moved, and nothing was
       rounded onto a neighbour — see TOKENIZE-AUDIT.md for why 26 in particular
       must never be folded into --sp-6. */
    --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px;
    --sp-6: 24px;
    --sp-6-5: 26px;   /* OFF the ×4 grid, and deliberately so. 10 declarations: the
                         testimonial rhythm (.testimonial p, .testimonial-who),
                         .fit-col li, .faq-list details p, and the two section-head
                         eyebrows. 24 and 28 straddle it, so folding it into either
                         is a silent 2px move on ten elements. The half-step name
                         says out loud that this rung is not on the grid. */
    --sp-7: 28px;     /* 6 declarations — the most-used off-ramp value on the page:
                         .fit-grid gap, the ≤900 accordion padding (.svc-head),
                         .hero's bottom pad, and the margin above a
                         block-closing CTA. Fills the ×4 hole between 6 and 8. */
    --sp-8: 32px;
    --sp-9: 36px;     /* 3 declarations: .niche-row and .svc-head padding,
                         the ≤900 testimonial seam. Fills the ×4 hole between 8 and 10. */
    --sp-10: 40px;
    --sp-11: 44px;    /* 6 declarations, three of them the single role now named
                         --gap-grid-tight below. Fills the ×4 hole between 10 and 12. */
    --sp-12: 48px;
    --sp-16: 64px; --sp-18: 72px; --sp-24: 96px; --sp-40: 160px;
    /* elevation */
    --elev-1: 0 1px 2px color-mix(in srgb, var(--brand-900) 4%, transparent);
    --elev-2: 0 6px 16px -8px color-mix(in srgb, var(--brand-900) 12%, transparent);
    --elev-3: 0 14px 28px -16px color-mix(in srgb, var(--brand-900) 28%, transparent);
    --elev-4: 0 18px 30px -18px color-mix(in srgb, var(--brand-900) 18%, transparent);
    --elev-5: 0 24px 60px -24px color-mix(in srgb, var(--brand-900) 35%, transparent);
    /* motion */
    --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
    --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-spring: cubic-bezier(0.2, 1.1, 0.3, 1);
    --dur-fast: 0.2s; --dur-base: 0.35s; --dur-slow: 0.55s; --dur-xl: 0.9s;
    /* ── tracking ──
       Three tokens so tracking is a decision made once, not a literal scattered
       across 20 rules. Each is a single-line revert.
       The two text tokens are NOT at the same setting any more, and the split
       is the point. --track-body stays at +3%: that was judged on General Sans
       at body sizes, General Sans is still the body face, and it holds — at
       300 the sans blurs together when close-set and the extra tracking keeps
       each letterform distinct.
       --track-display went back to the tight editorial -0.015em on 28 Aug,
       with Playfair. The +3% it briefly carried was a BOSKA argument: Boska's
       verticals clot when set tight, so opening it up bought legibility.
       Playfair is a different animal — wider fitting, less vertical stress —
       and it is what the -0.015em was tuned against in the first place. Do not
       carry a tracking value across a face change; re-judge it. */
    --track-body: 0.03em;       /* +3%. Was -0.012em, tuned tight for the previous sans */
    --track-display: -0.015em;  /* tight editorial. Briefly +3% for Boska — see above. */
    --track-label: 0.08em;      /* uppercase small-caps: eyebrows, footer headings, link-buttons */

    /* ── faces ── */
    --sans: 'General Sans', system-ui, sans-serif;
    /* HOLDING POSITION, 28 Aug 2026: back on Playfair Display until a new
       display face is chosen. Boska was trialled the same day and rejected —
       too thin, and it failed in the direction the whole chain kept chasing:
       300 washed out, 400 carried less optical weight than the paragraph
       beneath it, 500 held on paper but went fragile reversed out on the navy
       panels (reversed type optically thins — light strokes bleed outward on a
       dark ground, and Boska is high-contrast, so its hairlines lose first).
       700 was the next available rung and the trial was called off there.
       Do not re-litigate Boska. The two faces load from DIFFERENT hosts now:
       Playfair is Google Fonts, General Sans is Fontshare, so a display-face
       change means editing the <link>s in index.html AND both templates/. */
    --serif: 'Playfair Display', Georgia, 'Times New Roman', serif;
    /* --sf is the device-mock system stack. It imitates Apple, not CCD, and is
       deliberately excluded from every brand type change. Never repoint it. */
    --sf: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Roboto, Arial, sans-serif;
    /* monospace stack for tool UI. Numbers in tool UI (dates, ids, dimensions,
       counts) always set in --mono with tabular-nums. Not used on the marketing site. */
    --mono: ui-monospace, "SF Mono", Menlo, monospace;

    /* ── weights ──
       Named so a weight change is one edit, not a sweep of literals. Long-form
       body copy reads Light (set on body, in index.html); UI, buttons, nav,
       small-caps labels and table figures hold at regular/medium, because 300
       on warm paper at small sizes goes thin and grey. The display face is
       NOT light — see --fw-display below. */
    --fw-light: 300;
    --fw-regular: 400;
    --fw-medium: 500;
    --fw-semibold: 600;
    --fw-bold: 700;
    /* THE display-face weight. All seven serif display sites read this one
       token, so the display voice is a single-line change.
       300 was tried first and rejected on sight: Boska is a high-contrast
       display serif and its thin strokes go genuinely fragile at Light — on
       warm paper it reads washed out rather than airy. 400 was then tried and
       also failed, less obviously but more damningly: at 390px the h2 carried
       LESS optical weight than the General Sans paragraph under it, inverting
       the hierarchy.
       Both of those findings are about BOSKA, which is no longer the display
       face — see --serif above. 500 is what the site shipped on Playfair for
       months and it is where Playfair is back. The Boska chain briefly took
       this token to 700 on 28 Aug; that was a response to Boska's thinness and
       it went with Boska. Playfair is a much sturdier face at the same number
       and does not need it. */
    --fw-display: 500;


    /* ══════════════════════════════════════════════════════════════════
       BLOCK 2 — SEMANTIC (brand)

       THESE ARE THE KNOBS. Changing this block changes the whole site.

       Every token here is an alias onto a block-1 primitive, and every one
       is named for what it MEANS rather than what it measures. That is the
       whole trade: repoint --surface-raised once and every raised surface
       follows, without a component ever learning which hex it landed on.
       Nothing in this block should ever hold a literal that also exists in
       block 1 — if you are about to paste a hex or a pixel here, it wants
       to be a primitive first.
       Documented exceptions, each marked in place below: the five *-ondark
       hexes (contrast-tuned, not aliases); the display/title clamps (fluid,
       so there is no scale step to point at); the leading, z-index and
       alpha ladders (no primitive ramp exists for a ratio, a layer or a
       percentage, and inventing three would be its own task).
       ══════════════════════════════════════════════════════════════════ */

    /* ── semantic tokens (aliases — rules below use these) ── */
    --bg: var(--neutral-bg);
    --bg-soft: var(--neutral-soft);
    --bg-card: var(--white);
    --ink: var(--neutral-ink);
    --muted: var(--neutral-muted);
    --on-accent: var(--white);           /* text/icons sitting ON --accent */
    --accent: var(--brand-500);          /* FILL only — buttons, bars, rules, icon strokes */
    --accent-text: var(--brand-500);     /* TEXT/links — #1D4E89 is 8.18:1 on paper */
    --accent-deep: var(--brand-600);
    --accent-ink: var(--brand-900);
    --accent-200: var(--brand-200);
    --accent-soft: var(--brand-50);
    --accent-bright: var(--brand-300);
    --hl-marker: var(--brand-500);        /* headline marker highlight — the accent */
    /* status semantic aliases — for internal tools, plugins, client approval screens */
    --status-warning: var(--warning);
    --status-warning-soft: var(--warning-soft);
    --status-error: var(--error);
    --status-error-soft: var(--error-soft);
    /* font for tool UI only — not used on the marketing site */
    --font-mono: var(--mono);
    /* ── ON-DARK VARIANTS ──────────────────────────────────────────────
       A link colour that works on paper does NOT work on a dark ground.
       Blue carries ~1/10th of green's luma weight (0.0722 vs 0.7152), so
       hue-rotating a green system to blue at equal HSL lightness lands far
       too dark: the primary on the ink is 2.11:1. These are luminance-matched
       replacements — use them on every dark or azure band.
         All five were re-derived when the ink went slate. #22344C is a
       materially LIGHTER ground than the old #001A47, which cost the whole
       set about 1.35x: the previous --label-ondark measured 4.49:1 on it,
       i.e. under AA. Re-measure this block against --brand-900 on any future
       ink change — it does not survive one untouched.
         --accent-ondark is #9CEDFF as of 2026-09-01, a deliberate owner
       decision. It is hue 191 at 100% saturation, while the other on-dark
       tokens (--ink-ondark, --muted-ondark, --label-ondark, --chip-ondark)
       all sit at hue 212–216. It is therefore the site's ONE off-hue colour,
       and that is intentional — not drift to be "corrected" back toward the
       ramp. The ramp comment elsewhere in this file says "hue ~213 held down
       the whole scale"; this token is the stated exception. */
    --accent-ondark: #9CEDFF;   /* links/CTAs/display on the ink — 9.60:1 */
    --ink-ondark:    #E6EEF9;   /* body text on the ink — 10.80:1 */
    --muted-ondark:  #D0DCEE;   /* secondary text on the ink — 9.11:1 */
    --label-ondark:  #A0BAE0;   /* small-caps labels on the ink — 6.37:1 */
    --chip-ondark:   #2E4763;   /* chip/pill fill on a dark or azure band */
    /* brand fills are FLAT — the editorial language has no gradients outside
       the device mocks. These aliases stay so consumers need no rewrite. */
    --grad-accent: var(--accent);
    --grad-accent-deep: var(--accent-deep);
    --grad-ink: var(--accent-ink);
    --grad-cta: var(--cta-blue);
    --grad-cta-deep: var(--cta-blue-deep);
    --line: #d5d5d5;           /* card + navbar borders (also used inside device mocks — leave neutral) */
    --rule: var(--brand-100);  /* the site's OWN dividers: list rows, accordions, footer */
    --wash: var(--brand-25);   /* open accordion panel background */
    --hairline: color-mix(in srgb, var(--neutral-ink) 14%, transparent);
    --shadow: 0 1px 2px color-mix(in srgb, var(--brand-900) 5%, transparent),
              0 12px 32px -12px color-mix(in srgb, var(--brand-900) 12%, transparent);
    --nav-h: 74px;              /* stuck header height, border included */
    --nav-gap: 14px;            /* floating nav card's inset from the viewport top */
    --page-scale: 1;            /* site renders at natural scale */
    --peek: 64px;               /* how much of the next band shows above the hero's fold */
    /* layers */
    /* The two high rungs are the page furniture and already existed. The
       low band is new vocabulary: index.html declares z-index 39 times and
       37 of those are bare literals (1 x14, 2 x10, 0 x4, 3 x3).
       Deliberately only three low rungs. The 3/4/5 and -1 declarations are
       not a taller scale — they are local stacking arithmetic inside one
       component (the #process card collage overlaps six cards in a fixed
       order, and .btn::before parks its fill at -1 inside its own isolate).
       That arithmetic is meaningful only against its siblings, so giving it
       global names would be a lie. Leave it as literals. */
    --z-base: 0;        /* pinned into the local stacking context, nothing above it */
    --z-raised: 1;      /* the common "sit above the decorative layer" rung */
    --z-overlay: 2;     /* content over a raised element */
    --z-backdrop: 90; --z-nav: 100;

    /* ── SURFACES ──
       What a plane IS, not what colour it happens to be. Four planes carry
       the whole page: the paper it is printed on, the cards that sit on it,
       the soft bands that recess into it, and the dark panels that invert
       it. Repoint one and every instance follows. */
    --surface-page: var(--bg);            /* the warm paper ground — body */
    --surface-raised: var(--bg-card);     /* cards, the nav shell, tiles */
    --surface-sunken: var(--bg-soft);     /* recessed section bands */
    --surface-inverse: var(--accent-ink); /* dark panels: .callbox, .niches-head */

    /* ── BORDERS ──
       Three weights of edge, by role. --border-subtle is the site's own
       divider (list rows, accordions, the footer); --border-strong is the
       heavier card/nav edge; --border-accent is a line that is making a
       point rather than separating two things. */
    --border-subtle: var(--rule);
    --border-strong: var(--line);
    --border-accent: var(--accent);

    /* ── TEXT ROLES (size) ──
       The type ramp in block 1 says how big; these say what for. Mapped
       from what index.html sets TODAY, not from what a scale would like.
       --text-display and --text-title are the h1/h2 clamps written out
       verbatim (index.html:128 and :129): they are fluid, so there is no
       --fs-* step to alias, and carrying the clamp here is what lets the
       page CSS stop owning it. */
    --text-display: clamp(2.375rem, 4.4vw, 4rem);    /* h1 — index.html:128 */
    --text-title:   clamp(2.125rem, 3.6vw, 3.25rem); /* h2 — index.html:129 */
    --text-heading: var(--fs-lg);    /* h3 — index.html:132 */
    --text-body:    var(--fs-base);  /* component body copy: .niche-desc, .svc-desc, .callbox p */
    --text-lede:    var(--fs-lede);  /* standfirsts under a section heading — index.html:1627 */
    --text-caption: var(--fs-sm);    /* captions, nav links, meta rows — index.html:205, :861 */
    --text-label:   var(--fs-xs);    /* small-caps labels + micro headings — index.html:988 */
    /* tool-UI type rungs — for plugins and internal tools only, deliberately off
       the site's rem ramp. Never used on the marketing site. */
    --text-ui-xs: 10px;
    --text-ui-sm: 11px;
    --text-ui: 12px;

    /* ── TRACKING ROLES ──
       Block 1 already names the three values; these say what they are for.
       They are pure aliases and were added late: the --track-* trio reads as
       purpose-named, so it was left in block 1 with the primitives and page
       CSS had to reach past the semantic layer to use it. It no longer does.
       Same order as block 1 so the two lists read against each other. */
    --tracking-body:    var(--track-body);     /* the +3% set on <body> — site.css:61 */
    --tracking-display: var(--track-display);  /* h1/h2 and every serif display line — site.css:173 */
    --tracking-label:   var(--track-label);    /* uppercase small-caps: .btn, footer headings — site.css:463 */

    /* ── LEADING ──
       There was no line-height token of any kind in this file, and
       index.html carries 36 line-height declarations, none tokenized. These
       six are the real clusters, counted off the page — not a ratio scale
       someone liked the look of:
         1     x4   1.14  x6   1.25  x3   1.4  x8   1.5  x4   1.65  x1
       The 20-odd remaining declarations are one-offs (1.08, 1.1, 1.12,
       1.15, 1.3, 1.35, 1.55, 1.7) and several of those are optical
       corrections on a single mock element. They are NOT a missing rung;
       do not round them onto one. */
    --leading-none:    1;      /* figures, badges, anything set solid */
    --leading-tight:   1.14;   /* h1/h2 — index.html:128, :129 */
    --leading-snug:    1.25;   /* dense UI rows, .pv-notif-* */
    --leading-normal:  1.4;    /* ledes and section intros — index.html:1627 */
    --leading-relaxed: 1.5;    /* the .callbox / mid-length paragraph setting — index.html:1885 */
    --leading-body:    1.65;   /* long-form body copy, set on body — index.html:56 */

    /* ── TEXT ROLES (colour) ──
       Deliberately separate from the size roles: a caption and a heading can
       share a colour, and a colour change should not have to know about type
       sizes to land. */
    --text-on-page:      var(--ink);        /* body copy on the paper ground */
    --text-muted:        var(--muted);      /* secondary / supporting copy */
    --text-on-accent:    var(--on-accent);  /* copy sitting on an accent fill */
    --text-heading-color: var(--accent-ink); /* h1/h2 — index.html:127 */

    /* ── SPACING INTENTS ──
       The --sp-* ramp says how far; these say between what. Read the
       caveat before trusting any of it: spacing is the least tokenized
       thing on this site by a wide margin.
       var(--sp-*) is still referenced EXACTLY ONCE in page CSS — --sp-40, via
       --space-section. Against that, index.html's <style> block carries 591 px
       literals, of which only 158 are spacing at all (see the bucket counts in
       TOKENIZE-AUDIT.md at the repo root; 267 of the rest belong to the mock
       illustrations and are excluded by policy).
       The audit finally MEASURED the spacing, scoped to brand selectors, and
       the frequency table is:
         40px x17  28px x13  20px x10  26px x10  24px x9  16px x8  48px x7
         36px x6   44px x6   18px x5   14px x4   38px x4  72px x4
       So the old warning here was right — the page's real rhythm is 26 and 28
       and the ramp offered 24 and 32 — but it is no longer merely flagged.
       BLOCK 1 has since gained --sp-6-5 (26), --sp-7 (28), --sp-9 (36) and
       --sp-11 (44), derived from that table. Adding them repointed nothing and
       changed no pixel; the migration of consumers is a separate job.
       The standing rule is unchanged and now has rungs to obey it with: do NOT
       migrate 26px onto --sp-6 or 28px onto --sp-8. That is a 2px move on
       twenty-three elements dressed up as housekeeping. 26px goes to --sp-6-5,
       28px goes to --sp-7, and nothing gets rounded to a neighbour.
       Each role below is aliased onto the rung its dominant literal already
       sits on, with the evidence cited. Where the true value is still not on
       the ramp it is written out verbatim rather than rounded. */
    --space-section: var(--sp-40);        /* section { padding: var(--sp-40) 0 } — index.html:147 */
    --space-section-tight: 56px;          /* the ≤900/≤640 section padding — index.html:2766, :2915.
                                             NOT on the --sp-* ramp (48 and 64 straddle it). Left as
                                             the true literal rather than rounded; if the ramp should
                                             gain a 56px rung, that is a separate, rendering change. */
    --space-block: var(--sp-16);          /* 64px between sibling blocks in a section — index.html:1680, :2725 */
    --space-stack: var(--sp-5);           /* 20px, a lede under its heading — index.html:1627, :1719, :1867 */
    --space-inline: var(--sp-2);          /* 8px, icon-to-label inside a row — the commonest gap on the page */
    --space-gutter: var(--sp-12);         /* .container's 48px page gutter — index.html:88 */
    --gap-card: var(--sp-5);              /* 20px between cards in a card grid — .bento, index.html:563 */
    --gap-grid: var(--sp-10);             /* 40px column gutter — the floor of every split's clamp:
                                             index.html:1608, :2240, :2506 */
    --gap-grid-tight: var(--sp-11);       /* 44px. The SAME three splits once they collapse to one
                                             column at ≤900: .niches-split, .services-split,
                                             .faq-wrap. A stacked split needs MORE air than its
                                             column gutter, not less, which is why this is 44 and
                                             --gap-grid is 40 — they are two states of one role,
                                             not a scale step apart by accident. */
    --space-eyebrow: var(--sp-6-5);       /* 26px between an .eyebrow and the h2 under it, in a
                                             section head: .niches-head .eyebrow,
                                             .services-head .eyebrow. Note the exception rather
                                             than pretending it away — .callbox .eyebrow sits at
                                             20px, because inside a card the head is tighter. If
                                             that ever unifies, it unifies onto THIS token. */
    --space-cta: var(--sp-7);             /* 28px above the button that closes a block:
                                             .pc-center .btn, .callbox .btn. Again one honest
                                             exception: .niches-head .btn is 38px, which is a
                                             one-off and stays literal until someone decides the
                                             three should agree. */

    /* ── FONT ROLES ──
       This is what makes a face swap a one-line change: three roles, and
       only these three should appear in page CSS. --font-ui points at the
       body face today and is a separate token anyway, so a future UI face
       does not have to be won by find-and-replace.
       Note --sf is NOT here on purpose: it is the device-mock system stack
       and lives in block 1 outside the brand type system. */
    --font-display: var(--serif);
    --font-body:    var(--sans);
    --font-ui:      var(--sans);

    /* ── WEIGHT ROLES ── */
    --weight-body:     var(--fw-light);     /* body copy runs Light — index.html:46 */
    --weight-regular:  var(--fw-regular);   /* Regular where the body's Light is too faint: nav and
                                               footer links — site.css:386, :661 */
    --weight-emphasis: var(--fw-medium);    /* emphasis inside body copy, quiet UI */
    --weight-heading:  var(--fw-semibold);  /* h1–h4 base — index.html:102 */
    --weight-display:  var(--fw-display);   /* the serif display voice — index.html:127 */
    --weight-label:    var(--fw-bold);      /* uppercase small-caps + buttons — index.html:260, :2519 */

    /* ── RADIUS INTENTS ──
       Card and chip both sit at 2px today. They stay separate so "round the
       cards" is one edit that does not also round the chips. */
    --radius-card: var(--radius);
    --radius-chip: var(--r-chip);
    --radius-pill: var(--r-pill);

    /* ── ELEVATION INTENTS ── */
    --elevation-card:    var(--shadow);   /* .bcard, .fit-col — index.html:602, :2572 */
    --elevation-raised:  var(--elev-3);   /* the nav shell and floating panels — index.html:166 */
    --elevation-overlay: var(--elev-5);   /* the one true overlay — index.html:2059 */

    /* ── MOTION INTENTS ──
       WARNING, and it is the reason this block is only five tokens: the
       bare CSS keyword `ease` appears 50 times in index.html and it is NOT
       --ease-out. `ease` is cubic-bezier(0.25, 0.1, 0.25, 1); --ease-out is
       (0.2, 0.8, 0.2, 1). Different curves, visibly different motion.
       --motion-ease names the HOUSE curve. It is not a drop-in for `ease`,
       and swapping the 50 keyword sites onto it is a rendering change that
       has to be judged on screen, not a find-and-replace.
       Second warning, on the durations: roughly 30 of the duration values
       in index.html are CHOREOGRAPHY ARITHMETIC, not style — index.html:
       788-792 documents the sums (1 + 0.8 = 1.8, 5.6 + 0.9 = 6.5,
       10.8 + 1.2 = 12). Those can never snap to a --dur-* rung; the
       sequence breaks the moment one addend moves. These three tokens are
       for UI transitions only. */
    --motion-fast:         var(--dur-fast);
    --motion-base:         var(--dur-base);
    --motion-slow:         var(--dur-slow);
    --motion-ease:         var(--ease-out);       /* the house curve, 29 uses */
    --motion-ease-in-out:  var(--ease-standard);

    /* ── ALPHA LADDER ──
       41 color-mix() calls across index.html use 20 distinct ad-hoc
       percentages, and several are near-duplicates that nobody chose on
       purpose: --ink at both 7% and 8%, --ink-ondark at 24% / 32% / 34%,
       --accent-200 at 52% and 55%. This ladder exists so a later task can
       collapse those clusters onto named rungs. Each rung below is the
       modal value of a real cluster, not a round number.
       Nothing consumes these yet — collapsing the call sites moves pixels
       and is deliberately a separate, reviewable change.
       Usage: color-mix(in srgb, var(--ink) var(--alpha-faint), transparent)
       --alpha-none is the odd one and it is load-bearing: mixing a colour
       to 0% is NOT the same as the `transparent` keyword. index.html:
       347-356 explains why — fading to `transparent` interpolates through
       transparent BLACK and leaves a grey bruise in the gradient. The 10
       zero-percent sites are correct. Naming the rung is how they stop
       looking like a mistake someone should tidy up. */
    --alpha-none:   0%;    /* fade to this colour at zero, NOT to `transparent` */
    --alpha-faint:  7%;    /* --ink hairlines and washes — 8 sites (7% and 8%) */
    --alpha-subtle: 9%;    /* --accent quiet fills — 3 sites */
    --alpha-soft:   18%;
    --alpha-medium: 32%;   /* the --ink-ondark 24/32/34 cluster */
    --alpha-strong: 55%;   /* the --accent-200 52/55 pair */


    /* ══════════════════════════════════════════════════════════════════
       BLOCK 3 — MOCK (illustration, NOT brand)

       These are ILLUSTRATION colours and they are deliberately outside the
       brand swap. The device mock-ups on this page are drawings of real
       products — Gmail compose, the Gmail inbox, iMessage, the Google
       wordmark, a macOS window — and their job is to be recognisable as
       those products. Google's blue is Google's blue. Repainting it when
       CCD's palette changes would not make the page more on-brand, it would
       make the drawing wrong, and a reader who clocks that the "Gmail"
       screenshot is the wrong colour stops believing the screenshot.
       So: a palette override edits block 2. It must never reach in here.

       This block dresses three things: the hero Gmail compose mock
       (.gmail*, .gm-*), the email marquee (.email-card, .email-track), and
       the "Built to work your way" bento internals (.bcard*, .pv-* —
       Shopify/Yotpo logos, iMessage bubbles, the timezone panel, the inbox
       mock).
       ══════════════════════════════════════════════════════════════════ */

    /* ══ PRODUCT MOCK-UP COLORS — OUTSIDE THE BRAND SWAP ══
       The Gmail compose, Gmail inbox, iMessage and Google-logo mock-ups imitate
       those products, not CCD. A palette override must never touch this block. */
    --mock-white: #FFFFFF;
    --mock-ink: #111111;
    /* moved here from the neutral primitives in block 1: the comment already
       said "mock colour, stays put", so it was an illustration token filed
       under the brand. Value unchanged. */
    --surface-dark: #262B2A;    /* dark window chrome on the call mock — mock colour, stays put */
    --surface-dark-2: #1B1F1E;
    /* also moved here from block 1, same reason: the comment already declared
       these outside the brand swap, so they were illustration tokens filed
       under the brand. They ground the participant tiles inside the same call
       mock as the two surfaces above. Values unchanged. */
    /* illustration palette (collage tiles — not part of the brand swap) */
    --tile-warm: #E8DFCE; --tile-clay: #E5D5D0;
    /* the mocks keep the shapes of the products they imitate: the site's own
       radius scale went square, theirs must NOT follow it. Never zero these. */
    --mr-xs: 5px; --mr-sm: 10px; --mr-md: 14px; --mr-lg: 18px; --mr-xl: 28px;
    /* The campaign-strip cards. A deliberate exception to the square
       editorial language, and it belongs in THIS block rather than the
       --r-* scale for the same reason the mocks do: a rounded card reads
       as a phone screen, which is where these emails are actually opened.
       It imitates a device, not CCD. Don't fold it back into --r-md. */
    --r-email: 14px;
    --scrim: rgba(0, 0, 0, 0.55);        /* photo scrim — neutral by intent */
    /* Gmail compose */
    --gmail-surface: #F2F6FC;
    --gmail-title: #041E49;
    --gmail-icon: #444746;
    --gmail-rule: #E8EAED;
    --gmail-ink: #1F1F1F;
    --gmail-muted: #5E5E5E;
    --gmail-blue: #0B57D0;
    --gmail-blue-deep: #0842A0;
    --gmail-shadow: 0 8px 12px rgba(9, 30, 66, 0.12), 0 16px 34px -18px rgba(9, 30, 66, 0.25);
    --gmail-shadow-sm: 0 1px 3px rgba(9, 30, 66, 0.3);
    /* Gmail inbox */
    --gm-bar: #F1F3F4;
    --gm-icon: #5F6368;
    --gm-read: #F3F5F6;
    --gm-meta: #80868B;
    /* iMessage + macOS chrome */
    --imsg-bubble-in: #E9E9EB;
    --imsg-bubble-out: #0B93F6;
    --imsg-ink-dark: #1C1C1E;
    --imsg-grey: #8E8E93;
    --imsg-field: rgba(120, 120, 128, 0.12);
    --imsg-heart-1: #FCD7EC;
    --imsg-heart-2: #F8A9D4;
    --imsg-heart-3: #EF6FB7;
    --mac-red: #FF5F57;
    --mac-amber: #FEBC2E;
    --mac-green: #28C840;
    /* Google logo */
    --logo-google-blue: #4285f4;
    --logo-google-green: #34a853;
    --logo-google-yellow: #fbbc04;
    --logo-google-red: #ea4335;
    --logo-google-red-deep: #c5221f;
  }

/* ≤900 keeps natural scale (as the whole site now does) and drops the bar to
   64px so the header doesn't eat a phone's fold. */
@media (max-width: 900px) {
    :root {
        --page-scale: 1;
        --nav-h: 64px;
    }
}
