/*
 * ═══════════════════════════════════════════════════════════════════════════
 * RESOLVED THEME INPUTS, A SHARED COMPONENT PALETTE.
 *
 * Five-color authoring expands through theme-palette.js into the compatible
 * input slots consumed here. That resolver also supplies contrast-safe derived
 * overrides; manual overrides remain visible to the contrast audit. This file
 * supplies the shared CSS derivations for current and legacy themes: the app links it, and the
 * admin's live preview links it too, over https, from this same file. A
 * stylesheet may be linked cross-origin without CORS, which is the only
 * reason the preview can be honest rather than a second implementation that
 * agrees for a while.
 *
 * ─────────────────────────────────────────────────────────────────────────
 * WHY THE ARITHMETIC IS THE BROWSER'S AND NOT ANYBODY'S JAVASCRIPT.
 *
 * Every rule below is a `color-mix`. Nothing here parses a colour, converts
 * a space, or rounds a channel — which means there is no colour maths to
 * duplicate, no rounding to disagree about, and a theme whose input is
 * `oklch(...)` or a named colour works exactly as well as one whose input is
 * a hex. The two historical bugs in this repo's colour handling were both
 * parsers (a regex that ate a `)`, and a splitter that only knew commas).
 * This file cannot have that class of bug because it does not parse.
 *
 * ─────────────────────────────────────────────────────────────────────────
 * THE ONE TRICK THAT MAKES A SINGLE RULE SERVE LIGHT AND DARK THEMES.
 *
 * A derivation written as "20% toward black" only works for one polarity —
 * it deepens a dark theme correctly and muddies a light one. So NOTHING here
 * mixes toward black or white except the page, which recedes in both. Every
 * other rule mixes toward `--sq-in-text` (the ink) or `--sq-in-canvas` (the
 * ground), and those two swap ends when the theme does. A ladder built from
 * the ink toward the ground keeps its spacing and its direction whichever
 * way round the theme is.
 *
 * ─────────────────────────────────────────────────────────────────────────
 * WHAT IS NOT DERIVED — and this paragraph has now been wrong twice, so it
 * is worth stating what changed rather than quietly editing the list.
 *
 * It once read "the semantic hues, the brand pink and teal, and the admin
 * severity scale keep their ratified values — a theme changes the room, not
 * what 'error' means." The pink and the teal left that list on 31 Aug, when
 * Q ruled the signal and the accent into the theme. The four semantic hues
 * left it on 4 Sep, for a measured reason: the token atlas found them painted
 * 1,705 times with no theme able to move any of it, so "a theme changes the
 * room" was true of the walls and false of everything on them.
 *
 * What remains genuinely not derived is the ADMIN SEVERITY SCALE, which is
 * admin-scoped by UIC 29.14 and named by no consumer surface. Their `*Text`
 * and `*-soft` variants ARE derived, because those encode "on a ground of a
 * particular polarity" and nothing else.
 *
 * A comment that lists what a rule excludes goes stale every time the rule
 * grows. Prefer reading the declarations below, which cannot.
 *
 * `--sq-action-primary-onColor` is the one value the browser cannot work out
 * for itself: choosing black or white ink for the action colour needs a
 * luminance comparison, and CSS has no branch. The server computes it from
 * the six and serves it as `--sq-in-action-ink`, so it is decided once, in
 * one language, rather than in the app and the admin separately.
 * ═══════════════════════════════════════════════════════════════════════════
 */
/*
 * TWO SELECTORS, AND BOTH ARE LOAD-BEARING.
 *
 * `[data-theme="custom"]` alone is what lets the admin's live preview be a
 * DIV — a panel of sample UI inside a page that is itself dark — instead of a
 * second copy of every line below. Custom properties inherit, so a container
 * works exactly as well as a root.
 *
 * But on <html> that selector scores 0,1,0, exactly what `:root` in
 * tokens.css scores, and a tie is broken by SOURCE ORDER. Which stylesheet a
 * bundler happens to emit last is not something to bet a palette on: it made
 * the app set all six inputs, carry data-theme="custom", and still resolve
 * --sq-bg-canvas to the ratified #0D0D10 — a theme applied everywhere except
 * on screen. So the root case is spelled out as well, at 0,2,0, and the file
 * wins wherever it is loaded from.
 */
:root[data-theme="custom"],
[data-theme="custom"] {
  /* ── grounds ─────────────────────────────────────────────────────────── */
  --sq-bg-canvas: var(--sq-in-canvas);
  /* The page sits BEHIND the canvas, and in both polarities it recedes by
     going darker — dark's #08080B under #0D0D10, light's paper under its
     panel. This is the only rule in the file with a fixed direction, and it
     is fixed because the relationship is. */
  --sq-bg-page: color-mix(in srgb, var(--sq-in-canvas) 93%, black);
  --sq-surface-1: var(--sq-in-surface);
  /*
     5%, NOT 18%, AND THE RATIFIED PALETTE IS WHERE THE NUMBER COMES FROM.
     Dark ships #14141C for surface-1 and #1E1E28 for surface-2 — a step of
     ten units out of 255. The first draft moved 18% toward the ink and
     produced #3D3D43, a visibly different grey, which is what a derivation
     tuned by eye rather than against the shipped values looks like. 5%
     reproduces #1F1F27 against a ratified #1E1E28. */
  --sq-surface-2: color-mix(in srgb, var(--sq-in-surface) 95%, var(--sq-in-text));
  --sq-surface-crewTile: color-mix(in srgb, var(--sq-in-surface) 88%, var(--sq-semantic-info));
  /*
   * ═══════════════════════════════════════════════════════════════════════
   * THE COLUMNS ARE THEIR OWN INPUTS NOW — Q, 5 Sep 2026.
   *
   * They derived from SURFACE and CANVAS, which made one picker move fifteen
   * things: the nav column, the list column, every card, field, sheet, tab,
   * row and the received bubble. Q: "surface should stay the way it is but it
   * shouldn't control lists", and "each one of the 3 design columns should get
   * its own canvas". Measured before changing it — Surface moved 15 tokens and
   * the three columns could never move together, because main was on CANVAS
   * and the other two on SURFACE.
   *
   * THE FALLBACK IS THE MIGRATION. `var(--sq-in-navSurface, var(--sq-in-surface))`
   * uses the new input when a theme sets one and the old one when it does not,
   * so every theme stored before today — and the ratified palette — renders
   * exactly as it did, with nothing to backfill and no schema version to check.
   */
  --sq-surface-nav: var(--sq-in-navSurface, var(--sq-in-surface));
  --sq-surface-list: var(--sq-in-listSurface, var(--sq-in-surface));
  --sq-surface-main: var(--sq-in-mainCanvas, var(--sq-in-canvas));

  /* Actions and tabs as surfaces (Q, 5 Sep). Derived from the rungs they sit
     on today, so a theme moves them with the ladder and an override can lift
     either off it. The inks follow the global ladder until a palette needs
     otherwise — action.primary's own ink is the server-computed one, because
     it is the one surface in the app that takes DARK ink. */
  /*
   * THE FILLED CONTROL'S EDGE — the frameless default, which is the fill itself.
   *
   * `theme-light.mjs` requires every token the light block overrides to be derived here too, and it
   * is right: a custom theme that leaves this at the dark value would draw the dark accent's edge in
   * a pale room. CSS cannot ask whether the fill separates from its surroundings, so the derivation
   * is the frameless answer — an edge the same colour as the fill, which draws as no edge — and
   * `theme-palette.js` moves it to a visible tone for an applied theme when the fill cannot identify
   * the control. Same division as the reaction-chip roles: CSS states the safe default, the palette
   * does the arithmetic.
   */
  --sq-action-primary-edge: var(--sq-action-primary);
  /* JS resolves these accent text roles against their actual grounds. Until palette
     prepaint runs, use body ink rather than an unreadable raw action fill. */
  --sq-action-primary-text: var(--sq-in-text);
  --sq-accent-text: var(--sq-in-text);
  --sq-signal-text: var(--sq-in-text);
  --sq-action-softBg: var(--sq-in-surface);
  --sq-action-softInk: var(--sq-in-text);
  --sq-action-border: color-mix(in srgb, var(--sq-in-border) 87%, var(--sq-in-text));
  --sq-action-activeBg: color-mix(in srgb, var(--sq-in-surface) 95%, var(--sq-in-text));
  --sq-action-disabledInk: color-mix(in srgb, var(--sq-in-text) 42%, var(--sq-in-canvas));
  --sq-tab-bg: var(--sq-in-surface);
  --sq-tab-selectedBg: color-mix(in srgb, var(--sq-in-surface) 95%, var(--sq-in-text));
  --sq-tab-border: var(--sq-in-border);
  --sq-tab-ink: color-mix(in srgb, var(--sq-in-text) 80%, var(--sq-in-canvas));
  --sq-tab-inkSelected: var(--sq-in-text);
  --sq-tab-underline: var(--sq-in-accent);

  /*
   * THE FILTER PILLS — Q, 5 Sep 2026: "filter pills has no control over them".
   *
   * All/Unread/Crews/DMs/Pinned/Muted, and the same strip on Quests and the shared views.
   * They painted `var(--sq-ground)` — the column they sit on — so there was nothing to move:
   * a filter row could only ever be the colour of its list. They are a CONTROL, like a tab,
   * and get the same treatment: a ground, a selected ground, ink for both, and a hairline.
   *
   * Selected defaults to the ACTION colour because that is what the strip does today — the
   * chosen filter is a lime pill — and its ink to the action's computed ink, so a theme that
   * moves the action keeps readable text on it without anybody choosing a colour.
   */
  --sq-filter-bg: var(--sq-in-listSurface, var(--sq-in-surface));
  --sq-filter-ink: var(--sq-action-softInk);
  --sq-filter-border: var(--sq-in-border);
  --sq-filter-selectedBg: var(--sq-in-action);
  --sq-filter-inkSelected: var(--sq-action-primary-onColor);
  --sq-voice-wave-unplayed: color-mix(in srgb, var(--sq-in-text) 48%, var(--sq-in-surface));
  --sq-voice-wave-played: var(--sq-in-accent);

  /* The remaining components. Each derives from the rung it sits on today, so
     a theme moves them with the ladder and an override lifts one off it. */
  --sq-card-bg: var(--sq-in-surface);
  /*
   * A CARD'S BORDER IS A HAIRLINE, AND HAIRLINES FOLLOW THE BORDER INPUT — 5 Sep 2026.
   *
   * This derived a step off the SURFACE, giving #1E1E28, while every card in app.css draws
   * `1px solid var(--sq-border-subtle)` at #2A2A38. So the token disagreed with the product by
   * a visible amount, and repointing the rules onto it would have redrawn every card edge —
   * which is why the wiring pass skipped it and left the token painted nowhere.
   *
   * The rules were right and the derivation was invented. Cards now follow the border input,
   * like every other hairline, so the token means what the app already does and a theme moves
   * card edges by moving borders.
   */
  --sq-card-border: var(--sq-in-border);
  --sq-card-ink: var(--sq-in-text);
  /* What sits INSIDE a card — the status pill, the chip, the score — shared by Quest and
     Radar so a theme that moves cards moves the things on them too. */
  --sq-card-innerBg: color-mix(in srgb, var(--sq-in-surface) 95%, var(--sq-in-text));
  --sq-card-innerBorder: var(--sq-in-border);
  --sq-card-innerInk: color-mix(in srgb, var(--sq-in-text) 80%, var(--sq-in-canvas));
  --sq-field-bg: color-mix(in srgb, var(--sq-in-surface) 95%, var(--sq-in-text));
  --sq-field-border: var(--sq-in-border);
  --sq-field-ink: var(--sq-in-text);
  --sq-bubble-in-bg: color-mix(in srgb, var(--sq-in-surface) 95%, var(--sq-in-text));
  --sq-bubble-in-ink: var(--sq-in-text);
  --sq-bubble-in-metadataInk: color-mix(in srgb, var(--sq-bubble-in-ink) 60%, var(--sq-bubble-in-bg));
  --sq-bubble-in-readInk: var(--sq-bubble-in-ink);
  /*
   * THE SENT BUBBLE LEAVES THE BRAND — Q, 5 Sep 2026: "brand should not impact
   * sent or recording state". Brand moved seven tokens together, so recolouring
   * an avatar recoloured every sent bubble. Defaults to the brand, so the 31 Aug
   * ruling still describes an untouched theme: the sent bubble IS the brand at
   * full strength until somebody says otherwise.
   */
  --sq-bubble-out-bg: var(--sq-in-bubbleOut, var(--sq-in-brand));
  /* The server-computed ink for the brand, because the bubble IS the brand at
     full strength under a theme — the case text.primary measured 4.01:1 on. */
  --sq-bubble-out-ink: var(--sq-in-bubbleOut-ink, var(--sq-in-brand-ink));
  /* Small metadata uses a readable ink, never a fixed-alpha version of it. */
  --sq-bubble-out-metadataInk: color-mix(in srgb, var(--sq-bubble-out-ink) 60%, var(--sq-bubble-out-bg));
  --sq-bubble-out-readInk: var(--sq-bubble-out-ink);
  --sq-attachment-bg: var(--sq-in-surface);
  --sq-attachment-border: var(--sq-in-border);
  --sq-attachment-ink: color-mix(in srgb, var(--sq-in-text) 80%, var(--sq-in-canvas));

  /* Embedded content belongs to its message ground. Media pixels remain untouched;
   * only its plate, copy and decorative border follow this pair. Explicit theme
   * overrides still win through the normal inline custom-property contract. */
  /*
   * MIXED TOWARD THE BUBBLE'S INK, NOT TOWARD WHITE — 6 Sep 2026.
   *
   * These lifted the attachment card off its bubble with `92%, white`, which is the one thing
   * this file's own header rules out: "NOTHING here mixes toward black or white except the
   * page, which recedes in both." Toward white the card lifts in a dark theme and washes OUT
   * in a light one, so the same rule reads as depth on one polarity and as a smudge on the
   * other. Mixing toward the bubble's own ink lifts in both, and it is what the sibling
   * border rule below already does.
   */
  --sq-bubble-in-attachmentBg: color-mix(in srgb, var(--sq-bubble-in-bg) 92%, var(--sq-bubble-in-ink));
  --sq-bubble-in-attachmentBorder: color-mix(in srgb, var(--sq-bubble-in-ink) 24%, var(--sq-bubble-in-bg));
  --sq-bubble-in-attachmentInk: var(--sq-bubble-in-ink);
  --sq-bubble-out-attachmentBg: color-mix(in srgb, var(--sq-bubble-out-bg) 92%, var(--sq-bubble-out-ink));
  --sq-bubble-out-attachmentBorder: color-mix(in srgb, var(--sq-bubble-out-ink) 24%, var(--sq-bubble-out-bg));
  --sq-bubble-out-attachmentInk: var(--sq-bubble-out-ink);

  /*
   * A REACTION CHIP IS A PLATE INSIDE A BUBBLE, so it follows the attachment rule above rather
   * than the app's accent. The drawing paints the viewer's chip in the accent, which is
   * unreadable in every theme that also paints the sent bubble with its accent — see the long
   * note in theme-palette.js, which derives the same six roles with a contrast-checked ink for
   * applied themes. These are the built-in themes' fallback and the ratios they produce are
   * asserted by test/message-contrast.mjs, so `mine` cannot drift back onto the accent here
   * without that suite saying so.
   */
  --sq-bubble-in-reactionBg: color-mix(in srgb, var(--sq-bubble-in-ink) 10%, var(--sq-bubble-in-bg));
  --sq-bubble-in-reactionBorder: color-mix(in srgb, var(--sq-bubble-in-ink) 20%, var(--sq-bubble-in-bg));
  --sq-bubble-in-reactionInk: var(--sq-bubble-in-ink);
  --sq-bubble-in-reactionMineBg: color-mix(in srgb, var(--sq-bubble-in-ink) 22%, var(--sq-bubble-in-bg));
  --sq-bubble-in-reactionMineBorder: color-mix(in srgb, var(--sq-bubble-in-ink) 45%, var(--sq-bubble-in-bg));
  --sq-bubble-in-reactionMineInk: var(--sq-bubble-in-ink);
  --sq-bubble-out-reactionBg: color-mix(in srgb, var(--sq-bubble-out-ink) 10%, var(--sq-bubble-out-bg));
  --sq-bubble-out-reactionBorder: color-mix(in srgb, var(--sq-bubble-out-ink) 20%, var(--sq-bubble-out-bg));
  --sq-bubble-out-reactionInk: var(--sq-bubble-out-ink);
  --sq-bubble-out-reactionMineBg: color-mix(in srgb, var(--sq-bubble-out-ink) 22%, var(--sq-bubble-out-bg));
  --sq-bubble-out-reactionMineBorder: color-mix(in srgb, var(--sq-bubble-out-ink) 45%, var(--sq-bubble-out-bg));
  --sq-bubble-out-reactionMineInk: var(--sq-bubble-out-ink);
  --sq-sheet-bg: var(--sq-in-surface);
  --sq-sheet-border: var(--sq-in-border);
  --sq-sheet-ink: var(--sq-in-text);
  /*
   * A ROW FOLLOWS ITS COLUMN, NOT THE GENERAL SURFACE — Q, 5 Sep 2026:
   * "lists colour is not reflecting across any list".
   *
   * This followed SURFACE while the column it sits in followed listSurface, so moving the
   * List picker repainted the strip BEHIND the rows and the rows covered it. Every row in
   * every list is the list, as far as anyone looking at it is concerned.
   *
   * It still falls back through listSurface to surface, so a theme that sets neither is
   * unchanged.
   */
  --sq-list-rowBg: var(--sq-in-listSurface, var(--sq-in-surface));

  /* ── the ink ladder ──────────────────────────────────────────────────── */
  --sq-text-primary: var(--sq-in-text);
  --sq-text-secondary: color-mix(in srgb, var(--sq-in-text) 80%, var(--sq-in-canvas));
  --sq-text-tertiary: color-mix(in srgb, var(--sq-in-text) 60%, var(--sq-in-canvas));
  --sq-text-disabled: color-mix(in srgb, var(--sq-in-text) 42%, var(--sq-in-canvas));
  --sq-text-dim: color-mix(in srgb, var(--sq-in-text) 32%, var(--sq-in-canvas));

  /* ── hairlines ───────────────────────────────────────────────────────── */
  --sq-border-subtle: var(--sq-in-border);
  /* Same correction: dark's pair is #2A2A38 and #454558, a 13% step toward
     the ink. 45% gave #86868E — a hairline that reads as a rule. */
  --sq-border-strong: color-mix(in srgb, var(--sq-in-border) 87%, var(--sq-in-text));

  /* ── brand ───────────────────────────────────────────────────────────── */
  --sq-brand-purple: var(--sq-in-brand);
  /*
   * ═══════════════════════════════════════════════════════════════════════
   * THE IDENTITY TOKENS MOVE HERE, AND THAT IS THE ONLY WAY TO REACH THE
   * CONVERSATION SCREENS AT ALL.
   *
   * Those screens are drawn from compiled frames as INLINE STYLES, and an
   * inline style outranks every stylesheet — so nothing written in app.css
   * can repaint a message bubble or a read tick. What the frames DO write is
   * `var(--sq-brand-teal)` and `var(--sq-brand-purpleTint)`, 516 and 56 times
   * respectively, and a custom property IS reachable: redefine the token and
   * every inline reference to it follows.
   *
   * Three findings drove this, each reported as "X is not picked up":
   *
   *  · THE SENT BUBBLE is `brand.purpleTint` in the frames, never
   *    `chat.outgoing` — the two tokens hold the identical #211B3B and the
   *    hex→token pass picked the other one. 42 `stack` parts across
   *    CHAT-001..007. Moving `chat.outgoing` alone moved nothing anybody
   *    could see.
   *  · THE BLUE is `brand.teal`, and it was not in the palette a theme could
   *    author at all. Q, 31 Aug 2026: "the blue color is not part of the
   *    theme.. it needs to be part of across the whole app."
   *  · THE PINK is the attention colour and now follows the signal slot. Q,
   *    same day: "the pink should follow the signal color."
   * ═══════════════════════════════════════════════════════════════════════
   */
  --sq-brand-pink: var(--sq-in-signal);
  --sq-brand-teal: var(--sq-in-accent);
  /* The ink for an accent PLATE — the accent is a line or a glyph almost
     everywhere, but a filled chip needs to know what to write on it, and the
     answer is the same WCAG comparison the action and signal colours use. */
  --sq-brand-teal-onColor: var(--sq-in-accent-ink);
  /* Focus is its own ROLE that happens to share teal's value in the ratified
     palette. Both move with the accent, which keeps them equal — a theme that
     wanted to split them would be answering a question nobody has asked. */
  --sq-focus: var(--sq-in-accent);
  /* Recording is the brand colour by rule, not by coincidence — the palette
     has said so since the ratified set, and a theme must not split them
     silently. */
  /* Recording leaves the brand with the sent bubble, same ruling, same default.
     R-11 still holds: recording is neither error nor success, so it aliases a
     brand-ish hue — it just no longer MUST be the brand. */
  --sq-state-recording: var(--sq-in-recording, var(--sq-in-brand));
  /* 18% of the brand over the ground. The ratified palette derives both of
     these the same way and to the byte; keeping the number here means a
     themed outgoing bubble sits at the same strength as the shipped one. */
  /*
   * NOT A TINT ANY MORE, because in the frames this token IS the sent bubble.
   * Q: "sent bubbles colors should be follow the same color from brand... not
   * light or darker", then "the sent bubble is not picked.. to be the brand
   * color without opacity". Full strength, exactly as asked.
   *
   * The consequence is real and the admin states it: an 18% tint never had to
   * carry text and a mid-strength brand colour does. The frames give the
   * bubble no colour of its own — it inherits --sq-text-primary — so the
   * contrast table has a "Text on the sent bubble" row and it is the only
   * warning before somebody publishes a theme whose own messages cannot be
   * read. Reported, not enforced: that is the rule everywhere in this tool.
   */
  --sq-brand-purpleTint: var(--sq-in-brand);
  /* The ink for that ground, chosen by the server from the brand's own
     luminance. app.css writes it on the brand-coloured surfaces it can reach;
     the frame-drawn bubble inherits --sq-text-primary and is what the
     contrast table warns about. */
  --sq-brand-onColor: var(--sq-in-brand-ink);
  /*
   * THE SENT BUBBLE IS THE BRAND COLOUR, FULL STRENGTH. Q, 31 Aug 2026: "sent
   * bubbles colors should be follow the same color from brand... not light or
   * darker". The ratified palette tints it to 18% over the canvas, which is a
   * legible near-black purple and is not what a person choosing a brand colour
   * expects their own messages to look like.
   *
   * So this is the one rule in the file that departs from the shipped value on
   * purpose rather than within tolerance, and it is why `--sq-brand-purpleTint`
   * directly above still tints: that one is a wash BEHIND other things, and
   * this one is the thing itself. The admin's contrast table gained a "Text on
   * the sent bubble" row with this change, because a mid-strength brand colour
   * now has to carry text and an 18% tint never did.
   */
  /* The same surface under its older name — the frames still paint it. */
  --sq-chat-outgoing: var(--sq-in-bubbleOut, var(--sq-in-brand));

  /* ── the action colour, and the ink written on it ────────────────────── */
  --sq-action-primary: var(--sq-in-action);
  --sq-action-primary-onColor: var(--sq-in-action-ink);

  /*
   * ── the signal colour: "look at this", not "press this" ────────────────
   * The seventh input. Q, 31 Aug 2026: "notifications badges, live, happening
   * now badge should have a different color from the action buttons.. so make
   * them 7 colors and the 7th color would control these". An unread count and
   * a Send button are not the same instruction, and until now they wore the
   * same lime. Ships at the action colour's value in both builtins, so the
   * split changed nothing on screen; a theme moves them apart.
   */
  --sq-signal: var(--sq-in-signal);
  --sq-signal-onColor: var(--sq-in-signal-ink);

  /*
   * ── THE FOUR SEMANTIC HUES, WHICH A THEME COULD NOT REACH UNTIL NOW ─────
   *
   * Q, 4 Sep 2026, ruling on the atlas measurement: semantic yes, severity no.
   *
   * `infra/build_token_atlas.mjs` asks, for every colour the product paints,
   * whether a theme can move it. These four answered NO while being painted
   * 1,705 times between them — warning 756, error 499, success 298, info 152.
   * So a theme repainted the app and left every banner, failed-send mark,
   * delivered tick and warning strip in the shipped red, amber, green and
   * blue: half-applied by construction, and indistinguishable from a bug.
   *
   * They are now the last four inputs. Nothing moves today — the admin ships
   * them at their ratified values in BOTH modes, because unlike the pink and
   * the teal there has never been a light-mode override for them anywhere in
   * app.css, so the ratified value IS what a light theme renders.
   *
   * The `*Text` variants below need no change: they already mix from these
   * bases, so they follow a moved hue automatically, and they keep
   * self-correcting for paper because they mix toward `--sq-in-text`.
   *
   * THE ADMIN SEVERITY SCALE IS NOT HERE, deliberately. UIC 29.14 was
   * rewritten in August to scope it to the admin and forbid consumer surfaces
   * from naming it; a user theme moving it would re-couple exactly what that
   * rewrite separated.
   */
  --sq-semantic-success: var(--sq-in-success);
  --sq-semantic-warning: var(--sq-in-warning);
  --sq-semantic-error: var(--sq-in-error);
  --sq-semantic-info: var(--sq-in-info);

  /* ── semantic text and grounds ─────────────────────────────────────────
     EVERY PERCENTAGE BELOW WAS SOLVED, NOT CHOSEN. A `*Text` token is the
     base hue lightened until it reads on a dark ground, and the ratified
     palette already contains the answer for one theme: given the base, the
     ink and the shipped result, there is exactly one mix that lands on it.
     So each of these is p from `p·base + (1−p)·ink = shipped`, which makes
     the derivation reproduce the dark palette rather than approximate it,
     and makes the same rule sensible when the ink is near-black instead.
     test/theme-light.mjs recomputes all of them and fails past 20 units. */
  --sq-semantic-successText: color-mix(in srgb, var(--sq-semantic-success) 70%, var(--sq-in-text));
  --sq-semantic-warningText: color-mix(in srgb, var(--sq-semantic-warning) 54%, var(--sq-in-text));
  --sq-semantic-errorText: color-mix(in srgb, var(--sq-semantic-error) 67%, var(--sq-in-text));
  --sq-semantic-infoText: color-mix(in srgb, var(--sq-semantic-info) 66%, var(--sq-in-text));

  /* ── admin severity: scale kept, grounds and text re-aimed ───────────── */
  --sq-severity-critical-text: color-mix(in srgb, var(--sq-severity-critical-base) 59%, var(--sq-in-text));
  --sq-severity-high-text: color-mix(in srgb, var(--sq-severity-high-base) 53%, var(--sq-in-text));
  --sq-severity-medium-text: color-mix(in srgb, var(--sq-severity-medium-base) 47%, var(--sq-in-text));
  --sq-severity-low-text: color-mix(in srgb, var(--sq-severity-low-base) 66%, var(--sq-in-text));
  --sq-severity-informational-text: color-mix(in srgb, var(--sq-severity-informational-base) 42%, var(--sq-in-text));
  --sq-severity-disabled-text: color-mix(in srgb, var(--sq-severity-disabled-base) 66%, var(--sq-in-text));
  --sq-severity-critical-soft: color-mix(in srgb, var(--sq-severity-critical-base) 14%, var(--sq-in-canvas));
  --sq-severity-high-soft: color-mix(in srgb, var(--sq-severity-high-base) 14%, var(--sq-in-canvas));
  --sq-severity-medium-soft: color-mix(in srgb, var(--sq-severity-medium-base) 16%, var(--sq-in-canvas));
  --sq-severity-low-soft: color-mix(in srgb, var(--sq-severity-low-base) 14%, var(--sq-in-canvas));
  --sq-severity-informational-soft: color-mix(in srgb, var(--sq-severity-informational-base) 16%, var(--sq-in-canvas));
  --sq-severity-disabled-soft: color-mix(in srgb, var(--sq-severity-disabled-base) 14%, var(--sq-in-canvas));
}
