diff --git a/docs/README.md b/docs/README.md index 8defec90b..2d54af83c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -239,3 +239,8 @@ system" + "Using it" + "Operating it" only). for the first one. - [`reports/`](reports/README.md) — the report-relay convention (see that directory's own README, which indexes every report currently merged). +- [`proposals/mockups/`](proposals/mockups/README.md) — BINDING design + spec + companion static-HTML mockup bundles, one directory per round; + includes durability copies of specs recovered from session tmp storage + that committed code comments on open PR branches cite by filename (see + that directory's own README for why). diff --git a/docs/proposals/mockups/README.md b/docs/proposals/mockups/README.md new file mode 100644 index 000000000..fbe996093 --- /dev/null +++ b/docs/proposals/mockups/README.md @@ -0,0 +1,51 @@ +# docs/proposals/mockups/ + +Design-artifact bundles: a BINDING owner-approved spec (`SPEC-*.md`) plus +its companion static-HTML mockup, one directory per round. These are +design artifacts, not application code — see each bundle's own header for +its "how to view" instructions. Like the rest of `proposals/`, never +published to the wiki (per +[`../../documentation-process.md`](../../documentation-process.md)) — a +spec here isn't necessarily shipped yet, and some (see below) are +durability copies of work that landed only on an open PR branch, not +`master`. + +- [`proposal-h/`](proposal-h/README.md) — mockups for + [`../proposal-h-display-layout-spec.md`](../proposal-h-display-layout-spec.md) + (the living `/display` spec) and its historical predecessor + [`../proposal-h-unified-display-page.md`](../proposal-h-unified-display-page.md). + See that directory's own `README.md` for the file-by-file breakdown. +- [`rail-delegacy/SPEC-rail-delegacy.md`](rail-delegacy/SPEC-rail-delegacy.md) + — the `/editor` left rail delegacy round: the nine grey legacy + `AutofillCollapse` drop-downs removed, their contents folded into + designed elements. Companion mockup: + [`rail-delegacy-mockup.html`](rail-delegacy/rail-delegacy-mockup.html). +- [`editor-polish/SPEC-editor-polish.md`](editor-polish/SPEC-editor-polish.md) + — the unified `/editor` consolidated polish round (eleven owner-settled + items + the slot-menu cue), inherited on top of the rail-delegacy round + above. Companion mockup: + [`editor-polish-mockup.html`](editor-polish/editor-polish-mockup.html). +- [`cardback-pdfwait/SPEC-cardback-pdfwait.md`](cardback-pdfwait/SPEC-cardback-pdfwait.md) + — the `/editor` cardback flow (PKG 1) + PDF-generation wait experience + (PKG 2), inherited on top of both rounds above. Companion mockup: + [`cardback-pdfwait-mockup.html`](cardback-pdfwait/cardback-pdfwait-mockup.html). +- [`wtc-rebuild/SPEC-wtc-rebuild.md`](wtc-rebuild/SPEC-wtc-rebuild.md) — + the "What's That Card?" (`/whatsthat`) rebuild spec: Tokyo-11 theme + adoption for the page's own prior bespoke identity. Companion mockup: + [`wtc-mockup.html`](wtc-rebuild/wtc-mockup.html). + +## Why these four are here (durability, not a new proposal) + +`rail-delegacy/`, `editor-polish/`, `cardback-pdfwait/`, and +`wtc-rebuild/` were brought into the repo as verbatim durability copies +(2026-07-24) of specs that previously lived only in a session's tmp +directory — which does not survive past that session — while committed +code comments on several open PR branches already cite each spec by +filename as its binding authority. Same gap +[`../../reference/funnel-spec.md`](../../reference/funnel-spec.md) closed +when it was recovered during the 2026-07-23 D-lettering sweep (it had +been cited from `docs/features/grid-selector.md` but never itself +committed). Each file's own header names which committed comments cite +it. These are reference copies of specs authored for their respective +(still-open, not-yet-merged) PRs, not a statement that the surfaces they +describe have shipped to `master`. diff --git a/docs/proposals/mockups/cardback-pdfwait/SPEC-cardback-pdfwait.md b/docs/proposals/mockups/cardback-pdfwait/SPEC-cardback-pdfwait.md new file mode 100644 index 000000000..15a7241eb --- /dev/null +++ b/docs/proposals/mockups/cardback-pdfwait/SPEC-cardback-pdfwait.md @@ -0,0 +1,408 @@ +# /editor flow — cardback flow (PKG 1) + PDF-generation wait experience (PKG 2) + +> Durable copy, recovered 2026-07-24 from session tmp storage (same +> durability convention as [`../../../reference/funnel-spec.md`](../../../reference/funnel-spec.md)) — +> this is the owner-approved BINDING spec that committed code comments +> across open PR branches (e.g. `CardbackApplyPrompt.tsx`, `CommonCardback.tsx`, +> `PrePrintSaveGate.tsx`, `SlotCardbackControl.tsx`, `useCardbackReminderGate.tsx`) +> cite by filename as their authority. Content below is verbatim from the +> 2026-07-24 original, including its OWNER AMENDMENTS section. + +Companion to `cardback-pdfwait-mockup.html` (same directory). Owner reviews the mockup in a browser +BEFORE any implementation. Verified with Playwright at **390px (native + forced)** and **1400px**; +zero page errors (see §H). + +**Inherited base (unchanged unless a row says REVISES):** `SPEC-editor-polish.md` + +`SPEC-rail-delegacy.md` + the corrected-bundle #302 palette. Every token tagged `I` there is still +binding; this round reads as the SAME design language (dark `#0f2537/#22303f/#2b3e50/#4e5d6c` chrome, +`$primary #df6919`, radius `0`, pill `10px`, Lato). All values below are **BINDING** (owner standing +rule 2026-07-23: a visual regression against an approved mockup is a defect). + +**Grounded in shipped code (read, not assumed):** +`PrePrintSaveGate.tsx` (flush → optional Save/Skip → `router.push("/print")`; dismiss = cancel the +whole attempt); `CommonCardback.tsx` (`CommonCardback` / `CardbackToolbarButton` / +`CommonCardbackGridSelector`; a pick dispatches `bulkReplaceSelectedImage({currentImage, selectedImage, face:Back})` **and** `setSelectedCardback` — so the toolbar entry is already project-wide canonical); +`cardbackSlice.ts` (`selectCardbacks` list); `PDFGenerator.tsx` (`imageFetchProgress {completed,total}` +from `pdfRenderService.onImageProgress`; `usePostExportContributionPrompt().notifyExportSucceeded()`); +`PostExportContributionPrompt.tsx` (`` → `/whatsthat`, once-per-session); +`QuestionFeed.tsx` (Level-1 YES / NOT SURE / NO / SKIP, `ThumbButton` min-height floor, +`QuestionFeedCounts`); `docs/features/pdf-generator.md` (eager-WASM history); +`docs/features/printing-tags.md` (the vote-queue funnel + capped-implicit mechanics — LOCKED, no new +mechanic this round). + +Legend: `I` = inherited verbatim; `N` = introduced this round; `REV` = revises a named prior decision. + +--- + +## A. Where each surface lands, at a glance + +| # | Item | Surface it touches | Kind | +| --- | ----------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------ | +| 1a | No-cardback reminder | a NEW gate step inside `usePrePrintSaveGate.startPrintFlow` (before nav) | N (gate step) | +| 1b | Apply-all + set-default prompt (toolbar) | inline footer of `CommonCardbackGridSelector`'s `GridSelectorModal` | N (inline, no stacked modal) | +| 1b | Apply-all + set-default prompt (rail) | inline block under the per-slot cardback control on the left rail | N (inline, no modal) | +| 1c | Site-default cardback | assumed to exist; UI only. Config/seed = Annex A-1 | seam only | +| 2a | Generation progress bar | `PDFGenerator.tsx` left column, replaces the bare "Fetching images: N/M" text | N (`ProgressBar`) | +| 2b | Embedded What's That Card? game | `PDFGenerator.tsx` right column, shown while `isDownloading` | N (embeds `` verbatim) | +| 2b | Game outro = PostExportContributionPrompt | replaces the game on finish (one nudge, not two) | REV #166 placement | +| 2c | Memory-safety constraint | implementer note (lazy-load on start, tear down on finish) | N (constraint) | + +--- + +## B. react-bootstrap primitive mapping (no new dependencies) + +| Element (this round) | Primitive(s) | Component reused / additive prop | +| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cardback reminder gate (1a) | `Modal` (md) — Header/Body/Footer, `Button variant="primary"` + `Button variant="outline-light"` | a new step composed INTO `usePrePrintSaveGate` (same hook that owns the Save/Skip `Modal`); the cardback picker it opens is the existing `CommonCardbackGridSelector` | +| Cardback grid (both entries) | `GridSelectorModal` (toolbar) / inline grid (rail) | `CommonCardbackGridSelector` verbatim — same `selectCardbacks`, same `bulkReplaceSelectedImage`+`setSelectedCardback` on pick | +| Apply-all + set-default prompt (1b) | `Alert`-shaped inline panel with two `Button size="sm"` (distinct variants) + a "Not now" `Button variant="link"` | NEW presentational panel; additive callback surfaced from `CommonCardback`/`CommonCardbackGridSelector` `onClick` (after the existing dispatch), plus a new `applyCardbackToAll` thunk + a `setDefaultCardback` preference write | +| Per-slot cardback control (rail) | `Card.Img`/`` swatch + `Button size="sm" variant="outline-light"` ("Choose a different back…") | reuses the rail subject-image idiom (§D SPEC-rail-delegacy `.subject`) at the back-face | +| Progress bar (2a) | `ProgressBar` (`now`=determinate; `animated striped`=indeterminate; `variant="success"`=done) | `progress` SCSS already imported (`styles.scss`); driven by the existing `imageFetchProgress` state | +| Game embed frame (2b) | `Card`/`div` frame + `next/dynamic(() => import("QuestionFeed"), {ssr:false})` | `QuestionFeed` rendered verbatim; NO new voting mechanic | +| Game outro (2b) | `PostExportContributionPrompt` (``) | the SHIPPED component, unchanged — repositioned as the embed's outro | +| Generate/Save buttons (context) | `Button` + `Spinner` | unchanged `PDFGenerator` buttons | + +Shared components gain only additive, optional, behaviour-preserving props. No display-only fork. + +--- + +## C. PACKAGE 1 — cardback flow + +### C.1 (1a) Export-time reminder — a GATE STEP, not a nag loop + +Placement is inside `usePrePrintSaveGate.startPrintFlow()`, ordered so it is a deck-completeness +decision that runs BEFORE the persistence decision and BEFORE navigation: + +``` +startPrintFlow(): + flushDraftNow(); notifyPromoteDraftPrePrint(); // unchanged (D9(3)a) + ── NEW gate step ──────────────────────────────────────────────────────────── + if ( ridingUntouchedDefault && !suppressedThisSession(projectId) ) + show CardbackReminder: + • "Choose a cardback" → opens CommonCardbackGridSelector; on pick → continue ↓ + • "Use current & continue" → suppressThisSession(projectId); continue ↓ + • dismiss (✕ / Esc / backdrop) → CANCEL the whole print attempt (no nav, nothing saved) + ── existing branch (unchanged) ────────────────────────────────────────────── + if (isAuthenticated && isProjectDirty) show Save/Skip Modal → proceedToPrint() + else proceedToPrint() // router.push("/print") +``` + +- **Fire condition — `ridingUntouchedDefault`.** The reminder appears ONLY when the project cardback + is still the site/user default and the user has never explicitly set one this project. A deck with a + chosen cardback never sees it. (Implementer seam: a boolean derived from projectSlice — "cardback was + explicitly set" — since a default always resolves a non-null `projectCardback`, null-ness alone can't + distinguish "chose the default" from "never chose". → Annex A-1.) +- **Not a nag loop.** It fires at most once per print attempt, and "Use current & continue" sets a + per-project **session** suppress (mirroring the post-export prompt's `sessionStorage` once-rule), so a + second export in the same session is silent. `CB1`. +- **Dismissal semantics (`CB2`).** ✕ / Esc / backdrop = **cancel the print attempt** — deliberately + identical to the sibling Save gate's own rule (`PrePrintSaveGate` module comment). It is not a trap: + "Use current & continue" is a first-class footer button right beside the primary, so escaping via the + X is never the only way to proceed. (This is the one genuine dismissal fork; the safer-consistency + choice is taken — see `OQ-A` only if the owner prefers dismiss = "use current".) +- **Copy** names the risk plainly ("most printers put a back on every card") and reassures ("keep the + default and continue — this only asks once per print"). Primary CTA = "Choose a cardback". + +### C.2 (1b) Apply-all + set-default prompt — two distinct, skippable choices, from both entries + +On selecting a cardback, a prompt offers **two independent, clearly-distinct, individually-skippable** +actions. It is rendered INLINE, never as a second modal stacked over the picker (honours +`CommonCardback.tsx` §4.4′'s modal-stacking ban): + +- **Toolbar entry (`CardbackToolbarButton` / `CommonCardback`) = project-wide canonical.** The pick + already runs `bulkReplaceSelectedImage({currentImage: projectCardback, …, face:Back})` + + `setSelectedCardback`, so every slot that FOLLOWS the project cardback updates automatically. The + prompt's "Apply to all" therefore means **also override the M slots that carry a deliberately-custom + back** — counted explicitly ("also overrides the 1 card with a custom back"), and never pre-applied. + The prompt renders as the inline footer of the `GridSelectorModal` body (same modal, no new one). +- **Rail entry (per-slot) = per-slot.** A pick sets ONLY that slot's back. The prompt renders inline + in the rail directly under the slot's cardback control (no modal at all — the rail per-slot picker is + already the "no modal, ever" surface). "Apply to all card backs in this deck" is an explicit, + **never-pre-checked** opt-in with a visible trap-guard line: _"Per-slot pick stays per-slot. 'Apply + to all' is never pre-checked — a single-slot choice can't silently rewrite the deck."_ (owner-ratified + no-trap rule). + +Both entries share the SAME prompt body (one component, can't drift). The two choices are made visually +distinct by **colour + label + separate action button** (not two look-alike checkboxes): + +- **"Apply to all card backs"** → `.applybtn` (primary-tinted `#df6919` border / `#ffb27d` text). +- **"Set as my default cardback"** → `.defbtn` (info-tinted `#5bc0de` border / `#8fd7ea` text). + +Each button, once tapped, flips to a green "…✓" done state; either can be skipped ("Not now" link, or +simply leaving — the base selection already stands). Neither action is required to have selected a +cardback. `CB3`. + +### C.3 (1c) Site default cardback — UI assumes it exists; the seed is a backend/config seam + +The reminder and "Set as my default" both presuppose a resolvable default. Design treats it as present. +**Two seams, noted not designed (Annex A-1/A-2):** (1) which source's cardback DOCUMENT ships as the +site fallback (the owner-drive cardback) is a backend/config seed; (2) where a user's "my default" +persists (localStorage for anonymous, account for authenticated) is an app-state seam. No backend +designed here. + +--- + +## D. PACKAGE 2 — PDF-generation wait experience + +### D.1 (2a) Progress bar — determinate where the signal exists, honest indeterminate where it doesn't + +Two phases, driven by what the pipeline actually exposes: + +1. **Fetching images (determinate).** Real signal = `imageFetchProgress {completed,total}` already + surfaced by `pdfRenderService.onImageProgress`. `ProgressBar now={min(completed/total,1)*100}`, label + "Fetching images: N of ~M". `total` is **approximate** (it undercounts duplicate cards, so `completed` + can exceed it — `pdf.worker.ts` comment) → it is shown as "~M" and the bar is **capped at 99% / never + shows a false 100%** until the phase actually ends. This replaces today's bare + `pdf-image-fetch-progress` text line, not adds to it. +2. **Assembling PDF (indeterminate).** After fetch, `@react-pdf/renderer` lays out + encodes with **no + progress callback exposed** (only `onImageProgress` exists). → `ProgressBar animated striped` with an + honest "Assembling PDF…" label. Seam noted in the box itself. `PB1` (Annex A-3): if a future render + seam exposes assemble progress, swap this leg to determinate; until then, indeterminate is the honest + presentation, not a placeholder to fill with a fake number. +3. **Done.** `ProgressBar variant="success" now={100}` + "✓ PDF ready — saved to your device." + +The box carries both `isDownloading` and `isSavingToDrive` (the Save-to-Drive path has the same fetch +phase). It never blocks the buttons above it. + +### D.2 (2b) Embedded What's That Card? game — reuse the real funnel, verbatim + +While generation runs, the right column (today: the stale live preview) is replaced by an embed frame +that renders **`` verbatim** — the exact vote-queue funnel from `/whatsthat` +(`docs/features/printing-tags.md`). **No new voting mechanic, no forked component**: Level-1 +YES / NOT SURE / NO / SKIP, Level-2 candidate grid, Level-3 exclusion chips, `QuestionFeedCounts`, and +the capped-implicit / weighted-consensus rules are all unchanged. The embed adds only chrome: + +- **Embed frame** (`.gameembed`): header "While your PDF builds — help identify a card?" + a `lazy-loaded on generate · torn down on finish` note, and a **persistent build-status ribbon** (`.geband`) that + keeps the PDF's progress visible so the two coexist (the user always sees the build state while + playing). +- **Entry.** The embed (and thus `QuestionFeed`) is **lazy-loaded only when generation starts** + (`isDownloading` true) via `next/dynamic({ssr:false})` — an idle user configuring the PDF never + carries the game's weight. (2c.) +- **Generation finishes mid-round.** The build ribbon flips to green "✓ Your PDF is ready — saved to + your device," and the game is **torn down** (unmount → memory reclaimed) and **replaced by the + outro** (D.3). Nothing is lost: `QuestionFeed` POSTs each answer the instant it's tapped + (`castVote`/`castImplicitVote`), so an already-submitted answer survives the unmount; an + un-submitted, half-considered card is simply dropped (never a partial vote). Copy in the embed foot + states this ("leaving mid-card never loses a vote"). +- **Exit.** Dismissing the outro, or navigating away, is the terminal teardown. + +### D.3 (2b) Relationship with PostExportContributionPrompt — the outro IS the prompt (one nudge) + +Owner note: they reinforce. Proposed relationship — **the existing `PostExportContributionPrompt` +becomes the game embed's outro**, not a second, separate nudge: + +- When the game embed was shown for this export, on finish the embed's game unmounts and the + **`PostExportContributionPrompt` (`` → `/whatsthat`)** renders in its place, with + the CTA reading "Keep going on What's That Card? →" (context-aware, since the user was just playing). +- The standalone bottom-of-settings mount of `PostExportContributionPrompt` is **suppressed when the + embed handled this export** — so there is exactly one nudge, governed by the unchanged + once-per-session rule (`usePostExportContributionPrompt`). When the embed did NOT run (reduced-motion + opt-out, game unavailable, or a generation too short to have shown it), the standalone prompt fires as + today. `PE1`. + +### D.4 (2c) Memory-safety — binding constraint for the implementer + +Generation is WASM/memory-heavy (`docs/features/pdf-generator.md`: `@react-pdf/renderer` instantiates a +Yoga WASM binary; a large export peaks memory). The game embed MUST NOT compound that peak: + +1. **Lazy-load only after generation starts.** The `QuestionFeed` module is `next/dynamic({ssr:false})` + and imported only when `isDownloading` becomes true — never eagerly bundled/instantiated on the print + page. (Mirrors the bug-1 lazy-WASM fix's own posture.) +2. **Do not contend for the full-resolution fetch budget.** `QuestionFeed`'s own hero images use the + small/large thumbnail tiers, NOT the semaphore-capped full-resolution path the PDF export saturates + (`pdfImage.ts` `FULL_RESOLUTION_FETCH_CONCURRENCY=3`) — so the game's image loads do not steal the + PDF's paced CDN budget. Verify this holds at wiring time; if any game image would hit the + full-resolution path, gate it. `MS1`. +3. **Tear down on finish.** The moment `isDownloading` goes false, unmount `` (revoking + any object URLs it holds) so a SECOND generation's peak isn't inflated by a lingering game — the + lightweight outro `Alert` is all that remains. Governing-premise clean throughout: transient display + only, nothing stored. + +--- + +## E. BINDING token table (`I`/`N`; every element of the affected surfaces) + +Palette tokens (all `I`, #302): `--body #0f2537`, `--panel #4e5d6c`, `--raised #22303f`, +`--conf #2b3e50`, `--text #ebebeb`, `--muted #8fa0b0`, `--light #abb6c2`, `--primary #df6919` +(hover `#be5915`), `--success #5cb85c`, `--danger #d9534f`, `--warning #ffc107`, `--info #5bc0de`, +`--input-border #4e5d6c`, `--divider #16202b`, radius `0`, pill `10px`. + +### E.1 Cardback reminder gate (1a) — real Modal on Superhero + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N | +| ---------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------- | +| `.mdialog` (Modal content) | `width:500px`, `max-width:calc(100%-24px)`; r`0` | `#4e5d6c` (real `$modal-content-bg`) / `#ebebeb` / `1px rgba(0,0,0,.2)` | shadow `0 12px 40px rgba(0,0,0,.6)` | I (framework) / N (composition) | +| `.mhead .mtitle` | `18px/700` | `#ebebeb` | `padding:14px 16px`; border-bottom `rgba(0,0,0,.2)` | I | +| `.mhead .mx` close | `20px` | `#ebebeb` @ `opacity:.7` | — | I | +| `.mbody` | `14px` | text `#ebebeb`; sub `#8fa0b0 13px` | `padding:16px`; p `mb:12px` | I | +| **`.cbremind .curback`** | **`88px`** wide, aspect `63/88` | cardback art; cap `rgba(0,0,0,.6)`/`#a99 9px`; border `1px rgba(235,235,235,.15)` | `flex:0 0 88px; gap:14px` | **N** | +| **`.dfnote` (1c seam note)** | `12px` | `#8fa0b0`; seam phrase `#ffd76a` | `mt:10px; pt:8px`; top border `#16202b` | **N** | +| `.mfoot` buttons | `.btn`=`14px / 6px 14px` r`0` | primary `#df6919`/`#fff`; "Use current" `outline-light` `#abb6c2` | `padding:12px 16px`; gap `8px`; top border `rgba(0,0,0,.2)` | I | + +### E.2 Cardback grid + apply/default prompt (1b) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N | +| ------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- | -------------------------------------- | +| `.mdialog.wide` (grid modal) | `width:640px` | as `.mdialog` | — | I | +| **`.cbgrid`** | `repeat(auto-fill,minmax(78px,1fr))`; `max-height:230px` scroll | `#22303f` / — / `1px #16202b` | `gap:8px; padding:4px` | **N** | +| **`.cbtile`** | aspect `63/88` | cardback art; sel `2px #df6919`; `.lab` `rgba(0,0,0,.55)`/`#cdd6df 8px`; ★ default `#ffc107` | — | **N** (reuses GridSelector tile idiom) | +| **`.cbprompt`** panel | — | `#22303f` / — / `1px #16202b` + left `3px #df6919` | `padding:10px 12px; mt:12px` | **N** | +| `.cbprompt .ct` title | `13px/700` | `#ebebeb` | `mb:2px` | N | +| `.cbprompt .cintro` | `12px` | `#8fa0b0` | `mb:10px` | N | +| **`.choice`** row | — | top border `#16202b` (none on first) | `gap:10px; padding:8px 0` | **N** | +| `.choice .cl .h` / `.s` | `13px` / `11px` | `#ebebeb` / `#8fa0b0` | — | N | +| **`.applybtn`** (apply-all) | `13px / 4px 10px` r`0` | transparent / `#ffb27d` / `1px #df6919`; hover fill `#df6919`/`#fff`; done `#8fe08f`/`1px #5cb85c` | — | **N** | +| **`.defbtn`** (set default) | `13px / 4px 10px` r`0` | transparent / `#8fd7ea` / `1px #5bc0de`; hover fill `#5bc0de`/`#062430`; done `#8fe08f`/`1px #5cb85c` | — | **N** | +| `.cbprompt .skip a` | `12px` underline | `#8fa0b0` | `mt:8px`; right-aligned | N | +| **`.cbprompt .trapnote`** (rail only) | `11px` | `#ffd76a`; leading `⚠` | `mt:8px; gap:5px` | **N** | +| **rail `.slotback .bthumb`** | `54px` wide, aspect `63/88` | back-face art; cap `#a99 8px`; border `1px rgba(235,235,235,.15)` | `flex:0 0 54px; gap:9px` | **N** | +| rail `.slotback .bchoose` | `btn-sm` `13px / 4px 8px` | `outline-light` `#abb6c2` | `mt:6px` | N | + +### E.3 Generation progress bar (2a) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N | +| --------------------------- | --------------------------------------- | ----------------------------------------------------------------- | ---------------------------- | ------------------------------- | +| **`.progressbox`** | — | `#22303f` / — / `1px #16202b` | `padding:10px 12px; mt:12px` | **N** | +| `.plabel` | `12px`; frac tabular | `#ebebeb`; frac `#8fa0b0` | `mb:6px` | N | +| **`.progress`** track | `height:10px`; r`0` | `#16202b` | — | **N** (Bootstrap `ProgressBar`) | +| **`.bar` determinate** | `width:min(completed/total,1)`, cap 99% | `#df6919` | transition `.3s` | **N** | +| **`.bar.indet` (assemble)** | `width:100%` | `#df6919` + `rgba(255,255,255,.18)` stripe; `barstripe 1s linear` | — | **N** | +| **`.bar.done`** | `width:100%` | `#5cb85c` | — | **N** | +| `.psub` / `.psub.donec` | `11px` | `#8fa0b0` / `#8fe08f` | `mt:6px` | N | +| **`.seamtag`** | `11px` | `#ffd76a` | `mt:6px` | **N** (2a seam label) | + +### E.4 Game embed + outro (2b) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N | +| ------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------- | +| **`.gameembed`** frame | fills right col; `min-height:420px` (phone `520`) | `#22303f` / — / `1px #16202b` | column flex | **N** | +| `.gehead` | `12px/700` uppercase; dot `8px` | `#2b3e50` / `#8fa0b0`; dot `#df6919`; `.lz` `#8fa0b0 10px` mono | `padding:8px 12px; gap:8px`; bottom border `#16202b` | N | +| **`.geband`** build ribbon | `11px`; mini bar `6px` | `#0b1520` / `#ebebeb`; mini `#16202b`→`#df6919`; done `#8fe08f` + `#5cb85c` fill | `padding:6px 12px; gap:8px` | **N** | +| `.gecard` hero | `132px`, aspect `63/88` | art; name `rgba(0,0,0,.6)`/`#ebebeb 9px`; border `1px rgba(235,235,235,.15)` | — | I (QuestionFeed hero idiom) | +| `.gecount` / `.geq` | `11px` / `14px` (qn `12px`) | `#8fa0b0` / `#ebebeb` | — | I | +| **`.tbtn`** (ThumbButton) | full-width ≤`340px`; `min-height:44px`; `15px/600` r`0` | yes `#5cb85c`; notsure `#ffc107`; no `#d9534f`; skip `link`/`#ffc107` `min-height:36px` (all outline→hover fill) | `gap:8px` | I (QuestionFeed `ThumbButton` floor — reproduced) | +| `.gefoot` | `10px` | `#8fa0b0` | `padding:6px 12px`; top border `#16202b` | N | +| **`.outro`** (PostExportContributionPrompt) | — | **`#cff4fc` / `#055160` / `1px #b6effb`** (REAL BS5 `alert-info`); link `#04414d`/700 | `margin:14px; padding:14px 16px` | **I** (shipped ``, verbatim) | +| `.spinner` (Generate btn) | `1.15em`, `.18em` ring | `rgba(255,255,255,.4)` / right transparent; `spin .75s` | — | I (`Spinner.tsx`) | + +### E.5 Print-page + editor context (reaffirmed, unchanged) + +Appbar (`.abtn`/`.cbtoolbtn`/`.pill`/`.gear`), three-region rail language (left rail `#4e5d6c`, center +sheet `#0f2537`, right project rail `#4e5d6c`), `.finishbtn`/`.genbtn` primary `#df6919`, `.genbtn2` +outline-primary, `.setrow` `12px #8fa0b0`/`#ebebeb`, and the phone stacking rule (all three regions +stack full-width in one scroll container so every surface is reachable) are `I` from the inherited +rounds / shipped `PDFGenerator` two-column layout. No value changes. + +--- + +## F. FEATURES ACCOUNTED FOR + +| Ref | Requirement (dispatch) | Accounted where | Demoed in mockup | +| --- | --------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------- | +| 1a | Reminder when heading to save/export/print w/o a cardback | §C.1; gate step in `usePrePrintSaveGate` | Scene "1a · no-cardback reminder" | +| 1a | Dismissal semantics — gate step, not a nag loop | `CB1`/`CB2` (once-per-attempt, session suppress, dismiss=cancel) | gate footer buttons + ✕ | +| 1b | Prompt: apply-to-all + separate set-default; distinct + skippable | §C.2; `.applybtn` vs `.defbtn` + "Not now" | both apply scenes | +| 1b | Toolbar = project-wide canonical | §C.2 (bulkReplace already runs; prompt overrides customs) | "1b · apply — toolbar" | +| 1b | Rail = per-slot; apply-to-all must not be a trap | §C.2 + `.trapnote`, never-pre-checked | "1b · apply — rail" | +| 1c | Site default = owner-drive cardback; assume it exists, note seam | §C.3; Annex A-1/A-2; `.dfnote` seam chip | gate + prompt seam notes | +| 2a | Progress bar; determinate if signal exists, honest indeterminate else | §D.1; `.bar` / `.bar.indet` / `.bar.done`; `.seamtag` | Fetching / Assembling / Done phases | +| 2b | Embedded WTC game, reuse real funnel mechanics/components | §D.2; `` verbatim, no new mechanic | "2 · PDF wait + game" | +| 2b | Embed frame, entry/exit, finish-mid-round | §D.2 (lazy-load entry, ribbon flip, unmount+outro, per-answer POST) | fetch → done transition | +| 2b | Coexist with PostExportContributionPrompt (prompt as outro) | §D.3; `PE1` (one nudge) | Done → outro scene | +| 2c | Memory-safety: lazy-load on start, tear down on finish | §D.4; `MS1` (no full-res contention) + teardown | embed header note | + +--- + +## G. Accessibility + +Reminder gate is a `Modal role="dialog" aria-modal` with focus on the primary; ✕/Esc/backdrop cancel +(consistent). Apply-all / set-default are two labelled `button`s (colour is never the only signal — +each has a distinct text label and a "✓" done text). The prompt's "Not now" is a real link/button. The +`ProgressBar` carries `role="progressbar"` with `aria-valuenow` in the determinate phase and +`aria-busy`/text label in the indeterminate phase (no false numeric). `QuestionFeed` keeps its shipped +a11y verbatim (the `ThumbButton` min-height floor, per-option glyph/text). The outro `Alert` keeps its +`Alert.Link`. Phone: all three regions stack in one scroll container; the modal becomes a bottom-sheet; +every control reachable (§H). + +--- + +## H. Verification + +Playwright (`shot.js`, chromium), screenshots inspected at **1400px** and **390px (native + forced)**. +**Zero page errors across all frames.** + +- `v-1400-gate.png` — 1a reminder gate over the editor; current-cardback preview, "Choose a cardback" + primary + "Use current & continue", 1c seam note. +- `v-1400-apply-toolbar.png` — 1b toolbar: `GridSelectorModal` grid + inline apply/default prompt + (project-wide copy, "also overrides custom"). +- `v-1400-apply-rail.png` — 1b rail: per-slot control + inline prompt with the never-pre-checked + trap-guard line; no stacked modal. +- `v-1400-pdf-fetch.png` / `v-1400-pdf-assemble.png` / `v-1400-pdf-done.png` — 2a determinate fetch + bar → indeterminate assemble stripe → green done; 2b game embed (ThumbButton stack + build ribbon) → + on done, game replaced by the real info-alert outro; Generate button re-enables on done. +- `v-390-native-gate.png` — phone: reminder as a bottom-sheet Modal over the stacked editor. +- `v-390-native-apply-rail.png` — phone: rail (with slot cardback + inline prompt), center sheet, and + right project rail + Print/Export all reachable in one scroll. +- `v-390-native-pdf-fetch.png` / `v-390-native-pdf-done.png` — phone print page stacks: settings → + progress → game embed (then outro), fully reachable. +- `v-phone-forced-*.png` — forced-phone frames at a wide window (transform-scaled), same reachability. + +**Bug caught + fixed in review:** (1) the phone frame first HID the left rail, hiding the 1b-rail +prompt — fixed by stacking all three regions full-width in one scroll container (every surface reachable +on the phone). (2) the "done" phase left the Generate button reading "Generating…" — fixed so it +re-enables once the download resolves (`generating = fetch||assemble`). + +--- + +## I. Annex — the seams (noted, NOT designed here) + +- **A-1 (1a/1c) Default-cardback config + "was cardback explicitly chosen".** Which source's cardback + DOCUMENT ships as the site fallback (the owner-drive cardback) is a backend/config seed. Separately, + the reminder's fire condition needs a projectSlice boolean distinguishing "chose the default" from + "never chose" (null-ness alone can't, since a default always resolves non-null). Both are + backend/state seams; the UI assumes a resolved default + a derivable "explicit" flag. +- **A-2 (1b) "My default" persistence.** "Set as my default cardback" writes a user preference: + localStorage for anonymous, account-persisted for authenticated (mirroring how favourites/projects + already split). Not designed here. +- **A-3 (2a) Assemble-phase progress signal.** `pdfRenderService` exposes only `onImageProgress`; the + `@react-pdf/renderer` layout/encode phase has no progress callback. The spec's indeterminate assemble + bar is the honest presentation; if a render seam later exposes assemble progress, swap that leg to + determinate (`PB1`). + +--- + +## J. Open questions — ONLY the genuine owner forks + +- **OQ-A (1a dismissal).** Ruled `CB2` = dismiss (✕/Esc/backdrop) cancels the whole print attempt, to + match the sibling Save gate exactly. The alternative — dismiss = "use current & continue" (a valid + default exists, so proceeding is safe) — is defensible too. Confirm the safer-consistency default, or + flip it? (Recommendation: keep cancel-on-dismiss; "Use current & continue" is a first-class button, so + it isn't a trap.) +- **OQ-B (1b toolbar apply-all scope).** For the project-wide toolbar entry, should "Apply to all" also + overwrite the M slots a user **deliberately** gave a custom back, or leave those custom slots alone? + Ruled here: it DOES override them, but the count names it ("also overrides the 1 custom back") and it + is never pre-applied. Confirm, or should custom slots be excluded from "apply to all"? + (Recommendation: override-with-count — one predictable meaning of "all"; the per-slot rail entry + remains the way to keep a slot different.) +- **OQ-C (2b game persistence after finish).** Ruled `PE1`/`D.2` = on finish the game is torn down and + the `/whatsthat` outro replaces it (memory-safety + one-nudge). The alternative is keeping the game + alive inline post-finish so a user can keep playing without leaving the page (heavier: the embed lingers + into any second generation's memory peak). Confirm teardown-to-outro, or keep-playing-inline? + (Recommendation: teardown-to-outro; the outro's link carries anyone who wants to keep going to + `/whatsthat`, where the funnel already lives.) + +## OWNER AMENDMENTS — 2026-07-24 post-review (BINDING, supersede conflicting rows above) + +1. OQ-A RULED (overrules the spec's recommendation): dismissing the no-cardback reminder + (✕/Esc/backdrop) means "use current default & continue" — the print attempt PROCEEDS. + The explicit "Use current & continue" button stays; dismiss is its shorthand, not a cancel. +2. OQ-B RULED: apply-to-all uses override-with-count AND shows the affected cards — the + prompt renders thumbnails (front + current custom back) of every slot whose custom back + would be overwritten, above the count line. Never pre-checked, unchanged. +3. NEW ELEMENT (owner addition): the sheet cell's corner flip icon (⟲, from EP6) carries a + small non-default-back indicator (dot/badge within the existing icon footprint) whenever + that slot's cardback differs from the deck default — passive awareness before any + warning. Same gating as the flip icon itself (card-holding cells only); tooltip/aria + "Custom cardback". Token-conformant; no new color roles (reuse warning or info token). +4. OQ-C RULED: teardown-to-outro as recommended (game unmounts on finish; the existing + /whatsthat outro prompt is the single post-export nudge). diff --git a/docs/proposals/mockups/cardback-pdfwait/cardback-pdfwait-mockup.html b/docs/proposals/mockups/cardback-pdfwait/cardback-pdfwait-mockup.html new file mode 100644 index 000000000..af396fe0e --- /dev/null +++ b/docs/proposals/mockups/cardback-pdfwait/cardback-pdfwait-mockup.html @@ -0,0 +1,644 @@ + + + + + +ProxyPrints — cardback flow + PDF-wait experience (two owner-ruled packages) + + + + +
+ View + + + + + + Scene + + + + + + PDF phase (2a) + + + + + Two owner-ruled packages on the /editor → print flow. Frames transform-scaled to any width; review every surface on the phone frame. +
+ +
+ + + + diff --git a/docs/proposals/mockups/editor-polish/SPEC-editor-polish.md b/docs/proposals/mockups/editor-polish/SPEC-editor-polish.md new file mode 100644 index 000000000..dc1dd6509 --- /dev/null +++ b/docs/proposals/mockups/editor-polish/SPEC-editor-polish.md @@ -0,0 +1,410 @@ +# Unified /editor — consolidated polish round (eleven owner-settled items + the ⋯ slot-menu cue) + +> Durable copy, recovered 2026-07-24 from session tmp storage (same +> durability convention as [`../../../reference/funnel-spec.md`](../../../reference/funnel-spec.md)) — +> this is the owner-approved BINDING spec that committed code comments +> across open PR branches (e.g. `AutofillCollapse.tsx`, `ConfidenceElement.tsx`, +> `DisplayPage.tsx`, `SelectVersionResults.tsx`, `SlotActionsSection.tsx`, +> `SourcesAccordion.tsx`) cite by filename as their authority. Content below +> is verbatim from the 2026-07-24 original, including its OWNER AMENDMENTS +> section. + +Companion to `editor-polish-mockup.html` (same directory). Owner reviews the mockup in a browser +BEFORE any implementation. Verified with Playwright at **390 / 900 / 1400px** (see §H). + +**Inherited base (unchanged unless a row below says REVISES):** `SPEC-rail-delegacy.md` (the nine-grey- +accordion delegacy round) and its corrected-bundle #302 palette. Every token there tagged `I` is +still binding; this round supplies only the DELTA — the rows this round revises and the rows it +introduces — plus reaffirms the surrounding inherited rows so §D is a complete page table (owner +standing rule 2026-07-23: approved token values are binding; a visual regression against an approved +mockup is a defect). + +**LOCKED, not reopened:** RD7 (mismatch-only rail badge). D14's confirmed-`✓` / "N% confident" pill +treatment (owner likes it — untouched by items 8/9). The ratified implicit-vote chip mechanics +(`printing-tags.md` "Implicit votes" + `grid-selector.md` compliance-fix): funnel chips ARE the +implicit vote (`castImplicitVote`, weight 0.25, per-outcome cap 1.0 < quorum, never human-backed, +SENSITIVE-excluded; SUGGESTED chips from `suggestedFilterTagNames`, dashed + `⌇`). No new voting +mechanic anywhere this round. + +Legend per token row: `I` = inherited verbatim; `N` = introduced this round; `REV RDx` = revises a +named rail-delegacy decision. All values are **BINDING**. + +--- + +## A. What changes, at a glance (the eleven items + cue → where they land) + +| # | Item | Surface it touches | Kind | +| --- | ----------------------------------------------- | ----------------------------------- | ------------------------------ | +| 1 | Ghost tile gains a thumbnail + `+N` | Select Version grid | REV inherited ghost tile | +| 2 | Whole-page contrast pass (absorb #413 residual) | app chrome + Filters panel controls | N tokens + reaffirm | +| 3 | Sources de-grey + densify | Sources block | REV sources rows | +| 4 | Slot Actions relocate to the top | rail head (was bottom stack) | REV RD5 | +| 5 | Subject image much larger | rail head | REV RD8 | +| 6 | Per-slot Front/Back toggle (E24) | rail head + sheet cell corner | N (never built) | +| 7 | Data-driven Sort | Select Version header | REV RD2 / O5 | +| 8 | Wrong-printing affordance restored/improved | D14 `.idtoggle` + `.notthis` | REV (post-#413 look) | +| 9 | Scryfall compare-on-hover, beside the subject | D14 pill → rail-head reveal | N (relocates existing popover) | +| 10 | Round spinner instead of loading bar | Select Version loading state | N (reuse `Spinner.tsx`) | +| 11 | Foreign-order consent / disclaimer UI | consent toast + sheet cells | N (built on `useConsentToast`) | +| cue | ⋯ slot-menu cue bigger/contrast + gated | sheet cell (`PagePreview.tsx`) | REV `SlotMenuCue` | + +--- + +## B. react-bootstrap primitive mapping (no new dependencies) + +| Element (this round) | Primitive(s) | Component reused / additive prop | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Ghost tile thumbnail + `+N` (1) | `` (first hidden copy's `smallThumbnailUrl`) + absolute `+N` overlay | the same tile renderer in `SelectVersionResults.tsx`; additive: a `hiddenCopies` group + count | +| App-chrome action buttons contrast (2) | `Button variant="outline-*"` with explicit border/text token | existing editor toolbar buttons — colour tokens only, no structural change | +| Filters panel controls fully themed (2) | `DPIFilter`/`SizeFilter`/`LanguageFilter`/`TagFilter`/`MatureContentFilter` | unchanged components; the residual is CSS-token coverage on the dark rail, not new markup | +| Sources block de-grey + dense (3) | `AutofillCollapse`? **NO** — the shipped inline `SourcesAccordion` (already inline, owner answer #3); `react-bootstrap-toggle` restyled via `.rail-source-toggle` (shipped) at 31px; `Form.Control` filter input; virtualized list | `SourcesAccordion.tsx` (tokens only + a height/virtualization prop) | +| Front/Back toggle (6) | `ToggleButtonGroup type="radio"` (2 segments) near the subject; sheet-cell `Button` at the reserved E24 corner | new toggle; sheet corner is the reserved `PagePreview.tsx:618` slot (additive `onSlotFlip` prop, render-gated like `SlotMenuCue`) | +| Compact top Slot Actions (4) | 3 × `Button size="sm"` icon buttons (Change/Duplicate/Delete) in a row | `SlotActionsSection` — same actions (`getCardSlotMenuActions`), icon-compact layout instead of full-width stack | +| Larger subject image (5) | `Card.Img`/`` at a bigger preview footprint | reuses the selected tile's thumbnail URL (not a second full render) | +| Data-driven Sort (7) | `Form.Select size="sm"` over a NEW ordering list; client-side comparator over already-carried fields | replaces the 6-option `SortByOptions` select on this surface (see §F annex) | +| Wrong-printing affordance (8) | `Button` toggling a `Collapse` (identify panel) restyled to the pre-#413 pill idiom; `Button variant="outline-danger"` (`.notthis`) restyled to a pill | `IdentifyPanel` toggle (DisplayPage) + `ConfidenceElement`'s `.notthis` — CSS only, behaviour preserved | +| Compare-on-hover / -tap (9) | `OverlayTrigger`/custom reveal anchored beside the subject image; trigger moved to the D14 pill; `(pointer:coarse)` → tap-toggle | `ConfidenceElement` — reuses `buildScryfallReferenceImageUrl` (already client-side; §F annex) | +| Round spinner (10) | `` (`components/Spinner.tsx`, `.spinner-border`) | the site's canonical spinner, reused verbatim | +| Consent toast + disclaimer (11) | `useConsentToast()` / `ConsentToast` (react-bootstrap `Alert` `role="alertdialog"`, `bottom-end`); denied-state cell placeholder + deck banner | `consent/*` infra reused; new call site + hidden-state render | +| ⋯ cue bigger/gated (cue) | the existing `SlotMenuCue` styled button | `PagePreview.tsx` — size/contrast tokens + render gate on "slot holds a card" | + +Shared components gain only additive, optional, behaviour-preserving props. No display-only fork. + +--- + +## C. Rail-head composition after items 4/5/6 (the one structural change this round) + +``` +┌ rail-head (#22303f) ───────────────────────────────────────────────┐ +│ [ SUBJECT IMAGE ] Slot 3 · Front (item 5: 116px, was 66)│ +│ [ 116px wide ] Lightning Bolt │ +│ [ aspect 63/88 ] requested ≠ shown: 2ED 161 (RD7 mismatch only) │ +│ [ front|back art] [ Front | Back ] (item 6 toggle) │ +│ [⇄] [⧉] [🗑] (item 4: compact, │ +│ More details › was full-width RD5)│ +│ └ item 9 Scryfall compare reveal floats HERE, beside the subject │ +├ More details ▸ ── full CardMetaTable + Download/Favourite (rev #1, unchanged) +├ D14 band ─────── LOCKED; ONE canonical id "2X2 · 117"; pills are the item-9 compare trigger +├ identify panel ─ item 8 pill "🔍 Wrong printing? Search the right one" → PrintingTagPicker +├ artist line ──── unchanged +├ Sources ──────── item 3: de-greyed + densified +├ Select Version ─ header [N · Sort▾(item 7) · Filters▾] → filters panel → grid (item 1 ghost) / spinner (item 10) +└ control stack ── item 4 REV: Print Options + Report ONLY (Slot Actions moved up) +``` + +The bottom control stack loses its Slot-Actions group (moved to the head). It keeps Print Options +(bleed) + Report — neither is a per-slot identity action. + +--- + +## D. BINDING token table (every element on the affected page; `I`/`N`/`REV`) + +Palette tokens are the inherited §0 #302 set (`--body #0f2537`, `--panel #4e5d6c`, `--raised #22303f`, +`--conf #2b3e50`, `--card-header #4e5d6b`, `--text #ebebeb`, `--muted #8fa0b0`, `--light #abb6c2`, +`--primary #df6919`/hover `#be5915`, `--success #5cb85c`, `--danger #d9534f`, `--warning #ffc107`, +`--info #5bc0de`, `--divider #16202b`, radius `0`, pill `10px`). All `I`. + +### D.1 Rail-head (items 4/5/6/9) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N/REV | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------- | +| `.rhead` | — | `#22303f` / `#ebebeb` / bottom `#16202b` | `padding:8px 10px` | I | +| `.rhead-row` | `position:relative` | — | `gap:10px; align-items:flex-start` | I | +| **`.subject` (5)** | **`116px` wide**, aspect `63/88` (≈`162px` tall) | art thumbnail; caption `rgba(0,0,0,.6)`/`#cdd6df` `8px`; border `1px rgba(235,235,235,.15)` | `flex:0 0 116px` | **REV RD8** (was 66px) | +| `.subject .backart` (6) | fills subject | dark `#2a2320`↔`#1f1a17` stripe / `#a99` `9px` | shown when `data-face=back` | N | +| `.subject.empty` | same `116×162` | transparent / `#8fa0b0` `10px` / `1px dashed #abb6c2` | centred | REV RD8 (footprint) | +| `.idcol` | slot `14/700`, name `15px`, empty name italic `#8fa0b0` | `#ebebeb`; face `#8fa0b0` `11px` uppercase | `flex:1; min-width:0` | I | +| `.mismatch` (RD7) | `10px` mono | `#ffc107` / `#111` | `mt:5px; padding:1px 7px`; hidden in empty state | I | +| **`.fbtoggle` (6)** | segment `11px/700`, `padding:2px 12px` | seg `#22303f`/`#8fa0b0`; active `#5bc0de`(info)/`#062430`; border `#6b7d8e` | `margin-top:7px`; divider `1px #6b7d8e` | **N** | +| **`.slotacts-top .iact` (4)** | **`32×30`** icon button, `14px` glyph | transparent / `#abb6c2` / `1px #abb6c2`; danger `#f0a6a3`/`1px #d9534f`; hover fills | `gap:6px; margin-top:8px; flex-wrap` | **REV RD5** (was full-width block stack at bottom) | +| `.detmore` (rev #1) | `11px` | `#8fa0b0` (hover `#ebebeb`) | `margin-top:8px; gap:4px` | I | +| `.detbody` block (rev #1) | rows `11px`; key `10px` uppercase `#8fa0b0` flex `0 0 88px`; value `#ebebeb`; id-copy `#5bc0de` dotted; tag pill `#4e5d6c`/`#ebebeb` r10 | top border `#16202b` | `mt:8px; pt:8px` | I | +| More-details actions ×2 | `btn-sm` | `btn-outline-light` | `gap:6px; mt:8px` | I | +| **`.compare` reveal (9)** | `width:150px`, aspect `63/88` inner img | `#0b1520` / — / `1px #5bc0de`; shadow `0 8px 22px rgba(0,0,0,.6)`; cap `9px #8fa0b0`, `b`→`#5bc0de` | `position:absolute; left:126px; top:0; z-index:40; padding:5px` | **N** | + +### D.2 D14 band + identify (items 8/9 — pills LOCKED, affordances restyled) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N/REV | +| ------------------------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------- | +| `.d14` band | `12px`, wrap, `gap:8px` | `#2b3e50` / — / bottom `#16202b` | `padding:8px 10px` | I | +| `.seticon` + `.check`/`.score` | `30×30` circle; check `15×15`; score `9px/800` | as inherited (`#4e5d6c`/`#7f8fa0`; check `#5cb85c`; score `#df6919`) | inherited | I | +| `.idtext` | `12px` mono | `#ebebeb` | — | I | +| **`.statepill.confirmed`** | `11px/700` | — / `#a7e08a` / `#3f7a2f`; r10 | `padding:1px 8px` | **I — LOCKED** | +| **`.statepill.suggested`** | `11px/700` | — / `#ffb27d` / `#df6919`; r10 | `padding:1px 8px` | **I — LOCKED** | +| `.statepill.cmp` trigger (9) | — | (look unchanged) `cursor:zoom-in`; hover/focus or tap toggles the compare reveal | — | N (behaviour only; pill look untouched) | +| **`.notthis` ✗ (8)** | `11px/600`, `padding:2px 10px` | `rgba(217,83,79,.12)` / `#f0b3b1` / `1px rgba(217,83,79,.55)`; **radius `10px`**; hover → solid `#d9534f`/`#fff` | `margin-left:auto` | **REV** (was flat `btn-outline-danger` bar; owner answer #2 opacity-.6-on-confirmed preserved) | +| **`.idtoggle` (8)** | `12px/600`, `padding:3px 12px` | `rgba(223,105,25,.12)` / `#ffb27d` / `1px rgba(223,105,25,.55)`; **radius `10px`**; leading `🔍`; hover → solid `#df6919`/`#fff` | in `.idhang` `padding:0 10px 8px` | **REV** (was bland full-width `1px #6b7d8e` grey bar, post-#413) | +| `.idbody` / `.cons` / `.idsearch` / `.candgrid` / `.cand` / `.attrfollow` | as inherited | as inherited | as inherited | I | + +### D.3 Sources (item 3 — de-grey + densify; REVISES the inherited inline Sources rows) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N/REV | +| ------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------- | +| **`.src-summary`** | `12px/700` label; count `12px` | **`#22303f`** / `#ebebeb`; count `#8fa0b0`, N `#5cb85c` | `padding:7px 10px; gap:8px` | **REV** (was `#4e5d6b` grey band) | +| **`.src-pins`** | pinchip `11px` | **`#22303f`**; chip `#2b3e50`/`#ebebeb`/`rgba(0,0,0,.22)`; ★ `#ffc107` | `gap:4px; padding:0 10px 7px` | **REV** (was grey) | +| **`.src-body`** | — | **`#22303f`** | `padding:8px 10px` | **REV** (de-greyed) | +| **`.src-body .filter`** (primary find path) | `14px` | `#2b3e50` / `#ebebeb` / **`1px #abb6c2`** (emphasised) | `padding:7px 10px; mb:8px` | **REV** (border brightened; is now the primary path) | +| Bulk buttons ×3 / Save-defaults seam | `btn-sm` | `btn-outline-light` / `btn-outline-success` disabled (#353) | `gap:6px; mb:6/8px` | I | +| **`.src-cap`** count/cap caption | `10px` | `#8fa0b0`, N `#ebebeb` | `mb:5px` | **N** (states "Showing 10 of 254 — filter to narrow") | +| **`.src-list`** | `max-height:186px` scroll; **virtualized** | `#2b3e50` / — / `rgba(0,0,0,.22)` | — | REV (virtualization added) | +| **`.src-row`** | **`height:34px`** | / — / bottom `rgba(0,0,0,.22)` | `padding:2px 8px; gap:8px` | **REV** (was ~70px screenshot / ≥38px spec) | +| **`.tgl` source toggle** | **`52×31`** (`--src-toggle-h`) | on `#df6919`/`#fff`; off `#4e5d6c`/`#8fa0b0`; `#6b7d8e` border; `10px/700` | — | **REV** (was `54×38`; see a11y note §G) | +| **`.src-row .pin` ★** | **`30×30`** | pinned `#ffc107` / unpinned `#5b6b7b`; `15px` | — | **REV** (was 38×38) | +| `.src-row .snm` | `12px` ellipsis | `#ebebeb` | `flex:1` | REV (13→12px) | + +### D.4 Select Version — Sort (7), grid (1), loading (10) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N/REV | +| ---------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ | -------------------------------------------------------- | +| `.svtitle` / `.svhead` / `.filtersbtn` | as inherited | as inherited | as inherited | I | +| **`.sortsel` (7)** | `12px`, `max-width:172px` | `#22303f` / `#ebebeb` / `1px #4e5d6c`; r0 | `padding:3px 6px` | REV RD2 (option list is data-driven; see §F) | +| `.vgrid` / `.vtile` (`72/88/112`) / corner tags / REQ / confirm ribbon | as inherited | as inherited | `gap:6px` | I | +| **`.vtile.ghost` (1)** | `72px`, aspect `63/88` | **thumbnail** stripe + `.gdim rgba(11,21,32,.62)` overlay; `.gplus` `16px/800 #fff` text-shadow; `.gcap` `7px` `rgba(0,0,0,.6)`/`#cdd6df`; outline `1px rgba(235,235,235,.15)` | grid gap | **REV** (was transparent `1px dashed #abb6c2` empty box) | +| **`.vloading` (10)** | min-height `140px`, gap `10px` | `#8fa0b0` `12px` | replaces `.vgrid` when loading | **N** | +| **`.spinner-border` (10)** | **`2.6em` ×`2.6em`**, border `.25em` | `#df6919` ring, right-transparent; `spin .75s linear infinite` | — | **N (reuse `Spinner.tsx`)** | + +### D.5 Filters panel (item 2 — residual absorbed; otherwise inherited) + +Every filter fieldset (`.fset` legend `10px` uppercase `#8fa0b0`; DPI/Size `.frange` track `4px` +`#3a4653`, fill `#df6919`, knob `12px #abb6c2`/`#fff`; `.ftree` `#2b3e50`/`1px #4e5d6c`; `.swtrack` +NSFW `34×18` off `#3a4653`/on `#df6919`), the funnel `.seg`/`.tchip`/`.tchip.suggested`(dashed +`⌇`)/ +`.implicit-note`, and the phone-inline vs desktop/tablet-float behaviour (RD4) are all **I** (inherited +verbatim). Item 2's contribution here is **coverage, not new tokens**: these controls must inherit +the dark-rail theme rather than fall through to bare browser widgets (the #413 residual). Reaffirmed +binding; no value changes. + +### D.6 App chrome contrast (item 2) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N/REV | +| --------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------- | +| **`.abtn`** editor toolbar action | `13px`, `padding:5px 12px` | `#22303f` / `#ebebeb` / `1px #46586a` (hover border `#abb6c2`) | `gap:10px` | **N** (fixes near-invisible grey-on-dark "Add"/"Add card" ghosts) | +| **`.abtn.primary`** | `13px` | transparent / `#8fe08f` / `1px #5cb85c` | — | **N** | +| Rail/right-rail buttons page-wide | `btn-sm` | outline variants carry text at ≥ `--light #abb6c2` on dark; solid `--primary` fills for primary actions | — | N (contrast floor, no structural change) | + +### D.7 Control stack (item 4 revision) + +| Element | Sizing | Colour | Spacing | I/N/REV | +| -------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------- | ------------------------------ | ----------------------------------------------- | +| `.cstack` Print Options group + `.bleedsel` + `.cs-hint` | as inherited | as inherited | `padding:8px 10px` | I | +| Slot-Actions group in `.cstack` | — | — | — | **REMOVED (REV RD5 → moved to rail head, D.1)** | +| Report button `.reportbtn` → `.reportpanel` chips | `btn-sm` / `11px` chips | `btn-outline-danger` / `#22303f`/`#ebebeb`/`1px #4e5d6c` (hover danger) | `cs-foot` top border `#16202b` | I | + +### D.8 Sheet cell (the ⋯ cue + E24 flip + item-11 hidden state) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N/REV | +| --------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| **`.slot-cue` ⋯ (cue)** | **`26×26`**, glyph **`17px`** | **`rgba(11,21,32,.92)`** / `#fff` / **`1.5px #abb6c2`**; r4; shadow `0 1px 3px rgba(0,0,0,.5)`; hover `#df6919` | `bottom:3px; right:3px; z-index:3` | **REV** (`PagePreview` `SlotMenuCue` was `20×20`, `rgba(22,32,43,.85)`, `13px`, `1px #7f8fa0`) | +| **cue render gate** | — | rendered ONLY when the slot holds a card **and** a context menu is wired | — | **REV** (was gated on `onSlotContextMenu != null` alone) | +| **`.slot-flip` ⟲ (E24, item 6)** | `26×26`, glyph `14px` | `rgba(11,21,32,.92)` / `#fff` / `1.5px #abb6c2`; r4; hover `#5bc0de`/`#062430` | `top:3px; right:3px; z-index:3`; gated to filled cells | **N** (ships the reserved `PagePreview.tsx:618` corner) | +| **`.cell.ext` hidden state (11)** | fills cell | `#141f2b` / `#8fa0b0` `8px` / `1px dashed #46586a`; lock `🔒 15px` | shown when `data-shared=hidden`; cue/flip suppressed | **N** | +| **`.extbanner` (11)** | `11px` | `rgba(11,21,32,.95)` / `#ebebeb` / `1px #5bc0de`; link `#5bc0de` | `position:absolute; top:10px; centred; z-index:20` | **N** | + +### D.9 Consent toast (item 11) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N/REV | +| -------------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------- | +| **`.consent`** (react-bootstrap `Alert`, `role=alertdialog`, `bottom-end`) | `width:300px`, `max-width:calc(100%-28px)` | `#0b1520` / `#ebebeb` / `1px #5bc0de` + left `4px #5bc0de`; shadow `0 10px 30px rgba(0,0,0,.6)` | `position:fixed; right:14px; bottom:14px; z-index:120; padding:12px 14px` | **N** | +| `.consent .ct` title / `.cx` close | `13px/700` / `15px` `#8fa0b0` | — | `mb:6px` | N | +| `.consent .cm` disclaimer | `11px` | `#abb6c2`; risk phrase `#ffc107` | `mb:10px; line-height:1.45` | N | +| `.consent .cbtns` | `btn-sm` | Show → `btn-outline-info`; Keep hidden → `btn-outline-light` | `gap:8px` | N | + +--- + +## E. Owner decisions logged this round (EP = editor-polish; all PROPOSED, pending sign-off) + +- **EP1 (item 1).** The ghost tile renders the first hidden copy's thumbnail, dimmed + (`rgba(11,21,32,.62)`), with a centred `+N` and a "more copies" caption. What it compresses: + additional copies of the **same identified printing** (not distinct printings). +- **EP2 (item 2).** A page-wide button-contrast floor: outline buttons carry text at ≥ `--light` + (`#abb6c2`) on every dark surface; the editor toolbar's near-invisible grey ghosts get an explicit + `1px #46586a` border + `#ebebeb` text (`.abtn`). The #413 unstyled residual — the Filters float's + DPI/Size/Language/Tags/NSFW controls — is absorbed: all inherit the dark-rail theme (no bare + browser widgets). No token in the funnel/filter body changes value; this is coverage. +- **EP3 (item 3, REVISES the inherited Sources rows).** The grey `#4E5D6B` header/pins/body band is + killed → dark `#22303f` throughout. Rows densify to `34px` (toggle `52×31`, pin `30×30`, name + `12px`). The filter input becomes the primary find path (brighter `1px #abb6c2` border) and the + list is **virtualized** with a "Showing N of 254 — filter to narrow" cap caption. +- **EP4 (item 4, REVISES RD5).** Slot Actions (Change / Duplicate / Delete) relocate from the + full-width bottom stack to a **compact icon row** (`32×30` buttons) in the rail head, beside the + subject image. The bottom control stack keeps only Print Options + Report. No full-width slot-action + buttons anywhere. +- **EP5 (item 5, REVISES RD8).** The subject image grows from `66px` to **`116px`** wide (aspect + `63/88`), anchoring the relocated actions row and the Front/Back toggle. Still a preview, not a + second full render — Select Version stays the art surface. Verified it does not starve the 390px + grid (phone reachability holds, §H). +- **EP6 (item 6).** A per-slot **Front/Back** segmented toggle (`ToggleButtonGroup`, info-blue active) + sits beside the subject image; flipping it shows the back-face art in the subject preview. The same + action ships at the sheet cell's reserved E24 top-right corner (`PagePreview.tsx:618`) as a `⟲` + flip button, render-gated to filled cells. +- **EP7 (item 7, REVISES RD2/O5).** Sort is replaced by data-driven orderings: **Confirmation status · + Community vote weight · Resolution (DPI) high→low · File size low→high · Pinned sources first · Name + (A→Z) · Date added**. Five are client-side over already-carried fields today; "Community vote + weight" needs a backend numeric that does not exist yet (§F annex, Q1). +- **EP8 (item 8, REVISES the post-#413 look).** The wrong-printing affordance is restyled from the + bland full-width grey bar to the **pre-#413 DeckbuilderConfirmAffordance pill idiom**: a tinted, + rounded (`radius:10px`), higher-contrast control (`.idtoggle` primary-tinted; `.notthis` + danger-tinted). Behaviour unchanged (`.idtoggle` opens the picker; `.notthis` casts the `isNoMatch` + vote and stays de-emphasised at `opacity:.6` on a confirmed printing per owner answer #2). The D14 + Confirmed/`% confident` pills are **not** touched. +- **EP9 (item 9).** The Scryfall compare reveal moves its trigger from the set-icon to the D14 + Confirmed/`% confident` **pill** (owner ask), and its anchor to **beside the subject image**. Touch + equivalent: the pill is a `tap-toggle` under `(pointer:coarse)` (no hover). Display-only, nothing + stored (governing premise + #271). **No backend seam is required** — the URL is already derived + client-side (§F annex). +- **EP10 (item 10).** The unified page's loading bar is replaced with the site's canonical round + spinner, `components/Spinner.tsx` (`.spinner-border`), tinted `--primary`, in the Select Version + loading state. +- **EP11 (item 11).** The deferred-half foreign-order consent UI ships as: a bottom-right + `ConsentToast` (`useConsentToast`, `Alert role=alertdialog`) shown to a recipient of a shared deck + containing orphan Drive IDs — **deny-by-default** (dismiss/Escape = decline, per the shipped + contract); a denied resting state where orphan cells show a `🔒 External image hidden` placeholder + with cue/flip suppressed; and a deck-level "N external images hidden — Review" banner (the + reversible "Show/Hide" the deferred spec calls for). Owner's self-import/own-decks surfaces are + unaffected (already allowed by default). Backend half tracked by issue #414. See Q2. +- **EPcue (cue).** The `⋯` `SlotMenuCue` grows to `26×26`, glyph `17px`, higher-contrast + (`rgba(11,21,32,.92)` bg, `1.5px #abb6c2` border, `#fff` glyph, drop-shadow) so it reads over card + art, and its render gate tightens from "context menu wired" to "**slot holds a card** AND context + menu wired" (empty slots show no cue). + +Honoured LOCKED decisions (unchanged): RD7 (mismatch-only badge), D14 + #271 + owner answer #2 (pill +treatment, ✗-stays-de-emphasised), RD4 (tier-conditional Filters float), RD6 (metadata in More +details only), the ratified implicit-vote chip mechanics, §F one continuous grid. + +--- + +## F. FEATURES ACCOUNTED FOR — the eleven items + cue + +| # | Item | Accounted where | State demoed in mockup | +| --- | ----------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------- | +| 1 | Ghost tile thumbnail + `+N` | §D.4 `.vtile.ghost`; EP1 | always-on ("+9 · more copies") | +| 2 | Whole-page contrast + #413 residual | §D.5, §D.6; EP2 | app-chrome `.abtn`; themed filter controls (tablet float shot) | +| 3 | Sources de-grey + densify | §D.3; EP3 | Sources block (all frames) | +| 4 | Slot Actions → top | §D.1 `.slotacts-top`, §D.7 (removed) ; EP4 | rail head icon row | +| 5 | Subject image larger | §D.1 `.subject`; EP5 | rail head 116px | +| 6 | Front/Back toggle (E24) | §D.1 `.fbtoggle`, §D.8 `.slot-flip`; EP6 | `Face` demo toggle (front/back) + sheet `⟲` | +| 7 | Data-driven Sort | §D.4 `.sortsel`; EP7 | Sort dropdown option list | +| 8 | Wrong-printing affordance | §D.2 `.idtoggle`/`.notthis`; EP8 | D14 band + identify pill | +| 9 | Scryfall compare-on-hover/-tap | §D.1 `.compare`, §D.2 `.statepill.cmp`; EP9 | `Compare` demo toggle (beside subject) | +| 10 | Round spinner | §D.4 `.vloading`/`.spinner-border`; EP10 | `Grid` demo toggle (loading) | +| 11 | Foreign-order consent/disclaimer | §D.8 `.cell.ext`/`.extbanner`, §D.9 `.consent`; EP11 | `Shared deck` demo toggle (owner/consent/denied) | +| cue | ⋯ cue bigger/contrast + gated | §D.8 `.slot-cue`; EPcue | sheet cells (filled show cue, empty don't) | + +### Annex — backend fields (item 7 sort, item 9 compare): what the response ALREADY carries + +Read from `schema_types.ts` `Card`/`CardDocument` and `ConfidenceElement.tsx`/`scryfallReference.ts`. + +**Item 7 sort — already carried (client-side comparator, no backend work):** + +- `dpi: number` → "Resolution (DPI) high→low" ✓ +- `size: number` → "File size low→high" ✓ +- `printingTagStatus: PrintingTagStatus` + `canonicalCard` / `suggestedCanonicalCard` → + "Confirmation status" (resolved → suggested → unknown) ✓ +- `sourceName`/`sourceId`/`priority: number` → "Pinned sources first" (joined with the rail's own + pinned-sources set, already client-side) ✓ +- `name`, `dateCreated`, `dateModified` → Name / Date added ✓ (the current `SortByOptions`) + +**Item 7 sort — NOT carried (needs a backend seam):** + +- A numeric **community vote weight / net polarity per card**. The response carries a _status_ enum + (`printingTagStatus`) and the resolved/suggested printing, but no per-card numeric weight. + `suggestedCanonicalCardConfidence` exists as a **currently-always-`undefined` seam field** — the + same one D14's "N% confident" reads. "Community vote weight" as a sort ordering depends on that (or + a sibling) numeric landing. → **Q1.** + +**Item 9 compare — DEVIATION from the dispatch's annex (reported honestly):** + +- The dispatch says "frontend needs the canonical printing's Scryfall image URI in the payload (small + backend seam)." **This is not accurate against the shipped code.** `buildScryfallReferenceImageUrl(expansionCode, collectorNumber)` + (`features/display/scryfallReference.ts`) already derives the image URL client-side via Scryfall's + documented `?format=image` redirect — no Scryfall UUID lookup, no payload field, no backend change. + `ConfidenceElement.tsx` already uses it for the set-icon popover today. **Item 9 is pure frontend**: + move the trigger to the pill, anchor the reveal beside the subject, add the touch tap-toggle. The + payload already carries `canonicalCard`/`suggestedCanonicalCard` (`expansionCode`+`collectorNumber`), + which is all the URL builder needs. (Posture-clean either way: transient display, nothing stored.) + +--- + +## G. Accessibility + +Cue/flip are focusable `button`s with `aria-label` (`26×26` ≥ the practical touch floor). +`ToggleButtonGroup` Front/Back carries `aria-pressed`; the D14 pill compare-trigger is a +`role=button tabindex=0` with an `aria-label` naming the compare action (colour never the only +signal). The consent toast is `role="alertdialog"` with focus moved to "Show images" on appear +(matches the shipped `ConsentToast`). The spinner keeps `role="status"` + visually-hidden "Loading…". +Sort is a labelled `Form.Select`. Filter fieldsets keep text legends; funnel chips keep `+/−/·` glyph +state; NSFW stays `role="switch"`. + +**Flagged a11y tension (owner-ruled, item 3/EP3):** the densified source **toggle drops to `31px`** +and the pin to `30px`, below the inherited §G `≥38px` (`ToggleButtonHeight`) guideline. The owner +explicitly ruled "31px-toggle class or plain switch," so this is binding, not an open question — but +it is a real regression against that guideline and is recorded here. Mitigation carried in the +mockup: the **row itself is `34px` and the whole row is the toggle's hit surface**, so the effective +tap target exceeds the visible toggle. If the owner later wants the 38px floor back, revert +`--src-toggle-h` to `38px` and row height to `≥40px` (costs the vertical density EP3 buys). + +--- + +## H. Verification + +Playwright (`shot.js`, chromium), screenshots inspected at **390px (native phone)**, **900px (forced +tablet)**, **1400px (desktop + forced-phone-on-wide)**. **Zero page errors across all frames.** + +- `v-1400-auto.png` — three-region desktop: big subject + Front/Back + compact slot-action icons + + restyled wrong-printing pills + de-greyed dense Sources + data-driven Sort + ghost "+9" tile; + sheet cells carry `⟲` flip + `⋯` cue, empty cell 7 carries neither. +- `v-1400-railhead.png` / `-details.png` / `-back.png` / `-empty.png` — rail-head states: lean head, + metadata disclosure, back-face subject + "Slot 3 · Back", dashed empty state (mismatch suppressed). +- `v-1400-compare.png` — item 9: Scryfall reference revealed **beside** the 116px subject image. +- `v-1400-d14-confirmed.png` — LOCKED D14 confirmed: green `✓` + "Confirmed" pill + `✗` at opacity .6. +- `v-1400-loading.png` — item 10: round `.spinner-border` (orange) replacing the bar. +- `v-1400-consent.png` / `v-1400-shared-hidden.png` — item 11: bottom-right consent alertdialog; + denied resting state (locked cells + deck "N hidden — Review" banner). +- `v-1400-sheet-cue.png` — EPcue: enlarged high-contrast `⋯`/`⟲` on filled cells; empty cell gated off. +- `v-1400-rail-allopen.png` / `v-1400-filters-float.png` — full rail with filters+identify+report + open; RD4 desktop Filters float centred with backdrop. +- `v-390-auto-full.png` / `v-390-all-open.png` / `v-390-rail-bottom.png` — native phone bottom-sheet: + the whole rail (head → D14 → identify → artist → Sources → Select Version → control stack) reachable + by scrolling; the 116px subject does not starve the grid. +- `v-390-compare.png` — item 9 touch reveal (tap-toggle) at 390px. +- `v-390-consent.png` — phone consent (real `ConsentToast` is `position:fixed bottom-end`; in the tall + native auto frame it anchors at frame-bottom, best seen in the desktop shot; behaviour is identical). +- `v-900-tablet.png` / `v-900-tablet-filters.png` — tablet start-drawer + Filters floated to viewport + centre with the fully-themed DPI/Size/Language/Tags/NSFW controls (item 2 residual absorbed). +- `v-1400-forced-phone.png` / `v-phone-bottom-stack.png` — forced-phone frame at 1400px, scrolled to + the bottom control stack (Print Options + Report), proving phone reachability to the very end. + +--- + +## I. Open questions — ONLY the two that are genuinely the owner's + +Everything in the dispatch is ruled; these two are real forks the dispatch does not settle: + +- **Q1 (item 7).** "Community vote weight" as a sort ordering needs a per-card numeric weight the + response does not carry today (the `suggestedCanonicalCardConfidence` seam is always `undefined` + until a backend PR populates it). **Show the option now, disabled/greyed until the seam lands, or + omit it until then?** The other five orderings are client-side-ready and ship immediately either + way. (Recommendation: ship the five now; add "vote weight" when the numeric exists.) +- **Q2 (item 11).** The shipped `useConsentToast` stores one accept/decline **per key, in + sessionStorage** (survives reloads, not "clear site data"; dismiss = terminal decline — no + "ask again" state). The deferred-half spec asks for a **per-deck recipient opt-in** with a + **reversible Hide**. Two sub-questions the base mechanism doesn't answer natively: **(a)** scope the + consent key per shared-deck id (so different shared decks ask independently) — yes/no? **(b)** the + reversible "Show/Hide after an initial decline" is beyond the base toast's `Promise` + contract (the consent-toast doc itself flags this needs a small mechanism extension, not a variant) + — is the deck-level banner's "Review/Show" toggle (mockup's approach) the intended reversibility, or + do you want the toast itself re-openable? (Recommendation: per-deck key (a=yes); reversibility lives + in the persistent deck banner, leaving the base toast's contract untouched.) + +## OWNER AMENDMENTS — 2026-07-24 post-review (BINDING, supersede conflicting rows above) + +1. "More details" disclosure RELOCATES: it renders directly UNDER the D14 confidence band + (the printing-accuracy banner: confirmed/%confidence pills + canonical printing id), not + in the rail-head block. Rail-head keeps: subject image (116px), identity text, actions + icon row, Front/Back toggle. Ruled without a design re-pass; implementer places it with + the band's existing spacing rhythm (§D.2 divider tokens apply at the new boundary). +2. §I Q1 RULED (recommended option): ship the five client-side sort orderings now + (Confirmation status, DPI, File size, Pinned-first, Name/Date); "Community vote weight" + waits for the numeric seam — render nothing for it (no disabled placeholder). +3. §I Q2 RULED (recommended option): consent key scoped per shared-deck id; reversibility + lives in the deck-level "N external images hidden — Review" banner; the base + useConsentToast Promise contract stays untouched. diff --git a/docs/proposals/mockups/editor-polish/editor-polish-mockup.html b/docs/proposals/mockups/editor-polish/editor-polish-mockup.html new file mode 100644 index 000000000..c0ab5baf3 --- /dev/null +++ b/docs/proposals/mockups/editor-polish/editor-polish-mockup.html @@ -0,0 +1,1061 @@ + + + + + +ProxyPrints — unified /editor polish round (11 owner-settled items + slot-menu cue) + + + + +
+ View + + + + + + Subject + + + + Face (6) + + + + D14 (9) + + + + Compare (9) + + + + Details + + + + Identify (8) + + + + Filters + + + + Grid (10) + + + + Report + + + + Shared deck (11) + + + + + Unified /editor polish — 11 items + ⋯ cue. Frames transform-scaled to any width; review the full rail on a phone. +
+ +
+ + + + diff --git a/docs/proposals/mockups/rail-delegacy/SPEC-rail-delegacy.md b/docs/proposals/mockups/rail-delegacy/SPEC-rail-delegacy.md new file mode 100644 index 000000000..88c63f99f --- /dev/null +++ b/docs/proposals/mockups/rail-delegacy/SPEC-rail-delegacy.md @@ -0,0 +1,378 @@ +# /editor LEFT RAIL — delegacy round (nine grey drop-downs folded into designed elements) + +> Durable copy, recovered 2026-07-24 from session tmp storage (same +> durability convention as [`../../../reference/funnel-spec.md`](../../../reference/funnel-spec.md)) — +> this is the owner-approved BINDING spec that committed code comments +> across open PR branches (e.g. `DisplayPage.tsx`, `RequestedPrintingBadge.tsx`, +> `SelectVersionResults.tsx`) cite by filename as their authority. Content below +> is verbatim from the 2026-07-24 original, including its OWNER AMENDMENTS +> section where present. + +Companion to `rail-delegacy-mockup.html` (same directory). Owner reviews the mockup in a +browser BEFORE any implementation. + +**Premise (owner-approved 2026-07-24):** every grey legacy `AutofillCollapse` drop-down section +is REMOVED from the /editor left rail. Its contents fold into designed elements (or are SCRAPPED) +per the ruled disposition list. This spec is the layout + binding tokens for what replaces them. + +**Inherited, not renegotiable:** `SPEC-display-left-rail-CORRECTED.md` §0 (the #302 palette) and +§2 (density: blocks butt together, one 1px `#16202b` hairline between, each block's own compact +padding carries the rhythm). Every token from that round is reproduced verbatim below and tagged +`I`. The D14 confidence element and its Y/N decisions (#271 + owner answer #2), the Sources inline +design (owner answer #3), the continuous Select-Version grid (§F), and the `Offcanvas responsive` +shell (§B) are all inherited unchanged. This round only removes the grey accordions and rehomes +their contents. + +Every value below is **BINDING** (owner standing rule, 2026-07-23). `I` = inherited (reproduce +verbatim); `N` = introduced/normalized this round (flagged, and `.propose`-labelled in the mockup +where it changes the current look). + +--- + +## A. Breakpoint behaviour (inherited from corrected-bundle §B — UNCHANGED) + +The left rail is ONE node: `Offcanvas responsive="lg"`. Content composition is IDENTICAL at every +breakpoint (D2) — one `leftRail()` builder in the mockup proves it can't drift. Only the chrome +differs. + +| Tier | Width | Bootstrap | Left-rail chrome | +| ------- | ---------- | --------- | ----------------------------------------------------------------- | +| Phone | `<768` | xs+sm | `placement="bottom"` 72vh bottom-sheet; rounded top + drag handle | +| Tablet | `768–991` | md | `placement="start"` drawer; "Card details" edge handle when idle | +| Laptop | `992–1199` | lg | Inline sticky **380px** column | +| Desktop | `≥1200` | xl | Inline sticky **380px** column | + +Inline width `380px`; `overflow-y:auto` own scroll container; sticky `top:0`. **Phone +reachability (hard requirement, verified):** the whole rail — rail-head → D14 → identify panel → +artist → Sources → Select Version + Filters panel → bottom control stack (bleed / slot actions / +Report) — lives in the one `overflow-y:auto` container and is reachable by scrolling the 72vh +sheet. Verified in the forced-phone frame scrolled to its end (`v-phone-bottom-stack.png`) and the +native 390px frame. **Implementation note (fidelity):** inside the drawer the rail must +`flex:1 1 auto; min-height:0` so `.lscroll` owns the scroll — the inline `380px` is a WIDTH, and +if it leaks into the column-flow drawer as a height the sheet can't reach its bottom (caught and +fixed in the mockup; carry the same rule into the real Offcanvas body). + +--- + +## B. Rail composition order (this round — revised 2026-07-24) + +``` +┌ rail-head ─────────── SUBJECT-CARD IMAGE (rev #3) + LEAN identity (Slot · name) + "More details" +│ disclosure holding the WHOLE Card-Details metadata block (rev #1). No +│ printing identifier here (rev #2 — it lives once, in D14). +├ D14 confidence band ─ LOCKED (#271 + owner #2); owns the ONE canonical printing id "2X2 · 117" (rev #2) +├ identify panel ────── Printing-Tags voting surface HANGS OFF D14 (item 6), opened on demand +├ artist line ───────── ArtistSection (unchanged) +├ Sources ───────────── inline AutofillCollapse (inherited, owner answer #3) — NOT one of the nine +├ Select Version ────── header row [N versions · Sort ▾ · Filters ▾] (item 2) → Filters panel +│ (item 3 power set + item 5 UNIFIED into the one funnel chip surface, O1; +│ phone=in-rail expand, desktop/tablet=centred float, O3) → continuous grid (§F) +└ control stack ─────── Print Options + Slot Actions + Report (item 7), one designed stack +``` + +NO grey `AutofillCollapse` anywhere in the rail. `RailSection`/`AutofillCollapse` mounts for the +demoted zone (`DisplayPage.tsx` ~L1171-1264) are deleted; `GridSelectorFilters`' `AutofillCollapse` +sections (Jump/Sort/Filter) are deleted from the rail path. + +--- + +## C. react-bootstrap primitive mapping (no new dependencies) + +| Element (this round) | Primitive(s) | Source component reused | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| **Subject-card image** (rev #3) | `Card.Img`/`` of the slot's selected art at a preview footprint; dashed placeholder empty state | reuses the same thumbnail URL the selected `.vtile`/`CardImage` renders — NOT a second full render | +| "More details" disclosure (rev #1) | `Button` (link-style) toggling a `Collapse`; body = the FULL `CardMetaTable` (`AutofillTable`) + `ClickToCopy` + `AddCardToFavorites` + Download `Button` | `CardMetaTable` + `CardDownloadFavorite` (whole block) | +| Requested≠resolved mismatch flag (rev #2) | `RequestedPrintingBadge`, rendered ONLY when the query's requested printing differs from the resolved/suggested printing | `RequestedPrintingBadge` (additive `showOnlyOnMismatch`-style prop) | +| Sort dropdown (item 2) | `Form.Select size="sm"` (6 `SortByOptions`) | replaces `NullableSortByFilter` tree-select (see O5) | +| Filters toggle (item 2) | `Button variant="outline-light" size="sm"` + chevron, `aria-expanded` | new toggle | +| Filters panel — phone (O3) | in-rail `Collapse` wrapping `fieldset`s (in place) | new container | +| Filters panel — desktop/tablet (O3) | `Overlay`/floating panel centred in the viewport + backdrop; same `fieldset` body | new container (shared body node) | +| Border / Frame filter chips (O1) | `ToggleButtonGroup type="radio"` — positive-or-off (`FUNNEL_AXES`), re-tap clears to "any" | funnel chips (`attributeChips.ts`) | +| Treatment filter chips (O1) | `ToggleButton`/`Button` tri-state (`TreatmentChipRow`/`nextChipState`) | funnel chips (`attributeChips.ts`) | +| SUGGESTED chip state (O1) | dashed border + trailing `⌇` on chips carried only via `suggestedFilterTagNames` | ratified funnel F3 (`chipMembershipState`) | +| Implicit-vote awareness line (O1) | plain muted text row under the chips | ratified funnel column element | +| DPI / Size fieldsets (item 3) | `DPIFilter` / `SizeFilter` (range sliders) | unchanged | +| Languages / Tags fieldsets (item 3) | `LanguageFilter` / `TagFilter` (tree-selects) | unchanged | +| Mature-content (NSFW) fieldset (item 3) | `MatureContentFilter` (`Form.Check` switch) | unchanged | +| Identify-printing panel (item 6) | `Button` toggle → `Collapse` → `PrintingTagPicker` (`Form.Control`+`Row/Col` `Button` thumbnails + `OverlayTrigger`/`Popover`) + `AttributeVotingPanel` follow-up | `PrintingTagsBlock` | +| Print-options select (item 7) | `Form.Select size="sm"` (Auto / Force bleed / Force trimmed) | `PrintOptionsSection` | +| Slot-action buttons (item 7) | stacked `Button size="sm" variant="outline-light\|outline-danger"` | `SlotActionsSection` | +| Report button + panel (item 7) | one `Button size="sm" variant="outline-danger"` → `ReportCardPanel` chip grid | `ReportCardPanel` | + +Shared components gain only additive, optional, behaviour-preserving props (e.g. `RequestedPrintingBadge` +render-only-on-mismatch; `GridSelectorFilters` already carries `hiddenSections`; the funnel chip +components already own the SUGGESTED-state + implicit-vote wiring). No display-only fork of any +/editor component, and — per O1 — no new voting mechanic: the funnel chips behave exactly as the +ratified implicit mechanics, `AttributeVotingPanel` stays the one explicit surface. + +--- + +## D. BINDING token table — every rail element (sizing · colouring · spacing) + +Colours are the §0 #302 tokens. `I` = inherited (reproduce verbatim); `N` = introduced this round. + +### D.0 Palette tokens (all inherited — `styles.scss` #302 + Superhero) + +| Token | Value | Token | Value | +| ---------------- | ------------------ | ------------------ | --------------------- | +| body bg | `#0f2537` | primary (hover) | `#df6919` (`#be5915`) | +| panel/card/2ndry | `#4e5d6c` | success | `#5cb85c` | +| card-header | `#4e5d6b` (inline) | danger | `#d9534f` | +| raised/input bg | `#22303f` | warning | `#ffc107` | +| D14 band | `#2b3e50` | info | `#5bc0de` | +| text | `#ebebeb` | input border | `#4e5d6c` | +| muted | `#8fa0b0` | divider (hairline) | `#16202b` | +| light | `#abb6c2` | radius / pill | `0` / `10px` | + +### D.1 Inherited elements (verbatim from corrected-bundle §D.1 — reproduce, do not redraw) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N | +| ----------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------- | -------------------- | +| `.rail-head` | — | `#22303f` / `#ebebeb` / bottom `#16202b` | `padding:8px 10px` | I | +| `.rail-head .slot` | `14px/700`; face `11px` uppercase `#8fa0b0` | text `#ebebeb` | face `margin-left:6px` | I | +| `.rail-head .name` | `15px` | `#ebebeb` | `margin-top:1px` | I | +| `RequestedPrintingBadge` | `12px` mono | `#4e5d6c`/`#fff`; degraded `#ffc107`/`#111` | `padding:2px 8px; mt:5px` | I | +| `.d14` band | `12px`, wrap, `gap:8px` | `#2b3e50` / — / bottom `#16202b` | `padding:8px 10px` | I | +| `.d14 .seticon` | `30×30` circle | `#4e5d6c` / — / `#7f8fa0`; `50%` | `flex:0 0 30px` | I | +| `.seticon .check` (confirmed) | `15×15` circle | `#5cb85c` / `#fff` / `2px #2b3e50` | `right/bottom:-3px` | I | +| `.seticon .score` (suggested) | `9px/800` tabular | `#df6919` / `#fff` / `2px #2b3e50`; radius `10px` | `right/bottom:-7px; pad 1px 4px` | I | +| `.d14 .idtext` | `12px` mono | `#ebebeb` | — | I | +| `.statepill.confirmed` | `11px/700` | — / `#a7e08a` / `#3f7a2f`; radius `10px` | `padding:1px 8px` | I | +| `.statepill.suggested` | `11px/700` | — / `#ffb27d` / `#df6919`; radius `10px` | `padding:1px 8px` | I | +| `.d14 .notthis` (`✗`) | `btn-sm` | `btn-outline-danger`; confirmed → `opacity:.6` | `margin-left:auto` | I | +| `.artist-line` | `13px` | `#22303f` / `#ebebeb` / bottom `#16202b` | `padding:8px 10px` | I | +| Artist support button | `btn-sm` | `btn-outline-primary` (`#df6919`) + `↗` | `.by margin-bottom:6px` | I | +| Sources header | `12px/700` label | `#4e5d6b` / `#ebebeb`; count N `#5cb85c` | `padding:7px 10px; gap:8px` | I | +| Sources pinned chip strip | `11px` | chip `#22303f`/`#ebebeb`/`rgba(0,0,0,.22)`; ★ `#ffc107` | `gap:4px; pad 0 10px 7px` | I | +| Sources filter input | `14px` | `#22303f` / `#ebebeb` / `#4e5d6c`; radius `0` | `padding:6px 10px; mb:8px` | I | +| Sources bulk buttons ×3 | `btn-sm` | `btn-outline-light` (`#abb6c2`) | `gap:6px; mb:6px` | I | +| Sources "Save defaults" seam | `btn-sm block` disabled | `btn-outline-success` (`#5cb85c`); `title` #353 | `mb:8px` | I | +| Sources list container | `max-height:150px` scroll | `#22303f` / — / `rgba(0,0,0,.22)` | — | I (height N)\* | +| Source row | row ≥38px | / — / bottom `rgba(0,0,0,.22)` | `padding:8px; gap:8px` | I | +| Source toggle | **`54×38`** | on `#df6919`/`#fff`; off `#4e5d6c`/`#8fa0b0`; `#6b7d8e` border | — | I | +| Source pin ★ | **`38×38`** hit | pinned `#ffc107` / unpinned `#5b6b7b` | `min 38px` | I | +| `.select-version-heading` | `14px/600` | `#ebebeb` | `margin:0; padding:8px 0 4px` | I | +| Border/Frame filter segments | `11px` | seg `#22303f`/`#ebebeb`; active `#df6919`/`#fff`; `#4e5d6c` border | `padding:3px 8px` | I | +| Treatment tri-state chip | `11px` | neutral `#6b7d8e`; inc `#5cb85c`/`.22`; exc `#d9534f`/`.22`/strike | `padding:2px 7px; gap:3px` | I | +| Result grid `.vgrid` | `flex-wrap` | — | `gap:6px` | I | +| Tile `.vtile` | **`72/88/112`px** | outline `rgba(235,235,235,.15)`; sel `2px #df6919` | — | I | +| Tile REQ / ✓ / Alt / ? tags | `8px`/`7px/800` | `#df6919` / `rgba(92,184,92,.92)` / `rgba(91,192,222,.92)` / `rgba(120,135,150,.92)` | corners | I | +| Tile confirm ribbon (§F-a) | `16px` triangle | `#ffc107` / `#111` `?` | top-right | I | +| Ghost "+N" tile | `72px` dashed | transparent / `#abb6c2` / `1px dashed #abb6c2` | grid gap | I | +| `.btn-sm` (all) | **`14px / 4px 8px`**, radius 0 | per variant (§0) | — | I (was N last round) | + +\*Sources list `max-height` trimmed `190px→150px` this round to buy vertical room for the folded +content; a fidelity-relevant delta, flagged. Revert to `190px` if the owner prefers the taller list. + +### D.2 Introduced elements (new this round — need knowing sign-off) + +| Element | Sizing | Colour (bg / text / border) | Spacing | I/N | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------- | +| **rail-head row** `.rhead-row` (rev) | — | inside `.rail-head` `#22303f` | `gap:10px; align items flex-start` | N | +| **Subject-card image** `.subject` (rev #3) | **`66px` wide**, aspect `63/88` (≈`92px` tall) | art thumbnail / caption `rgba(0,0,0,.55)`/`#cdd6df` `7px`; border `1px rgba(235,235,235,.15)` | `flex:0 0 66px` | N | +| Subject empty state `.subject.empty` | same `66×92` footprint | transparent / `#8fa0b0` `9px` / `1px dashed #abb6c2` | centred "No art selected" | N | +| Identity column `.idcol` | `slot 14/700`, `name 15px`; empty name `italic #8fa0b0` | text `#ebebeb`; face `#8fa0b0` `11px` uppercase | `flex:1; min-width:0` | I (values) | +| Requested≠resolved flag `.mismatch` (rev #2) | `10px` mono | `#ffc107` / `#111` | `margin-top:5px; padding:1px 7px`; hidden in empty state | N | +| **"More details" toggle** `.detmore` (rev #1) | `11px` | `#8fa0b0` (hover `#ebebeb`); no border | `margin-top:6px; gap:4px` | N | +| "More details" body `.detbody` | rows `11px`; key `10px` uppercase | key `#8fa0b0` (flex 0 0 88px); value `#ebebeb` (mono where numeric); top border `#16202b`; id-copy `#5bc0de` dotted; tag pill `#4e5d6c`/`#ebebeb` r10 | `margin-top:8px; padding-top:8px` | N | +| "More details" actions ×2 (Favourite/Download) | `btn-sm` | `btn-outline-light` | `gap:6px; margin-top:8px` | N | +| **Identify panel band** `.idhang` (item 6) | — | `#2b3e50` (same as D14) / — / bottom `#16202b` | `padding:0 10px 8px` | N | +| Identify toggle `.idtoggle` | `12px` | transparent / `#abb6c2` / `1px #6b7d8e` (hover `#abb6c2`) | `padding:3px 8px; gap:6px` | N | +| Identify body `.idbody` | — | `#22303f` / — / `1px #16202b` | `margin-top:8px; padding:8px` | N | +| Identify consensus line `.cons` | `11px` | `#8fa0b0` (value `#ebebeb`) | `margin-bottom:6px` | N | +| Identify search input `.idsearch` | `12px` | `#2b3e50` / `#ebebeb` / `1px #4e5d6c`; radius `0` | `padding:5px 8px; mb:8px` | N | +| Candidate grid `.candgrid` | 4-col grid | — | `gap:6px` | N | +| Candidate tile `.cand` | aspect `63/88` | tile gradient / — / `1px #6b7d8e`; sel `2px #5cb85c`; No-match `#1b2836`/`#8fa0b0` | pad `2px` | N | +| Candidate label `.cl` | `7px` mono | `rgba(0,0,0,.65)` / `#ebebeb` | — | N | +| **Sort dropdown** `.sortsel` (item 2) | `12px` | `#22303f` / `#ebebeb` / `1px #4e5d6c`; radius `0` | `padding:3px 6px; max-width 150px` | N | +| **Filters toggle** `.filtersbtn` (item 2) | `btn-sm` | `btn-outline-light` (`#abb6c2`) + chevron | `padding:4px 8px; gap:5px` | N | +| SV header row `.svhead` | `12px` | `#8fa0b0` (`.n` `#ebebeb/700`) | `gap:6px; mb:6px; wrap` | N | +| **Filters panel — inline (phone)** `.fpanel.inline` | — | `#22303f` / — / `1px #16202b` | `padding:8px; mb:8px` | N | +| **Filters panel — float (desktop/tablet)** `.fpanel.float` (O3) | `width:440px`, `max-width:calc(100%-32px)`, `max-height:calc(100%-96px)` scroll | `#22303f` / — / `1px #7f8fa0`; shadow `0 12px 34px rgba(0,0,0,.6)` | `position:absolute; left:50%; top:64px; translateX(-50%); z-index:91` | N | +| Float panel title bar `.fptitle` (O3) | `12px/700` uppercase | `#4e5d6b` / `#ebebeb`; close btn `1px rgba(235,235,235,.2)` | `padding:8px 12px; sticky top:0` | N | +| Float backdrop `.fscrim` (O3) | full-frame | `rgba(0,0,0,.45)`; `z-index:90` | shown desktop/tablet only | N | +| Fieldset `.fset` | — | — | `margin-bottom:9px` | N | +| Fieldset legend `.lg` | `10px` uppercase | `#8fa0b0` | `margin-bottom:4px` | N | +| Fieldset row label `.frl` | `10px` uppercase | `#8fa0b0` | `flex:0 0 46px` | N | +| Fieldset separator `.fsep` | `1px` | `#16202b` | `margin:9px -8px` | N | +| **SUGGESTED chip** `.tchip.suggested` / `.seg .suggested` (O1) | `11px` | dashed `1px #abb6c2`; trailing `⌇` glyph `#abb6c2` | `.sg margin-left:2px` | N (ratified funnel F3 look) | +| **Implicit-vote awareness line** `.implicit-note` (O1) | `10px` | `#8fa0b0`; leading `ⓘ` `#5bc0de` | `margin-top:7px; gap:5px` | N | +| Range fieldset `.frange/.ftrack` | track `4px`; knob `12px` | track `#3a4653`; fill `#df6919`; knob `#abb6c2`/`#fff` border | `gap:8px` | N | +| Range value `.fval` | `10px` tabular | `#8fa0b0` | — | N | +| Tree-select box `.ftree` | `11px` | `#2b3e50` / `#8fa0b0` / `1px #4e5d6c`; chip `#4e5d6c`/`#ebebeb` r10 | `padding:4px 8px; gap:6px` | N | +| NSFW switch `.swtrack` | `34×18`; knob `14` | off `#3a4653`; on `#df6919`; knob `#abb6c2`→`#fff` | label `13px`/gap `8px` | N | +| **Control stack** `.cstack` (item 7) | — | `#4e5d6c` (rail) — no own bg | `padding:8px 10px` | N | +| Control-stack legend `.cs-legend` | `10px` uppercase | `#8fa0b0` | `margin-bottom:5px` | N | +| Bleed select `.bleedsel` | `13px` | `#22303f` / `#ebebeb` / `1px #4e5d6c`; radius `0` | `padding:4px 8px; width 100%` | N | +| Bleed hint `.cs-hint` | `10px` | `#8fa0b0` | `margin-top:4px` | N | +| Slot-action buttons ×3 | `btn-sm block` left-aligned | Change/Duplicate `btn-outline-light`; Delete `btn-outline-danger` | `gap:6px` | N (composition) | +| Control-stack foot `.cs-foot` | — | top border `#16202b` | `padding-top:8px` | N | +| Report button `.reportbtn` | `btn-sm block` | `btn-outline-danger` (`#d9534f`) + `⚑` | — | N (composition) | +| Report reason chips `.rchip` ×5 | `11px` | `#22303f` / `#ebebeb` / `1px #4e5d6c` (hover danger) | 3-col grid `gap:6px` | N | + +--- + +## E. Owner decisions logged this round (RD = rail-delegacy; all PROPOSED, pending sign-off) + +- **RD1 (owner-ruled, O1 UNIFIED, 2026-07-24).** There is exactly ONE chip surface — the existing + funnel per-axis chips (`attributeChips.ts` `FUNNEL_AXES`): Border/Frame as positive-or-off radio + segments, Treatment as a tri-state include/exclude cycle. The separate `.achip` "attribute vote" + fieldset is REMOVED. Per the ratified record (`docs/features/printing-tags.md` "Frontend consumer + (funnel round)" / "Implicit votes"; `grid-selector.md` funnel + Compliance-fix), the funnel chips + ARE the voting surface: picking a candidate while chips are active casts CAPPED IMPLICIT support + votes (`castImplicitVote`, weight 0.25, per-outcome cap 1.0 < quorum, never human-backed, never + tips a contest). SUGGESTED chips (dashed + trailing `⌇`) are driven by `suggestedFilterTagNames` + (NOT `tagVoteStatuses` — condition 6, no self-seeding), and exclude sensitive tags. An + implicit-vote awareness line states the passive-signal behaviour. The design implies NO new voting + mechanic. Explicit attribute voting remains ONLY where it already lives — the D14 identify + follow-up's `AttributeVotingPanel` (item 6). +- **RD2.** Sort (item 2) is a compact `Form.Select` of the 6 `SortByOptions`, inline in the SV + header (O5 accepted — the tree-select's inline search is dropped for now). +- **RD3.** Jump to Version is SCRAPPED (O4 ruled fully dead). The continuous grid + the Sources/ + identify searches are the funnel; the eventual power path is **search-in-the-filter-bar** (noted + in §H Futures), not a revived numeric jump. +- **RD4 (owner-ruled, O3, 2026-07-24).** The Filters panel is tier-conditional: **phone** = in-rail + `Collapse` expanding IN PLACE inside the one scroll container (no overlay-over-overlay in the + bottom-sheet); **desktop inline rail + tablet drawer** = a panel that FLOATS toward the CENTRE of + the viewport (a frame-level `Overlay` + backdrop, escaping the 380px column and the tablet + drawer's own clipping — there's more room there and no stacking hazard). One shared fieldset body + feeds both so they can't drift. +- **RD5.** Print Options + Slot Actions + Report collapse into one `.cstack` at the rail bottom; + Report is a single `btn-outline-danger` that expands to the `ReportCardPanel` reason chips + in place (ruled). +- **RD6 (owner-ruled, rev #1, 2026-07-24).** O2 answered: the WHOLE Card-Details metadata block — + Resolution/DPI, File size, Source, Source type, Class, Identifier, Language, Tags, dates, plus + `CardDownloadFavorite` (Download + Favourite) — lives ONLY in the "More details" `Collapse`. + Rail-head carries no metadata rows; it stays lean (subject image + Slot · name + toggle). This + RETIRES disposition item 4's "small muted rows under the card name". +- **RD7 (owner-ruled, rev #2, 2026-07-24).** The printing identifier appears exactly ONCE in the + rail. Proposed & applied canonical home = the **D14 confidence band** (`.idtext`) — the + always-visible "what printing is this" context, so identity never requires expanding a disclosure. + Removed from rail-head and from More details. `RequestedPrintingBadge` survives only as a + conditional mismatch flag (requested ≠ resolved), never a static second copy. +- **RD8 (owner-ruled, rev #3, 2026-07-24).** The rail TOP shows a **subject-card image** — a `66px` + preview of the slot's selected art (aspect `63/88`), left of the identity text, with a dashed + "No art selected" empty state. Restores the classic Card-details modal's subject preview that the + unified rail had dropped. Sized as a preview, not a second full render (Select Version stays the + art surface); at `66px` it does not starve the 390px grid — verified. + +Honoured LOCKED decisions (unchanged): D2 (identical composition per breakpoint), D3 (promote +identity/art, demote metadata — here "demote" means fold, not a grey accordion), D14 + #271 + +owner answer #2 (confidence element & the ✗-stays-de-emphasised rule), owner answer #3 (Sources +inline), owner answer #5 (source pins + #353 seam), §F (one continuous grid). + +--- + +## F. FEATURES ACCOUNTED FOR — every control from the nine removed grey sections + +| # | Removed grey section | Control / content it held | New home (this round) | +| --- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1 | **Jump to Version** | numeric "option #" jump + identifier input | **SCRAPPED** (RD3/O4) — grid funnel + searches replace it | +| 2 | **Sort** | `NullableSortByFilter` (6 sort orders, radioSelect) | → `Form.Select` in SV header row (item 2 / RD2) | +| 3 | **Filter** (remaining after #409) | DPI range (`DPIFilter`) | → Filters panel fieldset "Resolution (DPI)" | +| 3 | | Max size (`SizeFilter`) | → Filters panel fieldset "Max file size" | +| 3 | | Languages (`LanguageFilter`) | → Filters panel fieldset "Languages" | +| 3 | | Tags (`TagFilter`) | → Filters panel fieldset "Tags" | +| 3 | | NSFW (`MatureContentFilter`) | → Filters panel fieldset "Mature content" (switch) | +| 3 | _(pre-removed #409)_ | stock sources table (`SourceSettings`), `CanonicalCardFilter`, `ResolvedAttributeFilter` | already hidden from the rail's Filters panel (PR #409 `d8db75db`) — **accounted, not this round's drop** | +| 4 | **Card Details** | Canonical Card set/collector (the printing id) | → **D14 band, ONE canonical occurrence** (rev #2 / RD7) | +| 4 | | Resolution (DPI), File Size, Source Name/Type, Class, Identifier (`ClickToCopy`), Language, Tags, Date Created/Modified | → **"More details" disclosure** (rev #1 / RD6 — O2 answered) | +| 4 | | Download Image + Add to Favourites (`CardDownloadFavorite`) | → **"More details" body** (rev #1 / RD6) | +| 4 | | Canonical Artist | → already the promoted Artist line (ArtistSection) — **accounted** | +| — | _(rev #3 NEW, not one of the nine)_ | subject-card preview (classic modal had it) | → **subject-card image at the rail top** (RD8) | +| 5 | **Attributes** | attribute-lean signal on Border/Frame/Treatment | → **UNIFIED into the ONE funnel chip surface** (O1/RD1): the funnel chips already carry the SUGGESTED (`⌇`) lean from `suggestedFilterTagNames` and cast capped implicit votes on pick. No separate vote fieldset. | +| 5 | | explicit attribute voting (`AttributesSection`/`useTagVoting` deliberate taps) | → lives ONLY in the D14 identify follow-up's `AttributeVotingPanel` (item 6) — the single explicit surface (O6 resolved by O1's record) | +| 6 | **Printing Tags** | `PrintingTagPicker` (consensus line, search, candidate thumbnail grid, "No match", vote submit) | → identify panel hanging off D14 (item 6) | +| 6 | | `AttributeVotingPanel` follow-up (shown when printing unresolved) | → follow-up region inside the identify panel — the ONE explicit attribute-vote surface | +| 7 | **Print Options** | Bleed override `Form.Select` (Auto/Force bleed/Force trimmed); ineligible-card muted message | → `.cstack` "Print options" group (item 7) | +| 7 | **Slot Actions** | `getCardSlotMenuActions` → Change query / Duplicate / Delete (+ Unfilter printing when filtered) | → `.cstack` "Slot actions" button stack (item 7) | +| 7 | **Report** | `ReportCardPanel` flag button → 5 reason chips (NSFW/Low quality/Wrong card/Broken image/Other + free-text) | → `.cstack` single Report button → chips (item 7) | + +Not among the nine, unchanged: **View** (already hidden via `hiddenSections=["view"]`), **Sources** +(designed inline element, owner answer #3), **Artist**, **D14**, **RailHeader identity**, **Select +Version grid**. + +--- + +## G. Accessibility + +Hit targets ≥38px for source toggles/pins (`ToggleButtonHeight`); D14 set icon `tabIndex=0` + +popover on focus; Sort is a labelled `Form.Select`; the Filters toggle and identify toggle are +`button`s with `aria-expanded`/`aria-controls`; each filter fieldset is a `fieldset` with a text +legend; filter chips carry `+/−/·` glyph state, vote chips carry `✓/✗/·` glyph + `aria-label` +spelling the state (colour never the only signal); the NSFW control is a `role="switch"` with +`aria-checked`; candidate tiles and report chips are focusable `button`s. Grid keeps `role=list`. +AA contrast holds on the four rail surfaces (`#0f2537/#22303f/#2b3e50/#4e5d6c`). + +--- + +## H. Open questions — ALL RESOLVED (owner rulings 2026-07-24) + +Every question from the prior pass is now closed; nothing owner-facing remains open for this design. + +- ~~**O1.**~~ **RESOLVED — UNIFIED** (→ RD1): ONE chip surface (the funnel chips, which are the + implicit-vote surface per the ratified record); the separate `.achip` vote fieldset is removed; + explicit voting stays in the D14 follow-up only. Design implies no new voting mechanic. +- ~~**O2.**~~ **ANSWERED** (rev #1/#2 → RD6/RD7): whole metadata block + Download/Favourite in "More + details"; printing id lives once in D14; rail-head lean. +- ~~**O3.**~~ **RESOLVED — TIER-CONDITIONAL** (→ RD4): phone = in-rail expand in place; desktop + + tablet = float toward viewport centre. +- ~~**O4.**~~ **RESOLVED** (→ RD3): Jump to Version fully dead, no numeric-jump path preserved. +- ~~**O5.**~~ **RESOLVED — ACCEPTED**: plain 6-option `Form.Select` for Sort (for now). +- ~~**O6.**~~ **RESOLVED by O1's record**: the funnel chips carry the implicit lean; the ONE explicit + attribute-vote surface is the D14 identify follow-up. Voting flows are unchanged from this panel + and this page generally. + +**Futures (noted, not this round):** the eventual power-user replacement for the scrapped Jump to +Version is **search-in-the-filter-bar** — typing to narrow the version grid from the same bar that +holds the funnel chips. Out of scope here; recorded so the intent isn't lost. + +--- + +## I. Verification + +Playwright (`shot.js`, chromium) at **390px**, **900px** and **1400px**; screenshots inspected. + +Final-pass shots (2026-07-24, O1/O3): + +- `v-1400-floatpanel-el.png` — the desktop/tablet Filters panel floated to viewport centre: ONE chip + surface (Border/Frame radio + Treatment tri-state), the SUGGESTED "Showcase ⌇" dashed chip, the + implicit-vote awareness line, then DPI/Size/Languages/Tags/NSFW. No separate vote fieldset. +- `v-1400-filters-float.png` — same, in the full desktop frame with centred float + backdrop over + the sheet region. +- `v-900-tablet-drawer.png` — tablet: the Filters panel floats to viewport centre (escapes the start- + drawer's clipping), Card-details drawer still on the left. +- `v-390-filters-inline.png` — phone: the Filters panel expands IN PLACE inside the rail scroll (no + float; the `.fpanel.float` node isn't even rendered on the phone frame — verified `display:none-el`). + +Revision-pass shots (2026-07-24, subject/dedup): + +- `v-1400-railhead-lean.png` — rail-head now lean: `66px` subject-card thumbnail + Slot·name + + conditional mismatch flag + "More details ›"; NO printing id (rev #1/#2/#3 all visible). +- `v-1400-railhead-details.png` — "More details" open: full `CardMetaTable` block (Resolution, File + size, Source, Source type, Class, Identifier, Language, Tags, Created, Modified) + Favourite/ + Download; canonical printing id NOT repeated here (lives in D14). +- `v-1400-railhead-empty.png` — dashed "No art selected" subject placeholder + italic empty name; + mismatch flag correctly suppressed. +- Deduped identity confirmed: the only textual "2X2 · 117" identity display in the rail is the D14 + band (grid tiles / picker candidates are their own labels, not identity copies). + +Base shots (re-verified after revision): + +- `v-1400-auto.png` (desktop three-region), `v-1400-rail-allopen.png` (full rail, filters + + identify + report all open) — every folded element renders cohesively; no grey accordion remains. +- `v-1400-d14-confirmed.png` / suggested state via demo toggle — confirmed shows green ✓ + "Confirmed" + pill + the ✗ vote de-emphasised (opacity .6); suggested shows the score badge + "92% confident" + pill. **Locked D14/owner-#2 treatment reproduced exactly.** +- `v-390-auto-top.png`, `v-390-auto-full.png`, `v-390-filters-open.png`, `v-390-all-open.png` (native + 390 phone), `v-390-rail-bottom.png` (drawer scrolled) — full rail reachable in the bottom-sheet. +- `v-phone-bottom-stack.png` (forced-phone frame, real 72vh clip, scrolled to end) — the bottom + control stack (bleed select, Change/Duplicate/Delete, Report-expanded reason chips) all render and + **fit at 390px width**, proving phone reachability to the very bottom. +- `v-900-tablet-drawer.png` (forced tablet start-drawer) — identical rail interior at md. + +**Bug caught + fixed:** inside the drawer the rail's inline `flex:0 0 380px` was being read as a +380px HEIGHT in the column-flow drawer, capping the bottom-sheet so the control stack couldn't be +scrolled to. Added `.drawer .lrail{flex:1 1 auto; min-height:0}` + `.drawer .lscroll{flex:1 1 auto}`; +re-verified the scrolled-to-bottom shot. This is a real fidelity note to carry into the Offcanvas +body implementation (§A). Zero page errors across all frames. diff --git a/docs/proposals/mockups/rail-delegacy/rail-delegacy-mockup.html b/docs/proposals/mockups/rail-delegacy/rail-delegacy-mockup.html new file mode 100644 index 000000000..a768ac9f5 --- /dev/null +++ b/docs/proposals/mockups/rail-delegacy/rail-delegacy-mockup.html @@ -0,0 +1,908 @@ + + + + + +ProxyPrints — /editor left rail (delegacy round: nine grey drop-downs folded into designed elements) + + + + +
+ View + + + + + + Subject art + + + + More details + + + + D14 state + + + + Filters panel + + + + Identify printing + + + + Report + + + + Nine grey drop-downs removed. Forced frames are transform-scaled to fit any width — review the full rail (incl. tablet/phone drawers) on a phone. +
+ +
+ + + + diff --git a/docs/proposals/mockups/wtc-rebuild/SPEC-wtc-rebuild.md b/docs/proposals/mockups/wtc-rebuild/SPEC-wtc-rebuild.md new file mode 100644 index 000000000..d08c43841 --- /dev/null +++ b/docs/proposals/mockups/wtc-rebuild/SPEC-wtc-rebuild.md @@ -0,0 +1,363 @@ +# What's That Card? (/whatsthat) — rebuild spec (BINDING) + +> Durable copy, recovered 2026-07-24 from session tmp storage (same +> durability convention as [`../../../reference/funnel-spec.md`](../../../reference/funnel-spec.md)) — +> this is the owner-approved BINDING spec that committed code comments +> (`ChipCard.tsx`, `WhatsThatWords.tsx`, `cardPanel.tsx`) cite by filename +> as their authority. Content below is verbatim from the 2026-07-24 +> original; the per-viewport screenshot PNGs the original mentions were +> not carried over in this durability pass (bulk verification artifacts — +> the spec and its self-contained mockup HTML stand alone). + +Designer: Quorra · 2026-07-24 · read-only vs repo +Companion mockup: `wtc-mockup.html` (self-contained) + PNGs (this dir) +Grounding: implementation brief (`a4c9c7035834dad69`) + data brief (`a42091a50f00d5417`) + +Binding status (owner standing, 2026-07-23): the CSS token values below are +BINDING. A visual regression against `wtc-mockup.html` is a defect, not a nuance. +Every element on the affected page carries a sizing/coloring/spacing row. + +D-number scope note: D-numbers are per-proposal in this repo (proposal-h owns +its own D1–D19; the old WTC round used W4–W7). The decisions below are the +**WTC-rebuild round's** ledger, numbered WD1.. to avoid collision with either. + +--- + +## 0. Theme foundation — Tokyo-11 (palette 11 = "tokyoorange") + +The page renders **native** on the ruled Tokyo-11 token layer (owner-ratified in +the theme-options round). Orange **#ff9e64** is the ACTION colour; purple +**#bb9af7** is the ACCENT / identity colour; Semi radius; strict-AAA policy +(Tokyo-11 clears 7:1 on body text 7.54 and on the primary button 8.40 as-is). + +The page's prior bespoke identity is **killed**: gold `#f8d42b`/navy `#124063` +buttons (QuestionFeed styled overrides L133–248), starburst blue `#4d8ddf` +(starburstShape.ts / cardPanel MysteryCardFace), deep-blue field `#123a6b` +(whatsthat.tsx `HERO_FIELD_BLUE_DEEP`). The WTC personality is re-expressed +through the ruled accent (WD1). + +--- + +## 1. TOKEN TABLE — I (inherited from Tokyo-11) / N (new, WTC-identity, derived) + +### 1a. Core swap-surface (inherited verbatim — do not redefine on this page) + +| token | value | I/N | +| ----------------------- | --------- | --- | +| `--body` | `#1a1b26` | I | +| `--conf` | `#222234` | I | +| `--raised` | `#24283b` | I | +| `--panel` | `#2f3549` | I | +| `--divider` | `#16161e` | I | +| `--text` | `#c0caf5` | I | +| `--muted` | `#8c94bf` | I | +| `--primary` (action) | `#ff9e64` | I | +| `--accent` (identity) | `#bb9af7` | I | +| `--success` | `#9ece6a` | I | +| `--warning` | `#e0af68` | I | +| `--danger` | `#f7768e` | I | +| `--btn-ink` (derived) | `#1a1b26` | I | +| `--r-btn` / `--r-input` | `6px` | I | +| `--r-card` | `8px` | I | +| `--r-pill` | `10px` | I | + +### 1b. WTC-identity tokens (N — token-DERIVED from --accent/--body/--conf) + +These are the ONLY page-private tokens; each carries WTC identity deliberately +and derives from a ruled token (no invented colour). They re-express the three +retired bespoke elements. + +| token | value | replaces | I/N | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | --- | +| `--wtc-field` | `radial-gradient(125% 115% at 30% 34%, color-mix(in srgb, var(--accent) 15%, var(--body)) 0%, var(--body) 58%)` | deep-blue field `#123a6b` radial | N | +| `--wtc-mystery-face` | `linear-gradient(158deg, color-mix(in srgb,var(--accent) 26%,var(--conf)), color-mix(in srgb,var(--accent) 8%,var(--conf)))` | MysteryCardFace `#4d8ddf` | N | +| `--wtc-mystery-glyph` | `var(--accent)` | gold `?` mascot fill | N | +| `--wtc-reveal-glow` | `color-mix(in srgb, var(--accent) 55%, transparent)` | starburst blue/white burst | N | +| `--wtc-wordmark` | `var(--accent)` | gold/navy wordmark | N | + +### 1c. Per-element binding table (sizing / coloring / spacing) + +Every visible element on the affected page. Values are binding. + +| element | color | size / spacing | radius | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------- | +| page field wrapper | bg `--wtc-field` | pad `14px 16px 22px` | — | +| wordmark `h1` | `--wtc-wordmark`; `?` span = `--primary` | `font-size:clamp(26px,4.4cqi,44px)`; weight 900; text-shadow `0 0 22px color-mix(accent 26%)` | — | +| wordmark sub | `--muted` | `.34em` of wordmark; weight 600; mt 3px | — | +| solved pill | text `--muted`, count `--success` | pad `5px 12px`; font 12px | `--r-pill` | +| solved dots | filled `--success`, empty `--divider` | 6px circles, gap 3px | 50% | +| hero container | — | `max-width:1180px`; flex-wrap; gap `clamp(12px,2.2cqi,22px)` | — | +| subject `.subject` | — | `flex:1 1 300px; max-width:clamp(240px,30cqi,340px)` | — | +| subject card | bg `--raised`, border `--divider` | — | `--r-card` | +| subject art | bright card-art (real image in app) | `aspect-ratio:63/88` | — | +| subject art title | `#fff` on `linear-gradient(transparent,rgba(0,0,0,.72))` | `font-size:clamp(13px,3.4cqi,17px)`; weight 700 | — | +| subject caption | `--muted` on `--conf`; glyph `--accent` | pad `8px 11px`; font 12px | — | +| qhead prompt | `--text` | `font-size:clamp(17px,3.4cqi,22px)`; weight 800 | — | +| shapepill.easy | ink `--btn-ink` on `--success` | pad `3px 10px`; 11px; upper | `--r-pill` | +| shapepill.pick | ink `--btn-ink` on `--accent` | " | `--r-pill` | +| shapepill.neg | ink `--btn-ink` on `--danger` | " | `--r-pill` | +| shapepill.hard | `--accent` on transparent, 1px dashed `--accent` | " | `--r-pill` | +| qhint | `--muted` | 13px | — | +| `.btn` (base) | — | `min-height:44px`; font 15px/600; pad `6px 16px`; 1px border | `--r-btn` | +| `.btn.big` | — | font 17px/800; pad `10px 20px` | `--r-btn` | +| `.btn-primary` | `--btn-ink` on `--primary` | — | `--r-btn` | +| `.btn-secondary` | `--text` on `--raised`, border `--divider` | — | `--r-btn` | +| `.btn-accent` | `--btn-ink` on `--accent`; 800 | — | `--r-btn` | +| `.btn-ghost` (Skip) | `--muted`, transparent | — | `--r-btn` | +| `.btn-danger` | `--danger`, 1px `--danger`, transparent | — | `--r-btn` | +| action stack/grid | — | stack gap 9px; grid `repeat(auto-fit,minmax(clamp(120px,34cqi,180px),1fr))` gap 9px | — | +| suggested card | bg `--conf`, border `--divider` | pad 11px; gap 13px | `--r-card` | +| suggested thumb | card-art | `flex:0 0 clamp(70px,20cqi,104px)`; `aspect 63/88` | 6px | +| suggested name | `--text` | `clamp(16px,3.2cqi,19px)`; 800 | — | +| suggested set | `--muted` monospace | 13px | — | +| confidence pill | `--accent`, 1px `--accent` | pad `2px 9px`; 11px/700; dot 7px | `--r-pill` | +| landed feedback | `--success` on `color-mix(success 12%)`, 1px `color-mix(success 45%)` | pad `5px 12px`; 13px; mt 12px | `--r-pill` | +| candidate grid | — | `grid; gap clamp(7px,1.6cqi,11px); grid-template-columns:repeat(auto-fill,minmax(clamp(78px,15cqi,116px),1fr))` | — | +| candidate tile | bg `--raised`, border `--divider`; `.sel` outline `2px --accent` | art `aspect 63/88` | `--r-card` | +| candidate caption | name `--text` 700, set `--muted` monospace 10px | pad `5px 7px 6px`; 11px | — | +| negative wrapper | 1px `--danger` on `color-mix(danger 8%,--conf)` | pad 13px | `--r-card` | +| negative header | `--danger` | `clamp(15px,3cqi,18px)`; 800 | — | +| reason chip | `--text`, 1px `--danger`; `×` mark `--danger` | `min-height:44px`; pad `6px 12px`; 14px | `--r-btn` | +| reason-chip grid | — | `repeat(auto-fill,minmax(clamp(130px,40cqi,190px),1fr))` gap 8px | — | +| open wrapper | 1px dashed `--accent` on `color-mix(accent 6%,--conf)` | pad 14px | `--r-card` | +| search field | bg `--raised`, 1px `--divider`; input `--text` | height 46px; pad `0 12px`; gap 9px; font 15px | `--r-input` | +| open help links | `--accent`, underline `color-mix(accent 45%)` | 13px; mt 11px | — | +| picker button | `--text` on `--raised`, 1px `--divider`; `.unknown` `--muted` | `min-height:44px`; 14px | `--r-btn` | +| picker grid | — | `repeat(auto-fill,minmax(clamp(120px,34cqi,180px),1fr))` gap 8px | — | +| tag `h6` | `--text`, `` = `--accent` | `clamp(17px,3.4cqi,21px)`; 800 | — | +| group label | `--muted` upper | 10px/700; mt 12px mb 5px | — | +| chip (tri-state) | default `--text` 1px `--muted`; `.pos` `--accent` on `color-mix(accent 20%)`; `.neg` `--danger` on `color-mix(danger 16%)` line-through | `min-height:38px`; pad `4px 12px`; 13px | `--r-btn` | +| toggle (independent) | default `--text` 1px `--muted`; `.on` `--accent` on `color-mix(accent 20%)` | `min-height:38px`; box 14px | `--r-btn` | +| mystery-mini face | glyph `--wtc-mystery-glyph` on `--wtc-mystery-face`; glow `--wtc-reveal-glow` | 44×61px | 6px | + +--- + +## 2. QUESTION-SHAPE INVENTORY → interaction contract + +All shapes live in ONE component tree (QuestionFeed.tsx); the served +`QuestionFeedItem.type` selects which renders. The contract is preserved EXACTLY. + +| shape | serves (data brief supply) | question `type` | preserved flow (file:line) | distinct-shape rationale | +| ---------------------------- | -------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| **a · Confirm** (1-tap hero) | 45,154 single-candidate | `confirm_suggestion` w/ `suggestedPrinting` | Level 1 (QuestionFeed L12–13, L811): YES casts + `getAutoTagChips` auto-tags + advances; NOT SURE/NO → Level 2 (no vote); **singleton NO → isNoMatch + custom-art** (impl brief); SKIP → advance | one big card + ONE orange primary; only shape with a reflexive path, paired with equal-weight "No, different printing" | +| **b · Shortlist** (pick one) | 1,156 multi + 54,797 ambiguous (serve-time narrow) | `identify_printing` | Level 2 (L14–20): tap candidate → `APISubmitPrintingTag` + `getAutoTagChips`; None-of-these → `NoMatchReasonStrip`; no-re-presentation (`rejectedCandidateIds`) | grid of neutral tiles, **NO primary** — a real pick required | +| **c · Quick-negative** | 45,500 unknown-set/eliminated | `identify_printing` / `confirm_suggestion` negative exit | one-tap classified negative: `isNoMatch` + one of the 6 seeded reason tags (`NoMatchReasonStrip` L44–50: custom-art, altered-frame, upscaled, ai-art, no-collector-line, non-english) | danger-red framed, negative verb — visibly not a confirm | +| **d · Open-ended** | smallest slice: cold-start / no-evidence 32,838 | `identify_printing` (no shortlist) | search/typeahead identify; "Mark unidentifiable" = `isNoMatch`; Skip | dashed accent "tricky one", search field, **no button to reflex-tap** | +| **e1 · Artist** | artist family (13 near, byproduct) | `artist_vote` | `ArtistVotePicker` unforked (L183 `Form.Control` + candidate `Button` grid + "Unknown artist") | autocomplete idiom, not a discrete pick set | +| **e2 · Tag** | 51,163 Borderless (mostly byproduct) | `tag_vote` | `QueueTagQuestion` unforked (L88–110: Apply / Not applicable / Skip) | single yes/no attribute question | +| **L3 · Follow-up** | open exclusion groups only | any candidate-type post-pick | Level 3 (L22–29): tri-state chips (`level3ChipStates`) cast immediately; Confirm & continue / Skip; groups = `BORDER_COLOR_GROUP`/`FRAME_STYLE_GROUP` + independent toggles (Full Art/Borderless/Showcase/Extended) from attributeChips.ts | appears only after a pick; "one more thing" framing | + +Per-item state reset (preserved, L907–942): `chipStates`, `revealed`, +`filterExpanded`, `rejectedCandidateIds`, `followUp`, `rateLimited`, +`level3ChipStates` reset inside the fetch `.then()` (NOT a keyed useEffect) — +the stale-filter fix stays. + +Rate-limit banner (preserved, L884, L1017): honest pause condition, re-themed to +`Alert` on tokens; unchanged behavior. + +--- + +## 3. LAYOUT SPEC — container-first, one tree (retires the 768px dual layout) + +**Policy (owner-ratified 2026-07-24, WTC = first consumer):** components style +against their CONTAINER (`@container`), not the viewport; the layout folds +continuously (flex-wrap + `clamp()` + auto-fill/minmax); viewport breakpoints are +reserved for STRUCTURAL reordering only. The three retired horizontal-scrollers +(`MobileButtonRow`, `MobileCandidateScroller`, `MobileChipRow`) and `HeroGrid`'s +768px `grid-template-areas` swap are the anti-pattern being deleted. + +### The one hero container + +`.wtc-hero { container-type: inline-size; container-name: hero; display:flex; flex-wrap:wrap; gap:clamp(12px,2.2cqi,22px) }` — `.subject` (`flex:1 1 300px`) +and `.qpanel` (`flex:2.2 1 440px`) wrap intrinsically. No media query drives the +subject↔question column split; flex-basis does. + +### Continuous fold points (all container-query, no viewport sizing) + +| trigger | change | why it's allowed | +| ----------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| hero wraps (basis math) | subject column moves above qpanel | intrinsic flex-wrap, not a breakpoint | +| `@container hero (max-width:560px)` | subject → **compact-horizontal** (132px art + caption beside), full width | structural reorder of the subject card only; sizes stay `clamp()`-driven | +| `@container hero (max-width:380px)` | `.actgrid` → single column | container-driven, not viewport | +| candidate grid | `repeat(auto-fill, minmax(clamp(78px,15cqi,116px),1fr))` folds 6→4→3→2 cols continuously | intrinsic grid; replaces the horizontal scroller | +| reason/picker/action grids | `auto-fit minmax` | intrinsic; replaces stacked/scroller variants | + +### The ONE permitted viewport breakpoint (structural, never sizing) + +`@media (max-width:520px) { .wtc-head { flex-direction:column } }` — reorders the +wordmark above the solved-pill on a narrow viewport. No dimension changes here. + +### Page scroll model + +`PageColumn`'s `height:calc(100dvh - navbar)` bounding and the `min-height:100%` +"portrait static top block" hack are **retired** (WD4). The page is an ordinary +scrolling document; the subject compaction (WD3) keeps the confirm hero reachable +near the top on a phone without a bounded-height budget. Verified: no-JS phone +render puts the "Yes" button at y=912 in a 2367px scrollable document. + +### Type & spacing + +All type is `clamp(min, Ncqi, max)` (container units) — see §1c. No fixed +per-breakpoint font sizes. The retired `5.5rem` hard-coded mobile tile width +(impl brief flag) is gone, replaced by the `minmax(clamp())` grid. + +--- + +## 4. ELEMENT → react-bootstrap PRIMITIVE (no new dependencies) + +Repurposes /editor's existing component family; no display-only UI forked; shared +components gain only additive, optional, behavior-preserving props. + +| element | primitive / shared component | change | +| ----------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| Yes/Not sure/No/Skip; None-of-these; Confirm&continue | `ThumbButton` = `styled(Button)` (QuestionFeed L133+) | styled override **retokenized** (gold/navy → primary/secondary/ghost) — see §5 | +| candidate tiles | `CandidateButton` = `styled(Button)` (cardPanel L448) | reused; sizing via container grid, not fixed rem | +| subject card | `CardPanel` / `StaticCardPanel` (cardPanel L167/202) | reused; blue bg → token | +| mystery reveal | `MysteryCard` / `MysteryCardFace` (cardPanel L88) | reused; `--wtc-mystery-*` tokens | +| shortlist container | **`CandidateGrid`** styled `div` (CSS auto-fill) | NEW shared layout primitive replacing Bootstrap `Row/Col` (policy: auto-fit grids over per-feature folding). Not a new dependency. | +| confidence pill / "suggested" badge | react-bootstrap `Badge` | reused; `bg`/tokenized | +| solved-count pill | react-bootstrap `Badge` (pill) | additive, presentation-only | +| quick-negative reason chips | `NoMatchReasonStrip` + its `ChipCard`/`Button` | reused; tokenized danger frame | +| open-ended + artist search | react-bootstrap `Form.Control` | reused (ArtistVotePicker already uses it, L183) | +| tag question | `QueueTagQuestion` (`Button`) | reused unforked | +| follow-up tri-state chips + toggles | `ThumbChip` = `styled(Button)` (QuestionFeed L215+) | reused; gold→accent tokens | +| landed feedback / rate-limit | react-bootstrap `Alert` | reused; success/warning variants | +| moderator switcher | `Nav`/`Tab` (whatsthat.tsx L199) | unchanged | + +Additive props needed (all optional, behavior-preserving): + +- `CandidateGrid` — new styled div; no props beyond children. +- `MysteryCard` — no new prop; the face colour moves to a token so no API change. +- No shared component gains a required prop. + +--- + +## 5. FILE-LEVEL CHANGE ROWS + +| file | change | preserves | +| ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `frontend/src/styles/styles.scss` | (separate track) Tokyo-11 token layer active — WTC assumes it. WTC adds the 5 `--wtc-*` tokens (§1b) scoped to the page root. | — | +| `pages/whatsthat.tsx` | Delete `HERO_FIELD_BLUE_DEEP` + blue radial `StarburstBackground` → `--wtc-field`. Delete `PageColumn` `100dvh` bound + `@media max-width:767.98px { min-height:100% }` (WD4). Retint PWA `theme-color` (L263) to `--body` `#1a1b26`. Keep `VisuallyHiddenHeading` `

`. | moderator `Tab` branch; `NoBackendDefault`; ``/manifest | +| `features/questionFeed/QuestionFeed.tsx` | Delete the 3 styled gold/navy overrides (L133–248) → token variants. Delete `HeroGrid` 768px `grid-template-areas` swap → one flex `.wtc-hero` `@container`. Delete `MobileButtonRow`/`MobileCandidateScroller`/`MobileChipRow` + `mobileScrollbarCSS` (L402) → `auto-fit` grids / flex-wrap. Delete `Level2NarrowGrid` narrow special-case (L659). Wordmark drops the `WideWordmark`/`NarrowWordmark` CSS-display fork → one `WhatsThatWords` at `clamp()` size. | Level 1/2/3 flow; `getAutoTagChips`; `rejectedCandidateIds`; per-item reset (`.then()`); no-re-presentation; rate-limit; all `data-card-*` attrs + `mpc:card-selected` | +| `features/printingTags/cardPanel.tsx` | `MysteryCardFace` `STARBURST_OUTER_COLOR` → `--wtc-mystery-face`; `?` glyph fill → `--wtc-mystery-glyph`. `CardPanel`/`CandidateButton` inherit tokens. `revealAnimation` unchanged (already reduced-motion-gated). | reveal timing; stacking-context; zoom-on-hover (`ZoomableThumbnail`) | +| `features/printingTags/starburstShape.ts` + `BurstSvg` | Blue/white burst → accent-purple, OR retire in favour of `--wtc-reveal-glow` (WD5 — owner Q1). | — | +| `features/attributeVoting/*` (`ArtistVotePicker`, `QueueTagQuestion`, `NoMatchReasonStrip`, `ChipCard`) | No structural change; inherit tokens. | all vote-cast behavior | +| `features/attributeChips/attributeChips.ts` | No change (groups/labels reused as-is). | exclusion-group logic | + +--- + +## 6. FEATURES-ACCOUNTED CHECKLIST (vs the impl brief's component map) + +- [x] `confirm_suggestion` Level-1 hero (YES/NOT SURE/NO/SKIP) → shape **a** +- [x] Singleton "NO" → isNoMatch + custom-art → noted in shape **a** flow +- [x] `identify_printing` candidate grid (Level 2) → shape **b** +- [x] `getAutoTagChips` auto-cast on candidate pick → preserved (§2) +- [x] "None of these" → `NoMatchReasonStrip` (6 reason tags) → shape **c** +- [x] "Art matches, not official" one-tap (isNoMatch + custom-art) → shape **c** / **a** +- [x] Level 3 open-exclusion-group chips (tri-state) + independent toggles → shape **L3** +- [x] `AttributeChipPanel` "Filter by attribute" (opt-in) → carried as a Level-2 disclosure (tokenized; not re-drawn in mockup — behavior-preserved) +- [x] `artist_vote` → `ArtistVotePicker` → shape **e1** +- [x] `tag_vote` → `QueueTagQuestion` → shape **e2** +- [x] Moderation tab (gated) → `Nav`/`Tab`, unchanged +- [x] `MysteryCard` reveal + `?` mascot → re-themed (tokens), animation preserved +- [x] Wordmark (`WhatsThatWords`) → re-themed accent, single `clamp()` tree +- [x] Per-candidate `Spinner` on tap → preserved (impl brief flag: unchanged placement) +- [x] `data-card-*` attrs + `mpc:card-selected` event → preserved (card-dom-api.md) +- [x] Rate-limit banner → `Alert`, tokenized +- [x] No-re-presentation (`rejectedCandidateIds`) → preserved +- [x] Per-item state reset semantics → preserved +- [x] PWA manifest / installability → theme-color retint only +- [x] `APIRetractImplicitVote` (display funnel; not on this page) → untouched + +Nothing from the component map is dropped. The three horizontal-scroll wrappers +are the only DELETIONS, and they are replaced (not removed) by intrinsic grids. + +--- + +## 7. OWNER-DECISION LEDGER (WTC-rebuild round) + +| # | decision | basis | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | +| WD1 | WTC personality re-expressed through the ruled accent purple; bespoke gold/navy/starburst-blue/deep-blue-field identity killed | dispatch ruling 2; theme-fidelity lesson | +| WD2 | Orange `--primary` reserved for the confirm/action colour; purple `--accent` carries identity (wordmark, mystery card, selection, confidence pill) | Tokyo-11 palette-11 semantics | +| WD3 | Subject reference card compacts to horizontal (132px) on `@container hero <560px` instead of a full-portrait static top block | phone reachability without a bounded-height hack | +| WD4 | `PageColumn` 100dvh bounding + the `min-height:100%` portrait hack RETIRED; page is an ordinary scrolling document | container-first policy retires the anti-pattern | +| WD5 | Reveal theatrics default to the subtle `--wtc-field` + `--wtc-reveal-glow`; the full `BurstSvg` starburst is proposed for retirement (see Owner Q1) | soundness note (avoid gamified habituation) — owner call | +| WD6 | Quiet "N tagged this session" affordance kept as the ONLY reward surface; no streak/score/confetti | mix soundness note (rewarding, not habituating) — owner call (Q2) | +| WD7 | Question shapes are visually differentiated (confirm=orange primary, shortlist=neutral grid, negative=danger frame, open=dashed "tricky") so reflexive confirmation does not bleed across shapes | dispatch ruling 4 | +| WD8 | Candidate/reason/picker/action rows become `auto-fill minmax(clamp())` grids; the three horizontal scrollers + `Level2NarrowGrid` deleted | responsive policy (first consumer) | + +--- + +## ANNEX A — served-mix logging seam (backend) + +The ≥51% likely-resolve mix (46,310-card 1-click supply) is a **question_feed.py +selection-layer** concern only: it changes WHICH questions are served in what +proportion, and makes ZERO change to `vote_consensus.py` weights, MIN_VOTES/ +MIN_SHARE, or the D1/D4 mechanisms (data-brief soundness note, binding). + +UI implications actually built here: + +- The feed FEELS rewarding via the confirm shape's one-tap land + the quiet + "N tagged this session" affordance — **not** via any score/streak (WD6). +- Hard/open shapes (c/d) are visually distinct (WD7) so a run of easy confirms + does not condition reflexive tapping into the harder shapes. + +Backend seam to note (not built by this design): log served-mix composition +(ratio + family/reason per served question) per session in `question_feed.py`, +so a future audit can correlate click latency / agreement-rate against a +session's easy-question exposure (data brief, owner ask). The ONLY channel this +policy can touch soundness through is degraded human-vote signal quality from +habituation — never any weight/threshold/gate code path. + +## ANNEX B — survivor_pks / issue #433 dependency (shape b) + +The shortlist shape assumes a machine-narrowed candidate list exists on the +served item. Per the data brief, `CardScanLog.survivor_pks` is **null for 100%** +of the 134,370 to-review cards (documented gap, `survivor_pks` docstring: "an +open item, not built in this change"). Two ways to satisfy the shape; the UI is +agnostic to which: + +- (preferred) close #433 → persist `survivor_pks` at scan time; serve it on the + `QuestionFeedItem`. +- (fallback) `question_feed.py` recomputes candidate-narrowing live at serve + time (the ambiguous set is 54,797 cards; recompute cost is a backend concern). + +Until one lands, shape **b** degrades to shape **d** (open-ended) for the +ambiguous population — the UI already carries both, so no design change is +needed to ship, only a served-`type` downgrade. Confirm shape **a** (45,154) and +the negatives (shape **c**, 45,500) do NOT depend on #433 and are shippable now. + +## ANNEX C — reveal & progress animation (reduced-motion) + +- **Mystery reveal** (`MysteryCardFace` `revealAnimation`, 0.8s ease-out): the + purple "?" face fades to the scanned image on load. Already gated — + `prefers-reduced-motion: reduce` skips the fade (and therefore its + `onAnimationEnd`); the revealed image is shown statically. Preserved verbatim; + only the face COLOUR moves to `--wtc-mystery-face`. +- **Confirm-lands feedback** (`.landed` Alert): a brief fade-in on a successful + cast, then advance. Under reduced-motion: appears instantly, no transition. + Quiet by design (WD6) — success-tinted pill, no motion beyond the fade, no + sound, no confetti. +- **Reveal glow** (`--wtc-reveal-glow`): a static box-shadow on the mystery + face; no animation, so no reduced-motion concern. +- **Solved affordance**: static count + filled dots; no motion. +- If `BurstSvg` is retained (Owner Q1), its existing 5-frame animation must gain + a reduced-motion guard (it currently has none per the impl brief) — a + prerequisite of keeping it. + +--- + +## OWNER QUESTIONS (genuinely yours) + +1. **Starburst burst** — retire the `BurstSvg` explosion entirely in favour of the + calmer `--wtc-field` + reveal glow (WD5, aligns with the soundness note), or + re-express it as an accent-purple burst for more "game feel" (needs a + reduced-motion guard it lacks today)? Default in the mockup: retired. +2. **Reward surface** — keep the quiet "N tagged this session" count (mild + positive feedback, supports "feel rewarding"), or drop even that to avoid any + gamification per the data-brief habituation flag? Default: kept, quiet. +3. **Subject prominence on phone** — WD3 compacts the reference card to 132px on + narrow containers so the answer stays reachable near the top (zoom-on-tap + still available). Acceptable, or do you want the card to stay large and let + the answer sit below the fold? diff --git a/docs/proposals/mockups/wtc-rebuild/wtc-mockup.html b/docs/proposals/mockups/wtc-rebuild/wtc-mockup.html new file mode 100644 index 000000000..ea9b6ffe7 --- /dev/null +++ b/docs/proposals/mockups/wtc-rebuild/wtc-mockup.html @@ -0,0 +1,643 @@ + + + + + +ProxyPrints — What's That Card? rebuild (Tokyo-11) + + + + +
+ Question shape + + + + + + + + + View + + + + + + One layout tree, container-first. The hero is a @container; it folds by + flex-wrap + clamp() — no 768px dual layout, no horizontal scrollers. Tokyo-11 tokens + (orange acts, purple identifies), Semi radius. Card art = bright stand-ins. +
RE-EXPRESSED IDENTITY + Retired: gold #f8d42b buttons, navy, starburst blue + #4d8ddf, deep-blue field #123a6b. The WTC personality + (wordmark, the mystery-reveal, the game feel) now rides the ruled accent purple + #bb9af7; orange #ff9e64 is the confirm/action colour. +
+
+ + +
+ AUTO · fluid +
+ +
+ ProxyPrints + + EditorDisplayWhat's That Card? + + + +
+ +
+
+

What's That Card? + Help tag which real-world printing each scanned image depicts

+ + + 13 tagged this session + +
+ +
+ +
+
+
+
+
Lightning Bolt
+
+
+ ? + the scanned image you're identifying + no strong machine candidate for this one +
+
+
+ ? + + reveal moment: the purple “?” card flips to the scan on load + (respects prefers-reduced-motion) +
+
+ + +
+ + +
+
+ 1-tap confirm +

Is this the printing?

+
+
+ +
+ Lightning Bolt + Limited Edition Beta · LEB · #161 + strong machine match +
+
+
+ +
+ + + +
+
+ ✓ Tagged — nice. Next card loading… + (quiet confirm-lands feedback) +
+ + +
+
+ pick one +

Which printing is this?

+
+

Tap the exact match. Candidate list is narrowed at serve time + (survivor list, issue #433).

+
+ + + + + + +
+
+ + +
+
+ + +
+
+ not a printing +

No real printing matches?

+
+
+
⚑ Mark why this isn't a catalogued printing
+
One tap files a classified negative — clearly a different action from confirming a match.
+
+ + + + + + +
+
+
+ + +
+
+ + +
+
+ ◆ tricky one +

You tell us — which printing?

+
+

No strong machine candidate. This is one of the harder ones — + take your time.

+
+ +
+ Can't place it? + Mark unidentifiable + · + Skip +
+
+
+ + +
+
+ artist +

Who's the artist?

+
+

OCR read “C. Rush” + — confirm or search.

+ +
+ + + + +
+
+ + +
+
+ attribute +
+
+
Does Borderless apply to this card?
+

Single yes/no attribute — the near-threshold + “Borderless” pool (51,163 pairs) mostly resolves as a byproduct of a printing pick.

+
+ + + +
+
+
+ + +
+
+ ✓ matched · one more thing +

Confirm the attributes

+
+

Auto-tagged from your pick; adjust only what's wrong, then continue.

+ +
Border color
+
+ + + +
+ +
Frame style
+
+ + + +
+ +
Independent
+
+ + + + +
+ +
+ + +
+
+ +
+
+
+
+
+ +
+
+ body + conf + raised + panel + text + muted + primary/action + accent/identity + success + danger + warning +
+

Question shapes → interaction contract (all shapes are in the DOM; the strip toggles which is shown)

+ + + + + + + + + +
ShapeSupply (data brief)Preserved contractDistinct so reflex doesn't bleed
a · Confirm45,154 single-candidateLevel 1: Yes casts + auto-tags + advances; Not sure/No → Level 2; singleton No → isNoMatch+custom-artone big card + one orange primary; the only shape with a reflexive path — paired with equal-weight No
b · Shortlist1,156 multi + 54,797 ambiguous (serve-time narrow, #433)Level 2: pick → submit + getAutoTagChips auto-casts; None-of-these → reason stripgrid of neutral tiles, NO primary — a real pick is required
c · Quick-negative45,500 unknown-set/eliminatedone-tap classified negative (isNoMatch + reason tag) — the 6 seeded reason tagsdanger-red framed, negative verb — visibly not a confirm
d · Open-endedsmallest slice (cold-start / no-evidence 32,838)identify_printing with no shortlist; search/typeahead; mark unidentifiabledashed accent “tricky one”, search field, no button to reflex-tap
e1 · Artistartist family (13 near, byproduct)ArtistVotePicker unforked — search + candidate buttons + Unknownautocomplete idiom, not a discrete pick set
e2 · Tag51,163 Borderless (mostly byproduct)QueueTagQuestion unforked — Apply / Not applicable / Skipsingle yes/no attribute question
L3 · Follow-upopen exclusion groups onlytri-state chips cast immediately; Confirm & continue / Skip; no re-presentationappears only after a pick; “one more thing” framing
+

Full token table (I=inherited Tokyo-11 / N=new WTC-identity, token-derived), + question-shape → file:line mapping, features-accounted checklist, and the three annexes + (mix-logging seam, survivor_pks/#433 dependency, reveal/progress animation + reduced-motion) + ship in the binding spec alongside this mockup.

+
+ + + +