/* Vijil colour tokens.
 *
 * The design export hardcodes 59 distinct hex values across 7,391 inline
 * declarations and uses the design system's own tokens.css exactly once. These
 * are the real brand values, named from handoff.md's design-token section
 * rather than from the scaffold defaults in _ds/.../tokens/tokens.css, whose
 * --color-background is a grey-blue gradient that appears nowhere on the site.
 *
 * A hex-to-token map is 1:1, so where one value carries two meanings only one
 * name survives. #4a90e2 was both the light-blue accent and the Diamond product
 * colour, and --vj-diamond aliased --vj-accent. They are separate now: Diamond
 * is #1B6AC9 and the accent is unchanged, because the product palette had to be
 * chosen as a SET (see Products below) and could not be a borrowed accent.
 *
 * Tokenising is what makes a future dark mode tractable: redefining these
 * under a [data-theme] selector reaches the whole site, whereas 7,391 inline
 * hex literals cannot be overridden at all.
 */
:root {
  /* Grounds */
  --vj-ground-ivory: #f7f5f0;   /* business owner + all content pages */
  /* THE PAGE GROUND COMES FROM vijil-ui, 2026-09-22. The site loaded the
   * design system on every page (Site.astro DEFAULT_HEAD) and then painted
   * from ivory above, so the console and the website never converged.
   * --vj-ground-page is vijil-ui's own --color-background. It is a GRADIENT,
   * so anything that needs a paint value — an SVG fill or stroke, a stop
   * inside another gradient — takes --vj-ground-flat instead. */
  --vj-ground-page: var(--color-background, #eef2f7);
  --vj-ground-flat: #eef2f7;
  --vj-ground-navy:  #0b1626;   /* risk owner */
  --vj-ground-black: #060707;   /* developer */
  --vj-surface:      #ffffff;
  --vj-surface-alt:  #f3f1ea;   /* hover ground on light */
  --vj-ground-ink:   #0b0f16;   /* code blocks, deepest panels */

  /* Ink on light */
  --vj-ink:          #101828;
  --vj-ink-body:     #475467;
  --vj-ink-strong:   #344054;
  --vj-ink-muted:    #667085;
  --vj-ink-subtle:   #98a2b3;
  --vj-ink-serif:    #1f2937;   /* blog body, Newsreader */

  /* Ink on dark */
  --vj-ink-on-navy:  #ebeef5;
  --vj-ink-on-black: #f4f6f2;

  /* Brand */
  --vj-brand:        #0247a9;
  --vj-brand-hover:  #023b8d;
  /* THE SECOND BRAND COLOR, 2026-09-23. The wordmark is two dots, #0247A9
   * and #DE1616, and only the blue had a token — so anything wanting the
   * pair reached for --vj-danger, which is a STATUS red and says 'error'.
   * This is the mark's red and carries no state. */
  --vj-brand-red:    #DE1616;
  --vj-accent:       #4a90e2;
  --vj-accent-light: #6ea8e8;
  --vj-hairline:     #e6edf5;

  /* Products.
   *
   * FOUR HUES BECAUSE THE LIFECYCLE DIAGRAM DRAWS ALL FOUR AT ONCE. Colour is
   * carrying identity there, not decoration, so the set has to survive being
   * read side by side — including by the ~8% of men with a red-green deficiency.
   *
   * The previous set did not. Validated against the categorical-palette checks
   * (OKLab lightness band, chroma floor, CVD separation, contrast) it failed
   * three ways: #fbbf24 sat at 1.53:1 on ivory, #64748b had chroma 0.041 and
   * read as grey rather than as a colour at all, and violet-against-blue
   * collapsed to ΔE 5.4 under deuteranopia — Darwin and Diamond were the same
   * colour to those readers. This set's worst pair is ΔE 13.5.
   *
   * The official icon set ships all three product marks in #4285F4, which is
   * Google's brand blue exactly. These replace it.
   *
   * A DARK-GROUND SET EXISTS AND IS NOT DEFINED HERE YET, because nothing
   * renders a product colour on ink today and an unused token is the start of a
   * palette nobody can audit. When one does, these are the validated steps
   * against #0b0f16 — re-picked from the same hues, not lightened, because a
   * palette does not invert: #9D174D is 2.43:1 on ink and would vanish.
   *
   *   discover #10A79A · diamond #3B82F6 · dome #D97706 · darwin #E11D48 */
  --vj-discover:     #0D9488;   /* teal   — finding what is out there */

  /* THE THREE ROLES, AT TEXT WEIGHT. lifecycle-graph.js already owns
   * --biz / --risk / --dev for the business owner, the risk owner and the
   * agent developer, and those values are tuned for DIAGRAM MARKS, which
   * need 3:1. Label text at 12px needs 4.5:1, and against this site's card
   * ground (#e6ebf2, pixel-sampled) --biz measures 4.18 and --risk 3.39.
   * Same hue, lowered lightness until each clears AA. --dev already passes
   * at 5.55 and is repeated here so the three are read from one place. */
  --vj-discover-text:  #0b7a70;   /* 4.51:1 — --vj-discover is 3.24 at 13px */
  --vj-role-biz-text:  #14784e;   /* 4.58:1 — was #157F52 at 4.18 */
  --vj-role-risk-text: #8e5a0d;   /* 4.7+:1 on the card ground; --risk is 3.39 */
  --vj-role-dev-text:  #1B4FD8;   /* 5.55:1 */
  --vj-ink-label:      #5c6578;   /* 4.7+:1; --vj-ink-muted is 4.11 there */
  --vj-diamond:      #1B6AC9;   /* blue   — measurement */
  --vj-dome:         #B5610F;   /* amber  — defence */
  --vj-darwin:       #9D174D;   /* rose   — evolution */

  /* Retained: 20 uses still name it, and it now equals --vj-dome. Remove once
   * those call sites move to the token above. */
  --vj-dome-light:   #B5610F;

  /* Status and category */
  --vj-success:      #1a7f37;   /* How-to category, pass states */
  --vj-success-alt:  #157f52;
  --vj-danger:       #d92d20;
  --vj-danger-dark:  #b42318;
}

/* THE SIX PROPERTIES WE INHERITED FROM vijil-ui, now ours. The vendored
 * _ds/ directory was deleted on 2026-09-25 — it was the Console's React
 * component library, which this static site never mounted. These six are the
 * only things in it that changed a render, and public/prose.css plus
 * --vj-ground-page above read them.
 *
 * --color-background is the page ground and it is a GRADIENT, which is why
 * --vj-ground-flat exists for anywhere a paint value is required.
 *
 * The dark values come across too. The site sets data-theme="light" on every
 * page today, so they apply to nothing — but a day/night switch is a
 * [data-theme] override, and dropping half the pairs now would mean
 * rediscovering them later. What actually blocks that switch is ours, not
 * theirs: 1,365 inline hex literals across 58 distinct values, which no
 * selector can reach.
 */
:root {
  --color-background: linear-gradient(135deg, #eef2f7 0%, #e2e8f0 50%, #eef2f7 100%);
  --glass-backdrop-filter: blur(20px) saturate(180%);
  --glass-border: 1px solid rgba(255, 255, 255, 0.5);
  --glass-shadow: 0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  --glass-inner-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
  --glass-background: linear-gradient(
    135deg,
    rgba(255, 255, 255, 0.72) 0%,
    rgba(255, 255, 255, 0.48) 100%
  );
}

[data-theme='dark'] {
  /* -------------------------------------------------------------------------
     THE DARK PALETTE. LIVE. Site.astro reads prefers-color-scheme before first
     paint and sets data-theme from it, so this block paints the site for every
     visitor whose OS is in dark. It was written inert — the markup hardcoded
     "light" and nothing flipped it — and that sentence survived here after the
     switch was wired. Copilot caught it on #246. There is no toggle and no
     stored choice: the OS is the only input, and a change to it while the page
     is open is tracked.

     EVERY VALUE HAS A SOURCE. Nothing here was picked by eye.

     Neutrals come from the Console's own dark theme
     (vijil-console-frontend/src/global.css, [data-theme="dark"]): --text-primary
     #e5e7eb, --text-secondary #9ca3af, --text-tertiary #6b7280, --border-color
     rgba(255,255,255,.1), --card-background rgba(42,42,42,.8), --color-background
     #121212. The Console has three ink steps and this site has seven, so the
     middle steps interpolate; the three anchors are the Console's.

     Hues come from palettes this repo ALREADY ships. design/lorenz-loop.js
     defines :host([theme="navy"]) and :host([theme="black"]) with a full set —
     --blue #6ea8e8 / #83A8FF, --red #F2564E, --c-risk #E0A94A, --c-biz #4FC791.
     Those are decisions somebody made for this brand on a dark ground, so they
     are used rather than re-made. #6ea8e8 is already --vj-accent-light.

     CONTRAST IS COMPUTED, NOT ASSUMED. Every value below clears WCAG AA 4.5:1
     against the worse of the two dark grounds (#121212 and the #1e1e1e card).
     Two candidates failed and were lifted:
       --vj-ink-subtle   the Console's #6b7280 reads 3.45:1 on the card -> #858e9d
       --vj-danger-dark  BRIGHTENED rather than darkened. In light mode "dark" is
                         the stronger state because darker means more contrast on
                         white; on a dark ground the stronger state is the lighter
                         one, so the relationship inverts.

     Worth knowing: the dark palette is MORE accessible than the light one on
     several tokens. --vj-accent is 3.29:1 on white and 7.15:1 here;
     --vj-ink-subtle is 2.58:1 on white and 5.04:1 here. WEB-9 tracks the 45
     light-theme contrast NOTEs; this block does not add to them.

     NOT OVERRIDDEN, on purpose: --vj-ground-navy, --vj-ground-black,
     --vj-ground-ink are already dark grounds, and --vj-ink-on-navy /
     --vj-ink-on-black are already the light ink that sits on them. A theme that
     inverted those would turn a dark panel light inside a dark page.
     ------------------------------------------------------------------------- */

  /* grounds and surfaces */
  --vj-surface:       #1e1e1e;   /* Console --card-background, flattened */
  --vj-surface-alt:   #242424;
  --vj-ground-flat:   #121212;   /* Console --color-background */
  --vj-ground-ivory:  #1a1a1a;
  --vj-hairline:      rgba(255, 255, 255, 0.1);   /* Console --border-color */

  /* ink, lightest to darkest */
  --vj-ink:           #e5e7eb;   /* Console --text-primary */
  --vj-ink-serif:     #e5e7eb;
  --vj-ink-strong:    #d1d5db;
  --vj-ink-body:      #9ca3af;   /* Console --text-secondary */
  --vj-ink-label:     #9ca3af;
  --vj-ink-muted:     #9ca3af;
  --vj-ink-subtle:    #858e9d;   /* Console --text-tertiary, lifted for AA */

  /* brand and accent */
  --vj-brand:         #6ea8e8;   /* lorenz [theme="black"] --blue */
  --vj-brand-hover:   #8ebef0;
  --vj-accent:        #83a8ff;   /* lorenz [theme="navy"] --blue */
  --vj-accent-light:  #a8c4f5;

  /* state */
  --vj-danger:        #f2564e;   /* lorenz dark --red, both themes */
  --vj-danger-dark:   #ff7a72;   /* brighter, not darker — see above */
  --vj-brand-red:     #f2564e;
  --vj-success:       #4fc791;   /* lorenz dark --c-biz */
  --vj-success-alt:   #4fc791;

  /* the three roles, from the lorenz dark palettes */
  --vj-role-dev-text:  #83a8ff;
  --vj-role-risk-text: #e0a94a;
  --vj-role-biz-text:  #4fc791;

  /* the four products */
  --vj-discover:      #2dd4bf;
  --vj-discover-text: #5eead4;
  --vj-diamond:       #83a8ff;
  --vj-dome:          #e0a94a;
  --vj-dome-light:    #eabe6e;
  --vj-darwin:        #f472b6;

  /* glass, already here before this block grew */
  --color-background: #121212;
  --glass-backdrop-filter: blur(48px) saturate(200%);
  --glass-border: 1px solid rgba(255, 255, 255, 0.125);
  --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
  --glass-inner-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  --glass-background: linear-gradient(
    135deg,
    rgba(52, 52, 52, 0.75) 0%,
    rgba(42, 42, 42, 0.65) 100%
  );
}

/* ---------------------------------------------------------------------------
   SVG PRESENTATION ATTRIBUTES, REDIRECTED TO THE TOKENS THEY ALREADY EQUAL

   465 literals across 14 (attribute, value) pairs on 13 built pages. Every
   rule maps a value to the one token whose value it already is, so nothing
   changes until a token does — which is the point: it is what lets a theme
   reach the inline SVG diagrams.

   WHY RULES AND NOT EDITS. A presentation attribute does not resolve var() —
   `fill="var(--vj-brand)"` paints nothing. The usual fix rewrites each one as
   `style="fill:var(--vj-brand)"`: ~700 edits across 19 files, every one raised
   to inline specificity, reverted by another ~700 edits. A presentation
   attribute has NO specificity, so any selector outranks it. 14 rules do the
   same job and revert by deleting this block.

   SAFE HERE, MEASURED NOT ASSUMED. tokens.css is the FIRST of six stylesheets
   the layout links, so these selectors at (0,1,0) lose every tie to
   responsive.css, imported.css, gate.css and prose.css. Existing rules keep
   winning.

   AND THEY DO EXIST — an earlier draft of this comment claimed no class sets
   fill or stroke, which is wrong. responsive.css:643-649 sets both on
   `.vj-lifecycle [data-lc-node].is-lit > .vj-lc-box` and its labels. Those win
   twice over: (0,3,1) against (0,1,0), and a later stylesheet besides. The
   conclusion held; the stated reason did not, and the reason it did not is worth
   recording — the grep behind it was `fill:|stroke:` piped through `grep -v
   'var(--'`, and every one of those declarations uses var(), so the filter
   removed the only counter-evidence. A check that can only confirm its
   hypothesis is not a check.

   That block is also independent confirmation of the mechanism this one relies
   on. Its own comment says the lifecycle script paints the lit step into fill
   and stroke ATTRIBUTES, "which any rule here outranks".

   THE `i` FLAG IS LOAD-BEARING. Attribute values match case-sensitively in
   HTML and the sources carry both `#0247a9` and `#0247A9`.

   GENERATED FROM dist/, NOT FROM SOURCE, and that is the difference between
   this block and its first draft. Written from source it carried 21 rules for
   862 literals; two of those rules (`#de1616`, `#e6edf5`) matched ZERO built
   pages, because their only occurrences are inside a shadow root or in a script
   that no longer ships. A rule that cannot match anything is decoration.

   WHAT THIS CANNOT REACH, and how that case is handled instead: 150 literals
   inside design/lorenz-loop.js and lorenz-loop-developer.js. Those attach a
   shadow root, and document CSS does not cross a shadow boundary. They do not
   need it — each already defines a full palette on `:host` plus `:host([theme=
   "navy"])` and `:host([theme="black"])`. Theming them means setting that
   attribute, not writing a rule here. (Four pages set `theme="ivory"`, which
   matches no selector and falls through to the default; the default IS the
   ivory palette, so it renders correctly by coincidence rather than by rule.)

   <pre> and <code> are excluded: a hex in a code sample is content a reader is
   meant to copy, not a colour to repaint.

   Verified by scripts/snapshot-render.mjs two ways, because "identical" is also
   what a no-op produces:

     - every capture identical with these rules in place, at 1280px and 390px;
     - FIVE routes change when --vj-brand alone is repointed WITH these rules and
       none without them. Confirmed in the browser: 44 matched elements on /,
       computed fill rgb(2,71,169), resolved through the token.

   An earlier draft of this comment said four. The fifth was "/", dropped by a
   grep of mine whose route pattern required two slashes.
   --------------------------------------------------------------------------- */
[fill="#0247a9" i] { fill: var(--vj-brand); }      /* 185 on 7 page(s) */
[fill="#101828" i] { fill: var(--vj-ink); }      /*  99 on 5 page(s) */
[fill="#667085" i] { fill: var(--vj-ink-muted); }      /*  92 on 5 page(s) */
[stroke="#4a90e2" i] { stroke: var(--vj-accent); }    /*  25 on 2 page(s) */
[stroke="#0247a9" i] { stroke: var(--vj-brand); }    /*  21 on 3 page(s) */
[fill="#1a7f37" i] { fill: var(--vj-success); }      /*   9 on 1 page(s) */
[fill="#b42318" i] { fill: var(--vj-danger-dark); }      /*   9 on 1 page(s) */
[fill="#157f52" i] { fill: var(--vj-success-alt); }      /*   9 on 2 page(s) */
[stroke="#d92d20" i] { stroke: var(--vj-danger); }    /*   4 on 1 page(s) */
[fill="#0b0f16" i] { fill: var(--vj-ground-ink); }      /*   3 on 2 page(s) */
[stroke="#101828" i] { stroke: var(--vj-ink); }    /*   3 on 1 page(s) */
[stroke="#667085" i] { stroke: var(--vj-ink-muted); }    /*   3 on 1 page(s) */
[fill="#d92d20" i] { fill: var(--vj-danger); }      /*   2 on 1 page(s) */
[stroke="#157f52" i] { stroke: var(--vj-success-alt); }    /*   1 on 1 page(s) */

/* ---------------------------------------------------------------------------
   WHAT THE PALETTE CANNOT REACH

   Three things on this site are images or filters rather than colours, so no
   token touches them. All three were WEB-18.
   --------------------------------------------------------------------------- */

/* 1. THE WORDMARK IS SWAPPED, NOT RECOLOURED. /uploads/vijil-logo.svg has black
      letterforms; on a dark ground only the three dots survive. The white
      variant is the same mark with six fills changed, keeping the blue and red
      dots — a filter would have flattened those too. SiteHeader renders both. */
[data-theme='dark'] .vj-logo-dark { display: none !important; }
[data-theme='dark'] .vj-logo-light { display: block !important; }

/* 2. THE PARTNER AND CUSTOMER MARKS ARE ALREADY RECOLOURED, which is the fact
      that makes this simple. Every one carries `grayscale(1) brightness(.4)` or
      near it — the brand colours are gone on the light page too, so putting the
      same grey on the other side of the ground takes no new liberty with anyone's
      trademark. I filed WEB-18 saying these needed a backing plate because they
      must not be recoloured; they already are, and a plate would have been a
      white box on a dark page for no reason.

      invert() before the brightness, because brightness() MULTIPLIES: a mark
      whose art is near-black stays near-black however high you push it.

      MATCHED ON background:url, NOT ON THE FILTER STRING. The first version of
      this keyed on the inline filter alone, and a companion rule swept up every
      element with a bare `filter:grayscale(1)` and no brightness. Measured
      against the built site, that second rule matched twelve elements and all
      twelve were PHOTOGRAPHS OF PEOPLE — the team, the investors and the
      advisors on /company, plus Vin on /news — so dark mode turned every
      headshot on the site into a negative. Vin caught it, 2026-10-04.

      The clean split was in the markup the whole time, and it is not the
      brightness value. Every one of the 105 recoloured marks is a div with a
      `background:url(...)` plate; every one of the 12 photographs is an `img`.
      Matching the plate is what the rule means, so the rule now says it. */
[data-theme='dark'] [style*="background:url"][style*="grayscale(1) brightness(.4)"],
[data-theme='dark'] [style*="background:url"][style*="grayscale(1) brightness(.45)"],
[data-theme='dark'] [style*="background:url"][style*="grayscale(1) brightness(.35)"] {
  filter: grayscale(1) invert(1) brightness(.92) !important;
}

/* 2b. A GREYSCALED PHOTOGRAPH STAYS GREYSCALE. Vin, 2026-10-04: the headshots
       "should stay as they are". Grey artwork reads on either ground, so there
       is nothing to do and this comment exists to stop the next pass adding a
       rule back. */

/* 3. THE INVESTOR MARKS CARRY NO FILTER — they are published artwork in their
      own colours, which is a different case from the greyscaled rows and is why
      the rule above does not sweep them up.

      THE WHITE CHIP IS GONE. Vin, 2026-10-04: the three marks in individual
      white plates read as stickers on an otherwise dark page. Transparent is
      right; what the marks needed was lighter artwork, not a lighter box.

      Measured, each one is a different case:
        Mayfield   #62a143 green reads 5.98:1 on #121212. Nothing to do.
        Gradient   eight paths carry the near-black wordmark. The variant swaps
                   those eight and leaves the three red-triangle fills alone.
        BrightMind a PNG, so no fill to swap — 12,537 dark NEAR-NEUTRAL pixels
                   were lifted and the orange bars left exactly where they were.
                   Not mentioned in the request, but it has the same defect:
                   11,206 px of rgb(64,64,64) spelling "Brightmind" that vanish
                   on #121212.

      design/assets/ ALREADY HELD inv-*-w.png, and they are not usable: two of
      the three are fully transparent, with no opaque pixel in them, and the one
      with content is a 193x25 monochrome PNG that throws the red triangle away.
      Anyone reaching for the -w set would get nothing.

      content: url() rather than two images toggled in the markup, because these
      are three logos on four pages and the swap has no structural meaning. Where
      a browser does not support it the published mark renders, which is exactly
      today's behaviour. */
[data-theme='dark'] img[src$="inv-gradient.svg"] { content: url("/assets/inv-gradient-white.svg"); }
[data-theme='dark'] img[src$="inv-brightmind.png"] { content: url("/assets/inv-brightmind-white.png"); }

/* 3b. THE WORDMARK IN THE BODY GETS THE SAME SWAP AS THE ONE IN THE HEADER.
       SiteHeader renders both variants and toggles them by class (rule 1), but
       ten other places drop a bare /uploads/vijil-logo.svg into the page — the
       /partners stack diagram twice, /news, three blog posts and two partner
       pages. Those carry the black letterforms onto a dark ground, where only
       the three dots survive. Vin, 2026-10-04, on the stack diagram: flip it to
       white, same as the main site.

       The diagram's instance sits on a span whose background is --vj-surface,
       so in dark mode the plate under the mark is already near-black — which is
       why the white variant is the readable one there and not a liberty.

       Harmless on the header's hidden copy: .vj-logo-dark is display:none in
       dark, so swapping the source of an unpainted image changes nothing. */
[data-theme='dark'] img[src$="/uploads/vijil-logo.svg"] { content: url("/media-kit/vijil-logo-white.svg"); }

/* 3c. THE EMPTY SLOTS IN THE STACK DIAGRAM ARE AN OUTLINE, NOT A PLATE. Six
       chips on /partners mark capabilities the stack leaves open — SIEM,
       Observability, Authorization, Agent DevOps, Post-training. Each is drawn
       as a dashed outline over `rgba(247,245,240,.6)`, a near-ivory wash that is
       barely there on white. Over #121212 the same wash composites to about
       rgb(149,148,144): an opaque mid-grey slab carrying --vj-ink-muted text,
       which in dark is #9ca3af. Light grey on mid grey. Vin, 2026-10-04: "some
       of the chips are grey and the logos are hard to read."

       Clearing the fill restores what the chip is FOR. A dashed border over the
       page ground reads as an empty slot on either side; a filled one reads as
       an occupied slot whose label failed to render. The dashed line goes up to
       match, because rgba(16,24,40,.3) over #121212 is also invisible.

       Six elements, one page, one role — verified against dist/ before this was
       written, which is the check rule 2 above went without. */
[data-theme='dark'] [style*="background:rgba(247,245,240,.6)"] {
  background: transparent !important;
  border-color: rgba(255, 255, 255, 0.28) !important;
}

/* 3d. A FORM FIELD NEEDS ITS OUTLINE BACK. Every input, select and textarea on
       /contact, /demo and /free-trial carries `border:1px solid
       rgba(16,24,40,.22)` inline. On white that is a pale grey hairline; over
       the dark field it composites to within a point of the field itself, so
       the form reads as floating labels with no boxes. Vin, 2026-10-04.

       rgba(255,255,255,.22) is the same alpha mirrored, not a new value picked
       by eye — it lands near rgb(82,82,82) on the #1e1e1e field, which carries
       slightly more separation than the light-mode hairline has on white.

       !important because the border is inline, and an inline style outranks
       every selector. */
[data-theme='dark'] input,
[data-theme='dark'] select,
[data-theme='dark'] textarea {
  border-color: rgba(255, 255, 255, 0.22) !important;
}

/* 3e. TWO TOKENS CARRY THE OPPOSITE ROLE IN prose.css, and dark mode inverted
       them. Copilot found both on #246; they are real and they land on sixteen
       pages each.

       .vj-pre draws `background: var(--vj-ink)` with a hardcoded #e6edf6 text,
       which is correct while --vj-ink is #101828 and catastrophic once it is
       #e5e7eb: light text on light ground, 1.01:1. .vj-pre-plain draws
       `color: var(--vj-hairline)`, which is #e6edf5 in light — readable ink on
       the dark panel that encloses it — and rgba(255,255,255,0.1) in dark.

       Both are repaired here rather than in prose.css, and scoped to dark, so
       the light page keeps the exact pixels it has and none of its 40 contrast
       notes can move. --vj-ground-ink and --vj-ink-on-black are the tokens that
       mean "a ground that is dark in both themes" and "the ink that sits on
       one"; neither is overridden by the dark palette, which is why they are the
       right targets. The deeper fix is for prose.css to name those tokens
       directly, which is a light-mode change and belongs in its own PR. */
[data-theme='dark'] .vj-pre { background: var(--vj-ground-ink); }
[data-theme='dark'] .vj-pre-plain { color: var(--vj-ink-on-black); }

/* 4. WHITE INSIDE A DIAGRAM IS A SURFACE, which resolves the one ambiguity the
      token migration deliberately left open. #ffffff is --vj-surface where it is
      a ground and ink where it is a knockout on navy, so the light-mode pass
      refused to map it. In an inline SVG the card shapes are the overwhelming
      case: /product/dome carries 18 white fills and every contrast failure in
      dark mode sat on one of them — themed light text on an unthemed white card.

      Scoped to [data-theme='dark'] only, so the light page is untouched and this
      cannot regress the 40 notes it already has. Done as attribute rules for the
      same reason the block above exists: a presentation attribute does not
      resolve var(), and it has no specificity, so a selector outranks it. */
[data-theme='dark'] svg [fill="#fff" i],
[data-theme='dark'] svg [fill="#ffffff" i],
[data-theme='dark'] svg [fill="white" i] { fill: var(--vj-surface); }
/* THE WHITE-STROKE HALF OF THIS RULE IS DELETED, and the reason is the one
   this file already learned once. It mapped a white stroke to --vj-hairline on
   the theory that a white stroke is a card edge. Copilot disagreed on #246, and
   enumerating the built site settles it: all 13 white-stroke elements in dist/
   carry fill="none" — they are DRAWN MARKS, three checkmark ticks on
   /product/diamond and ten on /product/discover, with round linecaps. Not one
   is the edge of a shape. Turning them into a 10% white hairline erased every
   one.

   That is the same defect as the headshot rule two commits ago: a selector
   keyed on what the intended elements were assumed to look like, shipped
   without enumerating what it actually hits. A white tick on a coloured badge
   is knockout ink and stays white on either ground, so there is nothing to do.
   If a white shape EDGE ever ships, it needs its own rule and its own count. */

/* 5. THE CONSENT PANEL DOES NOT THEME ITSELF, and is deliberately NOT fixed
      here. consent.js writes its own style block with #fff, #F7F7F4, #101828 and
      #475467 hardcoded, so in dark mode a visitor gets a light card with dark
      text over a dark page — the first thing they see.

      I wrote the overrides, rendered them, and took them out. They themed the
      text and not the panel ground, so the toggle rows came back light grey on
      white — worse than the untouched banner, on a legal surface, which is not a
      trade worth making to close a gap.

      It also belongs to a component that is about to be replaced: PR #237
      rewrites this banner from two vendors to four purposes. Theming the version
      that is being deleted is work done twice. Filed instead.

      Found because check-rendered reported five contrast failures in dark on
      /product/dome that a direct browser read contradicted — the gate never
      dismisses the banner, so its pixel samples land on the panel rather than on
      the page. The false positive was wrong; the thing it was sitting on is
      real. */
