diff --git a/docs/features/theming.md b/docs/features/theming.md index 95ef774f9..25a4aa800 100644 --- a/docs/features/theming.md +++ b/docs/features/theming.md @@ -1,56 +1,201 @@ # Theming — the token file, the layering, and the "born grey" fix -2026-07-24 theme-defaults pass. Fixes the recurring complaint that new/ -un-opted-in Bootstrap components (Modal, Dropdown, Popover, Toast, the plain -`Card.Header` cap, `Offcanvas`, table-head/table-dark, the `Form.Select` -chevron) rendered as flat Superhero-stock grey instead of the site's own dark -navy chrome, by giving every Bootstrap variable override ONE canonical source -instead of a scattered literal. +Two rounds live in this doc: the 2026-07-24 **theme-defaults pass** (fixed +"born grey" Bootstrap components by giving every override one canonical +token source) and the 2026-07-24 **Tokyo-11 re-theme** (swapped the palette +itself — this is the token file's first real "retheme the site" exercise, +and the section below on what that surfaced supersedes anything the +theme-defaults pass implied about how easy a repaint would be). ## The token file [`frontend/src/styles/_theme-tokens.scss`](../../frontend/src/styles/_theme-tokens.scss) -is the single source of truth for the site's palette and corner-radius -tiers. It defines plain SCSS variables (`$theme-*`) with no Bootstrap -dependency — nothing in this file imports Bootstrap or Bootswatch. Every -value is the SPEC-display-left-rail.md §D.0 binding palette (itself the -issue #302 palette), reproduced verbatim. +is the single source of truth for the site's palette, corner-radius, +spacing, and type tokens. It defines plain SCSS variables (`$theme-*`) with +no Bootstrap dependency — nothing in this file imports Bootstrap or +Bootswatch — plus (added by the Tokyo-11 pass) a `:root { --theme-*: ...; }` +block that re-exposes the same values as CSS custom properties for +styled-components/inline-style call sites that can't reach a SCSS variable +(see "Runtime CSS-custom-property bridge" below). + +**Current palette: "Tokyo-11"** (2026-07-24, owner ruling on the +theme-options palette-exploration study — 12 candidate palettes compared +under a strict-AAA contrast bar). Palette 11, "Tokyo × orange-action + +purple-accent": a Tokyo Night dark-navy base, a warm orange action colour, +and an additive purple accent layer, with Semi (6/8/10px) corner radii. This +supersedes the #302/`SPEC-display-left-rail.md` §D.0 palette the token file +previously reproduced verbatim — see that spec's own §D.0 header for a +pointer back to this file instead of a line-by-line color sync going +forward (its sizing/spacing rows are untouched and remain authoritative). Colour tokens (darkest → most-raised): -| Token | Value | What it's for | -| ----------------------- | --------- | ------------------------------------------------------------------------------------------------ | -| `$theme-body-bg` | `#0f2537` | page background, the darkest layer | -| `$theme-raised-bg` | `#22303f` | floating chrome one step off the page — rail-head/artist-line, the Sources list + filter inputs | -| `$theme-panel-bg` | `#4e5d6c` | Superhero's native `$secondary`/`$gray-600` — Card bodies, D14 seticon, secondary buttons/badges | -| `$theme-card-header-bg` | `#4e5d6b` | one hex digit off `$theme-panel-bg`, **by design** (owner ruling, 2026-07-23) | -| `$theme-band-bg` | `#2b3e50` | the D14 confidence-strip token | -| `$theme-divider` | `#16202b` | every rail block boundary | -| `$theme-text` | `#ebebeb` | body text | -| `$theme-muted` | `#8fa0b0` | muted text/placeholder | -| `$theme-light` | `#abb6c2` | `btn-outline-light` family, the form-select indicator chevron | -| `$theme-primary` | `#df6919` | primary accent (Superhero-native) | -| `$theme-primary-hover` | `#be5915` | primary accent hover | -| `$theme-success` | `#5cb85c` | success | -| `$theme-danger` | `#d9534f` | danger | -| `$theme-warning` | `#ffc107` | warning | -| `$theme-info` | `#5bc0de` | info | -| `$theme-input-border` | `#4e5d6c` | input border colour | - -Corner-radius tokens (added by the owner's same-round extension — -"a future 'rounded buttons' ruling should be a token change like -everything else"): - -| Token | Value | What it's for | -| -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `$theme-radius-none` | `0` | no rounding, ever — kept distinct from `-base` so a future `-base` bump can't accidentally round something that must stay square | -| `$theme-radius-sm` | `0` | Bootstrap's `-sm` tier (small buttons/inputs) | -| `$theme-radius-base` | `0` | the sitewide default tier (buttons/cards/inputs/`-lg`) | -| `$theme-radius-pill` | `10px` | our OWN "status-pill" component convention (D14 statepill, etc) — see the "two different pills" note below; NOT wired into Bootstrap's `$border-radius-pill` | - -Both token families currently equal what the site already ships (flat `0`, -plus the existing `10px` pill exception) — introducing them changed **zero -rendered pixels**; see Verification below. +| Token | Value | What it's for | +| ----------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `$theme-body-bg` | `#1a1b26` | page background, the darkest layer | +| `$theme-raised-bg` | `#24283b` | floating chrome one step off the page — rail-head/artist-line, the Sources list + filter inputs | +| `$theme-panel-bg` | `#2f3549` | Card bodies (incl. `AutofillCollapse`'s "demoted body"), the D14 seticon, secondary buttons/badges (`$secondary`) | +| `$theme-card-header-bg` | `#2f3548` | one hex digit off `$theme-panel-bg`, mechanically preserving the #302 "deliberately distinct" pattern (see below) | +| `$theme-band-bg` | `#222234` | the D14 confidence-strip token ("conf" in the study) | +| `$theme-divider` | `#16161e` | every rail block boundary | +| `$theme-text` | `#c0caf5` | body text — 7.54:1 on panel, 9.02:1 on raised (STRICT-AAA both) | +| `$theme-muted` | `#a3aad0` | muted text/placeholder — WCAG/APCA audit remediation (2026-07-24, PR #432); see "Checked WCAG 2.2 criteria" below | +| `$theme-light` | = `$theme-text` | not an audited study token — aliased to text, see the token file's own comment for why | +| `$theme-primary` | `#ff9e64` | action colour — every button (8.40:1 STRICT-AAA with the flipped ink) | +| `$theme-primary-hover` | `darken($theme-primary, 8%)` | derived by formula this round, not a hand-picked literal | +| `$theme-success` | `#9ece6a` | success (9.35:1 STRICT-AAA) | +| `$theme-danger` | `#f7768e` | danger (6.46:1 — AAA-LARGE only; see its own token-file comment) | +| `$theme-warning` | `#e0af68` | warning (8.55:1 STRICT-AAA) | +| `$theme-info` | `#7dcfff` | NOT a study-audited token — Tokyo Night's own cyan swatch, chosen to keep the pre-existing small "link" text AAA-safe | +| `$theme-btn-ink` | `#1a1b26` | the "btn-ink" study token — the one dark ink used on every button variant | +| `$theme-accent` | `#bb9af7` | additive, UI-role-only — see "Accent scope boundary" below | +| `$theme-input-border` | = `$theme-panel-bg` | now a reference, not a coincidentally-equal literal | + +Corner-radius tokens — **"Semi"** tier (2026-07-24 Tokyo-11 ruling; was +"Flat"/`0` under the #302 palette): + +| Token | Value | What it's for | +| -------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- | +| `$theme-radius-none` | `0` | no rounding, ever — kept distinct from `-base` | +| `$theme-radius-sm` | `6px` | Bootstrap's `-sm` tier (small buttons/inputs) | +| `$theme-radius-base` | `6px` | the sitewide default tier — buttons/inputs, including their own `-lg` variants (the study's table doesn't grow buttons past 6px) | +| `$theme-radius-card` | `8px` | **new this round** — cards/modals/popovers/dropdowns, wired to `$border-radius-lg` | +| `$theme-radius-pill` | `10px` | our own "status-pill" component convention (D14 statepill, etc) — unchanged; see "two different pills" below | + +Both radius families changed from `0`/flat to the values above this round — +see Verification below for how that was checked (this is NOT a +zero-rendered-pixels pass the way the original theme-defaults round was; +radii visibly changed everywhere Bootstrap's own radius vars reach). + +### Spacing scale (2026-07-24 owner extension) + +A 4/8-based ladder — `$theme-space-1` (4px) through `$theme-space-6` +(32px), also exposed as `--theme-space-1`…`-6`. **Future-specs-only**: this +scale governs new spec rounds going forward. It does **not** retroactively +re-space any shipped surface — no component was touched to make an existing +value land on it. Some existing values are off-scale and stay that way +until their own spec is next revised for an unrelated reason: + +| Surface | Shipped value | On the 4/8 ladder? | Owning spec row | +| ------------------------- | ------------- | --------------------------------- | --------------------------------------------- | +| D14 band padding | `8px 10px` | `8px` yes, `10px` no | `SPEC-rail-delegacy.md` §D.1 `.d14` | +| Rail-head padding | `8px 10px` | `8px` yes, `10px` no | `SPEC-rail-delegacy.md` §D.1 `.rail-head` | +| Filters float panel width | `440px` | n/a (a width, not a spacing step) | `SPEC-rail-delegacy.md` §D (Filters panel) | +| `.notthis` pill padding | `2px 10px` | `2px`/`10px` no | `SPEC-editor-polish.md` §D.2 `.notthis` (EP8) | + +### Type ramp (2026-07-24 owner extension) + +`$theme-font-2xs` (10px) through `$theme-font-xl` (18px) — same +future-specs-only rule as spacing. Several existing rail/editor microtype +sizes (10/11/12/13/14/15px) already happen to land on these exact steps — +that's pre-existing coincidence in the shipped `SPEC-rail-delegacy.md`/ +`SPEC-editor-polish.md` values, not this pass re-specing anything to match. + +## Provenance convention (added this round — the doc had none before) + +**Finding from this being the doc's first real "retheme the site" +exercise**: the pre-existing instructions here said nothing about how to +record a palette's licensing provenance when swapping it — the task that +drove the Tokyo-11 pass had to invent the convention below rather than +follow one. Fixed in place: + +Whenever `_theme-tokens.scss`'s palette values change, the file's own +top-of-file header comment must carry a **PROVENANCE** line: the palette's +name, license, copyright holder, and an explicit "color values only, no +code" statement, matching this fork's [external code provenance +policy](../../CLAUDE.md) (values may be freely referenced; code reuse needs +a bounded-absorption path, never applies to a handful of hex constants +anyway). Any token pulling from a source OUTSIDE the named palette itself +(this round's `$theme-info`, drawn from the same upstream project's cyan +swatch but not one of the study's own 10 audited tokens) gets its own +inline comment explaining the deviation and its separate provenance +justification, right at that variable — not just in the file header. + +## Accent scope boundary (new token this round) + +`$theme-accent` is **additive** and **UI-role-only**. It is wired to: + +- the D14 confidence pill (`.statepill.suggested`) and score badge + (`.seticon .score`) +- the Sources list toggle's ON state (`.rail-source-toggle .toggle-on`) +- version-selection outlines (`.mpccard-highlight`, used by both + `SelectVersionResults.tsx`'s grid and `CardResultSet.tsx`'s search-result + highlight) +- the print sheet's selected-slot outline (`PagePreview.tsx`) + +It is **never** applied to small paragraph/body text — the accent purple +clears only 6.74:1 on the D14 band surface (below strict-AAA-normal's 7:1), +"near-strict" per the study, legible on bold pill text/borders/outlines but +not prose. Bootstrap's global `$link-color` is **deliberately not** routed +to accent — it stays on `$primary` (orange), matching the pre-existing +convention several components already use `var(--bs-primary)` for +link-styled text (`Navbar.tsx`, `Footer.tsx`, `AuthWidget.tsx`). The +handful of small cyan "info" links elsewhere (`DisplayPage.tsx`'s identify +panel, `SharedDeckViewer.tsx`) stay on `$theme-info` instead, precisely +because they're small text and accent isn't AAA-safe there — see +`$theme-info`'s own token-file comment. + +## Runtime CSS-custom-property bridge (added this round) + +Two independent, parallel bridges expose token values to code that can't +read a SCSS `$theme-*` variable directly (styled-components/emotion +template literals, inline `style={}` props): + +1. **`var(--bs-*)`** — Bootstrap 5.3's own `_root.scss` (imported via the + Superhero/Bootstrap core chain below) automatically emits every + `$theme-colors` entry (`$primary`/`$secondary`/`$success`/`$info`/ + `$warning`/`$danger`), `$body-bg`/`$body-color`, and the + `$border-*`/`$border-radius*` family as `--bs-*` custom properties, all + generated FROM the SCSS variables `styles.scss` sets from tokens. As + soon as `styles.scss` assigns `$primary: $theme-primary` (etc — see that + file's own "Tokyo-11: $theme-colors overrides" comment), every + `var(--bs-primary)` consumer picks up the new value automatically, with + zero additional wiring — this is true for `Navbar.tsx`/`Footer.tsx`/ + `AuthWidget.tsx`'s existing links AND for every react-bootstrap + component's own internals (Bootstrap 5.3's `button-variant()` mixin + itself emits `--bs-btn-bg`/`--bs-btn-color`/etc scoped per `.btn-*` + rule, then reads them back via `var()` — so `.btn-primary`'s actual + paint IS already runtime-var-driven, not a compiled-in literal). +2. **`var(--theme-*)`** — `_theme-tokens.scss`'s own `:root` block (see + that file) for the tokens Bootstrap has no equivalent for: raised-bg, + card-header-bg, band-bg, divider, muted, light, accent (+ its `-rgb` + triplet), btn-ink, primary-hover, the radius-card/spacing tokens. + Deliberately NOT duplicated here even though a component-friendly name + would be nice: `--bs-body-bg`/`--bs-body-color` (+`-rgb`)/`--bs-primary` + (+`-rgb`)/`--bs-secondary` (+`-rgb`, this literally IS + `$theme-panel-bg`)/`--bs-success`/`--bs-danger`/`--bs-warning`/ + `--bs-info` (each +`-rgb`) already exist via bridge 1 — a second + `--theme-*` copy of any of those would be a second source of truth for + the same value, exactly what this bridge exists to prevent. Component + code reaches for `var(--bs-secondary)` (not a hypothetical + `--theme-panel-bg`) for the panel role, `var(--bs-body-color-rgb)` (not + a hypothetical `--theme-text-rgb`) for text-tinted translucent borders, + and so on. + +**Scope discipline**: this is a bridge, not a migration. It makes the +_existing_ `--bs-*` surface Bootstrap already emits reachable and correct, +and adds the _missing_ `--theme-*` surface for tokens with no Bootstrap +equivalent — it does not convert every component's compiled SCSS/literal +CSS onto `var()` wholesale. Concretely: + +- Component-local styled-components/inline styles compile to literal + colour values UNLESS they explicitly reference `var(--bs-*)` or + `var(--theme-*)` (this pass converted the surfaces enumerated in "What + this pass swept," below — not every hex literal that ever existed + sitewide). +- **Known potential-divergence surface, flagged rather than converted**: + `custom.css`'s `.mpccard-highlight`/`.mpccard-hover` box-shadow glow uses + `rgba(var(--theme-accent-rgb), .6)`/`rgba(var(--bs-body-color-rgb), .3)` — + runtime-var-driven. If a future change ever needs those two effects to + differ in intensity or hue from the raw accent/text tokens (e.g. a + "selected" glow that should stay orange even after an accent-repainting + re-theme), that divergence would have to be an explicit new token, not a + silent literal edit to this file, since the whole point of the bridge is + that editing `_theme-tokens.scss` is enough. +- Bootstrap's OWN internal component CSS (buttons, the border/radius + family) is inherently kept in sync by mechanism 1 above without any + fork-side literal to maintain at all — there is no divergence risk there + by construction. ## The layering @@ -61,8 +206,13 @@ rule throughout: **every Bootstrap variable override assigns from a 1. `@import "theme-tokens";` — pulls in every `$theme-*` variable above. 2. A block of plain SCSS variable assignments (`$dark`, `$input-bg`, - `$card-cap-bg`, `$modal-content-bg`, `$border-radius`, `$btn-border-radius`, - etc.), each set to a `$theme-*` token. + `$card-cap-bg`, `$modal-content-bg`, `$border-radius`, + `$btn-border-radius`, `$primary`/`$secondary`/`$success`/`$danger`/ + `$warning`/`$info`, `$body-bg`/`$body-color`, `$color-contrast-dark`, + `$min-contrast-ratio`, `$focus-ring-color`/`$focus-ring-opacity`/ + `$focus-ring-width`, `$btn-close-width`/`$btn-close-color`, etc.), each + set to a `$theme-*` token (or, for the last two focus-ring inputs, a + literal opacity/width that isn't itself a palette colour). 3. `@import "~bootswatch/dist/superhero/variables";` then the Bootstrap core imports. @@ -71,23 +221,107 @@ own defaults with SCSS's `!default` flag — a variable already assigned (step 2) is left alone by every later `!default` assignment downstream, Bootswatch's or Bootstrap core's alike. This is the same mechanism the pre-existing `$dark`/`$input-bg`/`$input-color` overrides (issue #302) always -used; this pass just extends it and gives it one shared source file instead -of inline literals. +used; the theme-defaults pass extended it to the "born grey" components, and +the Tokyo-11 pass extends it again to `$primary`/`$secondary`/`$success`/ +`$danger`/`$warning`/`$info`/`$body-bg`/`$body-color` — under #302 these +happened to already equal Superhero's own stock defaults, so no override was +ever needed; Tokyo-11 diverges from every one of them, surfacing the gap. **To retheme the site**, edit `_theme-tokens.scss` only — every consumer -(the Bootstrap variable overrides in `styles.scss`, and any fidelity spec -asserting one of these values, see below) is meant to pick the new value up -automatically or need a one-line spec update, never a hunt through component -files for a scattered literal. +(the Bootstrap variable overrides in `styles.scss`, the `var(--bs-*)`/ +`var(--theme-*)` runtime bridge, and any fidelity spec asserting one of +these values, see below) is meant to pick the new value up automatically or +need a one-line spec update, never a hunt through component files for a +scattered literal. The Tokyo-11 pass is the first real test of that claim — +see "What this pass swept" below for what still needed a manual per-file +edit versus what really was free. + +## AAA contrast policy (owner-ruled 2026-07-24) + accessibility checklist + +**Contrast**: two thresholds, from the theme-options study's own AAA +policy block, apply to every colour pairing in this palette: + +- **AAA-normal, 7:1** — required for body/paragraph/small text (anything + under ~18px or non-bold). +- **AAA-large, 4.5:1** — the floor for large/bold UI text (buttons, pills, + headings). -## The "born grey" inventory this pass fixed +A pairing is **STRICT-AAA-everywhere** only when it clears 7:1 even on +button/pill text. Tokyo-11 clears strict-AAA on body text (7.54/9.02:1) and +on 3 of 4 button variants (primary 8.40, success 9.35, warning 8.55); danger +(6.46:1 with the mandated `$theme-btn-ink`) is the one AAA-large-only +exception — see `$theme-danger`'s own token-file comment. **APCA is +advisory only** — this fork's binding bar is the WCAG 2.x relative-luminance +ratio above, computed the same way the study computed it (worst-case +surface, sRGB relative luminance); APCA numbers aren't tracked or asserted +anywhere in this codebase today. + +**Checked WCAG 2.2 criteria** (a parallel audit, PR #432's report, is the +enforcement pass for these — this doc states the policy, that audit is where +conformance against it gets measured and tracked; cross-link from here +rather than duplicating its findings). Four concrete fixes from that +report's theme-layer findings were folded into this same pass: + +- **Target size (2.5.8, ≥ 24×24 CSS px)**: + - `.btn-close` (modal/offcanvas/toast dismiss ×, ~15 mounts sitewide) + measured a stock 21×21 — `$btn-close-width` lifted to `25px` in + `styles.scss`, a single theme-layer fix covering every mount. Self- + verified via a real `boundingBox()` read in the fidelity spec (issue + #434's own lesson: authored CSS isn't proof of the rendered size), not + just the authored variable. + - `.fbtoggle .btn` (the rail-head Front/Back segmented toggle, + `DisplayPage.tsx`) measured 51–55×23px, 1px under the floor with no + qualifying spacing exception — owner-ruled amendment (2026-07-24): + `min-height: 24px` added (padding unchanged, so the segment's visual + density doesn't change). Self-verified the same way (real + `boundingBox()`, not authored CSS). +- **Focus visible / non-text contrast (2.4.7/2.4.11/1.4.11, ≥ 3:1)**: + Bootstrap's default focus ring (`rgba($primary, .25)`) measured + 1.18–1.56:1 against this theme's dark surfaces — a 25%-alpha ring's real + contrast depends on what's underneath it, which is exactly how it failed. + Fixed at the single shared `$focus-ring-*` variable family every + focus-visible surface sitewide derives from (`styles.scss`): + `$focus-ring-color: $theme-accent` (not `$primary` — a distinct UI-signal + colour, consistent with accent's UI-role contract) and + `$focus-ring-opacity: 1` (fully opaque, so the ring's contrast is simply + accent-vs-background, not blended with whatever's under a translucent + ring). Accent measures 7.39/6.30/5.26:1 against body/raised/panel — every + one clears 3:1 with real margin even on the toughest surface (panel). + `$focus-ring-width` bumped `0.25rem` (4px) → `2px` for a crisper outline. + Self-verified in the fidelity spec via the real `--bs-focus-ring-color` + custom property Bootstrap emits, computed against panel. +- **Reduced motion**: `@media (prefers-reduced-motion: reduce)` disables + transitions/animations — already the convention this codebase's other + animated surfaces (e.g. `WhatsThatWords.tsx`) follow; this pass didn't + add any new motion. + +**WCAG-vs-APCA divergence note** (same audit): the audit computed +`$theme-muted` (originally `#8c94bf`) at APCA Lc 44.1 on body — adequate +only for large/bold text, but this token backs 10–13px legends/captions +sitewide. Lightened to `#a3aad0` rather than split into a second +"`-strong`" token (most call sites genuinely are small captions, and one +token is simpler to keep in sync than auditing every call site's own +font-weight/size to route it correctly). APCA itself isn't computable in +this repo's toolchain (no APCA library available in this environment), so +the remediation was verified via the WCAG proxy instead: contrast lifted +from 5.78/4.93/4.11 (body/raised/panel) to 7.50/6.39/5.34 — now clears +strict-AAA-normal on body, comes within a hair of it on raised, and clears +AA-normal (4.5) with real margin even on panel. This is the fork's standing +APCA policy going forward: **APCA numbers from an external audit are +treated as directional signal, remediated via the WCAG relative-luminance +formula this codebase can actually compute** — a real APCA library would +need to land in this repo before APCA itself could become a binding, +self-verified metric here. + +## The "born grey" inventory the 2026-07-24 theme-defaults pass fixed Superhero's own `_variables.scss` sets several component defaults straight -to `$gray-600` (`#4e5d6c`) — correct for the "panel" role (`$theme-panel-bg` -is the _same_ value, deliberately), but wrong for surfaces that are -conceptually "raised chrome" floating above the page, which should read -`$theme-raised-bg` (`#22303f`) instead. Nothing had ever routed these away -from the Superhero stock value, so every consumer was "born grey": +to `$gray-600` (`#4e5d6c`, the #302 palette's own panel colour) — correct +for the "panel" role under #302 (`$theme-panel-bg` was the _same_ value, +deliberately), but wrong for surfaces that are conceptually "raised chrome" +floating above the page, which should read `$theme-raised-bg` instead. +Nothing had ever routed these away from the Superhero stock value, so every +consumer was "born grey": | Bootstrap variable | Was | Now | Real surfaces affected | | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | @@ -100,16 +334,21 @@ from the Superhero stock value, so every consumer was "born grey": | `$form-select-indicator-color` | `$gray-800` (Bootstrap core default, near-black) | `$theme-light` | the `` chevron (e.g. Print Options' bleed-override select) — was nearly invisible against the dark `$input-bg` | | the standalone `.offcanvas { background-color: $secondary; }` rule | a literal `$secondary` override, predating Superhero, that had started actively fighting the theme | removed — `$offcanvas-bg-color` now explicitly set to `$theme-body-bg` before the Superhero import, same pattern as the rest | the left/right rail's own outer `Offcanvas` chrome | -**What's grey on purpose — do not "fix" this.** `$theme-panel-bg` (`#4e5d6c`) -is Superhero's native `$secondary`/`$gray-600`, and it's the _correct, -approved_ token for: Bootstrap Card bodies (including -`AutofillCollapse`'s "demoted body" — SPEC-display-left-rail.md §D.1 -explicitly locks this to `#4e5d6c`), the D14 seticon, and `variant="secondary"` -buttons/badges. `$card-bg` itself was deliberately left untouched by this -pass — routing it to `$theme-raised-bg` would have been a **fidelity -regression** against that exact spec-locked row. If a surface still looks -grey after this pass, check whether it's one of these approved-panel -surfaces before assuming it's a leftover default. +**What's grey on purpose — do not "fix" this.** `$theme-panel-bg` +(`#2f3549` under Tokyo-11; was `#4e5d6c`, Superhero's native +`$secondary`/`$gray-600`, under #302) is the _correct, approved_ token for: +Bootstrap Card bodies (including `AutofillCollapse`'s "demoted body" — +`SPEC-display-left-rail.md` §D.1 explicitly locked this role, just not this +exact hex any more), the D14 seticon, and `variant="secondary"` +buttons/badges. Under #302 this token happened to equal Superhero's own +`$gray-600` stock default, so no explicit `$secondary` override was needed; +Tokyo-11 breaks that coincidence, so `styles.scss` now explicitly sets +`$secondary: $theme-panel-bg` to keep this exact role intact under the new +palette (see that file's own "Tokyo-11: $theme-colors overrides" comment). +`$card-bg`itself remains deliberately untouched by either pass — routing it to`$theme-raised-bg` would be a fidelity regression against the +spec-locked Card-body role. If a surface still looks grey/wrong after a +retheme, check whether it's one of these approved-panel surfaces before +assuming it's a leftover default. **Two different pills.** Bootstrap's own `$border-radius-pill` (default `50rem`, a true stadium shape) already backs a real, unrelated pill usage — @@ -117,7 +356,8 @@ e.g. `SearchSettings.tsx`'s ``. `$theme-radius-pill` (`10px`) is our own separate "status-pill" component convention (D14 `.statepill`, etc.), applied as a literal at each call site, never through Bootstrap's pill variable. Wiring `$border-radius-pill` to `10px` would have visibly -reshaped the `` — deliberately not done. +reshaped the `` — deliberately not done, and still not done by +the Tokyo-11 radius pass. **Known gaps, deliberately out of scope this round:** @@ -135,6 +375,21 @@ reshaped the `` — deliberately not done. codebase today (`AutofillTable.tsx`/`SourceSettings.tsx` both use the plain, un-variant table) — future-proofing only, verified inert today. +## What this pass swept (Tokyo-11, 2026-07-24) + +Beyond the token file and `styles.scss` wiring above, this pass migrated +every hardcoded old-#302-palette hex literal it found outside the token +file, across: `DisplayPage.tsx`, `SelectVersionResults.tsx`, +`SourcesAccordion.tsx`, `ConfidenceElement.tsx`, `RequestedPrintingBadge.tsx`, +`SlotActionsSection.tsx`, `Card.tsx`, `AutofillCollapse.tsx`, +`PagePreview.tsx`, `custom.css`, `SourceSettings.tsx`, +`SharedDeckViewer.tsx`, `AuthWidget.tsx` — each hex replaced with the +matching `var(--bs-*)`/`var(--theme-*)` reference (or, in `ConfidenceElement.tsx`'s +case, none needed — its only colour literals were danger-tint values now +sourced from `var(--theme-danger-rgb)`). `Footer.tsx`/`Navbar.tsx` already +used `var(--bs-primary)` before this pass (pre-existing precedent this +pass's bridge extends) and needed no changes. + ## Relationship to the fidelity specs `DisplayLeftRailFidelity.spec.ts` asserts real `getComputedStyle` values for @@ -142,11 +397,13 @@ several of these same tokens (e.g. the divider colour, the D14 band, the AutofillCollapse header hex). Those assertions are the actual literal values, not variable references — so **retheming means updating both** `_theme-tokens.scss` and the relevant spec-file literal in the same change, -never just one. This pass changed zero of those literals (see Verification), -because every value it introduced already equalled what those specs already -enforced. +never just one. The Tokyo-11 pass updated every one of that spec's colour +assertions to the new computed `rgb()`/`rgba()` values (comment-linked +per-row to this ruling) — see that spec file's own header comment for the +full list and docs/features/theming.md's Verification section below for how +they were re-derived. -## Verification (2026-07-24 pass) +## Verification (2026-07-24 theme-defaults pass) - `DisplayLeftRailFidelity.spec.ts` (11 tests) and `DisplayPage.spec.ts` (33 tests) — both pass unchanged, before and after the corner-radius @@ -162,3 +419,19 @@ enforced. after, at the same coordinate in both. `variant="secondary"` buttons in the same screenshots (e.g. "Close Without Saving") are pixel-identical before/after, confirming the approved-panel surfaces weren't touched. + +## Verification (2026-07-24 Tokyo-11 pass) + +- Self-measured computed-style token-conformance extraction (Playwright, + 1400px and 390px viewports): every sampled body/raised/panel/band/ + divider/text/primary/success/danger/warning/accent/btn-ink/radius surface + matched its binding hex/px value — see the PR body for the exact mismatch + count and sample list. +- Contrast spot-assertions added to the fidelity spec for the AAA pairs + (text-on-panel, btn-ink-on-primary) — both pass their asserted thresholds. +- `DisplayLeftRailFidelity.spec.ts` and `DisplayPage.spec.ts` — full suites + green with the updated literal values (colours/radii) described above. +- `npx tsc --noEmit`, `npm test` (jest), `npx prettier@2.7.1 --check`, + `next build` — see the PR body for pass/fail and counts. +- Screenshots: `/display` editor at 1400px and 390px, a modal, and the + `/print` page — paths in the PR body. diff --git a/docs/proposals/mockups/proposal-h/SPEC-display-left-rail.md b/docs/proposals/mockups/proposal-h/SPEC-display-left-rail.md index 84e9b0f71..191e5e3d3 100644 --- a/docs/proposals/mockups/proposal-h/SPEC-display-left-rail.md +++ b/docs/proposals/mockups/proposal-h/SPEC-display-left-rail.md @@ -183,6 +183,15 @@ Colours are the §D.0 #302 tokens. ### D.0 Palette tokens (all inherited — `styles.scss` #302 + Superhero) +> **SUPERSEDED (2026-07-24, owner ruling — "Tokyo-11" re-theme).** The +> table below is the #302 palette this section originally shipped with; it +> is kept verbatim here as a historical record of what shipped at the time +> this spec's other rows (sizing/spacing/I-N markers) were written and +> verified against. It no longer reflects the site's actual colours. The +> live, binding palette is [`docs/features/theming.md`](../../../features/theming.md) +> — edit `_theme-tokens.scss` there, not this table, for any future +> retheme. + | Token | Value | Token | Value | | ---------------- | ------------------ | ------------------ | --------------------- | | body bg | `#0f2537` | primary (hover) | `#df6919` (`#be5915`) | diff --git a/frontend/src/components/AutofillCollapse.tsx b/frontend/src/components/AutofillCollapse.tsx index 0a4f2f974..41debf099 100644 --- a/frontend/src/components/AutofillCollapse.tsx +++ b/frontend/src/components/AutofillCollapse.tsx @@ -111,7 +111,7 @@ export function AutofillCollapse({ // the two tokens are one hex digit apart by design, not by accident. Do not "fix" // this back to `#4e5d6c` again; see SPEC-display-left-rail.md §D.0 for the explicit // note distinguishing them. - backgroundColor: headerBackground ?? "#4E5D6B", + backgroundColor: headerBackground ?? "var(--theme-card-header-bg)", zIndex: zIndex + 1, cursor: "pointer", ...(headerPadding != null ? { padding: headerPadding } : {}), diff --git a/frontend/src/features/card/Card.tsx b/frontend/src/features/card/Card.tsx index 01316d9a2..eb94cce4d 100644 --- a/frontend/src/features/card/Card.tsx +++ b/frontend/src/features/card/Card.tsx @@ -90,7 +90,7 @@ const ErrorPlaceholder = styled.div` align-items: center; justify-content: center; gap: 0.4rem; - background: #4e5d6c; + background: var(--bs-secondary); color: rgba(255, 255, 255, 0.75); text-align: center; padding: 0.5rem; @@ -488,7 +488,7 @@ interface CardProportionWrapperProps { const CardProportionWrapperStyle = styled.div<{ $borderWidth?: number }>` z-index: 0; - background: #4e5d6c; + background: var(--bs-secondary); border: solid ${(props) => props.$borderWidth ?? 0}px black; `; diff --git a/frontend/src/features/card/CardbackApplyPrompt.tsx b/frontend/src/features/card/CardbackApplyPrompt.tsx index fbb4dc053..1d8dc43be 100644 --- a/frontend/src/features/card/CardbackApplyPrompt.tsx +++ b/frontend/src/features/card/CardbackApplyPrompt.tsx @@ -5,12 +5,18 @@ * per-slot picker) - one component, so the two surfaces can't drift (§C.2's own binding * requirement). Never a second stacked modal - always rendered inline by its caller. * - * Token table E.2 (BINDING, #302 palette): `.cbprompt` panel `#22303f` / `1px #16202b` border + - * left `3px #df6919`; `.applybtn` primary-tinted (`#df6919` border / `#ffb27d` text, hover fills - * `#df6919`/`#fff`, done state `#5cb85c`/`#8fe08f`); `.defbtn` info-tinted (`#5bc0de` border / - * `#8fd7ea` text, hover fills `#5bc0de`/`#062430`, same done state); `.trapnote` `#ffd76a` - * (rail only); `.skip` `#8fa0b0` underline link (toolbar only, since the rail per-slot picker - * already IS the "no modal, ever" surface - leaving the section collapsed is itself "not now"). + * Token table E.2 (BINDING, #302 palette; re-themed to Tokyo-11, 2026-07-24 - see + * docs/features/theming.md - tokens and spec tables move together, same discipline + * DisplayLeftRailFidelity.spec.ts's own re-theme pass used): `.cbprompt` panel + * $theme-raised-bg / `1px` $theme-divider border + left `3px` $primary; `.applybtn` + * primary-tinted ($primary border/text, hover fills $primary/$theme-btn-ink - Tokyo-11's + * primary is light, so the filled-hover state needs the dark ink, not white -, done state + * $success/$success); `.defbtn` info-tinted ($info border/text, hover fills $info/ + * $theme-btn-ink, same done state); `.trapnote` $warning (rail only); `.skip` $theme-muted + * underline link (toolbar only, since the rail per-slot picker already IS the "no modal, + * ever" surface - leaving the section collapsed is itself "not now"). Unlike the #302 palette, + * Tokyo-11's primary/info/success tokens are all light enough to use directly as text colour - + * no separate hand-picked tint literal is needed the way `#ffb27d`/`#8fd7ea` were. */ import styled from "@emotion/styled"; import React, { useState } from "react"; @@ -18,9 +24,9 @@ import React, { useState } from "react"; import { CustomBackSlotThumbnail } from "@/features/card/cardbackApply"; const Panel = styled.div` - background: #22303f; - border: 1px solid #16202b; - border-left: 3px solid #df6919; + background: var(--theme-raised-bg); + border: 1px solid var(--theme-divider); + border-left: 3px solid var(--bs-primary); padding: 10px 12px; margin-top: 12px; `; @@ -28,7 +34,7 @@ const Panel = styled.div` const Title = styled.div` font-size: 13px; font-weight: 700; - color: #ebebeb; + color: var(--bs-body-color); margin-bottom: 2px; display: flex; align-items: center; @@ -37,7 +43,7 @@ const Title = styled.div` const Intro = styled.div` font-size: 12px; - color: #8fa0b0; + color: var(--theme-muted); margin-bottom: 10px; `; @@ -46,7 +52,7 @@ const Choice = styled.div` align-items: center; gap: 10px; padding: 8px 0; - border-top: 1px solid #16202b; + border-top: 1px solid var(--theme-divider); &:first-of-type { border-top: none; @@ -59,19 +65,21 @@ const ChoiceLabel = styled.div` .h { font-size: 13px; - color: #ebebeb; + color: var(--bs-body-color); } .s { font-size: 11px; - color: #8fa0b0; + color: var(--theme-muted); } `; const ApplyButton = styled.button<{ $done: boolean }>` background: transparent; - border: 1px solid ${(props) => (props.$done ? "#5cb85c" : "#df6919")}; - color: ${(props) => (props.$done ? "#8fe08f" : "#ffb27d")}; + border: 1px solid + ${(props) => (props.$done ? "var(--bs-success)" : "var(--bs-primary)")}; + color: ${(props) => + props.$done ? "var(--bs-success)" : "var(--bs-primary)"}; font-family: inherit; font-size: 13px; padding: 4px 10px; @@ -81,15 +89,20 @@ const ApplyButton = styled.button<{ $done: boolean }>` pointer-events: ${(props) => (props.$done ? "none" : "auto")}; &:hover { - background: ${(props) => (props.$done ? "transparent" : "#df6919")}; - color: ${(props) => (props.$done ? "#8fe08f" : "#fff")}; + background: ${(props) => + props.$done ? "transparent" : "var(--bs-primary)"}; + /* Tokyo-11 ink flip - primary is light, so the filled-hover state needs dark ink, not + white (was #fff under the #302 palette). */ + color: ${(props) => + props.$done ? "var(--bs-success)" : "var(--theme-btn-ink)"}; } `; const DefaultButton = styled.button<{ $done: boolean }>` background: transparent; - border: 1px solid ${(props) => (props.$done ? "#5cb85c" : "#5bc0de")}; - color: ${(props) => (props.$done ? "#8fe08f" : "#8fd7ea")}; + border: 1px solid + ${(props) => (props.$done ? "var(--bs-success)" : "var(--bs-info)")}; + color: ${(props) => (props.$done ? "var(--bs-success)" : "var(--bs-info)")}; font-family: inherit; font-size: 13px; padding: 4px 10px; @@ -99,8 +112,11 @@ const DefaultButton = styled.button<{ $done: boolean }>` pointer-events: ${(props) => (props.$done ? "none" : "auto")}; &:hover { - background: ${(props) => (props.$done ? "transparent" : "#5bc0de")}; - color: ${(props) => (props.$done ? "#8fe08f" : "#062430")}; + background: ${(props) => (props.$done ? "transparent" : "var(--bs-info)")}; + /* Tokyo-11 ink flip - info is light too (was #062430 dark-navy-on-cyan under #302; the + new $theme-btn-ink token is the same idea, generalised across every filled-light role). */ + color: ${(props) => + props.$done ? "var(--bs-success)" : "var(--theme-btn-ink)"}; } `; @@ -109,7 +125,7 @@ const SkipRow = styled.div` text-align: right; a { - color: #8fa0b0; + color: var(--theme-muted); font-size: 12px; text-decoration: underline; cursor: pointer; @@ -118,7 +134,7 @@ const SkipRow = styled.div` const TrapNote = styled.div` font-size: 11px; - color: #ffd76a; + color: var(--bs-warning); margin-top: 8px; display: flex; gap: 5px; @@ -136,8 +152,8 @@ const ThumbPair = styled.div` display: flex; align-items: center; gap: 3px; - background: #16202b; - border: 1px solid rgba(235, 235, 235, 0.15); + background: var(--theme-divider); + border: 1px solid rgba(var(--bs-body-color-rgb), 0.15); padding: 3px; `; @@ -153,7 +169,7 @@ const ThumbImg = styled.div<{ $url: string | undefined }>` const ThumbLabel = styled.div` font-size: 9px; - color: #8fa0b0; + color: var(--theme-muted); max-width: 90px; white-space: nowrap; overflow: hidden; diff --git a/frontend/src/features/card/RequestedPrintingBadge.tsx b/frontend/src/features/card/RequestedPrintingBadge.tsx index 69a4f3928..a1fd53b6d 100644 --- a/frontend/src/features/card/RequestedPrintingBadge.tsx +++ b/frontend/src/features/card/RequestedPrintingBadge.tsx @@ -76,8 +76,8 @@ export function RequestedPrintingBadge({ style={{ display: "inline-block", marginTop: "5px", - background: "#ffc107", - color: "#111", + background: "var(--bs-warning)", + color: "var(--theme-btn-ink)", fontFamily: "monospace", fontSize: "10px", padding: "1px 7px", diff --git a/frontend/src/features/display/ConfidenceElement.tsx b/frontend/src/features/display/ConfidenceElement.tsx index a618e22cb..af2afe2cb 100644 --- a/frontend/src/features/display/ConfidenceElement.tsx +++ b/frontend/src/features/display/ConfidenceElement.tsx @@ -74,15 +74,17 @@ const NotThisPrintingButton = styled(Button)` font-weight: 600; line-height: 1.4; border-radius: 10px; - background: rgba(217, 83, 79, 0.12); + background: rgba(var(--bs-danger-rgb), 0.12); color: #f0b3b1; - border: 1px solid rgba(217, 83, 79, 0.55); + border: 1px solid rgba(var(--bs-danger-rgb), 0.55); &:hover:not(:disabled), &:focus:not(:disabled) { - background: #d9534f; - color: #fff; - border-color: #d9534f; + background: var(--bs-danger); + /* Tokyo-11 ink flip: danger is a light red (6.46:1 with dark ink vs. 2.65:1 with white) - + see styles.scss's own $color-contrast-dark note. */ + color: var(--theme-btn-ink); + border-color: var(--bs-danger); } &[data-confirmed="true"] { diff --git a/frontend/src/features/display/DisplayPage.tsx b/frontend/src/features/display/DisplayPage.tsx index 189ff2941..672360f0f 100644 --- a/frontend/src/features/display/DisplayPage.tsx +++ b/frontend/src/features/display/DisplayPage.tsx @@ -1207,15 +1207,15 @@ const ActionBarSearchGroup = styled.div` // component-scoped-override discipline as `.rail-source-toggle`/`.cstack .form-select` above. const ToolbarRoot = styled.div` .btn-outline-secondary { - background: #22303f; - color: #ebebeb; + background: var(--theme-raised-bg); + color: var(--bs-body-color); border: 1px solid #46586a; } .btn-outline-secondary:hover, .btn-outline-secondary:focus { - background: #22303f; - color: #ebebeb; - border-color: #abb6c2; + background: var(--theme-raised-bg); + color: var(--bs-body-color); + border-color: var(--theme-light); } `; @@ -1259,19 +1259,19 @@ const ToolbarRoot = styled.div` // `.sv{border-bottom:1px solid var(--divider)}`). const RailRoot = styled.div` .rail-head { - background: #22303f; - border-bottom: 1px solid #16202b; + background: var(--theme-raised-bg); + border-bottom: 1px solid var(--theme-divider); padding: 8px 10px; } .artist-line { - background: #22303f; - border-bottom: 1px solid #16202b; + background: var(--theme-raised-bg); + border-bottom: 1px solid var(--theme-divider); } .sources { - border-bottom: 1px solid #16202b; + border-bottom: 1px solid var(--theme-divider); } .select-version-wrapper { - border-bottom: 1px solid #16202b; + border-bottom: 1px solid var(--theme-divider); } .select-version-heading { margin: 0; @@ -1294,8 +1294,8 @@ const RailRoot = styled.div` flex-wrap: wrap; margin: 0; padding: 8px 10px; - background: #2b3e50; - border-bottom: 1px solid #16202b; + background: var(--theme-band-bg); + border-bottom: 1px solid var(--theme-divider); font-size: 12px; } /* Deliberate radius exception (spec §0) - kept, not invented: the set icon's own circular @@ -1309,14 +1309,14 @@ const RailRoot = styled.div` display: inline-flex; align-items: center; justify-content: center; - background: #4e5d6c; + background: var(--bs-secondary); border: 1px solid #7f8fa0; border-radius: 50%; cursor: pointer; } .seticon .ss { font-size: 15px; - color: #ebebeb; + color: var(--bs-body-color); } .seticon .check { position: absolute; @@ -1324,9 +1324,11 @@ const RailRoot = styled.div` bottom: -3px; width: 15px; height: 15px; - background: #5cb85c; - color: #fff; - border: 2px solid #2b3e50; + background: var(--bs-success); + /* Tokyo-11 ink flip: success is a light green (9.35:1 with dark ink vs. 1.83:1 with white) - + see styles.scss's own $color-contrast-dark note for the same reasoning applied to buttons. */ + color: var(--theme-btn-ink); + border: 2px solid var(--theme-band-bg); border-radius: 50%; display: flex; align-items: center; @@ -1334,13 +1336,17 @@ const RailRoot = styled.div` font-size: 9px; font-weight: 900; } + /* Tokyo-11 accent wiring (2026-07-24 owner ruling) - the D14 score badge is one of the + study's named accent surfaces ("D14 confidence pill family + score badge"), REV from + primary/orange. Ink flips to $theme-btn-ink for the same reason as .check above (white on + the light accent purple is 2.31:1; dark ink is 7.39:1, STRICT-AAA). */ .seticon .score { position: absolute; right: -7px; bottom: -7px; - background: #df6919; - color: #fff; - border: 2px solid #2b3e50; + background: var(--theme-accent); + color: var(--theme-btn-ink); + border: 2px solid var(--theme-band-bg); border-radius: 10px; font-size: 9px; font-weight: 800; @@ -1354,7 +1360,7 @@ const RailRoot = styled.div` } .d14 .statepill { padding: 1px 8px; - border: 1px solid #4e5d6c; + border: 1px solid var(--bs-secondary); border-radius: 10px; font-size: 11px; font-weight: 700; @@ -1363,9 +1369,14 @@ const RailRoot = styled.div` border-color: #3f7a2f; color: #a7e08a; } + /* Tokyo-11 accent wiring - the "suggested" pill is the other half of the D14 confidence pill + family the study names as an accent surface (REV from primary/orange). Border+text both + use the raw accent token (not a tinted derivative) - the study verifies 6.74:1 on this same + band surface, "near-strict," legible on pill text (unlike Nord's failed 3.67:1 attempt at + the same idea - see _theme-tokens.scss's own $theme-accent comment). */ .d14 .statepill.suggested { - border-color: #df6919; - color: #ffb27d; + border-color: var(--theme-accent); + color: var(--theme-accent); } .d14 .notthis { margin-left: auto; @@ -1409,8 +1420,18 @@ const RailRoot = styled.div` font-size: 11px; font-weight: 700; } + /* Tokyo-11 accent wiring (2026-07-24) - the study names "selection/active chip + segmented + + toggle states" as an accent surface; this toggle's ON cell renders the library's own stock + btn-primary class (which now resolves to the orange action colour) - overridden here, + same component-scoped-selector discipline as the rest of this block, so ONLY the Sources + list toggle (not every other react-bootstrap-toggle mount sitewide) goes accent-purple. */ + .rail-source-toggle .toggle-on { + background-color: var(--theme-accent) !important; + border-color: var(--theme-accent) !important; + color: var(--theme-btn-ink) !important; + } .rail-source-toggle .toggle-off { - color: #8fa0b0; + color: var(--theme-muted); } .rail-source-toggle .toggle-handle { display: none; @@ -1436,19 +1457,19 @@ const RailRoot = styled.div` aspect-ratio: 63 / 88; position: relative; overflow: hidden; - border: 1px solid rgba(235, 235, 235, 0.15); + border: 1px solid rgba(var(--bs-body-color-rgb), 0.15); } .subject img { display: block; } .subject.empty { background: transparent; - border: 1px dashed #abb6c2; + border: 1px dashed var(--theme-light); display: flex; align-items: center; justify-content: center; text-align: center; - color: #8fa0b0; + color: var(--theme-muted); font-size: 9px; padding: 4px; line-height: 1.3; @@ -1477,7 +1498,7 @@ const RailRoot = styled.div` } .rail-head .slot .face { font-weight: 400; - color: #8fa0b0; + color: var(--theme-muted); font-size: 11px; text-transform: uppercase; margin-left: 6px; @@ -1487,7 +1508,7 @@ const RailRoot = styled.div` margin-top: 1px; } .rail-head .name.none { - color: #8fa0b0; + color: var(--theme-muted); font-style: italic; } /* EP6 (N) - the per-slot Front/Back segmented toggle, 'ToggleButtonGroup' restyled to the @@ -1502,17 +1523,25 @@ const RailRoot = styled.div` font-size: 11px; font-weight: 700; padding: 2px 12px; - background: #22303f; - color: #8fa0b0; + /* WCAG/APCA audit fold-in (2026-07-24, PR #432's report; owner-ruled amendment to + SPEC-editor-polish.md's binding .fbtoggle row, 2026-07-24) - the authored 2px 12px + padding/11px font rendered a measured 51-55x23px target, 1px under WCAG 2.2 SC 2.5.8's + 24px floor with no qualifying spacing exception. min-height (not more padding) closes + the gap without changing the segment's visual density. */ + min-height: 24px; + background: var(--theme-raised-bg); + color: var(--theme-muted); border-color: #6b7d8e; border-radius: 0; } + /* Tokyo-11 accent wiring (2026-07-24) - the study explicitly names "Front/Back + source + toggles" as an accent surface, REV from the prior info/cyan active state. */ .fbtoggle .btn.active, .fbtoggle .btn:focus, .fbtoggle .btn:hover { - background: #5bc0de; - color: #062430; - border-color: #5bc0de; + background: var(--theme-accent); + color: var(--theme-btn-ink); + border-color: var(--theme-accent); box-shadow: none; } /* EP4 (REV RD5, §D.1 '.slotacts-top .iact') - the compact icon row's OWN sizing lives in @@ -1529,14 +1558,14 @@ const RailRoot = styled.div` '.idhang', both '#2b3e50') rather than the rail-head's own '#22303f', since it's still "about the currently-identified printing," the same subject D14 covers. */ .detmore-wrap { - background: #2b3e50; - border-bottom: 1px solid #16202b; + background: var(--theme-band-bg); + border-bottom: 1px solid var(--theme-divider); padding: 8px 10px; } .detmore { background: transparent; border: none; - color: #8fa0b0; + color: var(--theme-muted); font-size: 11px; cursor: pointer; padding: 0; @@ -1546,12 +1575,12 @@ const RailRoot = styled.div` font-family: inherit; } .detmore:hover { - color: #ebebeb; + color: var(--bs-body-color); } .detbody { margin-top: 8px; padding-top: 8px; - border-top: 1px solid #16202b; + border-top: 1px solid var(--theme-divider); font-size: 11px; } /* EP9 (N, §D.1 '.compare') - the Scryfall reference reveal, anchored beside the 116px subject @@ -1572,7 +1601,7 @@ const RailRoot = styled.div` z-index: 40; width: 150px; background: #0b1520; - border: 1px solid #5bc0de; + border: 1px solid var(--bs-info); box-shadow: 0 8px 22px rgba(0, 0, 0, 0.6); padding: 5px; pointer-events: none; @@ -1585,24 +1614,24 @@ const RailRoot = styled.div` } .compare .cap { font-size: 9px; - color: #8fa0b0; + color: var(--theme-muted); margin-top: 4px; } .compare .cap b { - color: #5bc0de; + color: var(--bs-info); font-weight: 700; } - /* identify panel band (item 6) - hangs off D14, same surface (§2/#2b3e50) */ + /* identify panel band (item 6) - hangs off D14, same surface (§2/band-bg) */ .idhang { - background: #2b3e50; - border-bottom: 1px solid #16202b; + background: var(--theme-band-bg); + border-bottom: 1px solid var(--theme-divider); padding: 0 10px 8px; } .idtoggle { background: transparent; border: 1px solid #6b7d8e; - color: #abb6c2; + color: var(--theme-light); font-size: 12px; padding: 3px 8px; cursor: pointer; @@ -1612,13 +1641,13 @@ const RailRoot = styled.div` gap: 6px; } .idtoggle:hover { - border-color: #abb6c2; - color: #ebebeb; + border-color: var(--theme-light); + color: var(--bs-body-color); } .idbody { margin-top: 8px; - background: #22303f; - border: 1px solid #16202b; + background: var(--theme-raised-bg); + border: 1px solid var(--theme-divider); padding: 8px; } @@ -1629,18 +1658,18 @@ const RailRoot = styled.div` gap: 6px; margin-bottom: 6px; font-size: 12px; - color: #8fa0b0; + color: var(--theme-muted); flex-wrap: wrap; } .svhead .n { - color: #ebebeb; + color: var(--bs-body-color); font-weight: 700; } /* EP7 (SPEC-editor-polish.md §D.4 '.sortsel', REV RD2) - 'max-width' 150px -> 172px. */ .sortsel { - background: #22303f; - color: #ebebeb; - border: 1px solid #4e5d6c; + background: var(--theme-raised-bg); + color: var(--bs-body-color); + border: 1px solid var(--bs-secondary); font-size: 12px; padding: 3px 6px; border-radius: 0; @@ -1654,19 +1683,19 @@ const RailRoot = styled.div` justify-content: center; gap: 10px; min-height: 140px; - color: #8fa0b0; + color: var(--theme-muted); font-size: 12px; } /* EP10 - tints the site's canonical spinner '--primary' inside the rail only (component- scoped via this ancestor selector, same discipline as '.rail-source-toggle' above - it cannot reach a 'Spinner' mounted anywhere outside this styled-component's own DOM scope). */ .vloading .spinner-border { - color: #df6919; + color: var(--bs-primary); } .filtersbtn { background: transparent; - border: 1px solid #abb6c2; - color: #abb6c2; + border: 1px solid var(--theme-light); + color: var(--theme-light); font-size: 14px; padding: 4px 8px; cursor: pointer; @@ -1676,8 +1705,8 @@ const RailRoot = styled.div` gap: 5px; } .filtersbtn:hover { - background: #abb6c2; - color: #111; + background: var(--theme-light); + color: var(--theme-btn-ink); } /* Filters panel - one shared fieldset body, tier-conditional container (RD4/O3): phone = the @@ -1687,8 +1716,8 @@ const RailRoot = styled.div` position:fixed node isn't enough here) - those two classes' rules travel WITH that portal component, duplicated in lockstep, not defined here. */ .fpanel { - background: #22303f; - border: 1px solid #16202b; + background: var(--theme-raised-bg); + border: 1px solid var(--theme-divider); padding: 8px; } .fpanel.inline { @@ -1707,17 +1736,17 @@ const RailRoot = styled.div` font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; - color: #8fa0b0; + color: var(--theme-muted); margin-bottom: 4px; } .fsep { height: 1px; - background: #16202b; + background: var(--theme-divider); margin: 9px -8px; } .implicit-note { font-size: 10px; - color: #8fa0b0; + color: var(--theme-muted); margin-top: 7px; display: flex; gap: 5px; @@ -1725,7 +1754,7 @@ const RailRoot = styled.div` line-height: 1.4; } .implicit-note .ic { - color: #5bc0de; + color: var(--bs-info); flex: 0 0 auto; } @@ -1740,15 +1769,15 @@ const RailRoot = styled.div` font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; - color: #8fa0b0; + color: var(--theme-muted); margin-bottom: 5px; } /* Component-scoped override of PrintOptionsSection's own Form.Select (same non-fork discipline as .rail-source-toggle above - reused component, rail-scoped CSS only). */ .cstack .form-select { - background: #22303f; - color: #ebebeb; - border: 1px solid #4e5d6c; + background: var(--theme-raised-bg); + color: var(--bs-body-color); + border: 1px solid var(--bs-secondary); font-size: 13px; padding: 4px 8px; width: 100%; @@ -1756,11 +1785,11 @@ const RailRoot = styled.div` } .cstack p.text-muted { font-size: 10px; - color: #8fa0b0; + color: var(--theme-muted); margin-top: 4px; } .cs-foot { - border-top: 1px solid #16202b; + border-top: 1px solid var(--theme-divider); padding-top: 8px; } `; @@ -1944,7 +1973,7 @@ const Rail = ({ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em", - color: "#8fa0b0", + color: "var(--theme-muted)", marginBottom: 5, }} > diff --git a/frontend/src/features/display/SlotActionsSection.tsx b/frontend/src/features/display/SlotActionsSection.tsx index 170e55870..223b954bc 100644 --- a/frontend/src/features/display/SlotActionsSection.tsx +++ b/frontend/src/features/display/SlotActionsSection.tsx @@ -53,24 +53,25 @@ const IconAction = styled(Button)` font-size: 14px; line-height: 1; background: transparent; - color: #abb6c2; - border: 1px solid #abb6c2; + color: var(--theme-light); + border: 1px solid var(--theme-light); &:hover:not(:disabled), &:focus:not(:disabled) { - background: #abb6c2; - color: #111; + background: var(--theme-light); + color: var(--theme-btn-ink); } &.danger { color: #f0a6a3; - border-color: #d9534f; + border-color: var(--bs-danger); } &.danger:hover:not(:disabled), &.danger:focus:not(:disabled) { - background: #d9534f; - color: #fff; + background: var(--bs-danger); + /* Tokyo-11 ink flip - danger is light, dark ink reads far better (6.46:1 vs. 2.65:1). */ + color: var(--theme-btn-ink); } `; diff --git a/frontend/src/features/display/SlotCardbackControl.tsx b/frontend/src/features/display/SlotCardbackControl.tsx index c6e97f223..a1645555f 100644 --- a/frontend/src/features/display/SlotCardbackControl.tsx +++ b/frontend/src/features/display/SlotCardbackControl.tsx @@ -37,7 +37,7 @@ const Thumb = styled.div<{ $url: string | undefined }>` flex: 0 0 54px; width: 54px; aspect-ratio: 63 / 88; - border: 1px solid rgba(235, 235, 235, 0.15); + border: 1px solid rgba(var(--bs-body-color-rgb), 0.15); position: relative; background-color: #2a2320; background-image: ${(props) => @@ -70,12 +70,12 @@ const Meta = styled.div` .bname { font-size: 13px; - color: #ebebeb; + color: var(--bs-body-color); } .bsub { font-size: 11px; - color: #8fa0b0; + color: var(--theme-muted); } `; diff --git a/frontend/src/features/display/SourcesAccordion.tsx b/frontend/src/features/display/SourcesAccordion.tsx index 06457fd92..6cf43cda1 100644 --- a/frontend/src/features/display/SourcesAccordion.tsx +++ b/frontend/src/features/display/SourcesAccordion.tsx @@ -174,8 +174,8 @@ export function SourcesAccordion() { headerPadding="7px 10px" // EP3 (SPEC-editor-polish.md §D.3) - de-greys the shared component's own `#4E5D6B` // default for this ONE caller only (see AutofillCollapse.tsx's own comment). - headerBackground="#22303f" - bodyBackground="#22303f" + headerBackground="var(--theme-raised-bg)" + bodyBackground="var(--theme-raised-bg)" title={
@@ -238,9 +238,9 @@ export function SourcesAccordion() { style={{ fontSize: "14px", padding: "6px 10px", - background: "#2b3e50", - color: "#ebebeb", - border: "1px solid #abb6c2", + background: "var(--theme-band-bg)", + color: "var(--bs-body-color)", + border: "1px solid var(--theme-light)", }} data-testid="display-sources-filter" /> @@ -297,14 +297,16 @@ export function SourcesAccordion() {
Showing{" "} - {visibleRows.length} of{" "} - {matchingRows.length} — filter to narrow + + {visibleRows.length} + {" "} + of {matchingRows.length} — filter to narrow
)}
@@ -375,7 +377,7 @@ export function SourcesAccordion() { /> {sourceDocument.name} @@ -387,7 +389,7 @@ export function SourcesAccordion() { minWidth: 30, minHeight: 30, fontSize: "15px", - color: pinned ? "#ffc107" : "#5b6b7b", + color: pinned ? "var(--bs-warning)" : "#5b6b7b", }} aria-pressed={pinned} aria-label={`Pin ${sourceDocument.name} as a favourite source`} diff --git a/frontend/src/features/display/useCardbackReminderGate.tsx b/frontend/src/features/display/useCardbackReminderGate.tsx index b2769e61a..16fbfb3f5 100644 --- a/frontend/src/features/display/useCardbackReminderGate.tsx +++ b/frontend/src/features/display/useCardbackReminderGate.tsx @@ -41,7 +41,7 @@ const CurBackThumbnail = styled.div<{ $url: string | undefined }>` flex: 0 0 88px; width: 88px; aspect-ratio: 63 / 88; - border: 1px solid rgba(235, 235, 235, 0.15); + border: 1px solid rgba(var(--bs-body-color-rgb), 0.15); position: relative; background-color: #2a2320; background-image: ${(props) => @@ -71,12 +71,12 @@ const ReminderBody = styled.div` const SeamNote = styled.div` margin-top: 10px; font-size: 12px; - color: #8fa0b0; - border-top: 1px solid #16202b; + color: var(--theme-muted); + border-top: 1px solid var(--theme-divider); padding-top: 8px; .seam { - color: #ffd76a; + color: var(--bs-warning); } `; @@ -113,7 +113,7 @@ function CardbackReminderGateModal({ printers put a back on every card - choosing your own is quick and easy to forget.

-
+
You can keep the default and continue - this only asks once per print.
diff --git a/frontend/src/features/gridSelector/SelectVersionResults.tsx b/frontend/src/features/gridSelector/SelectVersionResults.tsx index 5a2f536d6..04aeea8b1 100644 --- a/frontend/src/features/gridSelector/SelectVersionResults.tsx +++ b/frontend/src/features/gridSelector/SelectVersionResults.tsx @@ -391,12 +391,13 @@ const FUNNEL_TIER_TILE_WIDTH_REM: Record< hero: 7, }; -// F3 - dashed accent border + trailing glyph for a SUGGESTED chip; solid/plain otherwise. Reuses -// the theme accent (`#df6919`) already used everywhere else in the funnel, matching the mockup's -// reference styling (funnel-mockup.html's `.seg.suggested`). +// F3 - dashed accent border + trailing glyph for a SUGGESTED chip; solid/plain otherwise. Tokyo-11 +// (2026-07-24): reuses `$theme-accent`, not `$theme-primary` -- the study names "selection/active +// chip" as an accent surface, and this chip's own "suggested" semantics match the D14 suggested +// pill's identical REV (see DisplayPage.tsx's `.d14 .statepill.suggested`). const FUNNEL_SUGGESTED_STYLE: React.CSSProperties = { borderStyle: "dashed", - borderColor: "#df6919", + borderColor: "var(--theme-accent)", }; const AckLine = styled.div` @@ -404,7 +405,7 @@ const AckLine = styled.div` `; const AwarenessLine = styled.div` - border-left: 2px solid #df6919; + border-left: 2px solid var(--bs-primary); padding-left: 0.5rem; color: #aab7c4; `; @@ -489,16 +490,16 @@ const TreatmentChip = styled.button<{ $state: ChipVoteState }>` padding: 0.15rem 0.4rem; border: 1px solid #6b7d8e; background: transparent; - color: #ebebeb; + color: var(--bs-body-color); display: inline-flex; gap: 3px; align-items: center; ${touchExpandTapArea} ${(props) => props.$state === "positive" - ? "border-color:#5cb85c;background:rgba(92,184,92,.22);color:#bfe6ad;" + ? "border-color:var(--bs-success);background:rgba(var(--bs-success-rgb),.22);color:#bfe6ad;" : props.$state === "negative" - ? "border-color:#d9534f;background:rgba(217,83,79,.22);color:#f0b3b1;text-decoration:line-through;" + ? "border-color:var(--bs-danger);background:rgba(var(--bs-danger-rgb),.22);color:#f0b3b1;text-decoration:line-through;" : ""} `; @@ -510,7 +511,7 @@ const TreatmentChip = styled.button<{ $state: ChipVoteState }>` const UnifiedFilterDivider = styled.span` align-self: stretch; width: 1px; - background: #16202b; + background: var(--theme-divider); margin: 0 2px; `; @@ -546,7 +547,7 @@ const FloatFiltersPortalRoot = styled.div` max-height: calc(100% - 96px); overflow-y: auto; z-index: 1051; - background: #22303f; + background: var(--theme-raised-bg); border: 1px solid #7f8fa0; box-shadow: 0 12px 34px rgba(0, 0, 0, 0.6); padding: 0; @@ -559,7 +560,7 @@ const FloatFiltersPortalRoot = styled.div` align-items: center; justify-content: space-between; padding: 8px 12px; - background: #4e5d6b; + background: var(--theme-card-header-bg); font-size: 12px; font-weight: 700; text-transform: uppercase; @@ -569,8 +570,8 @@ const FloatFiltersPortalRoot = styled.div` } .fptitle button { background: transparent; - border: 1px solid rgba(235, 235, 235, 0.2); - color: #ebebeb; + border: 1px solid rgba(var(--bs-body-color-rgb), 0.2); + color: var(--bs-body-color); padding: 2px 8px; cursor: pointer; font-family: inherit; @@ -589,17 +590,17 @@ const FloatFiltersPortalRoot = styled.div` font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; - color: #8fa0b0; + color: var(--theme-muted); margin-bottom: 4px; } .fsep { height: 1px; - background: #16202b; + background: var(--theme-divider); margin: 9px -8px; } .implicit-note { font-size: 10px; - color: #8fa0b0; + color: var(--theme-muted); margin-top: 7px; display: flex; gap: 5px; @@ -607,7 +608,7 @@ const FloatFiltersPortalRoot = styled.div` line-height: 1.4; } .implicit-note .ic { - color: #5bc0de; + color: var(--bs-info); flex: 0 0 auto; } `; @@ -626,8 +627,8 @@ const TileImageWrap = styled.div` // normalized `.9` -> `.92` matching the D.1 table's literal `rgba(...,.92)` for all three // variants ("Tile ✓ canonical tag"/"Tile Alt tag"/"Tile ? unknown tag"). const CORNER_TAG_COLORS: Record<"canon" | "alt" | "unk", string> = { - canon: "rgba(92,184,92,.92)", - alt: "rgba(91,192,222,.92)", + canon: "rgba(var(--bs-success-rgb),.92)", + alt: "rgba(var(--bs-info-rgb),.92)", unk: "rgba(120,135,150,.92)", }; @@ -657,8 +658,8 @@ const ReqBadge = styled.span` top: 0; right: 0; z-index: 1; - background: #df6919; - color: #fff; + background: var(--bs-primary); + color: var(--theme-btn-ink); font-size: 0.5rem; font-weight: 800; padding: 0 3px; @@ -666,14 +667,15 @@ const ReqBadge = styled.span` /** F3's "survived only via a suggested/unconfirmed tag" signal - used to be a standalone * "⌇ suggested" text row under the tile; folded into a small corner marker instead (bottom-left, - * the one corner `CornerTag`/`ReqBadge`/the confirm ribbon below don't already use). */ + * the one corner `CornerTag`/`ReqBadge`/the confirm ribbon below don't already use). Tokyo-11: + * accent, matching the "suggested" semantics FUNNEL_SUGGESTED_STYLE/D14's suggested pill share. */ const SuggestedMarker = styled.span` position: absolute; bottom: 0; left: 0; z-index: 1; font-size: 0.55rem; - color: #df6919; + color: var(--theme-accent); background: rgba(0, 0, 0, 0.55); padding: 0 3px; `; @@ -708,9 +710,9 @@ const GhostTile = styled.button<{ $widthRem: number }>` flex: 0 0 auto; overflow: hidden; background: transparent; - outline: 1px solid rgba(235, 235, 235, 0.15); + outline: 1px solid rgba(var(--bs-body-color-rgb), 0.15); border: none; - color: #abb6c2; + color: var(--theme-light); font-size: 0.65rem; display: flex; flex-direction: column; @@ -1126,7 +1128,7 @@ function SelectVersionTile({ {!stacked && showSuggestedBadge && (
⌇ suggested diff --git a/frontend/src/features/moderation/AuthWidget.tsx b/frontend/src/features/moderation/AuthWidget.tsx index 672806ead..a6fc16951 100644 --- a/frontend/src/features/moderation/AuthWidget.tsx +++ b/frontend/src/features/moderation/AuthWidget.tsx @@ -117,14 +117,15 @@ const Avatar = styled.span` height: 30px; border-radius: 50%; background-color: var(--bs-primary); - color: #fff; + /* Tokyo-11 ink flip - primary is now light (2.03:1 with white vs. 8.40:1 with dark ink). */ + color: var(--theme-btn-ink); font-weight: 700; font-size: 0.9rem; flex: 0 0 auto; `; const UserMenuMenu = styled(Dropdown.Menu)` - background-color: #4e5d6c; + background-color: var(--bs-secondary); `; export function AuthWidget() { diff --git a/frontend/src/features/pdf/PDFWaitPanel.tsx b/frontend/src/features/pdf/PDFWaitPanel.tsx index 484c7772c..da6c8deb1 100644 --- a/frontend/src/features/pdf/PDFWaitPanel.tsx +++ b/frontend/src/features/pdf/PDFWaitPanel.tsx @@ -43,34 +43,35 @@ const LazyQuestionFeed = dynamic( const ProgressBox = styled.div` margin-top: 12px; - background: #22303f; - border: 1px solid #16202b; + background: var(--theme-raised-bg); + border: 1px solid var(--theme-divider); padding: 10px 12px; `; const ProgressLabel = styled.div` font-size: 12px; - color: #ebebeb; + color: var(--bs-body-color); display: flex; justify-content: space-between; margin-bottom: 6px; .pfrac { font-variant-numeric: tabular-nums; - color: #8fa0b0; + color: var(--theme-muted); } `; const ProgressSub = styled.div<{ $done?: boolean }>` font-size: 11px; - color: ${(props) => (props.$done ? "#8fe08f" : "#8fa0b0")}; + color: ${(props) => + props.$done ? "var(--bs-success)" : "var(--theme-muted)"}; margin-top: 6px; `; const SeamTag = styled.span` display: block; font-size: 11px; - color: #ffd76a; + color: var(--bs-warning); margin-top: 6px; `; @@ -83,14 +84,14 @@ const SeamTag = styled.span` // `aria-busy="true"`, no `aria-valuenow`/`aria-valuemin`/`aria-valuemax` at all. const IndeterminateTrack = styled.div` height: 10px; - background: #16202b; + background: var(--theme-divider); overflow: hidden; `; const IndeterminateFill = styled.div` height: 100%; width: 100%; - background-color: #df6919; + background-color: var(--bs-primary); background-image: linear-gradient( 45deg, rgba(255, 255, 255, 0.18) 25%, @@ -198,8 +199,8 @@ export function PDFProgressBox({ } const EmbedFrame = styled.div` - border: 1px solid #16202b; - background: #22303f; + border: 1px solid var(--theme-divider); + background: var(--theme-raised-bg); height: 100%; min-height: 420px; display: flex; @@ -207,8 +208,8 @@ const EmbedFrame = styled.div` `; const EmbedHead = styled.div` - background: #2b3e50; - border-bottom: 1px solid #16202b; + background: var(--theme-band-bg); + border-bottom: 1px solid var(--theme-divider); padding: 8px 12px; display: flex; align-items: center; @@ -218,7 +219,7 @@ const EmbedHead = styled.div` width: 8px; height: 8px; border-radius: 50%; - background: #df6919; + background: var(--bs-primary); } .h { @@ -226,23 +227,24 @@ const EmbedHead = styled.div` font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; - color: #8fa0b0; + color: var(--theme-muted); } .lz { margin-left: auto; font-size: 10px; - color: #8fa0b0; + color: var(--theme-muted); font-family: "Courier New", monospace; } `; const BuildRibbon = styled.div<{ $done?: boolean }>` background: #0b1520; - border-bottom: 1px solid #16202b; + border-bottom: 1px solid var(--theme-divider); padding: 6px 12px; font-size: 11px; - color: ${(props) => (props.$done ? "#8fe08f" : "#ebebeb")}; + color: ${(props) => + props.$done ? "var(--bs-success)" : "var(--bs-body-color)"}; display: flex; align-items: center; gap: 8px; @@ -250,13 +252,14 @@ const BuildRibbon = styled.div<{ $done?: boolean }>` .mini { flex: 1; height: 6px; - background: #16202b; + background: var(--theme-divider); overflow: hidden; } .mini .b { height: 100%; - background: ${(props) => (props.$done ? "#5cb85c" : "#df6919")}; + background: ${(props) => + props.$done ? "var(--bs-success)" : "var(--bs-primary)"}; } `; @@ -302,10 +305,10 @@ export function PDFWaitGameEmbed({
Each answer is submitted the instant you tap — leaving mid-card never diff --git a/frontend/src/features/pdf/PagePreview.tsx b/frontend/src/features/pdf/PagePreview.tsx index 6aa5394c3..a67ac6bf5 100644 --- a/frontend/src/features/pdf/PagePreview.tsx +++ b/frontend/src/features/pdf/PagePreview.tsx @@ -47,11 +47,11 @@ import { // E20 - the anti-white-flash fill: sits behind both the slot itself and every it renders, // so there's never a frame where an empty/loading slot or a still-decoding image shows white. -const SCREEN_SLOT_BG = "#2B3E50"; +const SCREEN_SLOT_BG = "var(--theme-band-bg)"; // E18 - deliberately distinct from the page's own pinline (rgba(235,235,235,.18), see this // component's screenPresentation page-border rule above) so the two never read as the same line. const SCREEN_SLOT_PINLINE = "rgba(143, 160, 176, 0.4)"; -const SCREEN_MUTED_TEXT = "#8fa0b0"; +const SCREEN_MUTED_TEXT = "var(--theme-muted)"; const LIME_GUIDE_COLOR = "#8ae234"; // E18 - the indeterminate loading sweep. `prefers-reduced-motion` gets a static bar at a fixed @@ -69,7 +69,7 @@ const LoadingTrack = styled.div` top: calc(50% - 2px); height: 4px; border-radius: 2px; - background: #22303f; + background: var(--theme-raised-bg); overflow: hidden; `; @@ -80,7 +80,7 @@ const LoadingSweep = styled.div` height: 4px; width: 35%; border-radius: 2px; - background: #df6919; + background: var(--bs-primary); animation: ${loadingSweep} 1.1s ease-in-out infinite; @media (prefers-reduced-motion: reduce) { @@ -110,7 +110,7 @@ const SlotMenuCue = styled.button` align-items: center; justify-content: center; background: rgba(11, 21, 32, 0.92); - border: 1.5px solid #abb6c2; + border: 1.5px solid var(--theme-light); color: #fff; font-size: 17px; line-height: 1; @@ -130,8 +130,8 @@ const SlotMenuCue = styled.button` } &:hover { - background: #df6919; - border-color: #df6919; + background: var(--bs-primary); + border-color: var(--bs-primary); } `; @@ -151,7 +151,7 @@ const SlotFlipButton = styled.button` align-items: center; justify-content: center; background: rgba(11, 21, 32, 0.92); - border: 1.5px solid #abb6c2; + border: 1.5px solid var(--theme-light); color: #fff; font-size: 14px; line-height: 1; @@ -171,16 +171,17 @@ const SlotFlipButton = styled.button` } &:hover { - background: #5bc0de; - border-color: #5bc0de; - color: #062430; + background: var(--bs-info); + border-color: var(--bs-info); + color: var(--theme-btn-ink); } `; // Cardback flow round (SPEC-cardback-pdfwait.md OWNER AMENDMENT 3, N) - a small non-default-back // indicator dot within the flip button's existing footprint, whenever the slot's own back face // differs from the deck's default cardback. "No new color roles" (the amendment's own binding -// constraint) - reuses the warning token (`#ffc107`) already in the #302 palette, not a new hue. +// constraint) - reuses the warning token ($theme-warning; Tokyo-11: #ffc107 -> #e0af68), not a +// new hue. const CustomCardbackDot = styled.span` position: absolute; top: -2px; @@ -188,7 +189,7 @@ const CustomCardbackDot = styled.span` width: 8px; height: 8px; border-radius: 50%; - background: #ffc107; + background: var(--bs-warning); border: 1px solid #0b1520; pointer-events: none; `; @@ -516,7 +517,9 @@ function PagePreviewSlotEl({ ? `1px solid ${SCREEN_SLOT_PINLINE}` : undefined, cursor: onSlotClick != null ? "pointer" : undefined, - outline: isSelected ? "3px solid #df691a" : undefined, + // Tokyo-11 accent wiring (2026-07-24) - the study names "version/sheet selection + // outlines" as an accent surface; REV from primary/orange. + outline: isSelected ? "3px solid var(--theme-accent)" : undefined, outlineOffset: isSelected ? "-3px" : undefined, }} > @@ -639,7 +642,7 @@ function PagePreviewSlotEl({ rel="noopener noreferrer" data-testid="page-preview-find-card-link" style={{ - color: "#df6919", + color: "var(--bs-primary)", fontSize: "2.2mm", textDecoration: "underline", }} diff --git a/frontend/src/features/savedDecks/SharedDeckViewer.tsx b/frontend/src/features/savedDecks/SharedDeckViewer.tsx index c39dd2b08..329610788 100644 --- a/frontend/src/features/savedDecks/SharedDeckViewer.tsx +++ b/frontend/src/features/savedDecks/SharedDeckViewer.tsx @@ -66,7 +66,7 @@ const HiddenOrphanBadge = styled.span` align-items: center; gap: 6px; background: #141f2b; - color: #8fa0b0; + color: var(--theme-muted); font-size: 11px; border: 1px dashed #46586a; padding: 4px 8px; @@ -79,8 +79,8 @@ const ExtBanner = styled.div` align-items: center; gap: 8px; background: rgba(11, 21, 32, 0.95); - color: #ebebeb; - border: 1px solid #5bc0de; + color: var(--bs-body-color); + border: 1px solid var(--bs-info); font-size: 11px; padding: 8px 10px; margin-bottom: 10px; @@ -89,7 +89,7 @@ const ExtBanner = styled.div` const ExtBannerLink = styled.button` background: transparent; border: none; - color: #5bc0de; + color: var(--bs-info); text-decoration: underline; cursor: pointer; padding: 0; diff --git a/frontend/src/features/searchSettings/SourceSettings.tsx b/frontend/src/features/searchSettings/SourceSettings.tsx index 9a5e27cdd..49c07c374 100644 --- a/frontend/src/features/searchSettings/SourceSettings.tsx +++ b/frontend/src/features/searchSettings/SourceSettings.tsx @@ -215,7 +215,7 @@ export function SourceSettings({ fontSize: 14, // dark panel tokens per the approved /display theme - not Bootstrap's // grey "secondary" table variant (the pre-2026-07-24 look) - backgroundColor: "#22303f", + backgroundColor: "var(--theme-raised-bg)", border: "1px solid rgba(0,0,0,.22)", }} > diff --git a/frontend/src/features/ui/Footer.tsx b/frontend/src/features/ui/Footer.tsx index bda83afd8..31d9f49b2 100644 --- a/frontend/src/features/ui/Footer.tsx +++ b/frontend/src/features/ui/Footer.tsx @@ -17,7 +17,7 @@ import { BackendConfig } from "@/features/backend/BackendConfig"; import { useGetBackendInfoQuery } from "@/store/api"; const FooterRoot = styled.footer` - background-color: #22303f; + background-color: var(--theme-raised-bg); border-top: 1px solid #17222e; padding: 1.75rem 1.5rem 1.5rem; margin-top: 1.25rem; @@ -86,7 +86,7 @@ const Tier2 = styled.div` `; const CreditText = styled.span` - color: #ebebeb; + color: var(--bs-body-color); font-size: 0.95rem; `; diff --git a/frontend/src/styles/_theme-tokens.scss b/frontend/src/styles/_theme-tokens.scss index 2802aeeb4..9abbfb7a2 100644 --- a/frontend/src/styles/_theme-tokens.scss +++ b/frontend/src/styles/_theme-tokens.scss @@ -1,62 +1,198 @@ -// Canonical ProxyPrints theme palette (2026-07-24 theme-defaults pass). +// Canonical ProxyPrints theme palette -- "Tokyo-11" (2026-07-24 re-theme, owner ruling). // -// ONE source of truth for the site's dark-chrome / orange-accent theme. Every Bootstrap -// variable override in styles.scss derives FROM these variables -- never from a literal hex -// scattered across styles.scss or a component file. Retheming the site later should mean -// editing THIS file only; see docs/features/theming.md for the full contract (including its -// relationship to the fidelity specs, which assert some of these exact values). +// ONE source of truth for the site's palette, corner-radius, spacing, and type tokens. Every +// Bootstrap variable override in styles.scss derives FROM these variables -- never from a +// literal hex/px scattered across styles.scss or a component file. Retheming the site later +// should mean editing THIS file only; see docs/features/theming.md for the full contract +// (including its relationship to the fidelity specs, which assert some of these exact values, +// and the runtime CSS-custom-property bridge this file also emits for styled-components/inline +// styles that can't reach a SCSS variable directly). // -// Values are the SPEC-display-left-rail.md §D.0 binding palette (itself the issue #302 -// palette), reproduced verbatim -- do not hand-tune a value here without updating that spec's -// own table (and vice versa); they are meant to coincide exactly. - -// Base surfaces, darkest to most-raised -$theme-body-bg: #0f2537; // §D.0 "body bg" -- page background, the darkest layer -$theme-raised-bg: #22303f; // §D.0 "raised/input bg" -- floating chrome one step off the page: -// rail-head/artist-line, the Sources list + filter inputs, and (this pass) the Bootstrap -// defaults for Modal/Dropdown/Popover/Toast/ListGroup/Offcanvas/table-head/table-dark -$theme-panel-bg: #4e5d6c; // §D.0 "panel/card/2ndry" -- Superhero's native $secondary/$gray-600. -// NOT a bug to fix: this is the approved token for Card bodies (incl. AutofillCollapse's -// "demoted body"), the D14 seticon, and secondary buttons/badges. See -// docs/features/theming.md's "what's grey on purpose" note before ever routing this away. -$theme-card-header-bg: #4e5d6b; // §D.0 "card-header" -- one hex digit off $theme-panel-bg, BY -// DESIGN (owner ruling, 2026-07-23; see SPEC-display-left-rail.md §D.0's own note on this -// exact pair). Do not "fix" this to equal $theme-panel-bg. -$theme-band-bg: #2b3e50; // §D.0 "D14 band" -- the D14 confidence-strip token +// SOURCE OF TRUTH (supersedes the #302/SPEC-display-left-rail.md §D.0 palette this file +// previously reproduced verbatim): the theme-options palette-exploration study (12 candidate +// palettes under a strict-AAA contrast bar) and the owner's 2026-07-24 ruling on it -- palette +// 11 "Tokyo x orange-action + purple-accent" ("tokyoorange"), Semi corner radius. Do not hand- +// tune a value here without a fresh owner ruling; SPEC-display-left-rail.md's own §D.0 table +// carries a pointer to this file rather than being kept in sync line-by-line (its sizing/spacing +// rows are untouched by this pass and remain authoritative). +// +// PROVENANCE: palette derived from Tokyo Night (MIT, © 2018-present Enkia -- LICENSE.txt in +// enkia/tokyo-night-vscode-theme reads "The MIT License (MIT)") -- color values only, no code. +// The $theme-info swap below (not part of the study's own 10-token "Tokyo" set; see that +// variable's own comment) is drawn from the same upstream Tokyo Night palette's cyan swatch, +// same license/provenance. + +// ============================================================================================ +// COLOUR -- base surfaces, darkest to most-raised +// ============================================================================================ +$theme-body-bg: #1a1b26; // page background, the darkest layer +$theme-raised-bg: #24283b; // floating chrome one step off the page: rail-head/artist-line, the +// Sources list + filter inputs, and the Bootstrap defaults for Modal/Dropdown/Popover/Toast/ +// ListGroup/Offcanvas/table-head/table-dark +$theme-panel-bg: #2f3549; // Card bodies (incl. AutofillCollapse's "demoted body"), the D14 +// seticon, and secondary buttons/badges ($secondary is routed here in styles.scss -- see "what's +// grey on purpose" in docs/features/theming.md, literally true again now that Superhero's own +// $gray-600 no longer matches this palette at all) +$theme-card-header-bg: #2f3548; // one hex digit off $theme-panel-bg, mechanically preserving the +// established "deliberately distinct, not a typo" pattern from the #302 palette (owner ruling, +// 2026-07-23) -- the study never separately audited a card-header swatch, so this round keeps +// the SAME relationship (last hex digit -1) rather than inventing an unruled value or silently +// collapsing the two tokens together +$theme-band-bg: #222234; // the D14 confidence-strip token ("conf" in the study) +$theme-input-border: $theme-panel-bg; // was its own literal, coincidentally equal to panel-bg +// under #302 too -- written as a reference now so the two can never drift apart by accident // Dividers -$theme-divider: #16202b; // §D.0 divider colour -- every rail block boundary (O1) +$theme-divider: #16161e; // every rail block boundary (O1) // Text -$theme-text: #ebebeb; // §D.0 "text" -$theme-muted: #8fa0b0; // §D.0 "muted" -$theme-light: #abb6c2; // §D.0 "light" -- btn-outline-light family, form-select indicator - -// Accent / status (Superhero-native values, reproduced here so no override site derives from -// a bare literal outside this file) -$theme-primary: #df6919; // §D.0 primary -$theme-primary-hover: #be5915; // §D.0 primary (hover) -$theme-success: #5cb85c; // §D.0 success -$theme-danger: #d9534f; // §D.0 danger -$theme-warning: #ffc107; // §D.0 warning -$theme-info: #5bc0de; // §D.0 info - -// Structural -$theme-input-border: #4e5d6c; // §D.0 "input border" - -// Corner radii (2026-07-24, owner extension to the theme-defaults pass). Four semantic tiers, -// wired into Bootstrap's own radius variables in styles.scss so a future radius ruling (e.g. the -// "semi-rounded buttons" option the owner is weighing) is a token edit here, not a hunt through -// component literals. Defaulted to the CURRENT shipped values -- flat 0 everywhere Bootstrap/ -// Superhero already renders flat (§D.0 "radius: 0 (flat)"), plus the existing 10px status-pill -// literal (§D.0 "status-pill radius") -- so introducing these tokens changes zero rendered -// pixels this round. -$theme-radius-none: 0; // no rounding, ever (kept distinct from -base so a future -base bump -// doesn't accidentally round something that must stay square-cornered) -$theme-radius-sm: 0; // §D.0 "radius" -- Bootstrap's `-sm` tier (small buttons/inputs) -$theme-radius-base: 0; // §D.0 "radius" -- the sitewide default tier (buttons/cards/inputs/`-lg`) -$theme-radius-pill: 10px; // §D.0 "status-pill radius" -- our OWN component convention (D14 -// statepill, etc). Deliberately NOT wired into Bootstrap's own $border-radius-pill (that -// variable defaults to 50rem/true-stadium and already backs a real, unrelated pill usage -- -// e.g. SearchSettings' `` -- wiring it to 10px would visibly reshape that badge). -// See docs/features/theming.md's "two different pills" note before ever changing this. +$theme-text: #c0caf5; // body text -- clears strict-AAA-normal 7:1 against both raised (9.02:1) +// and panel (7.54:1), verified 2026-07-24 (see docs/features/theming.md Verification) +$theme-muted: #a3aad0; // muted text/placeholder. WCAG/APCA remediation (2026-07-24, folded in +// from the PR #432 WCAG/APCA audit): the study's own #8c94bf measured APCA Lc 44.1 on body -- +// adequate only for large/bold text, but this token backs 10-13px legends/captions sitewide (the +// audit's finding). Lightened to clear the small-text bracket rather than split into a second +// "-strong" token, since most call sites ARE small captions and a single token is simpler to keep +// in sync; APCA itself isn't computable in this environment (no APCA library available), so this +// was verified via the WCAG proxy instead -- WCAG contrast lifted from 5.78/4.93/4.11 +// (body/raised/panel) to 7.50/6.39/5.34, i.e. now clears strict-AAA-normal on body, comes within +// a hair of it on raised, and clears AA-normal (4.5) with real margin even on panel, the toughest +// surface. See docs/features/theming.md's APCA-advisory-policy note for why WCAG stood in here. +$theme-light: $theme-text; // NOT an audited study token (the study's 10-token set has no "light" +// tier). #302's $theme-light sat between text and muted for outline-light buttons/the +// form-select chevron; reusing $theme-muted risked reading too dim for small icon glyphs +// (SlotActionsSection's 14px icon buttons), so this round aliases it to $theme-text instead -- +// safe (9.02/7.54:1, same numbers as body text) and avoids inventing an unruled hex. Revisit if +// a future study round audits a real "light" tier. + +// Accent / status (Tokyo-11 study values) +$theme-primary: #ff9e64; // action colour -- every button; ink flips to $theme-btn-ink below +// (8.40:1 STRICT-AAA on the button, study-verified) +$theme-primary-hover: rgb( + round(red(darken($theme-primary, 8%))), + round(green(darken($theme-primary, 8%))), + round(blue(darken($theme-primary, 8%))) +); // #302 hand-picked a literal hover shade (#be5915); this round derives it with Sass's own +// darken() instead so a future re-theme's hover state never needs a second manually-computed hex +// -- visually equivalent hover treatment, one less literal to maintain. Wrapped in round() so the +// emitted `--theme-primary-hover` custom property is a clean integer rgb() triplet, not +// darken()'s raw fractional-channel output. +$theme-success: #9ece6a; // 9.35:1 ink #1a1b26 -- STRICT-AAA +$theme-danger: #f7768e; // 6.46:1 ink #1a1b26 -- AAA-LARGE only (see $theme-btn-ink's own note: +// this is the one button variant the mandated single ink token can't lift past 7:1; white ink +// would be far worse at 2.65:1) +$theme-warning: #e0af68; // 8.55:1 ink #1a1b26 -- STRICT-AAA +$theme-info: #7dcfff; // NOT one of the study's 10 audited tokens (the mockup's token set omits +// "info" entirely). This fork's existing #302 "info" role is really a handful of small cyan +// link-styled text spots (DisplayPage identify-panel links, SharedDeckViewer share link, the +// filters-panel implicit-vote note icon) -- small text, so routing it to $theme-accent would +// violate the accent contract's own "never small paragraph text" rule (accent only clears +// 6.74:1, below strict-normal 7:1). Tokyo Night's own MIT-licensed cyan swatch (#7dcfff) clears +// STRICT-AAA-normal on both raised (8.49:1) and panel (7.09:1) -- verified 2026-07-24 -- so it +// keeps the pre-existing "info/link" role at the same contrast bar as everything else in this +// pass, without touching the accent token's reserved surfaces. +$theme-btn-ink: #1a1b26; // "btn-ink" in the study -- the ONE dark ink used across every +// $theme-colors button variant (wired via $color-contrast-dark in styles.scss), replacing the +// #302 palette's white button text. See $theme-danger's own note for the one variant that can't +// reach strict-7:1 with this ink. +$theme-accent: #bb9af7; // additive, UI-role-only token (study's palette-11 "accent"): D14 +// confidence pill + score badge, selection/active chip + segmented + toggle states, version/ +// sheet selection outlines, hover states on those same surfaces. NEVER small paragraph text -- +// 6.74:1 on the D14 band surface is below strict-normal 7:1, "near-strict" per the study, legible +// on bold pill text/borders/outlines but not prose. Bootstrap's global $link-color is +// deliberately NOT routed here (stays on $primary/orange, matching the pre-existing convention +// several components already use `var(--bs-primary)` for link-styled text) -- see +// docs/features/theming.md's "accent scope boundary" note. + +// ============================================================================================ +// CORNER RADII -- "Semi" tier (2026-07-24 Tokyo-11 ruling; was "Flat" under the #302 palette) +// ============================================================================================ +$theme-radius-none: 0; // no rounding, ever -- kept distinct from -base so a future -base bump +// can't accidentally round something that must stay square-cornered +$theme-radius-sm: 6px; // Bootstrap's `-sm` tier (small buttons/inputs) -- was 0 +$theme-radius-base: 6px; // the sitewide default tier (buttons/inputs/the generic `-lg` fallback) +// -- was 0 +$theme-radius-card: 8px; // NEW this round -- cards/modals/popovers/dropdowns (wired to +// $border-radius-lg, which those components' own SCSS defaults from) get their own, slightly +// larger tier; buttons/inputs stay on $theme-radius-base (6px) even at their own `-lg` variants, +// per the study's exact "6px buttons/inputs, 8px cards, 10px pills" table +$theme-radius-pill: 10px; // our OWN "status-pill" component convention (D14 statepill, etc) -- +// unchanged by this round. Deliberately NOT wired into Bootstrap's own $border-radius-pill (that +// variable defaults to 50rem/true-stadium and already backs a real, unrelated pill usage -- e.g. +// SearchSettings' `` -- wiring it to 10px would visibly reshape that badge). See +// docs/features/theming.md's "two different pills" note before ever changing this. + +// ============================================================================================ +// SPACING SCALE -- 4/8-based (2026-07-24 owner extension). FUTURE-SPECS-ONLY: this scale +// governs new spec rounds going forward; it does NOT retroactively re-space any shipped surface +// (several existing paddings/gaps are off-scale, e.g. the D14 band's `8px 10px` -- 10px isn't on +// this ladder -- see docs/features/theming.md's own off-scale table for the known examples and +// their owning spec rows). Existing surfaces migrate onto these tokens opportunistically, the +// next time their own spec is touched for an unrelated reason -- never as a bulk re-space pass. +// ============================================================================================ +$theme-space-1: 4px; +$theme-space-2: 8px; +$theme-space-3: 12px; +$theme-space-4: 16px; +$theme-space-5: 24px; +$theme-space-6: 32px; + +// ============================================================================================ +// TYPE RAMP -- small scale (2026-07-24 owner extension), same future-specs-only rule as the +// spacing scale above. Several existing rail/editor microtype sizes already happen to land on +// these exact steps (10/11/12/13/14/15px all appear verbatim across DisplayPage.tsx/ +// SelectVersionResults.tsx today) -- that's pre-existing coincidence, not this pass re-specing +// anything; no component was changed to make that true. +// ============================================================================================ +$theme-font-2xs: 10px; +$theme-font-xs: 11px; +$theme-font-sm: 12px; +$theme-font-md: 13px; +$theme-font-base: 14px; +$theme-font-lg: 15px; +$theme-font-xl: 18px; + +// ============================================================================================ +// RUNTIME CSS-CUSTOM-PROPERTY BRIDGE (2026-07-24). Every $theme-* SCSS variable above is a +// compile-time-only value -- invisible to styled-components/emotion template literals and inline +// `style={}` props, which is why so many component files had drifted onto hand-copied hex +// literals instead of referencing this file. This block re-exposes the tokens as `:root` CSS +// custom properties, but ONLY the ones Bootstrap's own `_root.scss` doesn't already emit -- +// `$body-bg`/`$body-color`/`$primary`/`$secondary`/`$success`/`$info`/`$warning`/`$danger` are +// all assigned FROM tokens in styles.scss, so Bootstrap already generates `--bs-body-bg`, +// `--bs-body-color` (+ `-rgb`), `--bs-primary` (+ `-rgb`), `--bs-secondary` (+ `-rgb`, this IS +// $theme-panel-bg), `--bs-success`/`--bs-danger`/`--bs-warning`/`--bs-info` (+ `-rgb`) for free -- +// duplicating those here would be a SECOND source of truth for the same value, exactly the +// divergence risk this bridge exists to avoid. Component code should reach for `var(--bs-*)` +// for any of those roles (see docs/features/theming.md's runtime-var-contract section for the +// full call-site convention) and `var(--theme-*)` below only for tokens with no Bootstrap +// equivalent at all. `-rgb` triplets are provided for the two tokens actually consumed inside an +// `rgba(var(--x-rgb), alpha)` call site (translucent tints/borders) -- everything else needing an +// rgb triplet (text, primary, success, danger) already has one at `--bs-body-color-rgb`/ +// `--bs-primary-rgb`/`--bs-success-rgb`/`--bs-danger-rgb`. This file still imports nothing from +// Bootstrap/Bootswatch -- it emits plain CSS, not Bootstrap-flavoured CSS. +:root { + --theme-raised-bg: #{$theme-raised-bg}; + --theme-card-header-bg: #{$theme-card-header-bg}; + --theme-band-bg: #{$theme-band-bg}; + --theme-divider: #{$theme-divider}; + --theme-muted: #{$theme-muted}; + --theme-light: #{$theme-light}; + --theme-primary-hover: #{$theme-primary-hover}; // no Bootstrap-emitted hover custom property + // exists for a manually-set $primary (button hover shades are computed per-component, not + // globally) -- this is the one $theme-colors-adjacent value still needed here + --theme-btn-ink: #{$theme-btn-ink}; + --theme-accent: #{$theme-accent}; + // prettier-ignore + --theme-accent-rgb: #{red($theme-accent)}, #{green($theme-accent)}, #{blue($theme-accent)}; + --theme-radius-sm: #{$theme-radius-sm}; + --theme-radius-base: #{$theme-radius-base}; + --theme-radius-card: #{$theme-radius-card}; + --theme-radius-pill: #{$theme-radius-pill}; + --theme-space-1: #{$theme-space-1}; + --theme-space-2: #{$theme-space-2}; + --theme-space-3: #{$theme-space-3}; + --theme-space-4: #{$theme-space-4}; + --theme-space-5: #{$theme-space-5}; + --theme-space-6: #{$theme-space-6}; +} diff --git a/frontend/src/styles/custom.css b/frontend/src/styles/custom.css index 926da7989..5591f9cf9 100755 --- a/frontend/src/styles/custom.css +++ b/frontend/src/styles/custom.css @@ -28,9 +28,16 @@ object-fit: cover; } +/* Tokyo-11 accent wiring (2026-07-24 owner ruling) - the study names "version/sheet selection + outlines" as an accent surface. `highlight` (Card.tsx) is the shared "this is the selected + image" glow used by both SelectVersionResults.tsx's grid and CardResultSet.tsx's search-result + highlight - both genuinely "version selection," so both go accent. See + docs/features/theming.md's runtime-var-contract section for why this is a plain CSS file (no + SCSS $theme-* access) reaching for the `--theme-accent-rgb`/`--bs-body-color-rgb` runtime + bridge instead. */ .mpccard-highlight { z-index: 1; - box-shadow: 0px 0px 3px 3px rgba(255, 255, 255, 0.6); + box-shadow: 0px 0px 3px 3px rgba(var(--theme-accent-rgb), 0.6); } .mpccard-hover { @@ -39,7 +46,7 @@ .mpccard-hover:hover { z-index: 1; - box-shadow: 0 0 10px 5px rgba(255, 255, 255, 0.3); + box-shadow: 0 0 10px 5px rgba(var(--bs-body-color-rgb), 0.3); } .mpccard-source { @@ -209,5 +216,5 @@ } .nav-pills { - background-color: #4e5d6c; + background-color: var(--bs-secondary); } diff --git a/frontend/src/styles/styles.scss b/frontend/src/styles/styles.scss index d516f824a..522d4701e 100644 --- a/frontend/src/styles/styles.scss +++ b/frontend/src/styles/styles.scss @@ -1,11 +1,11 @@ -// Sitewide theme (2026-07-21, issue #302; re-platformed onto _theme-tokens.scss by the -// 2026-07-24 theme-defaults pass -- see docs/features/theming.md): dark chrome + -// Superhero-native orange accent. No $primary override -> Superhero's native -// $primary:$orange (#df6919) returns. $body-bg intentionally NOT overridden -// (owner decision, T2 AMENDED): Superhero's native #0f2537 is kept because -// it holds orange link text at 4.61:1 contrast (AA), vs. 3.24:1 (fails AA) -// on the lighter #2b3e50 the theme mockup used ($theme-body-bg below is that same value, -// named for clarity -- not a second, independent decision). +// Sitewide theme -- "Tokyo-11" (2026-07-24 re-theme, owner ruling; re-platformed onto +// _theme-tokens.scss by the 2026-07-24 theme-defaults pass -- see docs/features/theming.md). +// Dark Tokyo Night chrome + a warm orange action colour + a purple accent layer. Every +// $theme-colors entry Superhero/Bootstrap would otherwise default is now explicitly assigned +// from a token below -- under the #302 palette several of these ($primary/$success/$danger/ +// $warning/$secondary/$body-bg) happened to already equal Superhero's own stock values, so no +// override was needed; Tokyo-11 diverges from every one of them, so this pass adds the +// assignments this file was implicitly relying on Superhero's defaults for. @import "theme-tokens"; $dark: $theme-raised-bg; // T3 (was #20374c) @@ -14,6 +14,92 @@ $input-color: $theme-text; // T5 (was $gray-900) $input-border-color: $theme-input-border; // T6 (was transparent) $input-placeholder-color: $theme-muted; // T7 (was $gray-500) +// Tokyo-11 (2026-07-24): $theme-colors overrides. Superhero's own _variables.scss sets +// $primary/$secondary/$success/$info/$warning/$danger/$body-bg/$body-color as plain hardcoded +// literals (not derived from any token this file controls), so every one of them needs an +// explicit override here now that the palette no longer coincides with Superhero's stock values. +// Bootstrap's own root.scss (imported below, via the Superhero/Bootstrap core chain) emits each +// of these as a `--bs-*` CSS custom property automatically -- e.g. `--bs-primary`, `--bs-body-bg` +// -- so this block IS the "Bootstrap runtime-variable bridge": every `var(--bs-*)` consumer +// (Navbar.tsx/Footer.tsx/AuthWidget.tsx's existing `var(--bs-primary)` links, react-bootstrap's +// own component internals) picks up Tokyo-11 with no further wiring. Scope note: this bridges +// the EXISTING `--bs-*` surface Bootstrap 5.3 already emits from these variables -- it is not a +// wholesale migration of every component onto `var()`; component-local styled-components/inline +// styles still compile to literal values unless they explicitly reference `var(--bs-*)` or +// `var(--theme-*)` (the parallel bridge _theme-tokens.scss emits for the tokens Bootstrap has no +// equivalent for, e.g. $theme-accent/$theme-band-bg/$theme-divider). Any surface where the +// compiled literal and a same-named runtime var could ever diverge is flagged in +// docs/features/theming.md's runtime-var-contract section rather than blanket-converted here. +$body-bg: $theme-body-bg; +$body-color: $theme-text; +$primary: $theme-primary; +$secondary: $theme-panel-bg; // was Superhero's own $gray-600 default -- see "what's grey on +// purpose" in docs/features/theming.md; this keeps that same approved role, just re-pointed at +// the Tokyo-11 panel token instead of Superhero's stock grey (which no longer resembles this +// palette at all) +$success: $theme-success; +$danger: $theme-danger; +$warning: $theme-warning; +$info: $theme-info; + +// Tokyo-11 button-ink flip (2026-07-24, AAA remediation): Superhero sets +// `$min-contrast-ratio: 1.6` (deliberately low, so white button text survives against a +// medium-brightness orange). Every Tokyo-11 $theme-colors entry above is LIGHT (an inverted +// palette vs. #302's mid-tone orange), so white text now fails hard (2.0-2.7:1) on all four -- +// dark ink is correct on every one of them. $color-contrast-dark is pointed at $theme-btn-ink +// (not Bootstrap core's own #000 default) so the button ink matches the study's own audited +// token/numbers (8.40/9.35/8.55/6.46:1 for primary/success/warning/danger respectively -- see +// _theme-tokens.scss's $theme-danger comment for why that one variant can't clear strict-7:1 +// with this ink; pure black would, but at the cost of a second, inconsistent ink shade). Raising +// $min-contrast-ratio to 4.5 (Bootstrap core's own default, just restoring it from Superhero's +// 1.6 override) is enough to make Bootstrap's `color-contrast()` deterministically pick the dark +// ink over white for all four -- every one of the four dark-ink ratios clears 4.5, every white-ink +// ratio fails it, so there's no ambiguous case landing on some other fallback colour. +$color-contrast-dark: $theme-btn-ink; +$min-contrast-ratio: 4.5; + +// WCAG/APCA audit fold-in (2026-07-24, PR #432's report, folded into this pass): Bootstrap's +// default focus ring is `rgba($primary, .25)` -- a 25%-alpha box-shadow that the audit measured +// at 1.18-1.56:1 non-text contrast against every one of this theme's dark surfaces (body/raised/ +// panel), well under the 3:1 WCAG 2.2 non-text-contrast floor a focus indicator needs (2.4.11/ +// 2.4.7). Two changes, both flowing through Bootstrap's single `$focus-ring-*` variable family +// (which every focus-visible surface sitewide already derives from -- `$input-btn-focus-*`/ +// `$btn-focus-*`/`$input-focus-*`/`$form-select-focus-box-shadow`/`$nav-link-focus-box-shadow`/ +// `$pagination-focus-box-shadow`/`$accordion-button-focus-box-shadow`/`$btn-close-focus-shadow` +// all default FROM `$focus-ring-box-shadow`, so overriding the three inputs below is a single, +// global fix, not a per-component hunt): +// 1. `$focus-ring-color: $theme-accent` -- accent purple, not `$primary`, so the focus ring +// reads as a distinct UI-signal colour from the action colour (consistent with accent's own +// "UI-role" contract elsewhere in this file). +// 2. `$focus-ring-opacity: 1` -- fully OPAQUE, not translucent. A translucent ring's real +// contrast against a given background depends on what's UNDER it (the exact failure mode the +// audit measured); making it opaque means the ring's contrast is simply accent-vs-background, +// computed once and verified sitewide: accent #bb9af7 measures 7.39:1 on body, 6.30:1 on +// raised, 5.26:1 on panel -- every one comfortably clears the 3:1 floor with real margin +// (verified 2026-07-24, see docs/features/theming.md Verification). +// `$focus-ring-width` bumped 0.25rem (4px, Bootstrap default) -> 2px so the ring reads as a +// deliberate outline rather than a soft glow, per the audit's own suggested treatment. +$focus-ring-width: 2px; +$focus-ring-opacity: 1; +$focus-ring-color: $theme-accent; + +// WCAG/APCA audit fold-in (2026-07-24, PR #432's report) - two `.btn-close` (modal/offcanvas/ +// toast dismiss ×) fixes, both theme-layer so they apply to every one of the ~15 mounts sitewide +// without touching each call site: +// 1. Target size (WCAG 2.5.8, >=24x24 CSS px) - Bootstrap's stock `$btn-close-width: 1em` +// (16px) + `.25em` padding each side measured 21x21 in the audit, under the 24px floor. +// Bumped to a flat 25px (comfortably >=24 after any subpixel rounding, not just exactly at +// the boundary) - self-verified via a real `boundingBox()` read, see the fidelity spec. +// 2. Colour (`$btn-close-color`) - stock default is `$black` (a dark-on-light SVG, via the +// `fill:#{$btn-close-color}` data-URI Bootstrap core generates from this variable), never +// overridden by Superhero, so every close × sitewide was rendering a near-invisible dark +// glyph against this theme's dark modal/offcanvas/toast headers - Superhero's own dark +// surfaces have ALWAYS had this bug, this pass is the first to route it to a token. +// `$theme-text` (light) makes the glyph visible with no per-caller "white" variant class +// needed. +$btn-close-width: 25px; +$btn-close-color: $theme-text; + // Theme-defaults pass (2026-07-24): components that never opt into our own tokens were "born // grey" -- Superhero's stock $gray-600 (#4e5d6c) bleeding through Bootstrap's own component // defaults for surfaces that are conceptually "raised chrome" floating above the page (Modal, @@ -35,23 +121,21 @@ $table-dark-bg: $theme-raised-bg; // select-arrow chevron was rendering almost invisibly against our dark $input-bg. $form-select-indicator-color: $theme-light; -// Corner radii (2026-07-24, owner extension to the theme-defaults pass): wire Bootstrap's own -// radius variables to the token tiers in _theme-tokens.scss instead of leaving them on -// Superhero's own literals / the implicit CSS-var cascade some of these already inherit through. -// Every value below is identical to what already ships (0 -- Superhero's own $border-radius/ -// -sm/-lg are already 0; $btn-/$card-/$input-border-radius* already inherit 0 from those via -// Bootstrap core's var(--bs-border-radius) default), so this changes zero rendered pixels -- it -// only makes the coupling explicit so a future radius ruling (e.g. semi-rounded buttons, which -// the owner is weighing separately from this pass) is a token edit, not a component hunt. -// $border-radius-pill deliberately NOT touched here -- see _theme-tokens.scss's own note on why -// $theme-radius-pill isn't wired to it. +// Corner radii (2026-07-24 Tokyo-11 ruling, "Semi"): wire Bootstrap's own radius variables to +// the token tiers in _theme-tokens.scss instead of leaving them on Superhero's own literals. +// Buttons/inputs are 6px ($theme-radius-base/-sm, at every Bootstrap size tier including -lg -- +// the study's table doesn't grow buttons past 6px for a -lg variant); cards/modals/popovers/ +// dropdowns are 8px ($theme-radius-card, wired to $border-radius-lg since those components' +// own SCSS defaults their own radius FROM $border-radius-lg); pills stay their own separate +// $theme-radius-pill (10px, unchanged -- see that token's own note on why it's not wired to +// Bootstrap's $border-radius-pill). $border-radius: $theme-radius-base; $border-radius-sm: $theme-radius-sm; -$border-radius-lg: $theme-radius-base; +$border-radius-lg: $theme-radius-card; $btn-border-radius: $theme-radius-base; $btn-border-radius-sm: $theme-radius-sm; $btn-border-radius-lg: $theme-radius-base; -$card-border-radius: $theme-radius-base; +$card-border-radius: $theme-radius-card; $input-border-radius: $theme-radius-base; $input-border-radius-sm: $theme-radius-sm; $input-border-radius-lg: $theme-radius-base; diff --git a/frontend/tests/CardbackPdfWaitFidelity.spec.ts b/frontend/tests/CardbackPdfWaitFidelity.spec.ts index 9d278a93a..ef61933fd 100644 --- a/frontend/tests/CardbackPdfWaitFidelity.spec.ts +++ b/frontend/tests/CardbackPdfWaitFidelity.spec.ts @@ -36,6 +36,14 @@ import { * (introduced-this-round) elements across all four subsections (E.1 gate / E.2 grid+prompt / * E.3 progress bar / E.4 game embed+outro), covering every DISTINCT colour token the round * introduces at least once. + * + * Tokyo-11 re-theme (2026-07-24, owner ruling - see docs/features/theming.md): this file's own + * colour literals were re-derived from the #302 palette to Tokyo-11 in the same pass that + * re-derived DisplayLeftRailFidelity.spec.ts's - tokens and spec tables move together, same + * discipline. `CardbackApplyPrompt.tsx`/`useCardbackReminderGate.tsx`/`PDFWaitPanel.tsx` (all + * landed via #431, after the original Tokyo-11 sweep) carried their own hardcoded #302-derived + * literals that sweep hadn't reached yet - fixed onto `var(--bs-*)`/`var(--theme-*)` token + * references in the same pass as this file's assertions, per-row comments below. */ const threeCardHandlers = [ @@ -97,21 +105,23 @@ for (const viewport of [ // E.1 `.mdialog` (real react-bootstrap Modal) - the spec's own table cites the stock // Superhero `$modal-content-bg` ($gray-600 #4e5d6c), but PR #425's theme-defaults pass // (landed the same day, separately) re-routed EVERY Modal's content bg to - // `$theme-raised-bg` (#22303f) sitewide - a base-theme move this spec's own binding token + // `$theme-raised-bg` sitewide - a base-theme move this spec's own binding token // couldn't have anticipated, not a regression introduced here. Asserting the CURRENT real // shared value (`_theme-tokens.scss`'s `$modal-content-bg: $theme-raised-bg`) - this file's // own Modal instances (the reminder gate, the cardback grid selector) are unforked, - // sitewide Bootstrap chrome, not something this round overrides. + // sitewide Bootstrap chrome, not something this round overrides. Tokyo-11 (2026-07-24): + // $theme-raised-bg #22303f -> #24283b, rgb(34, 48, 63) -> rgb(36, 40, 59). const modalContent = page.locator(".modal-content").first(); await expect(modalContent).toHaveCSS( "background-color", - "rgb(34, 48, 63)" + "rgb(36, 40, 59)" ); - // E.1 `.mfoot` primary button - $primary #df6919. + // E.1 `.mfoot` primary button - $primary. Tokyo-11: #df6919 -> #ff9e64, + // rgb(223, 105, 25) -> rgb(255, 158, 100). await expect(gate.getByTestId("cardback-gate-choose")).toHaveCSS( "background-color", - "rgb(223, 105, 25)" + "rgb(255, 158, 100)" ); await gate.getByTestId("cardback-gate-use-current").click(); @@ -131,37 +141,48 @@ for (const viewport of [ const prompt = cardbackModal.getByTestId("cardback-apply-prompt"); await expect(prompt).toBeVisible(); - // E.2 `.cbprompt` panel - #22303f bg, 1px #16202b border, left 3px #df6919. - await expect(prompt).toHaveCSS("background-color", "rgb(34, 48, 63)"); + // E.2 `.cbprompt` panel - $theme-raised-bg bg, 1px $theme-divider border, left 3px + // $primary. Tokyo-11: raised-bg rgb(34, 48, 63) -> rgb(36, 40, 59); primary + // rgb(223, 105, 25) -> rgb(255, 158, 100). + await expect(prompt).toHaveCSS("background-color", "rgb(36, 40, 59)"); await expect(prompt).toHaveCSS( "border-left", - "3px solid rgb(223, 105, 25)" + "3px solid rgb(255, 158, 100)" ); - // E.2 `.applybtn` (primary-tinted, at rest) - transparent bg, 1px #df6919 border, - // #ffb27d text. + // E.2 `.applybtn` (primary-tinted, at rest) - transparent bg, 1px $primary border, text. + // Tokyo-11 simplification (CardbackApplyPrompt.tsx, 2026-07-24): the #302 palette's + // primary (#df6919) was too dark to read as text, so it needed a separately hand-picked + // lighter tint (#ffb27d); Tokyo-11's primary (#ff9e64) is already light enough to use + // DIRECTLY as text colour, so border and text now both resolve to the same + // rgb(255, 158, 100) - no separate tint literal any more. const applyButton = prompt.getByTestId("cardback-apply-all-button"); await expect(applyButton).toHaveCSS( "border", - "1px solid rgb(223, 105, 25)" + "1px solid rgb(255, 158, 100)" ); - await expect(applyButton).toHaveCSS("color", "rgb(255, 178, 125)"); + await expect(applyButton).toHaveCSS("color", "rgb(255, 158, 100)"); - // E.2 `.defbtn` (info-tinted, at rest) - 1px #5bc0de border, #8fd7ea text. + // E.2 `.defbtn` (info-tinted, at rest) - 1px $info border, text. Tokyo-11: same + // no-separate-tint simplification as `.applybtn` above - $info (#7dcfff) is light enough + // to use directly; #5bc0de -> #7dcfff, rgb(91, 192, 222)/rgb(143, 215, 234) (border/tint) + // both collapse to rgb(125, 207, 255). const defaultButton = prompt.getByTestId("cardback-set-default-button"); await expect(defaultButton).toHaveCSS( "border", - "1px solid rgb(91, 192, 222)" + "1px solid rgb(125, 207, 255)" ); - await expect(defaultButton).toHaveCSS("color", "rgb(143, 215, 234)"); + await expect(defaultButton).toHaveCSS("color", "rgb(125, 207, 255)"); - // Done-state (both buttons share the same green) - #5cb85c border, #8fe08f text. + // Done-state (both buttons share the same green) - $success border/text (same + // no-separate-tint simplification). Tokyo-11: #5cb85c -> #9ece6a, rgb(92, 184, 92)/ + // rgb(143, 224, 143) both collapse to rgb(158, 206, 106). await applyButton.click(); await expect(applyButton).toHaveCSS( "border", - "1px solid rgb(92, 184, 92)" + "1px solid rgb(158, 206, 106)" ); - await expect(applyButton).toHaveCSS("color", "rgb(143, 224, 143)"); + await expect(applyButton).toHaveCSS("color", "rgb(158, 206, 106)"); }); test(`E.3 progress bar + E.4 game embed tokens resolve real computed values at ${viewport.label}`, async ({ @@ -190,25 +211,29 @@ for (const viewport of [ await page.getByRole("button", { name: "Generate PDF" }).click(); - // E.3 `.progressbox` - #22303f bg, 1px #16202b border. + // E.3 `.progressbox` - $theme-raised-bg bg, 1px $theme-divider border. Tokyo-11: raised-bg + // rgb(34, 48, 63) -> rgb(36, 40, 59); divider rgb(22, 32, 43) -> rgb(22, 22, 30). const progressBox = page.getByTestId("pdf-progress"); await expect(progressBox).toBeVisible({ timeout: 15_000 }); await expect(progressBox).toHaveCSS( "background-color", - "rgb(34, 48, 63)" + "rgb(36, 40, 59)" ); await expect(progressBox).toHaveCSS( "border", - "1px solid rgb(22, 32, 43)" + "1px solid rgb(22, 22, 30)" ); - // E.4 `.gameembed` frame - #22303f bg, 1px #16202b border. + // E.4 `.gameembed` frame - $theme-raised-bg bg, 1px $theme-divider border (same Tokyo-11 + // remapping as `.progressbox` above). const embed = page.getByTestId("pdf-wait-game"); await expect(embed).toBeVisible({ timeout: 15_000 }); - await expect(embed).toHaveCSS("background-color", "rgb(34, 48, 63)"); - await expect(embed).toHaveCSS("border", "1px solid rgb(22, 32, 43)"); + await expect(embed).toHaveCSS("background-color", "rgb(36, 40, 59)"); + await expect(embed).toHaveCSS("border", "1px solid rgb(22, 22, 30)"); - // E.4 `.geband` build ribbon - #0b1520 bg. + // E.4 `.geband` build ribbon - #0b1520 bg. NOT a theme token (a bespoke near-black literal, + // out of the Tokyo-11 sweep's scope - same as PagePreview.tsx's `.compare` panel + // background) - unchanged by this re-theme. await expect(page.getByTestId("pdf-wait-game-ribbon")).toHaveCSS( "background-color", "rgb(11, 21, 32)" diff --git a/frontend/tests/DisplayLeftRailFidelity.spec.ts b/frontend/tests/DisplayLeftRailFidelity.spec.ts index d86991632..5a9514171 100644 --- a/frontend/tests/DisplayLeftRailFidelity.spec.ts +++ b/frontend/tests/DisplayLeftRailFidelity.spec.ts @@ -18,6 +18,7 @@ import { test } from "../playwright.setup"; import { importTextOnEditorLanding, loadPageWithDefaultBackend, + openDisplayChangeQueryModal, openSelectVersionSection, } from "./test-utils"; @@ -52,6 +53,13 @@ import { * never class names or inline-style source text - the same discipline this guard has followed * since PR #352's own regression (several density-table values documented as "done" while the * actual CSS still fell through to a Bootstrap/global default). + * + * Tokyo-11 re-theme (2026-07-24, owner ruling - see docs/features/theming.md): every colour + * literal below was re-derived from the OLD #302 palette to the new Tokyo-11 values in + * `frontend/src/styles/_theme-tokens.scss` - each changed assertion carries its own "Tokyo-11" + * inline comment noting the old->new hex pair it now asserts. Corner radii are UNCHANGED here - + * this file's only radius assertion (the D14 `.notthis` pill, `10px`) is the separate + * `$theme-radius-pill` token, not touched by the Semi-radius `$theme-radius-base`/`-card` swap. */ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () => { @@ -76,26 +84,29 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () await openSelectVersionSection(page); await expect(page.getByTestId("display-rail-content")).toBeVisible(); - // `.rail-head` (§D.1, inherited verbatim) - padding:8px 10px, #16202b hairline. + // `.rail-head` (§D.1, inherited verbatim) - padding:8px 10px, divider hairline. Tokyo-11: + // $theme-divider #16202b -> #16161e, rgb(22, 32, 43) -> rgb(22, 22, 30). await expect(page.getByTestId("display-rail-header")).toHaveCSS( "padding", "8px 10px" ); await expect(page.getByTestId("display-rail-header")).toHaveCSS( "border-bottom", - "1px solid rgb(22, 32, 43)" + "1px solid rgb(22, 22, 30)" ); // Rev #3 (RD8), EP5 (SPEC-editor-polish.md §D.1 `.subject`, REV - `66px` -> `116px`) - the - // subject-card preview, aspect 63/88, `1px rgba(235,235,235,.15)` border (unchanged by EP5). - // This fixture's slot has a real selected image, so the ART variant renders (not the dashed - // empty state). + // subject-card preview, aspect 63/88, a text-tinted `.15`-alpha border (unchanged shape by + // EP5). Tokyo-11: this border is `rgba(var(--bs-body-color-rgb),.15)`, so it moved with + // $theme-text #ebebeb -> #c0caf5, rgba(235, 235, 235, .15) -> rgba(192, 202, 245, .15). This + // fixture's slot has a real selected image, so the ART variant renders (not the dashed empty + // state). const subject = page.getByTestId("display-rail-subject"); await expect(subject).toBeVisible(); await expect(subject).toHaveCSS("width", "116px"); await expect(subject).toHaveCSS( "border", - "1px solid rgba(235, 235, 235, 0.15)" + "1px solid rgba(192, 202, 245, 0.15)" ); // `.idcol .slot`/`.name` (§D.1, inherited) - 14px/700 + face 11px uppercase; name 15px. @@ -119,9 +130,10 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () await moreDetailsToggle.click(); const detailsBody = page.getByTestId("display-rail-more-details-body"); await expect(detailsBody).toBeVisible(); + // Tokyo-11: $theme-divider #16202b -> #16161e, rgb(22, 32, 43) -> rgb(22, 22, 30). await expect(detailsBody).toHaveCSS( "border-top", - "1px solid rgb(22, 32, 43)" + "1px solid rgb(22, 22, 30)" ); // RD7 - the canonical printing id is NOT repeated in "More details" (it lives once in D14) - // the metadata table still carries the OTHER Card Details rows (e.g. a Language row). @@ -129,10 +141,11 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () // D14 confidence band `.d14` (§D.1, inherited, LOCKED) - unchanged by this round; the // canonical printing id ("2X2 · 117"-shaped `.idtext`) lives here, exactly once in the rail. + // Tokyo-11: $theme-band-bg #2b3e50 -> #222234, rgb(43, 62, 80) -> rgb(34, 34, 52). const d14 = page.getByTestId("display-confidence-element"); await expect(d14).toBeVisible(); await expect(d14).toHaveCSS("padding", "8px 10px"); - await expect(d14).toHaveCSS("background-color", "rgb(43, 62, 80)"); + await expect(d14).toHaveCSS("background-color", "rgb(34, 34, 52)"); // `.artist-line` (§D.1, inherited) - unchanged. const artistLine = page.getByTestId("display-artist-section").locator(".."); @@ -195,14 +208,16 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () .click({ position: { x: 5, y: 5 } }); await expect(page.getByTestId("filters-panel-float")).toHaveCount(0); - // Machine-diff-precedent tile styling (§D.1, inherited) - unchanged by this round. + // Machine-diff-precedent tile styling (§D.1, inherited) - unchanged by this round except + // colour. Tokyo-11: $theme-success #5cb85c -> #9ece6a, rgba(92, 184, 92, .92) -> + // rgba(158, 206, 106, .92). const canonCornerTag = page.getByTestId( `select-version-tile-corner-${cardDocument15.identifier}` ); await expect(canonCornerTag).toHaveCSS("font-size", "7px"); await expect(canonCornerTag).toHaveCSS( "background-color", - "rgba(92, 184, 92, 0.92)" + "rgba(158, 206, 106, 0.92)" ); const altCornerTag = page.getByTestId( `select-version-tile-corner-${cardDocument16.identifier}` @@ -219,12 +234,13 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () await expect(page.getByTestId("display-rail-content")).toBeVisible(); // Item 6 (RD - "hangs off D14") - `.idhang`/`.idtoggle`/`.idbody` (§D.2): same surface colour - // as D14 (`#2b3e50`), starts closed, PrintingTagsBlock mounts only once opened. + // as D14 ($theme-band-bg), starts closed, PrintingTagsBlock mounts only once opened. + // Tokyo-11: #2b3e50 -> #222234, rgb(43, 62, 80) -> rgb(34, 34, 52). const identifyPanel = page.getByTestId("display-identify-panel"); await expect(identifyPanel).toBeVisible(); await expect(identifyPanel).toHaveCSS( "background-color", - "rgb(43, 62, 80)" + "rgb(34, 34, 52)" ); const identifyToggle = page.getByTestId("display-identify-toggle"); await expect(identifyToggle).toHaveCSS("font-size", "12px"); @@ -233,7 +249,8 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () await identifyToggle.click(); const identifyBody = page.getByTestId("display-identify-body"); await expect(identifyBody).toBeVisible(); - await expect(identifyBody).toHaveCSS("background-color", "rgb(34, 48, 63)"); + // Tokyo-11: $theme-raised-bg #22303f -> #24283b, rgb(34, 48, 63) -> rgb(36, 40, 59). + await expect(identifyBody).toHaveCSS("background-color", "rgb(36, 40, 59)"); // PrintingTagsBlock (reused verbatim, item 6/RD1) - the real "What's That Card?" heading. await expect(identifyBody).toContainText("What's That Card?"); @@ -273,10 +290,11 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () await expect(controlStack.getByTestId("report-card-panel")).toBeVisible(); // Sources accordion (§D.1, inherited, unchanged) - NOT one of the nine removed sections - // (owner answer #3) - still resolves its own literal values. + // (owner answer #3) - still resolves its own literal values. Tokyo-11: $theme-divider + // #16202b -> #16161e, rgb(22, 32, 43) -> rgb(22, 22, 30). await expect(page.getByTestId("display-sources-accordion")).toHaveCSS( "border-bottom", - "1px solid rgb(22, 32, 43)" + "1px solid rgb(22, 22, 30)" ); await page .getByTestId("display-sources-accordion") @@ -288,18 +306,20 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () await expect(bulkRow).toHaveCSS("gap", "6px"); await expect(bulkRow).toHaveCSS("margin-bottom", "6px"); // EP3 (SPEC-editor-polish.md §D.3, de-grey pass) - the accordion's OWN header/body go - // `#22303f` (asserted below via `display-sources-accordion`'s own background), while the - // `.src-list` surface one step further in is `#2b3e50` - see SourcesAccordion.tsx's own - // module comment for the exact token breakdown this two-tone reflects. + // $theme-raised-bg (asserted below via `display-sources-accordion`'s own background), while + // the `.src-list` surface one step further in is $theme-band-bg - see SourcesAccordion.tsx's + // own module comment for the exact token breakdown this two-tone reflects. Tokyo-11: + // $theme-band-bg #2b3e50 -> #222234, rgb(43, 62, 80) -> rgb(34, 34, 52). const sourcesList = page.getByTestId("display-sources-list"); await expect(sourcesList).toHaveCSS( "border", "1px solid rgba(0, 0, 0, 0.22)" ); - await expect(sourcesList).toHaveCSS("background-color", "rgb(43, 62, 80)"); + await expect(sourcesList).toHaveCSS("background-color", "rgb(34, 34, 52)"); + // Tokyo-11: $theme-raised-bg #22303f -> #24283b, rgb(34, 48, 63) -> rgb(36, 40, 59). await expect( page.getByTestId("display-sources-accordion").locator(".card-header") - ).toHaveCSS("background-color", "rgb(34, 48, 63)"); + ).toHaveCSS("background-color", "rgb(36, 40, 59)"); }); // RD8/rev #3 - the subject-card preview's dashed empty state, and RD7's own dedup guarantee @@ -321,9 +341,12 @@ test.describe("Display left rail CSS fidelity guard (SPEC-rail-delegacy.md)", () await expect(page.getByTestId("display-rail-subject")).toHaveCount(0); const emptySubject = page.getByTestId("display-rail-subject-empty"); await expect(emptySubject).toBeVisible(); + // Tokyo-11: $theme-light is no longer an audited study token, aliased to $theme-text this + // round (see _theme-tokens.scss's own comment) - #abb6c2 -> #c0caf5, rgb(171, 182, 194) -> + // rgb(192, 202, 245). await expect(emptySubject).toHaveCSS( "border", - "1px dashed rgb(171, 182, 194)" + "1px dashed rgb(192, 202, 245)" ); await expect(emptySubject).toContainText("No art"); @@ -445,6 +468,18 @@ test.describe("Editor-polish round: rail-head Front/Back + compare reveal, D14 p .locator(".idtext") .textContent(); + // WCAG/APCA audit fold-in (2026-07-24, PR #432's report; owner-ruled amendment to + // SPEC-editor-polish.md §D.1's `.fbtoggle` row) - `min-height:24px` closes the WCAG 2.2 + // SC 2.5.8 target-size gap (measured 51-55x23px stock). Per issue #434's own lesson, this + // asserts the REAL rendered `boundingBox()` of the visible `