Compare commits

...
Author SHA1 Message Date
Abdul WahabandClaude Opus 5 b291f4217e Fit the questionnaire seed into the measured comp pipeline, fix CI
The live-reference test still asserted SKILL.src.md's pre-rewrite setup
phrasing, which failed every pipeline on main and here and blocked the
generated-output sync; the assertions now match the current text. The
sync list gains .veto, added in #675 but never listed. Two seed-world
seams in the comp-led phases close: DESIGN.md's named faces ride into
font-match as candidates instead of losing silently to a metric
lookalike, and a sampled spec color within render noise of a recorded
hex builds at the recorded value. The cue also stops claiming type
character it cannot carry; comp prompts name the recorded faces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 15:55:54 +05:00
Abdul WahabandClaude Opus 5 6efe4a43c8 Restore generated provider output to main's state
This PR stays source-first per 187e589e: the mid-rebase regeneration
swept in provider dirs the sync workflow owns, .veto included, which
main's CI produces after merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 10:21:53 +05:00
Abdul WahabandClaude Opus 5 66632b050a Map main's two new detector rules into the hook discipline groups
organic-clip-path and buried-raster landed on main after this branch's
hook-rule-presentation map; both are produced-imagery rules, grouped
under Imagery beside shape-assembled-illustration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 10:18:14 +05:00
Abdul WahabandClaude Opus 5 93218c543a Regenerate provider output and hook rules after rebase onto main
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 10:13:02 +05:00
Abdul Wahab 5b9974a50a update 2026-09-01 10:09:45 +05:00
Abdul Wahab 07ebdfd5b1 update 2026-09-01 10:09:45 +05:00
Abdul Wahab 35de8595b6 update 2026-09-01 10:09:45 +05:00
Abdul Wahab 82b03772e4 update 2026-09-01 10:09:03 +05:00
Abdul Wahab f775f9e585 Fix the three design-context findings from the command test pass
- edit_request replies now apply attached --answers/--context through the
  session, as visual-cues.md's edit-loop contract already promised
- the bundle's per-file cap rises to 8 MiB so the chosen cue image survives
  export/import (the 20 MiB bundle ceiling stays)
- the questionnaire refuses to start without visual-cues/cues.json instead
  of serving a broken palette screen

AI-assisted: implemented by Cursor Composer executing plan-27.
2026-09-01 10:09:03 +05:00
Abdul Wahab 618dcae7bf udpate 2026-09-01 10:09:03 +05:00
Abdul WahabandClaude Fable 5 065bd4f3ca Trim dead code and tighten the branch to surgical scope
- palette-picker.js: drop the superseded APP/DOCS/INDEX copy tables, the
  disabled font-rail hoist machinery, LOREM.caption, and an orphan marker
- design-context.css + dcx-hierarchy.css: drop the demo-era specimen,
  radius, and spacing-bar rule groups nothing renders
- index.astro: drop an unreferenced boards const
- de-export module-internal names (store, bindings, portability,
  visual-cues); remove picker-server's unused doc-session kill switch and
  document --doc in its help
- palette.mjs: realpath the entry guard so symlinked installs still run
  the CLI (same guard as visual-cues.mjs)
- revert serve-question.mjs copy ride-along; drop the plan/ gitignore
  entry; dedupe a build-picker comment; file design-context with the
  system category entries; alphabetize IMPECCABLE_SUB_COMMANDS
- new-work.md: drop a same-screen restatement of the comp/world ranking
- CLAUDE.md: correct the picker gate baseline to the measured 606 and
  explain the per-element multiplication; the trims removed the two
  dead-block font findings
- DESIGN-CONTEXT-ARCHITECTURE.md: list portability.mjs in the code layout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 10:08:22 +05:00
Abdul WahabandClaude Fable 5 6c2a79891c Remove stale design-context-categorization docs
PROVENANCE.md specs a design.json questionnaire/producedPaths schema that
shipped nowhere and contradicts the store DESIGN-CONTEXT-ARCHITECTURE.md
documents; design-context.html is an 89KB frozen prototype snapshot with
no inbound references (the port to picker/ is complete, and git history
keeps it); the README only indexed the two.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 10:08:03 +05:00
Abdul WahabandClaude Fable 5 187e589ef7 Drop generated provider output from the feature branch
The 17 harness dirs and plugin/ revert to main's state: feature PRs are
source-first per CLAUDE.md, the post-merge sync workflow regenerates them,
and the committed snapshot was stale by one commit (its context.mjs
predated the DESIGN_CONTEXT directive). Also add .agent and .github/agents
to the sync workflow's GENERATED_PATHS so those two outputs stop relying
on manual release syncs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 10:08:03 +05:00
Abdul Wahab 0aec856f45 update 2026-09-01 10:08:03 +05:00
Abdul Wahab 89c842e5f4 update 2026-09-01 10:07:07 +05:00
Abdul Wahab a5cf9266e6 Add new design context document 2026-09-01 10:06:21 +05:00
Abdul Wahab bba73a2283 update 2026-09-01 10:05:59 +05:00
Abdul Wahab 33ea98ae3e update 2026-09-01 10:05:00 +05:00
Abdul Wahab 7e9a873499 udpate 2026-09-01 10:05:00 +05:00
Abdul Wahab 4199ad4bc4 update 2026-09-01 10:05:00 +05:00
Abdul WahabandCursor 5400675585 Fix dead links in PROVENANCE.md after docs trim
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:04:59 +05:00
Abdul WahabandCursor 3202a3aab5 Trim design-context-categorization docs to prototype, provenance, and index
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:04:59 +05:00
Abdul WahabandCursor 4f069a39ef Untrack the picker gallery toolchain; tmp is scratch again
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:04:59 +05:00
Abdul WahabandCursor ba999bd799 Remove unreferenced mode illustration binaries
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:04:59 +05:00
Abdul Wahab 6343ec689b update 2026-09-01 10:04:59 +05:00
Abdul Wahab 8b6bbaef3d update 2026-09-01 10:04:59 +05:00
Abdul Wahab 6cfeb7d374 update 2026-09-01 10:04:59 +05:00
Abdul Wahab bccbc98c9b update 2026-09-01 10:04:59 +05:00
Abdul Wahab 07c8d43b5d update 2026-09-01 10:04:59 +05:00
Abdul Wahab 2fd4c86794 update 2026-09-01 10:04:59 +05:00
Abdul Wahab f68c27a13f update 2026-09-01 10:04:59 +05:00
Abdul Wahab 80e26259e1 update 2026-09-01 10:04:59 +05:00
Abdul Wahab 40ff2b6eda updated 2026-09-01 10:04:59 +05:00
Abdul Wahab be7d4d9242 update 2026-09-01 10:04:59 +05:00
Abdul Wahab cc8d992d8b update 2026-09-01 10:04:59 +05:00
Abdul Wahab fa5d7474b3 update 2026-09-01 10:04:59 +05:00
Abdul Wahab 6d0dc0287d update 2026-09-01 10:04:59 +05:00
Abdul Wahab 6fbf5b952f update 2026-09-01 10:04:59 +05:00
Abdul WahabandCursor 1537368b8c Make the questionnaire ask_instruction sites sentence-initial
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:04:59 +05:00
Abdul WahabandCursor 8b4507db53 Regenerate bun.lock after rebase onto main
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:04:59 +05:00
Abdul WahabandCursor a256b6e42b Scope the comp's authority and bind builds to the recorded world.
The comp rules composition; the world rules material. Adds the authority
ranking to new-work.md, rewrite and token discipline to the craft floor,
token and motion evidence checks to the finish reviewer, derived text
inks to the questionnaire seed, and stale-seed disclosure at attended
stops. From the ekihana run's confirmed drift.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:03:52 +05:00
Abdul WahabandCursor 9fbda768e6 Launch the picker in the harness browser first.
Name Cursor browser_navigate as the in-IDE viewport, then other in-app tools, then the system opener. Same ladder in serve-question stdout so agents stop using macOS open while the IDE browser works.

AI-assisted.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 72fec2849c Dedupe the seed interview against the browser questionnaire.
On the questionnaire path, chat now asks only assets, named references, and one anti-reference; color, type, and motion stay in the picker. Interview-only still asks all five. visual-cues.md drops the retired Q-keys and re-anchors font pairs from surfaces, references, and assets.

AI-assisted.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 44aa29fb63 Constrain dealt font pairs to Latin-script faces and English copy.
CJK and other non-Latin families render the picker's previews and scale sheets wrong until language-aware pairs exist; the fallback deck comment matches the new visual-cues Step 6 rule.

AI-assisted.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor c582fd4699 Require verbatim skip quote for seed pause and fix hidden doc shell.
Direction rolls on projects without DESIGN.md now refuse until the user accepts document --seed or re-runs with --seed-declined carrying their actual skip answer; IMPECCABLE_SEED_DECLINED=1 remains the unattended harness escape. Hide the design context shell when [hidden] so it does not reserve viewport space. Extend concept-seed tests for the seed gate.

AI-assisted.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 5758e4c6bd Recommend the seed questionnaire before rolling a direction on world-less projects
When new-work reaches world creation and DESIGN.md is missing, pause and
offer /impeccable document --seed (skippable) so the visual world can come
from the user's own choices instead of an assigned roll.

AI-assisted change (Cursor agent), directed by the maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor b56ca30fcd Restore dry-run guard around the release build step
The picker staleness check added on this branch had replaced main's
dry-run branch, so `--dry-run` executed the real build. Keep the picker
hash check but only on a real run, matching main's dry-run contract.

AI-assisted (agent-implemented, maintainer-directed).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 601a7a8755 Enrich design context Product, Brand, and Color sections.
Add product conversion/principles/surface copy and leading-surface badge; Brand voice and principles blocks; Color ink chips plus as-dealt/edited provenance tags. Extend visual-cues schema. Polish font-pair preview cells 24–25 and sync gallery replica.

AI-assisted.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor b4cfe626ff Polish Portfolio type-board captions for font-pair preview cells 19–20.
Add gallery avatars, tighten the experience index layout, and sync the improved previews gallery replica.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor a975a8bcc1 Fix font-pair preview polish for cells 22–24 and sync gallery experiments.
Tighten Landing, App UI, and Docs type-board copy, color, spacing, and gallery experiments so the improved preview gallery matches the live picker.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor da94340e9b Scale Docs board carry bars and fix Portfolio preview cells 19–21.
Board-stage read artboards get thicker bar weight in picker.css; the gallery replica adds matching experiments plus desktop caption circles for Portfolio restrained/committed/drenched and drenched color roles on cell 21.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 457c2d877c Improve strategy previews for Docs wireframe weight and App UI color roles.
Boost docs strategy-stage line and dot weight to match App UI legibility, keep phone artboard lines proportional via pv-text scaling, and extend the gallery replica experiments for cells 14–18.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 470b655096 Number questionnaire preview gallery cells 1 through 75.
Add sequential labels to figcaptions and iframe titles in the gallery builder, then regenerate baseline and improved HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor d886003879 Track questionnaire previews gallery and color strategy experiments.
Un-ignore tmp/questionaire for the gallery toolchain, baseline HTML, and the improved replica with drenched, committed, and full-palette color fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul Wahab 55617ac5d8 update 2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 60130bcea9 Add strategy-band contrast alert and expand design context fields.
Mirror the screen 02 neutral contrast badge on the strategy strip, share alert
painting between both strips, and render richer PRODUCT.md context in the
design document with matching visual-cues schema guidance.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor ae1c632594 Improve picker finish screen, neutral contrast alert, and DCX tile rest states.
Replace the neutral palette hint warning with an on-swatch badge, simplify the finish loader screen, and keep typography/material vignettes ink-only until hover.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 6d684d2d6a Mute design-context tile accents at rest like Color.
Product, Brand, Typography, and Material vignettes now use anim-join so their gold accent reads at 25% opacity before hover and brightens on hover, matching the Color tile.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor adabd62011 Move type scale and layout ahead of the five-card hub.
Type scale and layout return to the linear path before configure; the hub shrinks to five cards with updated navigation. Also fixes palette tooltip clipping, drops the design-context masthead and icon hub notes, and locks the mosaic shell to the viewport without overflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 0b867f0e25 Tighten configure hub pill sizing.
Pills shrink to content width and use slightly more padding so values read as compact chips rather than full-width rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor b629daef01 Refine configure hub cards with pills, notes, and layout polish.
Add the intro lede, per-row patina answer marks with DOM-sourced notes, mode-tile hover, and tighter card typography including inline pills and title arrows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 80211f3364 Add configure hub after the font pair screen.
Let questionnaire runs finish early or jump into the seven remaining questions from a summary card grid, with hub navigation and per-answer summaries.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 5dffc569ff Warn on screen 02 when the palette neutral fails contrast
Add neutralContrastIssue() with a 7:1 ink floor and a 3:1 primary
separation floor, re-run it from renderPreview(), and paint the palette
hint in vermilion while the working neutral is bad. The warning clears
as soon as the neutral is fixed.

Prepared with AI assistance (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 38b7029184 Build out the layout screen's three compositions
Each option now draws its own page rather than a shared field with a
different column count. Simple grid holds strict paired rows at equal
image measures, balanced alternates wide images against captions, and
freeform crops a lead, adds an upper portrait, and spills the card row
into a set of prints with their own captions and marks. All three carry
handset sequences at the reference measures.

Behind them: one shared alignment-grid component mounted on every body
that asks for it, code-native nav primitives placed at the reference
measures rather than left to their grids, a project image-tone contract
the boards key their fills on, and footer link groups.

Prepared with AI assistance (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor a5083e23e1 Default the color question to full palette and color the type boards
The default lives in each surface's fallback, not in the markup: show()
checks whichever row the fallback names every time a tab opens, so the
checked attribute on the option never survived. Persuade, Operate, and
Read now open on Full palette. Experience keeps Restrained because four
working colors are not on its allow list, and a default pointing at a
blocked row would leave that tab with nothing selected.

The font screen's boards also take the chosen palette under Restrained,
which is the one answer no strategy block rewrites. They were drawing a
page the palette had never reached while screen 03 kept its color under
the same answer.

Prepared with AI assistance (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor a34c9828aa Add the layout screen's portfolio board and default color to full palette
The index board grows the parts only a portfolio has: a project rail, a
support row, and a pagination rail restated at the foot of the handset
stack. They mount behind a prop rather than for every board, because the
motion screen reads the same index body through :nth-of-type and a stray
sibling would shift its scenes. Their sheet loads after the boundary,
corner, and depth sheets so it wins ties on source order.

The color strategy question now opens on Full palette.

Prepared with AI assistance (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor 05f228728e Fix uneven gallery squares and size the phone section copy
The card row's squares came out three different widths: centering the
cards in the gallery shrank each one to its own label, and the square is
a share of its card. The cards take their column again and their contents
do the centering.

On the handset the section title and its paragraph were the smallest type
on a board that has room for more, so both take a step up.

Prepared with AI assistance (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul WahabandCursor e2048614b5 Improve font pair and color strategy questionnaire screens
Font pair screen: drop the phone footer from the type boards, widen the
hero picture, split the editorial band evenly with the card row, center
the desktop cards, and fix the label line heights. The handset takes a
26px headline, stepped section spacing instead of one uniform gap, and a
single centered proof item between chevrons. Every word slot on these
boards now draws lorem rather than product copy, so a pair is judged on
its setting.

Color strategy screen: one-line question, an options panel that hugs its
rows, a preview whose height is free again, and the CTA stack in the same
place the font screen keeps it.

Also nudges the type scale specimen's top and left padding.

Prepared with AI assistance (Cursor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul Wahab 7d4e1bff0c update 2026-09-01 10:02:40 +05:00
Abdul Wahab 3214ead9c2 update 2026-09-01 10:02:40 +05:00
Abdul Wahab b12b67d344 update 2026-09-01 10:02:40 +05:00
Abdul Wahab cadb70a09e update 2026-09-01 10:02:40 +05:00
Abdul WahabandCursor bf8e3ee0c2 Add standalone design context prototype and WHAT spec.
Ship the browsable mosaic document as an isolated HTML file and document the build intent in WHAT.md so the categorization work has a concrete UI target.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00
Abdul Wahab c06bf3f558 Add design doc 2026-09-01 10:02:40 +05:00
Abdul Wahab ecadbc9556 update 2026-09-01 10:02:40 +05:00
Abdul Wahab 4c1fbbc7c0 update 2026-09-01 10:02:40 +05:00
Abdul Wahab 4b0c4a5e80 update 2026-09-01 10:02:40 +05:00
Abdul Wahab b4110667c9 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 4d919ec519 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 3e67293747 update 2026-09-01 10:02:39 +05:00
Abdul Wahab ca4f39e01d update 2026-09-01 10:02:39 +05:00
Abdul Wahab e0ddad3198 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 24cea3bd60 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 6b30ce5d02 update 2026-09-01 10:02:39 +05:00
Abdul Wahab cf80c3df1a update 2026-09-01 10:02:39 +05:00
Abdul Wahab 29598150b4 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 2feb2bdb9d update 2026-09-01 10:02:39 +05:00
Abdul Wahab 035e191ce5 udpate 2026-09-01 10:02:39 +05:00
Abdul Wahab 2f491f54c3 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 1be3f1145b Update 2026-09-01 10:02:39 +05:00
Abdul Wahab 1d192d1f39 Update 2026-09-01 10:02:39 +05:00
Abdul Wahab ba9b1cd8c3 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 8489467b99 Update 2026-09-01 10:02:39 +05:00
Abdul Wahab 0db6186617 Update 2026-09-01 10:02:39 +05:00
Abdul Wahab 154a84df56 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 336ee34b73 update 2026-09-01 10:02:39 +05:00
Abdul Wahab f3c26f1f42 update 2026-09-01 10:02:39 +05:00
Abdul Wahab 5602cdfc7e update 2026-09-01 10:02:39 +05:00
Abdul Wahab 7af6e831fb update 2026-09-01 10:02:39 +05:00
Abdul Wahab 83b4fbcb5d update 2026-09-01 10:02:39 +05:00
Abdul Wahab e9f36ad9d8 update 2026-09-01 10:02:39 +05:00
Abdul WahabandCursor 28abe9a071 Improve strategy preview wireframes on desktop and phone.
Restrained mode now reads the palette like screen 02: primary-tinted
headline, tertiary eyebrow, secondary gradient hero, outlined ghost
CTA, and four-role gallery accents. Artboards lift with a soft shadow;
the phone gains surface bars and a second copy line. Each strategy
block sets matching image-gradient stops so committed, full palette,
and drenched transitions stay depthful instead of flat fills.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:39 +05:00
Abdul Wahab 6444deb361 update 2026-09-01 10:02:39 +05:00
Abdul WahabandCursor 9af6fc1cb7 Restyle font pair cards after fonttrio's pairing anatomy.
The heading family name sits big in its own face; one sentence in the
body face names its partner in bold and carries the why line. The
sentence demonstrates the body font, so the Heading/Body labels go.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:39 +05:00
Abdul WahabandCursor 51ce043130 Rework typography screen to match the questionnaire pattern.
Screen 04 drops the kicker and the text eyebrow, renders six large
scrollable specimen cards (heading and body each labeled and set in
their own face), and reuses screen 03's dual desktop-plus-phone
artboards with both hero copy blocks typeset live. The type hero
widens its copy column so wide display faces clear the image block.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:39 +05:00
Abdul WahabandCursor 626bfbb0a8 Add color strategy screen and font-pair pipeline step.
Screen 03 lets operators compare restrained through drenched strategies on the dual-artboard preview; visual-cues now composes fonts.json before launch, and the picker server exposes it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:39 +05:00
Abdul WahabandCursor f7751fad0c Improve palette preview: dual artboard, drop register wiring.
Show landing and app wireframes side by side with palette-driven accents instead of PRODUCT.md register and /context.json, matching v4's per-surface modes. Assisted-by: Cursor AI agent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:39 +05:00
Abdul WahabandCursor d64909decf Reconcile questionnaire branch with main's site split and redesign
Post-rebase repairs after main moved 226 commits (site/ split to the
private impeccable-site repo, DESIGN.md spec now eight sections, register
extraction replaced by extractSectionValue, seed mode rewritten):

- document.md: transplant this branch's seed pipeline (assets, interview,
  visual cues, picker) onto main's rewritten file; adopt the canonical
  section vocabulary. The interplay with new-work's workshop is left for
  PR review.
- picker: vendor kinpaku tokens/kit CSS, hero art, and favicon under
  picker/ (site/ no longer exists here); drop the sharp resize step and
  ship the compressed JPEG directly; declare astro/sharp-free deps.
- picker-server: read the register via extractSectionValue('Register')
  since extractRegister is gone from context.mjs.
- palette.mjs / utils.js / test-suites.mjs / package.json / release.mjs:
  rebuild as main's version plus this branch's additions, undoing hunks
  the -X theirs replay had clobbered.
- craft.md / shape.md: reset to main (this branch's net change was zero).
- progress bar: animate transform instead of width (design hook finding).

Validation: bun run build green (prose validators clean), bun run test
198/198, picker-server suite 7/7.

AI-assisted (agent-implemented, maintainer-directed).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:39 +05:00
Abdul WahabandCursor c19c349fc6 Add screen 02 palette picker: cue deck, sampling rings, band tools
Deck of cue cards + seed palettes with scroll-snap browsing, draggable
color-sampling rings with loupe, four-band panel (copy hex+OKLCH, tint
strip, native custom color, card-local reset), /palettes.json route.

AI-assisted (agent-implemented, maintainer-directed).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:05 +05:00
Abdul Wahab ba7f3c8891 Add browser questionnaire foundation
Prepared with AI assistance.
2026-09-01 10:02:05 +05:00
Abdul Wahab 1f20997a6d Revert "Default the gemini provider to Nano Banana Pro."
This reverts commit 666d21dab4755efcf27f0aa34954ca91d8f67e39.
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 72ba69e3a4 Default the gemini provider to Nano Banana Pro.
gemini-3-pro-image is Google's strongest image model; IMAGE_GEN_MODEL still overrides for the cheaper flash tier.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 98ab04b94b Improve visual cue heroes for color real estate and Gemini safety.
Ban hex codes from image prompts to stop Nano Banana swatch strips, require crisp deep focus and set-dressed surfaces inside close product-world scenes, and add wordless and real-estate inspection retries. The Gemini wrapper now converts JPEG bytes to PNG and infers AQ.-prefixed keys.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor c23c587ed5 Add provider-flexible image API path and dedupe visual cue compiles.
Ship image-gen.mjs with bfl and gemini backends plus custom delegation, document the keyless flow in image-api.md, and tighten seed Step 4 so missing keys ask once and opt-out falls back to text-only seeding. Compile now removes in-folder hero intermediates so each cue keeps one PNG.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor a0f7fae4e2 Speed up visual cues wave: drop smoke test, slim spawns, brief-only reads.
Replace preflight generation with harness tool presence, move territories into brief.md, forbid specialist repo reads, and document native tool output paths.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor af01134825 Drop artifact sheets from visual cues for faster hero-only pipeline.
Each specialist now generates one hero image instead of a second chroma-key sheet, and the compile step replaces crop with a simpler cues.json shape.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul Wahab 93e0571f20 update 2026-09-01 10:00:42 +05:00
Abdul WahabandCursor f8981c61dd Switch artifact sheets to per-cue chroma keys kept in crops
Drop the two-pass matte pipeline in favor of one chroma-key sheet per
cue: each specialist picks the farthest-hue key, stages larger artifacts
with shadowless crisp edges, reports CHROMA in its reply, and crop records
the hex in cues.json for browser keying downstream.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 7593ec9d93 Matte transparent artifacts from two sheet passes; require square, well-lit generations
Artifact crops now ship with real alpha: each sheet is generated twice
(cream, then black via image edit) and fused by a new difference-matting
command, sidestepping models' inability to emit alpha and the chroma
key's spill. Crop and matte reject non-square inputs so squareness must
come from the generation call's 1:1 parameter, and the hero prompt now
demands bright, generous light so palettes stay sampleable.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor a84002d69b Fix cross-subagent image collisions on shared output filenames
A live run shipped one hero under three slugs: parallel subagents all
wrote the generation tool's output as a generic hero.png in the shared
project assets folder, then copied and referenced each other's files.
Spawn tasks now require slug-based output filenames, using only the
tool-reported path, a verify-your-own-files check before reporting, and
the parent hashes all heroes before cropping and re-spawns duplicates.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 2ae67485fb Stage hero palettes as large color fields, framed close
Live cues rendered the good colors as slivers: the hero prompt asked for
an atmospheric scene with a 60% neutral ground and a small accent. The
skeleton now frames tight on the subject and stages each color as a
large unbroken field (neutral about half, primary a continuous third,
tertiary small but legible), with the rationale in the fill rules.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 6e4dcef80c Fix: em dashes in the territory example set
AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 0473c9f7e0 Give every territory a hue ground and gate image gen on self-critique
Investigation of a live run showed five of six palettes converging on the
brief's deep-teal anchor: territories were scenic but hue-silent, and the
anchor reached every spawn unscoped. Territories now must close on a hue
ground (no two in one family), the anchor is owned by exactly one
territory with everyone else told to leave it, spawn tasks may not be
compressed, and each subagent critiques its palette and concept against
the rules before generating.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 71f683ddb6 Harden the parallel mandate and keep six cues on the serial path
Name the common spawn tools, require all six spawns in a single message,
and call out spawn-await-spawn as the serial-loop failure it is. The
no-subagent fallback now runs all six personas instead of four, so the
user gets six cues on every path.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor a6d818b31c Assign territories upfront and run the studio as one wave
Drop the sequential palette chain: the parent carves six visual,
emotional, or strategic territories from the brief, then all six
personas spawn in one parallel batch, each composing inside its own
territory and generating both images in the same task.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 4bf39fe09d Split the studio into a palette chain and an image wave
Personas now compose sequentially, each shown the color territories the
earlier personas claimed, replacing the similarity script and peer
critique as the uniqueness mechanism. Generation moves to a parallel wave
of dedicated image-prompt specialists that stage the finished palettes.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul Wahab e237df2b11 Revert "Replace peer critique with a similarity convergence loop"
This reverts commit 57e45628f39149cf3700da4a431f66a595754d1d.
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 24aaabdd26 Replace peer critique with a similarity convergence loop
Drop the six-way critique wave from the visual-cues studio: after the
compose wave, the OKLab similarity script loops with revision waves for
flagged personas only (capped at three) until every verdict is clear,
then the generation wave stages the settled palettes.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 54a5cbd379 Unify studio flow on wave numbering
Drop the parallel "round" vocabulary from the visual-cues studio so one
counting system covers the three spawn waves; the subagent template calls
the palette a first draft instead.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor a0fd0475f5 Compose cue palettes with six competing persona subagents
Replace the single-mind palette composition in the visual-cues pipeline
with a three-wave studio: six persona specialists compose in parallel, a
new OKLab similarity command plus peer critiques build each persona's
scorecard, then each revises and generates its hero and artifact sheet.

AI-assisted (Cursor agent), directed by abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor ec83174eb2 Generate visual cues for document seed color picks
Document seed mode's palette pick used to ask the user to imagine colors
from words. Add a visual-cues pipeline: compose four-hex palettes from the
brief (mood, references, PRODUCT.md), generate a hero scene plus a cream
artifact sheet per concept, crop the sheet into isolated artifacts, and
write cues.json so the user can pick a direction by eye.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul Wahab f92adeab17 Revert "Recognize the seed-collected image key in shape and craft."
This reverts commit 70eddbe2742948e1b8e95a3ef515de4e85b55a80.
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 65ac2a5f13 Recognize the seed-collected image key in shape and craft.
A stored IMAGE_GEN_API_KEY in .impeccable/.env now counts as image generation being available for shape probes and the craft codex flow. The key ask itself stays exclusive to document seed mode; shape and craft still never prompt for APIs or tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 840165463c Add editor voice and capability-gated visual cues to document seed.
Seed questions now avoid skill vocabulary (mirroring init.md's magazine-editor line), and a new step offers 2-4 generated palette cue images: native image_gen when the harness has it, otherwise an opt-in IMAGE_GEN_API_KEY in gitignored .impeccable/.env.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:42 +05:00
Abdul WahabandCursor 94ed8d7d45 Extend document seed mode to gather brand assets first.
Ask for logos, reference images, and moodboards before the five-question interview so the first DESIGN.md seed can ground taste selections in real visual context.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:00:41 +05:00
126 changed files with 43291 additions and 63 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "impeccable",
"metadata": {
"description": "Design fluency for AI harnesses. 1 skill, 23 commands, and curated anti-patterns for impeccable frontend design."
"description": "Design fluency for AI harnesses. 1 skill, 24 commands, and curated anti-patterns for impeccable frontend design."
},
"owner": {
"name": "Paul Bakaus",
@@ -11,7 +11,7 @@
"plugins": [
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"description": "Design fluency for frontend development. 1 skill with 24 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.1.2",
"author": {
"name": "Paul Bakaus",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"description": "Design fluency for frontend development. 1 skill with 24 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.1.2",
"author": {
"name": "Paul Bakaus",
@@ -21,10 +21,12 @@ concurrency:
env:
GENERATED_PATHS: >-
.agent
.agents
.claude
.cursor
.gemini
.github/agents
.github/skills
.grok
.hermes
@@ -35,6 +37,7 @@ env:
.rovodev
.trae
.trae-cn
.veto
.vibe
plugin
+14
View File
@@ -61,6 +61,14 @@ Thumbs.db
.impeccable/live/deferred-svelte-component-accepts.json
.impeccable/history/
.impeccable/config.local.json
# API keys for optional image-generation cues (document seed mode).
.impeccable/.env
# Generated visual-cue images + cues.json (document seed Step 4).
.impeccable/visual-cues/
# The design-context store itself is the user's own record and is trackable;
# only its session state and its generated exports stay local.
.impeccable/design-context/runtime/
.impeccable/design-context/exports/
**/.impeccable/hook.pending.json
.impeccable/provider-smoke/
src/__impeccable_provider_smoke_*.html
@@ -129,3 +137,9 @@ tmp/
# PNGs, the old card backup). The canonical generator is `bun run og-image`
# (scripts/generate-og-image.js); this dir is throwaway and safe to delete.
.og-build/
# Intermediate and generated output for the picker build. Release builds sync
# the generated picker into the tracked provider harness directories.
build-picker/
skill/scripts/picker/
+37 -1
View File
@@ -4,7 +4,8 @@
"ignoreFiles": [
"tests/fixtures/**",
"tests/framework-fixtures/**",
"tests/detect-antipatterns.test.js"
"tests/detect-antipatterns.test.js",
"picker/styles/design-context.css"
],
"ignoreValues": [
{
@@ -15,6 +16,41 @@
],
"createdAt": "2026-07-17T00:00:00.000Z",
"reason": "Live overlay chrome is injected over arbitrary host pages and builds a self-contained UI with its own small type scale; DESIGN.md's ramp describes the impeccable website, not this widget"
},
{
"rule": "broken-image",
"value": "*",
"files": [
"picker/pages/index.astro"
],
"createdAt": "2026-07-23T14:51:18.241Z"
},
{
"rule": "design-system-font-size",
"value": "*",
"files": [
"picker/styles/picker.css"
],
"createdAt": "2026-07-23T21:00:00.000Z",
"reason": "The picker is a self-contained questionnaire widget with its own kit-token type scale, and screen 04's cards render arbitrary Google Fonts at specimen sizes; DESIGN.md's ramp describes the impeccable website, not this tool (same footing as the live-browser.js entry)"
},
{
"rule": "design-system-font",
"value": "*",
"files": [
"picker/scripts/design-context.js"
],
"createdAt": "2026-07-30T13:11:54.730Z",
"reason": "Runtime font loader: builds a Google Fonts URL from the family the agent/user chose in the questionnaire; no literal face is being introduced"
},
{
"rule": "bounce-easing",
"value": "cubic-bezier(.34, 1.56, .64, 1)",
"files": [
"picker/scripts/palette-picker.js"
],
"createdAt": "2026-08-20T02:53:58.453Z",
"reason": "plan-18 step 10c: byte-exact port from tmp/motion-previews-premium.html installChoreographedPremium spring stagger"
}
]
},
+2
View File
@@ -64,6 +64,8 @@ TDD order is non-negotiable:
5. Add two adapters that wrap the pure check: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** adapters into **both** element loops in `cli/engine/detect-antipatterns.mjs` (browser loop ~line 1837, jsdom loop in `detectHtml` ~line 2058). Forgetting one is the most common mistake.
6. Verify on a live page at `http://localhost:4321/fixtures/antipatterns/{rule-id}.html` and on the homepage. The two adapter paths can disagree.
To scan the browser questionnaire, use `node cli/bin/cli.js detect skill/scripts/picker/` after `bun run build:picker`. Name the directory, never `index.html` on its own: the page links its stylesheet, so a page-only scan reads none of the picker's CSS. The baseline and the reason each accepted finding stands are in CLAUDE.md's **Picker anti-pattern gate**.
Conventions: wrap the identifying heading text in straight double quotes inside snippets so the fixture test can extract it. jsdom-specific helpers `resolveBackground()`, `resolveGradientStops()`, and `parseGradientColors()` exist because `background:` shorthand isn't decomposed and computed colors aren't normalized in jsdom — use them. Reference rules to copy from: `side-tab` (border), `low-contrast` (color+gradient), `icon-tile-stack` (sibling relationship), `flat-type-hierarchy` (page-level).
## Commit & Pull Request Guidelines
+26 -1
View File
@@ -2,7 +2,7 @@
## Architecture (v3.0+)
There is **one** user-invocable skill, `impeccable`, with **23 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `skill/`:
There is **one** user-invocable skill, `impeccable`, with **24 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `skill/`:
- `SKILL.src.md` — frontmatter (with the auto-trigger-optimized description and the `allowed-tools` list), shared design laws, and the **Commands** router table. Provider `SKILL.md` files are generated from this source.
- `reference/` — one `<command>.md` per command (`audit.md`, `polish.md`, `critique.md`, etc.), the shared playbooks the router loads outside the command table (`new-work.md`, `craft-floor.md`, `operate.md`, `routing.md`), and the native platform references (`ios.md`, `android.md`). When a sub-command is matched, the router loads its reference file.
@@ -230,6 +230,31 @@ bun run build:browser
**IMPORTANT**: Always use `node` (not `bun`) to run the detect CLI. Bun's jsdom implementation is extremely slow and will cause scans with HTML files to hang for minutes.
### Picker anti-pattern gate
After `bun run build:picker`, scan the **built directory**:
```bash
node cli/bin/cli.js detect skill/scripts/picker/
```
**Name the directory, not `index.html`.** The built page only links its stylesheet and its script, and the rules that read CSS or JS text run per file, so scanning the page alone never reads a line of either. That is how three `design-system-color` findings vanished with nothing fixed: the mode-tile demo palette moved out of an inline `style` attribute and into `picker.css`, and the gate went quiet while the values stayed put. Naming a bundle by path is not a fix either, because Vite content-hashes the filenames and a stale path silently narrows the scan back to the page.
Baseline: **606 findings**: 556 on `index.html`, 47 on the bundled stylesheet, and 3 on the bundled script. The headline is dominated by per-element multiplication, not by distinct problems: the jsdom pass reports once per matching element, so one deliberate declaration can carry hundreds of hits, and a single one does: `picker.css`'s artboard morph, which eases `margin` and `padding` while the wireframe previews an answer, accounts for 527 of the `index.html` findings on its own (the morph is the preview mechanic; easing geometry on a miniature board is the point, not thrash). The distinct accepted groups below are the real ledger, each with a reason on record, so hold 606 rather than treating it as a backlog:
- 18 `cramped-padding` on the palette strips, nine for each of the two: the editable one on screen 02 and the reading copy under screen 03's choices. Each strip reports its group, its four items, and its four swatches. A swatch is a color that has to reach its own edges, so `.picker-band` carries no padding and places its hex readout and drag grip by inset instead, which the rule has no way to see. Adding padding here would put a frame around every color the visitor picked.
- 3 `design-system-color` for the mode-tile demo palette (`#195856`, `#5f7c7a`, `#3a958b`). They are examples standing in for the palette the visitor is about to pick, which is the carve-out DESIGN.md's OKLCH-Only Rule already names. The argument is at the declaration in `picker/styles/picker.css`; read it before touching them.
- 8 `design-system-font-size` from `picker/styles/picker.css`, seven of them already waived there in `.impeccable/config.json`. They reappear under the built path only because that waiver is scoped to the source path. The eighth is `1.05rem` on `.picker-finish-lede`, the review copy on the last screen, which sits on the questionnaire's own kit-token ramp for the same reason the other seven do.
- 2 `border-accent-on-rounded` on `.ks-bento` in the vendored `kinpaku-kit.css`, which carries no `border-radius` at all. The rule wants a radius on the same line, and minification puts the whole stylesheet on one line, so every line-scoped regex rule reads this bundle with no line context. Check a snippet against the source before acting on it.
- 11 from the design context document, the review UI the picker hands off to after the last question. Nine are `picker/styles/design-context.css`: 5 `design-system-font-size` and 1 `design-system-radius` (`22px`) for the ported prototype's own type ramp and shape vocabulary, 2 `design-system-color` (`#7ba98f`, `#c26d5a`) for the tray status dots, and 1 `layout-transition` on the tile expander, which animates `width` and `height` because the tile morphing into a panel is the geometry it is animating. One is `picker/scripts/design-context.js`, whose Google Fonts URL is built from whichever family the run chose, so the rule sees a template where it wants a declared face. The last is `all-caps-body` on the doc masthead's mono note. The user locked that UI; both files carry a waiver in `.impeccable/config.json` saying so, and like the picker.css group those waivers are source-scoped, so the values resurface under the built path.
- 1 `undersized-ui-text` on the `Change` legend in the document's request-scope control, at 10.88px. Same slot and same argument as the micro-labels below.
The last 9 are the page findings the questionnaire has carried since those screens were built: 3 `cramped-padding` on the option panel and the icon sheet, both edge-to-edge grids whose rows hold their own padding, and 6 mono readouts and micro-labels under the text-size and tracking floors. They are the group to revisit first if the number is ever worth lowering.
**When the number moves, find out which file moved it before editing this section.** The distinct-group ledger read 39 until the design context document landed, and 15 findings arrived with it: 13 in its own two files (two later left with a dead type-specimen block), plus the finish-screen lede and the `Change` legend that ship in the picker's files but belong to that feature. The five other workstreams in the same batch added none. Building the tree at the previous commit in a `git worktree` and scanning both builds is how that was established, and it beats reasoning about which change is responsible.
For line numbers during triage, `node cli/bin/cli.js detect picker/styles/` scans the unminified sources. It is not the gate: the source scan's style-context heuristic skips custom-property declarations, so it misses the palette entirely. A served URL is worse than either, because the browser pass only measures the screen in front of it and the picker hides all but the active one.
## Versioning
**Feature PRs do not bump versions and do not add changelog entries.** Bumping is a release step, not part of the change that earns the release: a version in a feature branch conflicts with every other open branch, and a changelog entry describes a release that has not happened. Land the code first; the maintainer bumps and writes the changelog when cutting the release. This holds even though the "Bump when: ..." notes below name the source dirs — those say *which* component a change belongs to, not *when* to edit the manifest. The only PR that touches a manifest version is one whose purpose is the release itself.
+18
View File
@@ -9,3 +9,21 @@ The `skill/reference/ios.md` and `skill/reference/android.md` platform reference
**Original work:** https://github.com/ehmo/platform-design-skills
**Original license:** MIT
**Author:** ehmo
## Icon specimens in the design picker
`picker/data/icon-packs.json`, and the copy of it built into `skill/scripts/picker/`, holds twenty-four glyphs from each of the icon families the picker offers, redistributed unmodified apart from the removal of attributes the specimen does not draw with. They are shown so a user can compare the families and choose one; the choice, not this file, is what a project then installs. `scripts/vendor-icons.mjs` records the exact version each set was taken from.
| Set | License | Source |
|---|---|---|
| Lucide | ISC | https://github.com/lucide-icons/lucide |
| Feather | MIT | https://github.com/feathericons/feather |
| Phosphor | MIT | https://github.com/phosphor-icons/core |
| Heroicons | MIT | https://github.com/tailwindlabs/heroicons |
| Tabler | MIT | https://github.com/tabler/tabler-icons |
| Radix Icons | MIT | https://github.com/radix-ui/icons |
| Bootstrap Icons | MIT | https://github.com/twbs/icons |
| Iconoir | MIT | https://github.com/iconoir-icons/iconoir |
| Octicons | MIT | https://github.com/primer/octicons |
| Hugeicons (free core) | MIT | https://github.com/hugeicons/hugeicons-react |
| Remix Icon | Apache-2.0 | https://github.com/Remix-Design/RemixIcon |
+4 -3
View File
@@ -1,6 +1,6 @@
# Impeccable
Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 61 deterministic detector rules for AI-generated frontend design.
Design guidance for AI coding agents. 1 skill, 24 commands, live browser iteration, and 61 deterministic detector rules for AI-generated frontend design.
> **Quick start:** From your project root, run `npx impeccable install`, then run `/impeccable init` inside your AI coding tool. Full docs: [impeccable.style](https://impeccable.style).
@@ -12,7 +12,7 @@ Every model trained on the same SaaS templates. Skip the guidance and you get th
Impeccable adds:
- **One setup flow.** `/impeccable init` records durable product truth in `PRODUCT.md`, so later commands know the audience, purpose, operating context, constraints, voice, and evidence without confusing those facts with surface-level visual direction.
- **23 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more.
- **24 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more.
- **61 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key.
## What's Included
@@ -33,7 +33,7 @@ Start every new project with:
`init` inspects the project, asks only for material gaps in durable product truth, and writes `PRODUCT.md`. Visitor mode and visual direction are chosen later for each surface; incumbent or newly built visual systems are recorded separately in `DESIGN.md`.
### 23 Commands
### 24 Commands
All commands are accessed through `/impeccable`:
@@ -43,6 +43,7 @@ All commands are accessed through `/impeccable`:
| `/impeccable init` | One-time setup: gather durable product context, write PRODUCT.md, configure live mode when applicable, recommend next steps |
| `/impeccable document` | Generate root DESIGN.md from existing project code |
| `/impeccable extract` | Pull reusable components and tokens into the design system |
| `/impeccable design-context` | Reopen, revise, export, or import the design interview and its design context document |
| `/impeccable shape` | Plan UX/UI before writing code |
| `/impeccable critique` | UX design review: hierarchy, clarity, emotional resonance |
| `/impeccable audit` | Run technical quality checks (a11y, performance, responsive) |
+1 -1
View File
@@ -81,7 +81,7 @@ impeccable detect [options] [file-or-dir-or-url...]
## Part of Impeccable
This CLI is part of [Impeccable](https://impeccable.style), a cross-provider design skill pack for AI-powered development tools. The full suite includes 23 commands for Claude, Cursor, GitHub Copilot, Gemini, Codex, Hermes Agent, Veto, and more.
This CLI is part of [Impeccable](https://impeccable.style), a cross-provider design skill pack for AI-powered development tools. The full suite includes 24 commands for Claude, Cursor, GitHub Copilot, Gemini, Codex, Hermes Agent, Veto, and more.
## License
+550 -3
View File
@@ -21,6 +21,7 @@
"@babel/parser": "^8.0.4",
"ai": "^7.0.14",
"archiver": "^8.0.0",
"astro": "^7.0.0",
"playwright": "^1.59.1",
"svelte": "^5",
"zod": "^4.3.6",
@@ -63,6 +64,36 @@
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.122.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-GGPNftt0caaz9MDlmNQGHX8855Ojaduyy5pm9Sm1h7HalCn0cWNb5/bweadJF+4yzbal+QL6ztBa09WAAOzLmQ=="],
"@astrojs/compiler-binding": ["@astrojs/compiler-binding@0.4.0", "", { "optionalDependencies": { "@astrojs/compiler-binding-darwin-arm64": "0.4.0", "@astrojs/compiler-binding-darwin-x64": "0.4.0", "@astrojs/compiler-binding-linux-arm64-gnu": "0.4.0", "@astrojs/compiler-binding-linux-arm64-musl": "0.4.0", "@astrojs/compiler-binding-linux-x64-gnu": "0.4.0", "@astrojs/compiler-binding-linux-x64-musl": "0.4.0", "@astrojs/compiler-binding-wasm32-wasi": "0.4.0", "@astrojs/compiler-binding-win32-arm64-msvc": "0.4.0", "@astrojs/compiler-binding-win32-x64-msvc": "0.4.0" } }, "sha512-x2RjDUuWfwLNtc3mjAdSRInwqh/rqbLar9cm/5FOMbHvmYZB7yfKewzSclAxWjIZsypJDXv1lhaP2WG+P8TK3g=="],
"@astrojs/compiler-binding-darwin-arm64": ["@astrojs/compiler-binding-darwin-arm64@0.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZVUwHundaQyFNjE6uoa0usaC0WOCitDCLS/4mdb4rOiJXwVUuKJBMxI5WMzXLWmamsXtK/Z//ifLXvV5Yeh4Hw=="],
"@astrojs/compiler-binding-darwin-x64": ["@astrojs/compiler-binding-darwin-x64@0.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-FI6G8AY8u6fR1SI/QRR5yGMwtvZwP34CDmZpZ5HwJGa50UM1VISTLhqkhV4a476pmgd25X1Aur2dqw6hUnrlKA=="],
"@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lB9gLFJK7m82EnjaU8nlRBEfcwGNeHidW3sSjODTUjMNaoewVuUz9fwwdY5M4jiSXIqWLH3yl6TX8FTDKA74Sw=="],
"@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HPbvWqbxFxyaoQJhLxCaSjtYBx9KBo7JGVzEFZCmMl968a2PsSH0UfiODYgYPXofTOIsIH2aoCcrHXML0IA3ig=="],
"@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tQKolMxoJ/+0AmLWm1PmJ/i+z3i10ZU1bNuVjEDulCf48azEMtUNjTZgHJ5MPtpYRNc7dlETr8QujUfduzoC7Q=="],
"@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5v5YymudsxMHp3NBLCS8BUlu5CRqeLtWD9cKS/4nIhIEHCbpz9okmVV6I0HWqmBAPhWYcDa3vw/vltYPrOQCTA=="],
"@astrojs/compiler-binding-wasm32-wasi": ["@astrojs/compiler-binding-wasm32-wasi@0.4.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.2.2" }, "cpu": "none" }, "sha512-m/phuH3x3PREvv1OnkM44NoPh4MatUadix1fB1u5SvMLCyDTUZykDJbKnWf1cjnYmHdlB8HcjTjl6JrCqAIXcw=="],
"@astrojs/compiler-binding-win32-arm64-msvc": ["@astrojs/compiler-binding-win32-arm64-msvc@0.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-B9zYf3okEY83kM8gydlpH2BHP00w4ifxPqlYlWrgTwuD6wnkrJDCwBlgy1q31cERjCJRXN1lrE2VmkLvFjv/6g=="],
"@astrojs/compiler-binding-win32-x64-msvc": ["@astrojs/compiler-binding-win32-x64-msvc@0.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-zB0Nrv0dGc0zZWPGDRmmETTPhDRqyZjAjk+gWMlVrJX5U89obpB3VUUE1ZiHxOCN5LQojeLK6O8L/dnoHolvNQ=="],
"@astrojs/compiler-rs": ["@astrojs/compiler-rs@0.4.0", "", { "dependencies": { "@astrojs/compiler-binding": "0.4.0" } }, "sha512-koVikeon1kreEy+/JzLQRy3vzHHQVOjycs4degg4vFufKApZOwMZvSSAEztYNhmcQVfNVsVZZI4cEge3cexAbQ=="],
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.11.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA=="],
"@astrojs/markdown-satteri": ["@astrojs/markdown-satteri@0.4.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.11.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "satteri": "^0.10.3" } }, "sha512-wykOOW9KsUVcZweOpY/CeXpdKcCKZy6fQbdcteWFuI75+sQCiqxYM7VKsGa5b+aGl3cYQscFY37rsbbyal5MRw=="],
"@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="],
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="],
@@ -73,8 +104,144 @@
"@babel/types": ["@babel/types@8.0.4", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.4" } }, "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g=="],
"@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.10.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA=="],
"@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q=="],
"@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w=="],
"@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA=="],
"@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw=="],
"@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg=="],
"@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.10.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.2.3" }, "cpu": "none" }, "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw=="],
"@bruits/satteri-win32-arm64-msvc": ["@bruits/satteri-win32-arm64-msvc@0.10.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw=="],
"@bruits/satteri-win32-x64-msvc": ["@bruits/satteri-win32-x64-msvc@0.10.5", "", { "os": "win32", "cpu": "x64" }, "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ=="],
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="],
"@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
"@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="],
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="],
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -87,18 +254,86 @@
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
"@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="],
"@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="],
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="],
"@shikijs/langs": ["@shikijs/langs@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="],
"@shikijs/primitive": ["@shikijs/primitive@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="],
"@shikijs/themes": ["@shikijs/themes@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="],
"@shikijs/types": ["@shikijs/types@4.4.3", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.11", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="],
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
"@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
@@ -115,13 +350,21 @@
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"am-i-vibing": ["am-i-vibing@0.4.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"archiver": ["archiver@8.0.0", "", { "dependencies": { "async": "^3.2.4", "buffer-crc32": "^1.0.0", "is-stream": "^4.0.0", "lazystream": "^1.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^3.0.0", "tar-stream": "^3.0.0", "zip-stream": "^7.0.2" } }, "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"astro": ["astro@7.2.10", "", { "dependencies": { "@astrojs/compiler-rs": "^0.4.0", "@astrojs/internal-helpers": "0.11.0", "@astrojs/markdown-satteri": "0.4.0", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^9.0.0", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "find-proc": "0.1.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.5", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.35.4" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-uPtD+nK6kQXugyq4kiMPwj9OI3Sae6HSerA5X+fuv2CPaDwxmokFPPFlGRKIAXH0QAKu+zot2q6yrLG61wFmqg=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
@@ -129,6 +372,8 @@
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
@@ -161,19 +406,37 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"chromium-bidi": ["chromium-bidi@17.0.2", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg=="],
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
"compress-commons": ["compress-commons@7.0.1", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^7.0.1", "is-stream": "^4.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
"cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@@ -187,20 +450,36 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
"css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="],
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
"css-what": ["css-what@8.0.0", "", {}, "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw=="],
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"devtools-protocol": ["devtools-protocol@0.0.1666840", "", {}, "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg=="],
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
"dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
"domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="],
@@ -209,6 +488,8 @@
"domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
@@ -223,8 +504,12 @@
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
@@ -237,6 +522,8 @@
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
@@ -249,18 +536,36 @@
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-proc": ["find-proc@0.1.0", "", {}, "sha512-OaOpEYv2PiQ7SQ5LIrl+deA1XaWcxEjnpM6VuWXTUvn+teIXxeFTLDmu18/zDQpFmHN4o3oDBX+BT0AGwEhemg=="],
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
"fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="],
"fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
@@ -277,16 +582,32 @@
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="],
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"htmlparser2": ["htmlparser2@12.0.0", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "domutils": "^4.0.2", "entities": "^8.0.0" } }, "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
@@ -299,6 +620,12 @@
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
"is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
@@ -311,6 +638,8 @@
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
@@ -319,24 +648,66 @@
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
"magic-string": ["magic-string@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g=="],
"magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="],
"marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
@@ -347,10 +718,22 @@
"modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-mock-http": ["node-mock-http@1.0.5", "", {}, "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="],
@@ -359,26 +742,58 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
"p-limit": ["p-limit@7.3.2", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-Ll0w3fU24vYpXoZmjjZIee6bJQDgG0oAyo1PdmFYI8UDwJJddaHAypxIH9avUu+t+lSsAwKVsb1jDCMIIChliw=="],
"p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="],
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
"package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="],
"playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="],
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
"process-ancestry": ["process-ancestry@0.1.0", "", {}, "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"puppeteer": ["puppeteer@25.9.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "lilconfig": "^3.1.3", "puppeteer-core": "25.9.0", "typed-query-selector": "^2.12.2" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" } }, "sha512-2JqQszD2pyDTpIvBH1ZCXdrHgENVNdJIeOM6asbwHRgWknFiaLd1gNB91w/B/0hQHNpafkpxa90lPpBaJM87Hw=="],
@@ -387,6 +802,8 @@
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
@@ -395,24 +812,48 @@
"readdir-glob": ["readdir-glob@3.0.0", "", { "dependencies": { "minimatch": "^10.2.2" } }, "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw=="],
"readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
"rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"satteri": ["satteri@0.10.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.10.5", "@bruits/satteri-darwin-x64": "0.10.5", "@bruits/satteri-linux-arm64-gnu": "0.10.5", "@bruits/satteri-linux-arm64-musl": "0.10.5", "@bruits/satteri-linux-x64-gnu": "0.10.5", "@bruits/satteri-linux-x64-musl": "0.10.5", "@bruits/satteri-wasm32-wasi": "0.10.5", "@bruits/satteri-win32-arm64-msvc": "0.10.5", "@bruits/satteri-win32-x64-msvc": "0.10.5" } }, "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ=="],
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
@@ -421,8 +862,14 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
@@ -433,32 +880,80 @@
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"svelte": ["svelte@5.56.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g=="],
"svgo": ["svgo@4.1.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^6.0.0", "css-tree": "^3.0.1", "css-what": "^7.0.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "1.6.1" }, "bin": { "svgo": "bin/svgo.js" } }, "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q=="],
"tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="],
"teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tinyclip": ["tinyclip@0.1.15", "", {}, "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A=="],
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="],
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
"ultrahtml": ["ultrahtml@1.7.0", "", {}, "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unifont": ["unifont@0.7.5", "", { "dependencies": { "css-tree": "^3.1.0", "ohash": "^2.0.11", "undici": "^8.0.0" } }, "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -469,12 +964,16 @@
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"zip-stream": ["zip-stream@7.0.5", "", { "dependencies": { "compress-commons": "^7.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w=="],
@@ -483,10 +982,14 @@
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
"@bruits/satteri-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@modelcontextprotocol/sdk/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
@@ -495,14 +998,58 @@
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
"eventsource/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"magicast/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"magicast/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"svelte/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"svgo/css-select": ["css-select@6.0.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^7.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "nth-check": "^2.1.1" } }, "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw=="],
"svgo/css-what": ["css-what@7.0.0", "", {}, "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ=="],
"unifont/undici": ["undici@8.10.1", "", {}, "sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q=="],
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"magicast/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"magicast/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"svgo/css-select/boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"svgo/css-select/domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"svgo/css-select/domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"svgo/css-select/nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"svgo/css-select/domhandler/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"svgo/css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"svgo/css-select/domutils/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"svgo/css-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
}
}
+174
View File
@@ -0,0 +1,174 @@
# Design context architecture
How the design interview, the design context document, and everything the user
gives us during the seed process are organized, and why.
## The problem this layout solves
The design context is one product concept. Before this layout it was scattered
across two directories with no owner:
- `.impeccable/design-interview/` held the questionnaire submission, uploaded
fonts, staged brand assets, and two runtime files.
- `.impeccable/visual-cues/` held the cue generation workspace, and also, buried
inside `cues.json`, the `context` object carrying the entire chat half of the
interview plus the `modes` set. Neither is a generation artifact.
- The design context document was not a file at all. It was a client-side render
that existed only after a submit, died with its session, and could not be
reopened.
- Three scripts hardcoded the storage directory independently, with no shared
constant. The agent-side poll CLI treats a missing session file as a clean
exit, so a half-finished rename would fail silently and no test would catch it.
Export, import, resume, rebuild, and edit each had to reassemble state from those
places with implicit coupling. One canonical store with one code owner replaces
that.
## File layout, in the user's project
```
.impeccable/design-context/ the store: the one exportable unit
context.json { schemaVersion, modes, context } the chat half of the run
answers.json the questionnaire submission, flat form-field shape
assets/ brand files the user supplied
fonts/ font faces the user uploaded
cue.png the chosen hero, copied at submit
runtime/ session.json, journal.jsonl, draft.json (gitignored)
exports/ design-context.md, design-context.bundle.json (gitignored)
.impeccable/visual-cues/ the generation workspace, unchanged
brief.md, <slug>.png x6, cues.json (cues + palette only), fonts.json
```
The reasoning, in the order the decisions were made:
1. **Durable versus regenerable is the axis.** It matches the policy this repo's
own `.gitignore` states: generated sidecars and config may be tracked, but
runtime recovery state and local assets stay local. Everything in the store
above `runtime/` is a user decision or a user-supplied file, so it is
trackable and exportable. The workspace is regenerable process output and
stays ignored.
2. **`context.json` moves the chat half out of `cues.json`.** The context object
was never a cue artifact. It rode there because that was the only file the
picker served. With its own home it survives cue regeneration, imports without
dragging six hero images along, and can be edited by the save flow without
touching the generation workspace.
3. **`cue.png` makes the document self-sufficient.** The Color article renders
the chosen cue only when the cue manifest still lists it, which means the
document breaks once the workspace is cleaned or in a project that received
the context by import. Copying the one chosen hero into the store at submit
time costs a few hundred kilobytes and buys rendering independence. The five
unpicked heroes stay in the workspace as art direction leftovers.
4. **`runtime/` isolates ephemera.** Session discovery, the journal, and the
mid-questionnaire draft never belong in an export or a commit. One gitignore
line covers all of it permanently.
5. **PRODUCT.md and DESIGN.md stay at the project root.** They are the canonical
documents the whole toolchain reads. `context.json` carries distilled copies
with provenance, never replacements. Edits that touch product truth reconcile
back into PRODUCT.md through the agent rather than the store growing a second
product record.
## Code layout
The shape mirrors `skill/scripts/live/`, which solves the same class of problem.
```
skill/scripts/design-context/
store.mjs the only code that knows store paths or writes store files
bindings.mjs the editable-field registry for the document
session-routes.mjs HTTP handlers for the document edit session
portability.mjs the export bundle format and its import validation
skill/scripts/
picker-server.mjs static serving, boot contract, submit, autosave, spawn
picker-doc-session.mjs the session shell: http server, timers, token
picker-doc-poll.mjs the agent's poll CLI
design-context-export.mjs / design-context-import.mjs
picker/scripts/
boot.js one memoized fetch of the boot contract
hydrate.js restoring a previous run into the questionnaire
palette-picker.js the questionnaire; owns running hydration
design-context.js the document; document mode, pending ledger, save bar
```
Three ownership rules keep this correct as it grows.
**store.mjs is the single writer of store files.** Server, session, and import
all go through it. Every write is atomic: write a temporary file beside the
target, then rename over it, so a reader never sees a torn file.
**Reads come off disk per request.** Files are the truth. Process memory is a
cache at best. This is live mode's stat-keyed cache rule in its simplest form,
which is all a small store needs.
**During a live session, only the session process writes store files.** The agent
edits its own documents, DESIGN.md and PRODUCT.md, directly. Any answer or
context updates it needs to make ride in its reply payload for the session to
apply. Without that split, an agent doing a read, modify, write cycle on the
answers file can silently drop a change the session wrote in between.
## The live session
The document is a working surface, not a report. The patterns below are taken
from live mode, scaled to one page, one store, and one agent.
1. **Append-only journal with pure replay.** The session appends one line per
render-relevant event to `runtime/journal.jsonl`: applied changes, batch
transitions, and request transitions. A booting session replays the file to
recover its sequence number and any unacknowledged batch. Lines it cannot use,
including records written by an older release that carry no sequence number,
become diagnostics rather than failures.
2. **Stage, then apply.** Edits accumulate in a client-side ledger keyed by field
binding. Re-editing a field updates the new value but keeps the first original,
so the record of what changed stays true across repeated edits.
3. **Delivery that survives a dead agent.** One batch is outstanding at a time and
is journaled. A poll leases it, a reply acknowledges it, and a session restart
re-offers anything unacknowledged. An invalid reply returns a corrective hint
while the lease holds, so the agent can fix its own message.
4. **Server-owned truth, DOM as cache.** The state endpoint carries the journal
sequence and the current batch. The client compares sequence numbers and
re-fetches when the server has moved ahead. That re-fetch and re-render is the
hot reload. The count of staged changes before Apply is the client's own,
because nothing has reached the server yet.
5. **One in-flight lock.** While a batch is unacknowledged the document's edit
affordances are disabled. A re-render triggered by anything else re-applies
still-staged edits onto the fresh DOM, so a background event never wipes work
the user has not saved.
6. **Loopback discovery with a per-run token.** The session records its process
id, port, and token; readers probe liveness before trusting the record; the
token, not the origin, is the security boundary.
What was deliberately left out, because each answers a question this surface does
not ask: the evidence gathering and verification pipeline and the repair loop
(live cannot know which source file a DOM edit belongs to, while every field here
has an authoritative address and applying is a deterministic write); server-sent
events and reload-resume state (a two-second poll already covers it); poll lanes
and priorities and chunking (one agent, one batch); the roots manifest (one fixed
path under the project root).
## Data flow
```
SEED
agent stages assets into the store, runs the cue pipeline, writes the
workspace and the store's context.json, then launches the picker
client boots, prefills from a draft or a previous submission, autosaves
submit writes answers, copies the chosen hero, forks the session, exits 0
agent seeds DESIGN.md from the answers, then enters the edit loop
REOPEN
the picker server runs in document mode, reusing or spawning a session
the client renders the document directly, with no submit
the session ending is the agent's completion signal
EDIT
staged changes leave the ledger as one batch, the session applies them to
the store and queues the downstream work, the agent reconciles DESIGN.md
and PRODUCT.md prose and replies, the sequence advances, the tab re-fetches
and re-renders
EXPORT AND IMPORT
the store compiles to a readable markdown document plus a lossless bundle
importing a bundle rebuilds a working store in another project
```
+4 -2
View File
@@ -40,10 +40,11 @@
"LICENSE"
],
"scripts": {
"build:picker": "node scripts/build-picker.mjs",
"build:skills": "bun run scripts/build.js --skip-root-sync",
"build:skills:release": "bun run scripts/build.js",
"build": "bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
"build:release": "bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
"build": "bun run build:picker && bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
"build:release": "bun run build:picker && bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
"build:browser": "node scripts/build-browser-detector.js",
"build:extension": "node scripts/build-extension.js",
"clean": "rm -rf dist build",
@@ -93,6 +94,7 @@
"@babel/parser": "^8.0.4",
"ai": "^7.0.14",
"archiver": "^8.0.0",
"astro": "^7.0.0",
"playwright": "^1.59.1",
"svelte": "^5",
"zod": "^4.3.6"
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<style>
/* Light OS theme: black icon. Dark OS theme: pristine kinpaku gold,
matching the header logo (--ks-kinpaku, #ffb900) rather than the old
muddier #d8a83a. */
path { fill: #141207; }
@media (prefers-color-scheme: dark) {
path { fill: #ffb900; }
}
</style>
<path d="M7 1 L18 1 L8 31 L7 31 Q1 31 1 25 L1 7 Q1 1 7 1 Z"/>
<path d="M22 1 L25 1 Q31 1 31 7 L31 25 Q31 31 25 31 L12 31 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
srcDir: './picker',
outDir: './build-picker',
output: 'static',
build: {
assets: 'assets',
assetsPrefix: '.',
format: 'directory',
},
devToolbar: {
enabled: false,
},
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
+559
View File
@@ -0,0 +1,559 @@
---
/*
The wireframe landing page every option screen previews its answer on. One
copy so a change to the page's anatomy lands on every screen at once.
mode: 'bars' fills the hero with bar stacks, which is what a screen judging
color, layout, or depth needs. 'type' swaps them for real text elements and
adds the data-type-* hooks the font screen writes specimen copy into; that
screen is judging glyphs, so bars would tell it nothing.
Every body is drawn both ways for the same reason. The slots stay one tree and
one set of class names; what changes is whether a slot holds a data-type-*
hook or the bars that stand in for the words. Only the font screen's boards are
filled, so a hook on any other board would promise copy that never arrives,
and a paragraph is a stack of bars rather than one because the block a
structural answer acts on is the shape of the passage, not a single line.
phone: false drops the handset and leaves the desktop artboard alone in the
frame, for a screen whose answer only reads at desktop width.
*/
interface Props {
mode?: 'bars' | 'type';
/*
Which surface the board draws. The chrome and the frame are shared and only
the two bodies change, the same arrangement the four wireframes on the
surface question already use, so a change to the nav lands on all of them at
once. Named boards are the ones the font screen switches between.
*/
surface?: 'persuade' | 'operate' | 'read' | 'experience';
phone?: boolean;
class?: string;
strategyHook?: boolean;
typeHook?: boolean;
cursor?: boolean;
/*
Marks an artboard as one that accumulates the structural answers. The
committed layout, boundary, corner, and depth selectors key on it, which is
how each screen shows the page as decided so far without those answers
reaching back to the screens that came before them.
*/
carry?: boolean;
/*
The layout screen's boards carry parts no other board draws: the shared
alignment-grid component on every desktop and handset body, and on the
index a pagination rail for the handset plus the project rail and support
row that only the freeform answer places (layout-portfolio.css parks them
until it does). The index projects also take the data-project-index and
data-project-tone hooks that screen's sheet keys plate placement and
image treatment on. Mounted only where asked, because the motion screen
reads the same index body through :nth-of-type and an extra sibling would
shift its scenes.
*/
portfolioExtras?: boolean;
motionCell?: string;
}
const {
mode = 'bars',
surface,
phone = true,
class: className = '',
strategyHook = false,
typeHook = false,
cursor = false,
carry = false,
portfolioExtras = false,
motionCell,
} = Astro.props;
const type = mode === 'type';
const ops = surface === 'operate';
const docs = surface === 'read';
const index = surface === 'experience';
const classes = ['picker-artboard', className].filter(Boolean).join(' ');
// Empty string renders the bare attribute the font screen's selectors expect;
// undefined drops it entirely on the other screens.
const hook = type ? '' : undefined;
---
<div
class={classes}
data-artboard=""
data-surface={surface}
data-motion-cell={motionCell}
data-carry={carry ? '' : undefined}
data-strategy-preview={strategyHook ? '' : undefined}
data-type-preview={typeHook ? '' : undefined}
aria-hidden="true"
>
<div class="ps-desktop">
{portfolioExtras && <div class="ps-layout-grid" aria-hidden="true"></div>}
<div class="ps-nav">
<div class="ps-nav-lede">
{!type && <i class="ps-menu"></i>}
<div class="ps-brand-block" data-type-brand={hook}></div>
{!type && <i class="ps-brand-word"></i>}
</div>
{type ? (
<div class="ps-nav-bars"><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i></div>
) : (
<div class="ps-nav-bars"><i></i><i></i><i></i><i></i></div>
)}
{!type && (
<div class="ps-nav-tail">
<i class="ps-search"></i>
<div class="ps-nav-action" data-type-nav-action={hook}></div>
<i class="ps-avatar"></i>
</div>
)}
</div>
{ops ? (
<div class="ps-ops">
{/* The rail is where a tool keeps its navigation, so it carries the
longest run of micro labels on any board. The open one is a filled
plate rather than a recolor: a selection is a state, not a word. */}
<div class="ps-ops-rail">
{[0, 1, 2].map((n) => (
<span class:list={['ps-ops-item', n === 1 && 'ps-ops-item--on']}>
<i></i><b data-type-rail={hook}></b>
</span>
))}
</div>
<div class="ps-ops-main">
<strong class="ps-ops-title" data-type-section-title={hook}></strong>
<div class="ps-ops-metrics">
{[0, 1, 2].map(() => (
<span class="ps-ops-metric">
<b data-type-figure={hook}></b>
<span data-type-proof={hook}></span>
</span>
))}
</div>
{/* A chart is the one thing a dashboard has that no other surface
does, and it sets two kinds of text a pair has to be judged on
together: a heading over a plot, and a row of category labels
sitting under it at the smallest step on the board. The bars
themselves stay schematic, drawn as a share of the plot rather
than from numbers, because what is being chosen here is a pair
and not a reading of the data. */}
<div class="ps-ops-chart">
<b class="ps-ops-chart-title" data-type-chart-title={hook}></b>
<div class="ps-ops-plot">
{[46, 66, 100, 58, 44].map((h, n) => (
<i class:list={[n === 2 && 'ps-ops-bar--lead']} style={`--h: ${h}%`}></i>
))}
</div>
<div class="ps-ops-lanes">
{[0, 1, 2, 3, 4].map(() => <b data-type-lane={hook}></b>)}
</div>
</div>
<div class="ps-ops-table">
<span class="ps-ops-row ps-ops-row--head">
<i></i>
<b data-type-column={hook}></b><b data-type-column={hook}></b><b data-type-column={hook}></b>
</span>
{[0, 1, 2].map((n) => (
<span class:list={['ps-ops-row', n === 1 && 'ps-ops-row--on']}>
<i></i>
<b data-type-gallery-title={hook}></b>
<b data-type-gallery-meta={hook}></b>
<b data-type-amount={hook}></b>
</span>
))}
</div>
</div>
<div class="ps-ops-panel">
<b class="ps-ops-panel-title" data-type-panel={hook}></b>
<span class="ps-ops-field"><b data-type-panel={hook}></b><i class="ps-ops-chev"></i></span>
{[0, 1].map((n) => (
<span class="ps-ops-switch">
<b data-type-switch={hook}></b>
<i class:list={['ps-ops-toggle', n === 0 && 'ps-ops-toggle--on']}></i>
</span>
))}
</div>
</div>
) : docs ? (
<div class="ps-docs">
{/* The rail names sections of one document rather than pages of a site,
which is why it cannot take the nav's words: the two would read as
the same list drawn twice. */}
<div class="ps-docs-rail">
{[0, 1, 2, 3].map((n) => (
<span class:list={['ps-docs-item', n === 1 && 'ps-docs-item--on']}>
<i></i><b data-type-rail={hook}></b>
</span>
))}
</div>
{/* Two heading levels and the passages between them. A documentation
page is the one surface where the relationship between a page title
and the heading under it is visible at a glance, so the board shows
both rather than the single heading the drawing carries. */}
<div class="ps-docs-main">
<h2 class="pt-headline" data-type-headline={hook}>{!type && <Fragment><i></i><i></i></Fragment>}</h2>
<p class="ps-docs-lede" data-type-body={hook}>{!type && [0, 1].map(() => <i></i>)}</p>
<strong class="ps-docs-sub" data-type-section-title={hook}></strong>
<p class="ps-docs-para" data-type-passage={hook}>{!type && [0, 1, 2].map(() => <i></i>)}</p>
<p class="ps-docs-para" data-type-passage={hook}>{!type && [0, 1, 2].map(() => <i></i>)}</p>
{/* Prose, a list, and a callout are what a documentation page is
made of, and the board carried two of the three. The list is
also the only setting here that asks the body face to hold a
short ragged line against a marker. */}
<ul class="ps-docs-list">
{[0, 1].map(() => <li data-type-item={hook}></li>)}
</ul>
<div class="ps-docs-note">
<span class="ps-docs-note-copy">
<b data-type-note-label={hook}></b>
<span data-type-note-body={hook}>{!type && [0, 1].map(() => <i></i>)}</span>
</span>
</div>
</div>
</div>
) : index && !type ? (
/* The index as a wireframe, for the screen that judges movement rather
than glyphs. Same four bands as the set version below, drawn in bars:
a page title, two staggered entries, and the carousel rail. */
<div class="ps-index">
{portfolioExtras && (
<div class="ps-project-rail" aria-hidden="true"><i></i><i></i><i></i></div>
)}
<i class="ps-index-head"></i>
{[0, 1].map((n) => (
<div class:list={['ps-index-row', n === 1 && 'ps-index-row--flip']}>
<div
class="ps-image"
data-project-index={portfolioExtras ? String(n + 1) : undefined}
data-project-tone={portfolioExtras ? (n === 0 ? 'green' : 'light') : undefined}
></div>
<div class="ps-index-cap">
<i class="ps-index-name"></i>
<i class="ps-index-tag"></i>
<span class="ps-index-lines"><b></b><b></b></span>
</div>
</div>
))}
{portfolioExtras && (
<div class="ps-support" aria-hidden="true">
<div class="ps-image ps-support-image" data-project-tone="light"></div>
<span class="ps-support-meta"><i></i><i></i><i></i></span>
</div>
)}
<div class="ps-index-rail">
<i class="ps-index-arrow"></i>
<div class="ps-index-track">
{[0, 1, 2, 3].map((n) => (
<b class:list={[n === 0 && 'ps-index-stop--on']}></b>
))}
</div>
<i class="ps-index-arrow ps-index-arrow--next"></i>
</div>
</div>
) : index ? (
<div class="ps-index">
{/* The page title the drawing has no room for, over the two staggered
entries it draws. The second entry is what the handset shows too,
and it is the one that proves the caption ladder repeats: a pair set
on an index is read down a run of entries rather than at one of
them. Alternating the plate keeps the run from collapsing into a
single column at a width that has room for two. */}
<h2 class="pt-headline" data-type-headline></h2>
{[0, 1].map((n) => (
<div class:list={['ps-index-row', n === 1 && 'ps-index-row--flip']}>
<div class="ps-image"></div>
<div class="ps-index-cap">
<i class="ps-index-avatar" aria-hidden="true"></i>
<strong class="ps-index-title" data-type-gallery-title></strong>
<span class="ps-index-meta" data-type-gallery-meta></span>
<span class="ps-index-note" data-type-caption></span>
</div>
</div>
))}
{/* The chevrons stay shapes because an arrow is not a word. The track's
segments are, so they are set: a carousel's stops are named. */}
<div class="ps-index-rail">
<i class="ps-index-arrow"></i>
<div class="ps-index-track">
{[0, 1, 2, 3].map((n) => (
<b class:list={[n === 0 && 'ps-index-stop--on']} data-type-stop></b>
))}
</div>
<i class="ps-index-arrow ps-index-arrow--next"></i>
</div>
</div>
) : (
<Fragment>
<div class="ps-hero">
<div class="ps-hero-copy">
{type ? (
<Fragment>
<h2 class="pt-headline" data-type-headline></h2>
<p class="pt-body" data-type-body></p>
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
</Fragment>
) : (
<Fragment>
<div class="ps-eyebrow"></div>
<div class="ps-headline"><i></i><i></i></div>
<div class="ps-copy"><i></i><i></i><i></i></div>
<div class="ps-actions"><i></i><i></i></div>
</Fragment>
)}
</div>
<div class="ps-image"></div>
</div>
<div class="ps-proof">
{[0, 1, 2, 3].map((n) => (
<Fragment>
{n > 0 && <i class="ps-proof-divider" aria-hidden="true"></i>}
<div class="ps-proof-item">
<i></i>
{type
? <span data-type-proof></span>
: <span class="ps-proof-lines"><b></b><b></b></span>}
</div>
</Fragment>
))}
</div>
<div class="ps-layout-editorial-spacer" data-layout-editorial-spacer aria-hidden="true"></div>
<div class="ps-editorial">
<div class="ps-editorial-copy">
<strong data-type-section-title={hook}></strong>
{type
? <p class="pt-section-body" data-type-section-body></p>
: <span><i></i><i></i></span>}
<em data-type-section-link={hook}></em>
</div>
{/* Three cards once the labels are words, four while they are bars. The
type board sets three because a fourth column leaves the copy beside
it under the width the widest pairing needs, and the preview copy is
written to that count: a fourth card here would draw a picture with
no caption under it. The bar composition is measured at four by the
layout screens, so it keeps them. */}
<div class="ps-gallery">
{(type ? [0, 1, 2] : [0, 1, 2, 3]).map(() => (
<div class="ps-gallery-item">
<i></i>
{type
? <span><b data-type-gallery-caption></b></span>
: <span><b></b><b></b><b></b></span>}
</div>
))}
</div>
</div>
</Fragment>
)}
<div class="ps-footer">
{type ? (
<div class="ps-footer-links"><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></div>
) : (
<div class="ps-footer-links"><i></i><i></i><i></i><i></i></div>
)}
<div class="ps-footer-mark" data-type-footer-mark={hook}></div>
</div>
{/*
Off-grid marks, drawn only where the page has already left the field:
opacity rides --pvs-drift, so on the disciplined answers they are
rendered and invisible and the answers that pay for them are the ones
that asked. Absolute, so the desktop grid still has exactly its five
declared rows.
*/}
{!type && (
<Fragment>
<i class="ps-scatter ps-scatter--lead" aria-hidden="true"></i>
<i class="ps-scatter ps-scatter--trail" aria-hidden="true"></i>
</Fragment>
)}
{cursor && <i class="ps-cursor" data-cursor aria-hidden="true"></i>}
</div>
{phone && (
<div class="ps-phone">
<div class="ps-phone-top">
<div class="ps-nav-lede">
<div class="ps-brand-block" data-type-brand={hook}></div>
{!type && <i class="ps-brand-word"></i>}
</div>
{!type && (
<div class="ps-nav-tail">
<div class="ps-nav-action" data-type-menu-action={hook}></div>
<i class="ps-menu"></i>
</div>
)}
</div>
{ops ? (
<div class="ps-phone-body">
<strong class="ps-ops-title" data-type-section-title={hook}></strong>
<span class="ps-ops-metric">
<b data-type-figure={hook}></b>
<span data-type-proof={hook}></span>
</span>
{/* Three lanes rather than five. The handset keeps the leading one, so
the emphasis stays on the same lane the desktop board marks, and it
drops the tail: five labels at this width would either collide or
be set under the floor this preview is allowed to reach. */}
<div class="ps-ops-chart">
<b class="ps-ops-chart-title" data-type-chart-title={hook}></b>
<div class="ps-ops-plot">
{[52, 74, 100].map((h, n) => (
<i class:list={[n === 2 && 'ps-ops-bar--lead']} style={`--h: ${h}%`}></i>
))}
</div>
<div class="ps-ops-lanes">
{[0, 1, 2].map(() => <b data-type-lane={hook}></b>)}
</div>
</div>
{/* The table has nowhere to put four columns at this width, so it
collapses to the two cells that carry the row: what it is and what
it comes to. */}
<div class="ps-ops-table">
{[0, 1, 2].map((n) => (
<span class:list={['ps-ops-row', n === 1 && 'ps-ops-row--on']}>
<i></i>
<b data-type-gallery-title={hook}></b>
<b data-type-amount={hook}></b>
</span>
))}
</div>
<div class="ps-ops-panel">
{[0, 1].map((n) => (
<span class="ps-ops-switch">
<b data-type-switch={hook}></b>
<i class:list={['ps-ops-toggle', n === 0 && 'ps-ops-toggle--on']}></i>
</span>
))}
</div>
</div>
) : docs ? (
<div class="ps-phone-body">
{/* The crumb is the drawing's, and it is a phone element there because
the rail it stands in for has nowhere to go at this width. */}
<span class="ps-docs-crumb" data-type-crumb={hook}></span>
<h2 class="pt-headline" data-type-headline={hook}>{!type && <Fragment><i></i><i></i></Fragment>}</h2>
<p class="ps-docs-lede" data-type-body={hook}>{!type && [0, 1].map(() => <i></i>)}</p>
<strong class="ps-docs-sub" data-type-section-title={hook}></strong>
<p class="ps-docs-para" data-type-passage={hook}>{!type && [0, 1, 2].map(() => <i></i>)}</p>
<ul class="ps-docs-list">
{[0, 1].map(() => <li data-type-item={hook}></li>)}
</ul>
<div class="ps-docs-note">
<span class="ps-docs-note-copy">
<b data-type-note-label={hook}></b>
<span data-type-note-body={hook}>{!type && [0, 1].map(() => <i></i>)}</span>
</span>
</div>
</div>
) : index ? (
<div class="ps-phone-body">
{/* No page title here. At this width the display step would take the
card, and what a handset shows of an index is the work. */}
{portfolioExtras && (
<div class="ps-phone-project-rail" aria-hidden="true"><i></i><i></i><i></i></div>
)}
{portfolioExtras && <div class="ps-layout-grid" aria-hidden="true"></div>}
{[0, 1].map((n) => (
<Fragment>
<div
class="ps-image"
data-project-index={portfolioExtras ? String(n + 1) : undefined}
data-project-tone={portfolioExtras ? (n === 0 ? 'green' : 'light') : undefined}
></div>
<div
class="ps-index-cap"
data-project-index={portfolioExtras ? String(n + 1) : undefined}
>
{type ? (
<Fragment>
<i class="ps-index-avatar" aria-hidden="true"></i>
<strong class="ps-index-title" data-type-gallery-title></strong>
<span class="ps-index-meta" data-type-gallery-meta></span>
<span class="ps-index-note" data-type-caption></span>
</Fragment>
) : (
<Fragment>
<i class="ps-index-name"></i>
<i class="ps-index-tag"></i>
<span class="ps-index-lines"><b></b><b></b></span>
</Fragment>
)}
</div>
</Fragment>
))}
{portfolioExtras && (
<Fragment>
<div class="ps-support ps-phone-support" aria-hidden="true">
<div class="ps-image ps-support-image" data-project-tone="light"></div>
<span class="ps-support-meta"><i></i><i></i><i></i></span>
</div>
{/* The desktop pagination rail restated at the foot of the stack. */}
<div class="ps-index-rail ps-phone-pagination">
<i class="ps-index-arrow"></i>
<div class="ps-index-track">
{[0, 1, 2, 3].map((n) => (
<b class:list={[n === 0 && 'ps-index-stop--on']}></b>
))}
</div>
<i class="ps-index-arrow ps-index-arrow--next"></i>
</div>
</Fragment>
)}
</div>
) : (
<div class="ps-phone-body">
{portfolioExtras && <div class="ps-layout-grid" aria-hidden="true"></div>}
<div class="ps-image"></div>
{type ? (
<Fragment>
<h2 class="pt-headline" data-type-headline></h2>
<p class="pt-body" data-type-body></p>
</Fragment>
) : (
<Fragment>
<div class="ps-headline"><i></i><i></i></div>
<div class="ps-copy"><i></i><i></i></div>
</Fragment>
)}
<div class="ps-actions">
<i data-type-cta-primary={hook}></i><i data-type-cta-secondary={hook}></i>
</div>
<div class="ps-proof">
{[0, 1].map((n) => (
<Fragment>
{n > 0 && <i class="ps-proof-divider" aria-hidden="true"></i>}
<div class="ps-proof-item">
<i></i>
{type
? <span data-type-proof></span>
: <span class="ps-proof-lines"><b></b><b></b></span>}
</div>
</Fragment>
))}
</div>
<div class="ps-editorial-copy">
<strong data-type-section-title={hook}></strong>
{type
? <p class="pt-section-body" data-type-section-body></p>
: <span><i></i><i></i></span>}
</div>
<div class="ps-gallery">
{[0, 1].map(() => (
<div class="ps-gallery-item">
<i></i>
{type
? <span><b data-type-gallery-caption></b></span>
: <span><b></b><b></b></span>}
</div>
))}
</div>
</div>
)}
{!type && (
<div class="ps-phone-footer">
<Fragment><i></i><i></i><i></i></Fragment>
</div>
)}
</div>
)}
<slot />
</div>
+138
View File
@@ -0,0 +1,138 @@
---
/*
One question, one artboard: the shape screens 03 onward all share. A screen
that only remaps artboard slots needs nothing of its own here, so it arrives
as data and its design lives in its own stylesheet.
*/
import Artboard from './Artboard.astro';
import { SURFACE_ANSWERS, surfacesAsked, surfaceTabsAttrs } from '../data/surfaces.js';
interface Option {
value: string;
title: string;
desc: string;
/* The sentence that stands in for the description on a surface this option
does not suit. Present only on options some surface rules out. */
blocked?: string;
}
/* A screen whose answer reads differently on each surface draws a board per
surface instead of repainting one, the arrangement the font screen already
uses. `class` is the modifier that board's own scene keys on. */
interface Board {
surface: 'persuade' | 'operate' | 'read' | 'experience';
class?: string;
cell?: string;
}
interface Props {
id: string;
step: string;
slug: string;
name: string;
title: string;
legend: string;
cta: string;
options: Option[];
previewClass: string;
phone?: boolean;
boards?: Board[];
derived?: boolean;
}
const { id, step, slug, name, title, legend, cta, options, previewClass, phone = true, boards, derived = false } = Astro.props;
const titleId = `picker-${slug}-title`;
const artboardClass = phone ? previewClass : `${previewClass} picker-artboard--solo`;
// A question listed in the matrix is answered once per chosen surface, which
// buys it a tab strip on the frame and a field per surface to answer into.
// Everything else about the screen is unchanged by opting in.
const perSurface = SURFACE_ANSWERS[name];
// Only the surfaces the question is put to. A surface with no field here is a
// surface that was never asked, and that absence is the whole of how the script
// and the final document know it.
const asked = perSurface ? surfacesAsked(name) : [];
const tabsAttrs = perSurface ? surfaceTabsAttrs(name) : null;
---
<section class="picker-screen" data-screen={id} data-step={step} aria-hidden="true" aria-labelledby={titleId}>
<div class="picker-container">
<div class="picker-strategy" data-question={slug}>
<h2 id={titleId} class="picker-question-title">{title}</h2>
<div class="picker-strategy-grid">
<div class="picker-type-rail">
<fieldset
class="picker-strategy-options picker-strategy-choices picker-type-options"
style={`--pk-choices:${options.length}`}
>
<legend>{legend}</legend>
{options.map((option, index) => (
<label class="picker-strategy-option" data-blocked-reason={option.blocked}>
<input type="radio" name={name} value={option.value} checked={index === 0} />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">{option.title}</span>
<span class="picker-strategy-desc">{option.desc}</span>
</span>
</label>
))}
</fieldset>
{/* One field per surface, disabled until its tile is chosen, so a
surface nobody asked for stays out of the answers and a surface
nobody opened still leaves one. A flat question keeps the same
fields for the same reading and withholds the name, which is the
whole of what a form submits. */}
{asked.map((mode) => (
<input
type="hidden"
data-surface-field={`${name}-${mode}`}
name={perSurface.flat ? undefined : `${name}-${mode}`}
disabled
/>
))}
</div>
{boards || tabsAttrs ? (
/* All of them mounted and one shown, so a tab switch costs a hidden
attribute rather than a rebuild. The stage is the box the strip takes
its row on, above the frame, which is why a screen with one board and
a strip is wrapped in it too. */
<div class="picker-board-stage" data-surface-stage={derived ? '' : undefined}>
{tabsAttrs && <div class="picker-surface-tabs" {...tabsAttrs} hidden></div>}
{(boards ?? [{} as Board]).map((board) => (
<Artboard
class={[artboardClass, board.class].filter(Boolean).join(' ')}
surface={board.surface}
motionCell={board.cell}
phone={phone}
cursor={slug === 'motion'}
carry={slug !== 'motion'}
portfolioExtras={slug === 'layout'}
/>
))}
</div>
) : (
<Artboard
class={artboardClass}
phone={phone}
cursor={slug === 'motion'}
carry={slug !== 'motion'}
/>
)}
</div>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
{cta}
<span class="ks-button-arrow" aria-hidden="true">
<svg viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square">
<path d="M0 4h14M10 0l4 4-4 4"></path>
</svg>
</span>
</button>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
+429
View File
@@ -0,0 +1,429 @@
[
{
"id": "side-tab",
"name": "Side-tab accent border",
"description": "Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.",
"group": "slop",
"discipline": "Visual Details"
},
{
"id": "border-accent-on-rounded",
"name": "Border accent on rounded element",
"description": "Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius.",
"group": "slop",
"discipline": "Visual Details"
},
{
"id": "overused-font",
"name": "Overused font",
"description": "Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.",
"group": "slop",
"discipline": "Typography"
},
{
"id": "flat-type-hierarchy",
"name": "Flat type hierarchy",
"description": "Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).",
"group": "slop",
"discipline": "Typography"
},
{
"id": "gradient-text",
"name": "Gradient text",
"description": "Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.",
"group": "slop",
"discipline": "Color & Contrast"
},
{
"id": "ai-color-palette",
"name": "AI color palette",
"description": "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.",
"group": "slop",
"discipline": "Color & Contrast"
},
{
"id": "cream-palette",
"name": "Cream / beige palette",
"description": "A warm cream or beige page background has become the default \"tasteful\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.",
"group": "slop",
"discipline": "Color & Contrast"
},
{
"id": "nested-cards",
"name": "Nested cards",
"description": "Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.",
"group": "slop",
"discipline": "Layout & Space"
},
{
"id": "monotonous-spacing",
"name": "Monotonous spacing",
"description": "The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections.",
"group": "slop",
"discipline": "Layout & Space"
},
{
"id": "bounce-easing",
"name": "Bounce or elastic easing",
"description": "Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.",
"group": "slop",
"discipline": "Motion"
},
{
"id": "pulsing-dot",
"name": "Pulsing status dot",
"description": "Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer.",
"group": "slop",
"discipline": "Motion"
},
{
"id": "blinking-cursor",
"name": "Decorative blinking cursor",
"description": "A blinking text cursor animated into a hero or landing section simulates typing where no input exists. It borrows the dev-tool aesthetic as decoration. Real editable fields draw their own caret; anywhere else, let the composition hold attention without a fake prompt.",
"group": "slop",
"discipline": "Motion"
},
{
"id": "shape-assembled-illustration",
"name": "Shape-assembled illustration",
"description": "A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.",
"group": "slop",
"discipline": "Imagery"
},
{
"id": "organic-clip-path",
"name": "Organic contour drawn as clip-path",
"description": "A clip-path polygon with many arbitrary vertices, or a curved clip-path path(), is CSS approximating a torn edge, blob, or silhouette. It reads as the cheap version of the effect and is usually a produced or photographic material replaced with code. Derive an alpha matte from the real image, or ship the shape as a cut-out raster; keep clip-path for geometry (cut corners, diagonals, hexagons).",
"group": "quality",
"discipline": "Imagery"
},
{
"id": "buried-raster",
"name": "Raster buried under a wash or opacity",
"description": "A background image under a near-opaque gradient wash, or a raster on an element at near-zero opacity, never reaches the screen: the page shows the wash, and the produced texture or photo ships as a compliance token. Let the material show (a tint under 0.9 alpha, a blend mode, an opacity you can see) or remove the file.",
"group": "quality",
"discipline": "Imagery"
},
{
"id": "dark-glow",
"name": "Glowing shadow accents",
"description": "Colored glow shadows — a zero-offset chromatic halo (box- or text-shadow) on any background, or any colored blurred shadow on a dark background — are the default \"cool\" look of AI-generated UIs. Use neutral elevation shadows and subtle, purposeful lighting instead.",
"group": "slop",
"discipline": "Color & Contrast"
},
{
"id": "radial-halo",
"name": "Radial-gradient background halo",
"description": "A chromatic radial-gradient wash — saturated at the center, fading to transparent — used as a decorative background glow on a dark page. Same tell as glowing shadows, drawn with a gradient instead of a shadow. Ground the surface with a solid or subtly shifted background instead.",
"group": "slop",
"discipline": "Color & Contrast"
},
{
"id": "radial-spotlight-glow",
"name": "Decorative radial spotlight glow",
"description": "A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a \"spotlight.\" It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.",
"group": "slop",
"discipline": "Color & Contrast"
},
{
"id": "marquee",
"name": "Auto-scrolling marquee",
"description": "Continuously auto-scrolling content demands attention it has not earned and hides half its content at any moment. Reserve motion for content that changes; let readers move at their own pace.",
"group": "slop",
"discipline": "Motion"
},
{
"id": "icon-tile-stack",
"name": "Icon tile stacked above heading",
"description": "A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.",
"group": "slop",
"discipline": "Typography"
},
{
"id": "italic-serif-display",
"name": "Italic serif display headline",
"description": "Oversized italic serif (Fraunces, Recoleta, Playfair, Newsreader-italic) as the primary hero headline reads as taste in isolation but has become the universal AI-startup landing page hero. Set roman, or move to a non-serif display face. Editorial / magazine register may legitimately want this — judge by context.",
"group": "slop",
"discipline": "Typography"
},
{
"id": "hero-eyebrow-chip",
"name": "Hero eyebrow / pill chip",
"description": "A tiny uppercase letter-spaced label sitting immediately above an oversized hero headline — or the same shape rendered as a pill chip — is now the default AI SaaS hero. Drop the eyebrow, integrate the kicker into the headline, or run it as a navigation breadcrumb instead.",
"group": "slop",
"discipline": "Typography"
},
{
"id": "kicker-above-heading",
"name": "Kicker / eyebrow label above heading",
"description": "A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.",
"group": "slop",
"discipline": "Typography"
},
{
"id": "numbered-section-labels",
"name": "Tiny numbered section labels",
"description": "Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.",
"group": "slop",
"discipline": "Layout & Space"
},
{
"id": "em-dash-overuse",
"name": "Em-dash overuse",
"description": "Em-dash saturation in body copy is an AI cadence tell. Advisory only: humans use em-dashes legitimately, so this fires only on saturation — at least 8 em-dashes (— or --) at a density near one per 500 characters of body text — never on a long article that uses a few. Prefer commas, colons, periods, or parentheses.",
"group": "slop",
"discipline": "Copy"
},
{
"id": "marketing-buzzword",
"name": "Marketing buzzword",
"description": "Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.",
"group": "slop",
"discipline": "Copy"
},
{
"id": "aphoristic-cadence",
"name": "Aphoristic-cadence copy",
"description": "Three or more sections landing on a short rebuttal sentence (\"X. No Y.\" / \"X. Just Y.\") or a manufactured-contrast aphorism (\"Not a feature. A platform.\") reads as AI cadence, not voice. Once is fine; the pattern is the tell.",
"group": "slop",
"discipline": "Copy"
},
{
"id": "oversized-h1",
"name": "Oversized hero headline",
"description": "A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.",
"group": "slop",
"discipline": "Typography"
},
{
"id": "extreme-negative-tracking",
"name": "Crushed letter spacing",
"description": "Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.",
"group": "slop",
"discipline": "Typography"
},
{
"id": "broken-image",
"name": "Broken or placeholder image",
"description": "<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.",
"group": "quality",
"discipline": "Imagery"
},
{
"id": "script-error",
"name": "Uncaught script error on load",
"description": "A script threw an uncaught exception or failed to parse while the page loaded. Broken JavaScript silently kills reveals, interactions, and dynamic content, and can leave most of a page invisible. Fix the error before judging anything else.",
"group": "quality",
"discipline": "Quality"
},
{
"id": "content-hidden-at-rest",
"name": "Content invisible at rest",
"description": "A large share of the page text sits at opacity 0 or visibility hidden even after every reveal handler had a chance to run. This is the failed-reveal signature: the content shipped but never becomes visible. Make content visible by default and let JavaScript enhance its entrance instead of gating its existence.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "edge-flush-cards",
"name": "Cards flush against the scroller edge",
"description": "Cards inside a horizontal scroller or tab panel sit flush against the container edge at rest while keeping a gutter on the other side, so their edges and rounded corners get cut off. Usually the panel is sized wider than its clip box. Keep a consistent inset on both sides.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "text-occlusion",
"name": "Text occluded by an overlapping element",
"description": "Text is painted under an opaque element or a second text run, so part of it cannot be read. A decorative box, a stacked layer, or an inline element with leaked padding lands on the words instead of beside them. Give overlapping layers room, or move the text out from under the layer above it.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "first-viewport-column-overflow",
"name": "One column stretches the first viewport",
"description": "A multi-column opening section lets one column run far past the fold while its sibling fits in a single viewport, so the short column floats in dead space and the fold falls deep inside one section. Balance the columns, cap the tall one, or let the long content flow below the opening row.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "gray-on-color",
"name": "Gray text on colored background",
"description": "Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast.",
"group": "quality",
"discipline": "Color & Contrast"
},
{
"id": "low-contrast",
"name": "Low contrast text",
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
"group": "quality",
"discipline": "Quality"
},
{
"id": "layout-transition",
"name": "Layout property animation",
"description": "Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.",
"group": "quality",
"discipline": "Motion"
},
{
"id": "line-length",
"name": "Line length too long",
"description": "Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "cramped-padding",
"name": "Cramped padding",
"description": "Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 1216px) of padding inside bordered, outlined, or colored containers.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "body-text-viewport-edge",
"name": "Body text touching viewport edge",
"description": "Body paragraphs render flush against the left or right viewport edge with no container providing horizontal padding. Wrap content in a container with at least 16px (ideally 24-32px) of horizontal padding, or apply max-width with mx-auto.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "tight-leading",
"name": "Tight line height",
"description": "Line height below 1.3x the font size makes multi-line text hard to read. Use 1.5 to 1.7 for body text so lines have room to breathe.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "skipped-heading",
"name": "Skipped heading level",
"description": "Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "heading-rhythm",
"name": "Heading crowded against the previous block",
"description": "A heading binds to the content it introduces, so the rendered space above it should exceed the space below it. When headings across a page sit as close or closer to the block above than to their own content, every section reads as if it captions the previous one. Open up the space above each heading.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "justified-text",
"name": "Justified text",
"description": "Justified text without hyphenation creates uneven word spacing (\"rivers of white\"). Use text-align: left for body text, or enable hyphens: auto if you must justify.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "tiny-text",
"name": "Tiny body text",
"description": "Body text below 12px is hard to read, especially on high-DPI screens. Use at least 14px for body content, 16px is ideal.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "undersized-ui-text",
"name": "Undersized functional text",
"description": "Interactive and content-bearing UI text (links, buttons, nav items, labels, table cells, meta rows, timecodes) below 11px is a legibility failure, not a style choice. WCAG sets no absolute pixel floor, but functional text under 11px is a defensible quality bar: it fails on high-DPI and small viewports and it degrades tap and read targets. The 11px floor holds even inside a footer; only non-interactive legal smallprint gets the softer 10px floor. Being ON the DESIGN.md size ramp does not exempt a value here: adding 8px to the ramp launders the token but not the legibility problem, and that is exactly the escape hatch this rule closes. Exempts sup/sub, visually-hidden (sr-only) text, and code/terminal contexts. Decorative letterspaced micro-labels are still functional and stay in scope.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "all-caps-body",
"name": "All-caps body text",
"description": "Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "wide-tracking",
"name": "Wide letter spacing on body text",
"description": "Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "text-overflow",
"name": "Content overflowing its container",
"description": "Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "repeated-container-text",
"name": "Same text repeated inside one container",
"description": "The same literal text rendered three or more times in structurally different spots inside a single card or panel is redundant messaging — usually a status or label wired into every slot of a template. Say it once, in the slot where it matters most.",
"group": "quality",
"discipline": "Quality"
},
{
"id": "clipped-overflow-container",
"name": "Positioned child clipped by overflow container",
"description": "A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.",
"group": "quality",
"discipline": "Layout & Space"
},
{
"id": "design-system-font",
"name": "Font outside DESIGN.md",
"description": "A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "design-system-color",
"name": "Color outside DESIGN.md",
"description": "A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.",
"group": "quality",
"discipline": "Color & Contrast"
},
{
"id": "design-system-radius",
"name": "Radius outside DESIGN.md",
"description": "A border-radius value is outside the DESIGN.md rounded scale. Use a documented radius token or update the design system if the new shape is intentional.",
"group": "quality",
"discipline": "Visual Details"
},
{
"id": "design-system-font-size",
"name": "Font size outside DESIGN.md",
"description": "A literal font-size is off the type ramp documented in DESIGN.md typography. Use a documented size step or update the design system if the new step is intentional.",
"group": "quality",
"discipline": "Typography"
},
{
"id": "gpt-thin-border-wide-shadow",
"name": "Hairline border with wide shadow",
"description": "A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.",
"group": "fingerprints",
"discipline": "Visual Details"
},
{
"id": "repeating-stripes-gradient",
"name": "Repeating-gradient stripes",
"description": "Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.",
"group": "fingerprints",
"discipline": "Visual Details"
},
{
"id": "codex-grid-background",
"name": "Decorative grid-line background",
"description": "A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.",
"group": "fingerprints",
"discipline": "Visual Details"
},
{
"id": "theater-slop-phrase",
"name": "Theater framing copy",
"description": "Dismissing something as \"theater\" is a recurring generated-copy tic. Say plainly what the thing does or does not do.",
"group": "fingerprints",
"discipline": "Copy"
},
{
"id": "image-hover-transform",
"name": "Image hover transform",
"description": "Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.",
"group": "slop",
"discipline": "Motion"
}
]
File diff suppressed because one or more lines are too long
+256
View File
@@ -0,0 +1,256 @@
/*
The four kinds of surface a design system can be asked to serve, straight from
the skill's modes, and the questions that are answered once for each of them.
Values travel as `surface-modes` (multi-select) from screen 01b; the agent
pre-checks what PRODUCT.md suggests via the design-context store's
context.json, and the visitor corrects it. Persuade is the markup default so
the answer can never arrive empty on runs that carry no hint.
*/
export const SURFACE_MODES = ['persuade', 'operate', 'read', 'experience'];
/*
A question listed here is answered once per chosen surface rather than once
per run, because the answer that suits the marketing page rarely suits the
tool it sells. Each entry carries:
- `allow`, the options that surface can take. Anything left out stays in the
list, turned off, with the option's own `data-blocked-reason` in place of
its description. Whether an option is out belongs to the option and not to
the pairing, so the reason is written once beside the copy it replaces.
Omitted where the question rules nothing out, which is not the same as
listing nothing, so the attribute is left off rather than left empty.
- `fallback`, what the surface lands on when nobody ever opens its tab. Every
chosen surface leaves an answer, so every surface needs one. Omitted where
the options are not known until the run deals them, and the first row on
the list stands in.
- `answered` and `unanswered`, what the tab tells a screen reader. `{}` is
where the chosen option's own label goes, lowercased so it reads as part of
the sentence around it. `properName` turns that off for a question whose
options are named after something rather than described.
- `flat`, for a question whose tabs are there to look with rather than to
split. The per-surface reading is still kept while the screen is open, so an
option a surface rules out cannot be the answer left behind, but only the
leading surface's choice is written down. A key per surface would promise
whoever reads the answers a distinction the run has nowhere to spend.
A question that leaves a surface out of `surfaces` is not asked of that
surface at all, which is a stronger statement than withholding an option from
it. Withholding says the surface would answer this badly; leaving it out says
the surface has no stake in the question. So the tab strip only offers the
surfaces the question names, and a run whose surfaces are all left out never
sees the screen and records no answer for it. Motion and layout are scoped
that way today; the rest name all four.
Persuade is allowed everything on every question: earning attention is the
whole job of the surface, and no answer here is too much for it. The other
three are ruled by what the surface is for rather than by how loud an option
is. A tool has to stay scannable, which rules out the loudest answers and,
on boundaries, the emptiest one as well. A page read at length is one plane
and one column. On a portfolio the work leads, so it is the interface that
has to recede; the page itself is still allowed to be dramatic, which is why
drenched survives there and four working colors do not.
*/
export const SURFACE_ANSWERS = {
'color-strategy': {
tablist: 'Surface being colored',
answered: 'colored {}',
unanswered: 'no color strategy chosen yet',
surfaces: {
persuade: { allow: 'restrained committed full-palette drenched', fallback: 'full-palette' },
operate: { allow: 'restrained committed full-palette', fallback: 'full-palette' },
read: { allow: 'restrained committed full-palette', fallback: 'full-palette' },
// The one surface that cannot take four working colors, so it keeps the
// quietest answer its allow list has.
experience: { allow: 'restrained committed drenched', fallback: 'restrained' },
},
},
/*
Flat, and the only question here that rules nothing out. There is no pair
that suits a landing page and is forbidden on a dashboard; a pair that fails
the dashboard is a bad pair, and the model composing fonts.json is told to
rank all six against the strictest surface the run names. So no allow list
is written, and every row stays live on every tab.
The tabs earn their place all the same. One pair is chosen and one type
system comes out of the run, and the thing a visitor cannot otherwise do is
see that pair set as a dashboard, as a document, and as an index before
committing to it. The strip here is a way of looking rather than a second
decision.
*/
'font-pair': {
tablist: 'Surface the pair is shown on',
answered: 'set in {}',
unanswered: 'no pair chosen yet',
properName: true,
flat: true,
surfaces: {
persuade: {},
operate: {},
read: {},
experience: {},
},
},
/*
One of the two questions that are not put to every surface. Motion energy is
a claim on attention, and only two of the four surfaces are in a position to
make one: a landing page earning a decision and a portfolio presenting work.
A tool and a document are moved through rather than watched, and their motion
follows from what the interface is doing rather than from a house style, so
asking them would collect an answer nothing should act on. An app-UI-only or
docs-only run therefore never reaches this screen.
Both surfaces that are asked can take all three energies, so nothing is
withheld and no option carries a reason. Their defaults differ, because what
a surface does with movement when nobody says otherwise is the whole of what
it is for: a page earning a decision has to answer the pointer it is trying
to keep, and a portfolio stages the work's arrival, which is what a reveal
is for.
*/
'motion-energy': {
tablist: 'Surface being moved',
answered: '{} movement',
unanswered: 'no motion energy chosen yet',
surfaces: {
persuade: { fallback: 'responsive' },
experience: { fallback: 'choreographed' },
},
},
/*
The other question that is not put to every surface, and the only one that is
scoped and flat at once. How strict a layout feels is a claim about the
composition of the page itself, so it is asked of the two surfaces whose page
is the thing being looked at: one earning a decision, one presenting work. A
tool and a document are composed by what they have to hold instead, a rail
against a working column against a panel, one column at one measure, so the
question has nowhere to move on them and an answer taken there would be a
decision nobody made. An app-UI-only or docs-only run therefore never reaches
this screen either.
Both surfaces that are asked can take all three answers, so nothing is
withheld and no option carries a reason. Their defaults differ: a page
earning a decision needs one block to lead, which is what the breaks in the
aligned grid are for, and a gallery hangs work square so the only irregular
edges on the page are the work's own.
Flat, because one answer is all the run has to spend. The twelve-column ruler
on this screen describes a page, and it describes one whether the run ships a
dashboard or a gallery, so what the two of them leave is previewed on every
board the screens after it draw, the shell and the document included. A key
per surface would promise whoever reads the answers a distinction with
nowhere to land, and the tabs are left as a way of looking at the one answer
on both pages that asked for it.
*/
'layout-structure': {
tablist: 'Surface being laid out',
answered: '{} layout',
unanswered: 'no layout structure chosen yet',
flat: true,
surfaces: {
persuade: { fallback: 'balanced' },
experience: { fallback: 'simple-grid' },
},
},
/*
Open space is out on a tool. Operate asks for a ground of its own under
sidebars, toolbars, and panels, and the open answer is defined by every
ground being the same one. Its default is that second ground rather than
panels, which spend an edge and an inset on every object and buy density
back at the price of it.
Cards and panels are out on the two surfaces whose content is the point.
A page read at length is one column, and a page of work is the work.
*/
'boundary-style': {
tablist: 'Surface being separated',
answered: 'separated by {}',
unanswered: 'no boundary style chosen yet',
surfaces: {
persuade: { allow: 'open-space thin-dividers surface-changes cards-and-panels', fallback: 'open-space' },
operate: { allow: 'thin-dividers surface-changes cards-and-panels', fallback: 'surface-changes' },
read: { allow: 'open-space thin-dividers surface-changes', fallback: 'open-space' },
experience: { allow: 'open-space thin-dividers surface-changes', fallback: 'open-space' },
},
},
/*
The least mode-sensitive question in the run, and the matrix says so. A
radius is not a claim on the reader's attention the way a color or a shadow
is, and the two surfaces that look like candidates both survive scrutiny:
fully round controls are the house style of an entire mobile platform, so
ruling them out of app UI would put this screen against Material rather
than against a mistake, and a document has too few shapes for the answer to
reach. Only the portfolio blocks anything, and its default is sharp, since
a gallery hangs work square and the work's own edges are the shapes on the
page.
*/
'corner-style': {
tablist: 'Surface being shaped',
answered: '{} corners',
unanswered: 'no corner style chosen yet',
surfaces: {
persuade: { allow: 'sharp slightly-soft friendly pill', fallback: 'slightly-soft' },
operate: { allow: 'sharp slightly-soft friendly pill', fallback: 'slightly-soft' },
read: { allow: 'sharp slightly-soft friendly pill', fallback: 'slightly-soft' },
experience: { allow: 'sharp slightly-soft friendly', fallback: 'sharp' },
},
},
/*
Floating is out wherever the page is worked in or read at length, which is
the line color strategy already drew for drenched. Lift that deep is how an
overlay says it is temporary; spent on every resting panel it stops meaning
anything and leaves a dense screen harder to scan. The portfolio keeps it:
lifting the work off the page is a way of presenting the work.
*/
'depth-style': {
tablist: 'Surface being lifted',
answered: '{} depth',
unanswered: 'no depth style chosen yet',
surfaces: {
persuade: { allow: 'flat soft-lift floating', fallback: 'soft-lift' },
operate: { allow: 'flat soft-lift', fallback: 'flat' },
read: { allow: 'flat soft-lift', fallback: 'flat' },
experience: { allow: 'flat soft-lift floating', fallback: 'flat' },
},
},
};
/* The matrix reaches the browser on the surface tiles, which is already where
the script looks for everything a surface knows about itself. One pair of
attributes per question, read by name rather than by dataset key so the
question's own value is the lookup. A question this surface is not asked
contributes nothing, and the absent per-surface field is what the script
reads that from. */
export const surfaceAttrs = (mode) => Object.fromEntries(
Object.entries(SURFACE_ANSWERS).flatMap(([name, question]) => {
const { allow, fallback } = question.surfaces[mode] ?? {};
return [
...(allow ? [[`data-allow-${name}`, allow]] : []),
...(fallback ? [[`data-default-${name}`, fallback]] : []),
];
}),
);
/* Which surfaces a question is put to at all, in tile order. The per-surface
fields a screen renders are the browser's copy of this, and the leading
applicable surface is the one whose answer the bare key carries. */
export const surfacesAsked = (name) => SURFACE_MODES.filter(
(mode) => mode in SURFACE_ANSWERS[name].surfaces,
);
/* Everything the script needs to run a per-surface question is on its tab
strip, so a screen opts in by rendering one of these and nothing else. */
export const surfaceTabsAttrs = (name) => ({
'data-surface-tabs': name,
'data-surface-flat': SURFACE_ANSWERS[name].flat ? '' : undefined,
'data-surface-proper-name': SURFACE_ANSWERS[name].properName ? '' : undefined,
'data-surface-answered': SURFACE_ANSWERS[name].answered,
'data-surface-unanswered': SURFACE_ANSWERS[name].unanswered,
role: 'tablist',
'aria-label': SURFACE_ANSWERS[name].tablist,
});
+59
View File
@@ -0,0 +1,59 @@
---
import '../styles/picker.css';
// Loaded after the shared sheet so a screen's own answers win on source order
// as well as specificity. One file per question, each owning only the slots
// its options rewrite.
import '../styles/screens/motion.css';
import '../styles/screens/layout.css';
import '../styles/screens/boundaries.css';
import '../styles/screens/corners.css';
import '../styles/screens/depth.css';
// The layout screen's portfolio board, loaded after the three sheets above on
// purpose: it pins that board's material against their committed defaults by
// winning ties on source order. The header of the file has the argument.
import '../styles/screens/layout-portfolio.css';
// The design context document that replaces the picker after submission.
import '../styles/design-context.css';
// The lean design context document, ported from the standalone demo. Order
// mirrors the demo page's stylesheet order so the cascade lands identically;
// command and settings sit where dcx-document.css used to @import them.
import '../styles/dcx/dcx-audience.css';
import '../styles/dcx/dcx-detail.css';
import '../styles/dcx/dcx-components.css';
import '../styles/dcx/dcx-hierarchy.css';
import '../styles/dcx/dcx-command.css';
import '../styles/dcx/dcx-settings.css';
import '../styles/dcx/dcx-document.css';
import '../styles/dcx/dcx-rail.css';
import '../styles/dcx/dcx-hooks.css';
interface Props {
title?: string;
description?: string;
}
const {
title = 'Impeccable design interview',
description = 'Choose the design direction your agent will use.',
} = Astro.props;
---
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content={description} />
<title>{title}</title>
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Albert+Sans:wght@300;400;500;600;700&family=Alumni+Sans:wght@100;300;400;500;600;700&display=swap" rel="stylesheet" />
</head>
<!-- Declared here, not in picker.css, so Vite leaves the path alone. It is
server-absolute because a url() inside var() resolves against the
stylesheet that consumes it, which lives one directory down. -->
<body class="picker-page" style="--pk-foil: url('/assets/kinpaku-gold-leaf.jpg')">
<slot />
</body>
</html>
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
/** The boot contract: one fetch that tells the page how to start.
*
* Both scripts on the page read it, so it is fetched once and shared. A server
* that predates the contract answers 404, and the page starts the way it always
* did: a blank questionnaire.
*/
let bootPromise = null;
export function getBoot() {
bootPromise ??= fetch('/boot.json')
.then((response) => (response.ok ? response.json() : null))
.catch(() => null)
.then((data) => ({
mode: data?.mode === 'doc' ? 'doc' : 'questionnaire',
prior: data?.prior && typeof data.prior === 'object' && !Array.isArray(data.prior) ? data.prior : null,
priorSource: data?.priorSource || null,
doc: data?.doc || null,
}));
return bootPromise;
}
/* Restoring a previous run happens inside the questionnaire, which owns the
state being restored. The document waits on this before it renders, so it
never reads a form that is still half filled. It resolves either way: a run
with nothing to restore is ready immediately. */
let settle;
export const hydrationReady = new Promise((resolve) => { settle = resolve; });
/* The attribute is the observable half: it drives the note on the start screen
and gives anything watching the page one thing to wait for. */
export function markHydrated(source) {
document.body.dataset.hydrated = source || 'none';
settle(source || null);
}
+154
View File
@@ -0,0 +1,154 @@
const clamp = (value) => Math.min(1, Math.max(0, value));
const linearize = (value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
const gamma = (value) => value <= 0.0031308 ? 12.92 * value : 1.055 * value ** (1 / 2.4) - 0.055;
const parseHex = (hex) => hex.match(/[\da-f]{2}/gi).map((value) => Number.parseInt(value, 16) / 255);
function toLinearRgb([L, C, H]) {
const angle = H * Math.PI / 180;
const a = C * Math.cos(angle);
const b = C * Math.sin(angle);
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
return [
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
];
}
export function oklchToHex([lightness, chroma, hue]) {
const L = clamp(lightness);
let C = Math.max(0, chroma);
let rgb = toLinearRgb([L, C, hue]);
while (C > 0 && rgb.some((channel) => channel < 0 || channel > 1)) {
C = Math.max(0, C - 0.005);
rgb = toLinearRgb([L, C, hue]);
}
return `#${rgb.map((channel) => Math.round(clamp(gamma(channel)) * 255).toString(16).padStart(2, '0')).join('').toUpperCase()}`;
}
export function hexToOklch(hex) {
const [red, green, blue] = parseHex(hex).map(linearize);
const l = Math.cbrt(0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue);
const m = Math.cbrt(0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue);
const s = Math.cbrt(0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue);
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
const b = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
const C = Math.hypot(a, b);
return [L, C, C < 0.00001 ? 0 : (Math.atan2(b, a) * 180 / Math.PI + 360) % 360];
}
export function formatOklch(hex) {
const [L, C, H] = hexToOklch(hex);
return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(3)} ${H.toFixed(1)})`;
}
/** Expand one curated seed into the four questionnaire roles. */
export function seedToRoles(seed) {
const [L, C, H] = seed.oklch;
return {
primary: oklchToHex([L, C, H]),
secondary: oklchToHex([L < 0.5 ? L + 0.18 : L - 0.18, C * 0.6, H]),
tertiary: oklchToHex([0.62, Math.min(0.23, Math.max(C, 0.15)), (H + 60) % 360]),
neutral: oklchToHex([L < 0.55 ? 0.96 : 0.2, 0.01, H]),
};
}
// Relative luminance of the colors --pk-ink-dark and --pk-ink-light resolve
// to in styles/picker.css: oklch(14% 0.018 95) and oklch(99% 0.008 95).
const INK_DARK_LUMINANCE = 0.0027;
const INK_LIGHT_LUMINANCE = 0.9716;
// The ink sits on a color the user picked, so it must not follow the picker's
// own theme: --ks-champagne and friends invert between light and dark and
// would blank the label on exactly the swatches that need it most. Comparing
// both ratios beats a fixed lightness threshold, which picks the losing ink
// for mid-tones sitting near the cutoff.
export function contrastInk(hex) {
const swatch = relativeLuminance(hex);
const against = (ink) => ratio(ink, swatch);
return against(INK_DARK_LUMINANCE) >= against(INK_LIGHT_LUMINANCE)
? 'var(--pk-ink-dark)'
: 'var(--pk-ink-light)';
}
/** Hex counterpart to contrastInk(), for readableOn() grounds that are CSS vars. */
export function contrastInkHex(hex) {
return contrastInk(hex).includes('light')
? oklchToHex([0.99, 0.008, 95])
: oklchToHex([0.14, 0.018, 95]);
}
function relativeLuminance(hex) {
const [red, green, blue] = parseHex(hex).map(linearize);
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
}
function ratio(a, b) {
const [hi, lo] = a > b ? [a, b] : [b, a];
return (hi + 0.05) / (lo + 0.05);
}
/**
* The accent as type rather than as a fill. A color picked because it looks
* right as a shape on the page is not thereby readable as words on it, and a
* pale primary on paper is the common way that goes wrong. Only lightness
* moves, so the hue the user chose is the hue that shows up; a color that
* already clears the target comes back untouched.
*/
export function readableOn(accent, ground, target = 4.5) {
const groundLuminance = relativeLuminance(ground);
if (ratio(relativeLuminance(accent), groundLuminance) >= target) return accent;
const [L, C, H] = hexToOklch(accent);
// Toward whichever end of the range the ground leaves room in, by the same
// comparison contrastInk makes rather than a fixed lightness cutoff.
const darker = ratio(INK_DARK_LUMINANCE, groundLuminance) >= ratio(INK_LIGHT_LUMINANCE, groundLuminance);
const step = darker ? -0.015 : 0.015;
for (let lightness = L + step; lightness > 0.03 && lightness < 1; lightness += step) {
const candidate = oklchToHex([lightness, C, H]);
if (ratio(relativeLuminance(candidate), groundLuminance) >= target) return candidate;
}
return darker ? '#000000' : '#FFFFFF';
}
// Floors for judging the neutral on screen 02. The neutral paints the large
// surfaces of every preview, so two pairs matter: the fixed swatch inks that
// set text on it, and the primary fills that sit on it.
const NEUTRAL_INK_FLOOR = 7;
const NEUTRAL_PRIMARY_FLOOR = 3;
/**
* Plain-language warnings when the palette's neutral will cause contrast
* trouble: an array with one line per failed check, empty when it is safe.
* The checks run independently, so a neutral that fails both reports both,
* mid-tone first.
*
* Check 1: the better of the two fixed inks must reach 7:1 (the WCAG AAA
* body-text figure) on the neutral. The inks are near-black and near-white,
* so 4.5:1 is nearly impossible to fail; 7:1 is the floor that catches
* mid-tone neutrals which leave no headroom for muted and secondary text.
*
* Check 2: the primary must reach 3:1 (WCAG 1.4.11 non-text contrast)
* against the neutral, because primary button fills and accents sit directly
* on neutral surfaces and readableOn() only rescues text, never fills.
*
* The strings are read by people configuring a palette, not by developers:
* no ratios, no standards names. Step 8's test matches on them.
*/
export function neutralContrastIssue({ neutral, primary }) {
const surface = relativeLuminance(neutral);
const bestInk = Math.max(
ratio(INK_DARK_LUMINANCE, surface),
ratio(INK_LIGHT_LUMINANCE, surface),
);
const issues = [];
if (bestInk < NEUTRAL_INK_FLOOR) {
issues.push('This background is too close to a middle gray, so text on it will be hard to read. Try a much lighter or much darker color.');
}
if (ratio(relativeLuminance(primary), surface) < NEUTRAL_PRIMARY_FLOOR) {
issues.push('Your main color and this background are too similar, so buttons and cards will blend in. Try more difference between them.');
}
return issues;
}
+15
View File
@@ -0,0 +1,15 @@
/* Session-aware URLs for the document's own images (section foils, rail
textures, placeholder brand assets, the components photo).
The submit-flow picker server exits the moment /submit resolves, and article
images only load when a view opens, which is always after that exit. The doc
session lives on, so once design-context.js announces one on
window.dcxDocSession, every document asset routes through it (the session's
/assets route arrives with that announcement). Until then paths pass through
untouched and the page's own origin serves them — which is the whole story in
document mode, where the picker server stays up. */
export const dcxAsset = (assetPath) => {
const session = window.dcxDocSession;
if (!session?.base || !session?.token) return assetPath;
return `${session.base}${assetPath}?token=${encodeURIComponent(session.token)}`;
};
+163
View File
@@ -0,0 +1,163 @@
import { dcxAsset } from './assets.js';
(() => {
"use strict";
const SECTION_META = [
{
labels: ["Who they are"],
slug: "who-they-are",
variant: "people",
lede: "The core people this experience must speak to.",
icon: "audience-groups-foil.png",
},
{
labels: ["Emotional journey", "Emotional state"],
slug: "emotional-journey",
variant: "journey",
lede: "The change in confidence the experience should create.",
icon: "emotional-journey-foil.png",
},
{
labels: ["Needs"],
slug: "needs",
variant: "list",
lede: "What the experience must make clear and easy.",
icon: "needs-foil.png",
},
{
labels: ["Trust triggers"],
slug: "trust-triggers",
variant: "list",
lede: "The signals that turn interest into confidence.",
icon: "trust-triggers-foil.png",
},
{
labels: ["Who must not be excluded"],
slug: "inclusion",
variant: "list",
lede: "Access requirements that belong in the core experience.",
icon: "inclusion-foil.png",
},
];
let syncFrame = 0;
const findMeta = (label) => SECTION_META.find((meta) => meta.labels.includes(label));
const unwrapColumns = (article) => {
article.querySelectorAll(":scope > .dcx-cols").forEach((columns) => {
const fragment = document.createDocumentFragment();
[...columns.children].forEach((child) => fragment.appendChild(child));
columns.replaceWith(fragment);
});
};
const enhanceAudienceArticle = (article) => {
if (article.dataset.dcxAudienceEnhanced === "true") return;
article.classList.add("dcx-audience");
article.querySelector(":scope > header")?.classList.add("dcx-audience-hero");
unwrapColumns(article);
const sections = [...article.querySelectorAll(":scope > .dcx-block[data-label]")];
sections.forEach((section, index) => {
const label = section.dataset.label || "";
const meta = findMeta(label);
if (!meta) return;
const oldLabel = section.querySelector(":scope > .dcx-block-label");
oldLabel?.remove();
const body = document.createElement("div");
body.className = "dcx-audience-section-body";
while (section.firstChild) body.appendChild(section.firstChild);
const heading = document.createElement("div");
heading.className = "dcx-audience-section-heading";
const title = document.createElement("h3");
title.className = "dcx-audience-section-title";
title.id = `dcx-audience-${meta.slug}-title`;
title.textContent = label;
const lede = document.createElement("p");
lede.className = "dcx-audience-section-lede";
lede.textContent = meta.lede;
heading.append(title, lede);
const figure = document.createElement("figure");
figure.className = "dcx-audience-section-icon";
figure.setAttribute("data-dcx-hide-on-error", "");
figure.setAttribute("aria-hidden", "true");
const image = document.createElement("img");
image.src = dcxAsset(`/assets/audience/${meta.icon}`);
image.alt = "";
image.width = 256;
image.height = 256;
image.decoding = "async";
if (index > 0) image.loading = "lazy";
figure.appendChild(image);
const header = document.createElement("header");
header.className = "dcx-audience-section-head";
header.append(heading, figure);
section.classList.add("dcx-audience-section", `dcx-audience-section--${meta.variant}`);
section.id = `dcx-audience-${meta.slug}`;
if (!article.dataset.dcxCategory) {
section.setAttribute("role", "region");
section.setAttribute("aria-labelledby", title.id);
}
if (meta.variant === "journey") {
const journey = body.querySelector(".dcx-callout-pair");
if (journey) {
journey.classList.add("dcx-audience-journey");
if (journey.children.length > 1) {
const arrow = document.createElement("div");
arrow.className = "dcx-audience-journey-arrow";
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "→";
journey.children[0].after(arrow);
}
}
}
if (meta.variant === "list") {
body.querySelector(".dcx-list")?.classList.add("dcx-audience-list");
}
section.append(header, body);
});
article.dataset.dcxAudienceEnhanced = "true";
};
const syncAudience = () => {
syncFrame = 0;
const expander = document.querySelector(".dcx-expander");
if (!expander) return;
const continuousArticle = expander.querySelector('.dcx-main > .dcx-article[data-dcx-category="audience"]');
if (continuousArticle) {
enhanceAudienceArticle(continuousArticle);
return;
}
const audienceItem = expander.querySelector('.dcx-nav-list li[data-category="audience"].is-active');
const article = expander.querySelector(".dcx-main > .dcx-article");
if (audienceItem && article) enhanceAudienceArticle(article);
};
const scheduleSync = () => {
if (syncFrame) return;
syncFrame = requestAnimationFrame(syncAudience);
};
const observer = new MutationObserver(scheduleSync);
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener("pageshow", scheduleSync);
scheduleSync();
})();
+346
View File
@@ -0,0 +1,346 @@
import { dcxAsset } from './assets.js';
(() => {
"use strict";
const DETAIL_META = {
product: {
"Purpose": ["Why the product exists and what success looks like.", "/assets/product/product-purpose-foil.png"],
"Positioning": ["The strategic space this product should occupy.", "/assets/product/product-positioning-foil.png"],
"Primary conversion": ["The single action the experience should make inevitable.", "/assets/product/product-primary-conversion-foil.png"],
"What must be clear first": ["The facts people need before anything else.", "/assets/product/product-clear-first-foil.png"],
"Product principles": ["The rules that keep every product decision aligned.", "/assets/product/product-principles-foil.png"],
"Operating context": ["Where and how the product must work.", "/assets/product/product-operating-context-foil.png"],
"Surfaces": ["The chosen experiences and the job each one performs.", "/assets/product/product-surfaces-foil.png"],
},
brand: {
"Personality": ["The character every expression should carry.", "/assets/brand/brand-personality-foil.png"],
"Voice": ["How the brand sounds—and what it deliberately avoids.", "/assets/brand/brand-voice-foil.png"],
"Principles": ["The rules that turn character into consistent choices.", "/assets/brand/brand-principles-foil.png"],
"Commitments": ["The promises the brand must keep.", "/assets/brand/brand-commitments-foil.png"],
"Named references": ["Useful signals to borrow without becoming an imitation."],
"Anti-reference": ["The direction the brand must deliberately avoid."],
"Marks": ["The identity assets already available to the system."],
"Boards and references": ["Visual evidence that should inform the work."],
"Assets provided": ["The source material available for production."],
"Brand assets": ["Logos, moodboards, and visual references available to the system."],
},
color: {
"The cue": ["", "/assets/color/color-palette-foil.png"],
"Also generated": ["Adjacent directions retained as useful context."],
"Palette": ["The core color relationships for the experience.", "/assets/color/color-palette-foil.png"],
"Strategy per surface": ["How the palette changes emphasis across contexts.", "/assets/color/color-strategy-per-surface-foil.png"],
},
typography: {
"The pair": ["Two typefaces with distinct, complementary jobs.", "/assets/typography/typography-pair-foil.png"],
"Type scale": ["The hierarchy that gives content pace and proportion.", "/assets/typography/typography-type-scale-foil.png"],
"In running text": ["How the pair behaves when the content gets real."],
},
components: {
"Buttons": ["Variants, sizes, icons, and loading states."],
"Input fields": ["States, sizes, affixes, and multiline input."],
"Cards": ["Content, media, horizontal, and action compositions."],
},
material: {
"The page, as chosen": ["The page anatomy each surface committed, and what it represents."],
"Motion per surface": ["How movement supports each context without becoming spectacle."],
"Motion": ["How movement should support the experience."],
"Accessibility": ["The rules that keep the material system readable and operable."],
"Layout structure": ["The spatial system that organizes the page.", "/assets/material/material-layout-structure-foil.png"],
"Boundaries per surface": ["Where separation is visible—and where space does the work.", "/assets/material/material-boundaries-per-surface-foil.png"],
"Corners per surface": ["How edge character changes with context.", "/assets/material/material-corners-per-surface-foil.png"],
"Depth per surface": ["How hierarchy is expressed without decorative elevation.", "/assets/material/material-depth-per-surface-foil.png"],
"Iconography": ["The chosen icon library, its character, grid, stroke, and license."],
},
hooks: {
"How it runs": [""],
"Built-in rules": ["Choose what the detector watches."],
"Custom rules": [""],
},
};
let syncFrame = 0;
const slugify = (value) => value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
const wrapPrincipleContent = (section, category) => {
section.querySelectorAll(":scope .dcx-principles > li").forEach((item) => {
let copy = item.querySelector(":scope > .dcx-detail-principle-copy");
if (!copy) {
copy = document.createElement("div");
copy.className = "dcx-detail-principle-copy";
while (item.firstChild) copy.appendChild(item.firstChild);
item.appendChild(copy);
}
if (category === "product" && section.dataset.label === "Product principles") {
const detail = copy.querySelector(":scope > strong:first-child")?.nextSibling;
if (detail?.nodeType === Node.TEXT_NODE) {
detail.textContent = detail.textContent.replace(/^\s*[—–-]+\s*/, " ");
}
}
});
};
const demoteBodyHeadings = (body) => {
body.querySelectorAll("h3").forEach((heading) => {
const replacement = document.createElement("h4");
[...heading.attributes].forEach((attribute) => {
replacement.setAttribute(attribute.name, attribute.value);
});
while (heading.firstChild) replacement.appendChild(heading.firstChild);
heading.replaceWith(replacement);
});
};
const placeProductPlatform = (section, category) => {
if (category !== "product" || section.dataset.label !== "Purpose") return;
const callout = section.querySelector(":scope .dcx-purpose .dcx-callout");
const name = callout?.querySelector(":scope > .dcx-callout-name");
const platform = section.querySelector(":scope .dcx-purpose > .dcx-platform-pill");
if (!callout || !name || !platform) return;
platform.classList.remove("dcx-chip");
name.after(platform);
};
const removeProvenanceNotes = (section) => {
section.querySelectorAll(":scope .dcx-fan-note").forEach((note) => note.remove());
};
/* A "default" chip marks a surface that kept its preset instead of being
configured, and the sentence that said so was a provenance note, which the
pass above removes. Restate it in a class this pass keeps, so a value the
run never chose is never read as one it did. The "leading" chip shares the
class and means something else, hence the text test. */
const notePresetDefaults = (body) => {
const chips = [...body.querySelectorAll(".dcx-default-mark")];
if (!chips.some((chip) => chip.textContent.trim() === "default")) return;
if (body.querySelector(":scope > .dcx-default-note")) return;
const note = document.createElement("p");
note.className = "dcx-default-note";
note.textContent = "Entries marked default kept their surface's preset; this run never set them. Run /impeccable design-context edit to answer them.";
body.appendChild(note);
};
const compactColorArticle = (article) => {
if (article.dataset.dcxColorCompacted === "true") return;
const sections = new Map(
[...article.querySelectorAll(":scope > .dcx-block[data-label]")]
.map((section) => [section.dataset.label, section]),
);
const cueSection = sections.get("The cue");
const paletteSection = sections.get("Palette");
const strategySection = sections.get("Strategy per surface");
const cue = cueSection?.querySelector(":scope > .dcx-cue");
const fan = paletteSection?.querySelector(":scope > .dcx-fan");
cueSection?.querySelector(":scope > .dcx-fan-note")?.remove();
strategySection?.querySelector(":scope > .dcx-fan-note")?.remove();
sections.get("Roles and values")?.remove();
sections.get("Interview direction")?.remove();
if (cue && fan) {
const cueCard = cue.querySelector(":scope > .dcx-cue-card");
const panels = [...fan.querySelectorAll(":scope > .dcx-fan-panel")];
if (cueCard && panels.length) {
cueCard.querySelector(":scope > .dcx-cue-tag")?.remove();
cueCard.querySelector(":scope > .dcx-cue-name")?.remove();
const bands = document.createElement("ul");
bands.className = "dcx-cue-bands";
bands.setAttribute("aria-label", "Committed palette");
panels.forEach((panel) => {
const role = panel.querySelector(":scope .dcx-fan-name")?.textContent.trim() || "Color";
const value = panel.querySelector(":scope .dcx-fan-value")?.textContent.trim() || "";
const color = panel.style.getPropertyValue("--panel-swatch");
const ink = panel.style.getPropertyValue("--panel-ink");
const item = document.createElement("li");
item.className = "dcx-cue-band-item";
const swatch = document.createElement("div");
swatch.className = "dcx-cue-band";
swatch.style.setProperty("--band-color", color);
swatch.style.setProperty("--band-ink", ink);
const hex = document.createElement("code");
hex.className = "dcx-cue-band-value";
hex.textContent = value;
swatch.appendChild(hex);
const label = document.createElement("span");
label.className = "dcx-cue-band-label";
label.textContent = role;
item.append(swatch, label);
bands.appendChild(item);
});
cueCard.querySelectorAll(":scope > .dcx-cue-role").forEach((role) => role.remove());
cueCard.appendChild(bands);
paletteSection.remove();
}
}
const heroLede = article.querySelector(":scope > header > .dcx-lede");
if (heroLede) heroLede.textContent = "Committed palette and per-surface strategy.";
article.dataset.dcxColorCompacted = "true";
};
const replaceMaterialPagePreviews = (article) => {
const section = article.querySelector(':scope > .dcx-block[data-label="The page, as chosen"]');
const previews = section?.querySelector(":scope > .dcx-surfaces");
if (!previews) return;
/* One definition per chosen surface, published by the data layer at render
time (design-context.js renderDocument), appended under the boards so
the drawings and the words both survive. A missing list changes
nothing: the boards already stand on their own. */
const entries = Array.isArray(window.dcxSurfaceDefs) ? window.dcxSurfaceDefs : [];
if (!entries.length) return;
const definitions = document.createElement("dl");
definitions.className = "dcx-defs";
entries.forEach(({ label, description }) => {
const item = document.createElement("div");
item.className = "dcx-def";
const term = document.createElement("dt");
term.textContent = label;
const detail = document.createElement("dd");
detail.textContent = description;
item.append(term, detail);
definitions.appendChild(item);
});
previews.after(definitions);
};
const enhanceSection = (section, category, iconIndex) => {
const label = section.dataset.label || "Section";
const [ledeText, icon] = DETAIL_META[category]?.[label] || ["The decisions that define this part of the system."];
const slug = `${category}-${slugify(label)}`;
section.querySelector(":scope > .dcx-block-label")?.remove();
const body = document.createElement("div");
body.className = "dcx-detail-section-body";
while (section.firstChild) body.appendChild(section.firstChild);
const heading = document.createElement("div");
heading.className = "dcx-detail-section-heading";
const title = document.createElement("h3");
title.className = "dcx-detail-section-title";
title.id = `dcx-${slug}-title`;
title.textContent = label;
heading.appendChild(title);
if (ledeText) {
const lede = document.createElement("p");
lede.className = "dcx-detail-section-lede";
lede.textContent = ledeText;
heading.appendChild(lede);
}
const header = document.createElement("header");
header.className = "dcx-detail-section-head";
header.appendChild(heading);
if (icon) {
header.classList.add("dcx-detail-section-head--with-icon");
const figure = document.createElement("figure");
figure.className = "dcx-detail-section-icon";
figure.setAttribute("aria-hidden", "true");
figure.setAttribute("data-dcx-hide-on-error", "");
const image = document.createElement("img");
image.src = dcxAsset(icon);
image.alt = "";
image.width = 256;
image.height = 256;
image.decoding = "async";
if (section.closest(".dcx-article")?.dataset.dcxCategory || iconIndex > 0) image.loading = "lazy";
figure.appendChild(image);
header.appendChild(figure);
}
section.classList.add("dcx-detail-section", `dcx-detail-section--${slugify(label)}`);
section.id = `dcx-${slug}`;
if (!section.closest(".dcx-article")?.dataset.dcxCategory) {
section.setAttribute("role", "region");
section.setAttribute("aria-labelledby", title.id);
}
section.append(header, body);
demoteBodyHeadings(body);
body.querySelector(":scope > .dcx-list")?.classList.add("dcx-detail-list");
body.querySelector(":scope > .dcx-defs")?.classList.add("dcx-detail-defs");
wrapPrincipleContent(section, category);
placeProductPlatform(section, category);
removeProvenanceNotes(section);
notePresetDefaults(body);
};
const enhanceArticle = (article, category) => {
if (article.dataset.dcxDetailEnhanced === category) return;
if (category === "color") compactColorArticle(article);
if (category === "typography") {
article.querySelectorAll(":scope .dcx-pair-why").forEach((note) => note.remove());
article.querySelector(':scope > .dcx-block[data-label="Interview direction"]')?.remove();
}
if (category === "iconography") {
article.querySelector(':scope > .dcx-block[data-label="The hand"]')?.remove();
}
if (category === "material") {
replaceMaterialPagePreviews(article);
article.querySelectorAll(":scope > .dcx-block[data-label] > .dcx-fan-note")
.forEach((note) => note.remove());
}
article.classList.add("dcx-detail-article", `dcx-detail-article--${category}`);
article.querySelector(":scope > header")?.classList.add("dcx-detail-hero");
let iconIndex = 0;
article.querySelectorAll(":scope > .dcx-block[data-label]").forEach((section) => {
const hasIcon = Boolean(DETAIL_META[category]?.[section.dataset.label]?.[1]);
enhanceSection(section, category, iconIndex);
if (hasIcon) iconIndex += 1;
});
article.dataset.dcxDetailEnhanced = category;
};
const syncDetails = () => {
syncFrame = 0;
const expander = document.querySelector(".dcx-expander");
const continuousArticles = expander?.querySelectorAll('.dcx-main > .dcx-article[data-dcx-category]');
if (continuousArticles?.length) {
continuousArticles.forEach((article) => {
const category = article.dataset.dcxCategory;
if (category !== "audience" && DETAIL_META[category]) enhanceArticle(article, category);
});
return;
}
const activeItem = expander?.querySelector(".dcx-nav-list > li.is-active[data-category]");
const article = expander?.querySelector(".dcx-main > .dcx-article");
const category = activeItem?.dataset.category;
if (!article || !category || category === "audience" || !DETAIL_META[category]) return;
enhanceArticle(article, category);
};
const scheduleSync = () => {
if (syncFrame) return;
syncFrame = requestAnimationFrame(syncDetails);
};
const observer = new MutationObserver(scheduleSync);
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener("pageshow", scheduleSync);
scheduleSync();
})();
File diff suppressed because it is too large Load Diff
+963
View File
@@ -0,0 +1,963 @@
import RULES from '../../data/hook-rules.json';
(() => {
"use strict";
const STORAGE_KEY = "dcx-hooks-preview-v1";
const MOBILE_FAMILIES = window.matchMedia("(max-width: 560px)");
const REDUCED_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)");
const FAMILY_META = {
fingerprints: {
label: "Fingerprints",
description: "Recurring signatures found across generated interfaces.",
},
slop: {
label: "UI tells",
description: "Common generated-UI habits that make a design feel interchangeable.",
},
quality: {
label: "Quality floor",
description: "Measurable defects in legibility, hierarchy, overflow, and system consistency.",
},
};
const DISCIPLINE_ORDER = [
"Visual Details",
"Typography",
"Color & Contrast",
"Layout & Space",
"Motion",
"Imagery",
"Copy",
"Quality",
];
const EXCEPTION_KINDS = {
value: { label: "Silence one value", hint: "One rule stops flagging one exact value, everywhere." },
"rule-in-files": { label: "Silence a rule in files", hint: "One rule goes quiet in matching files and stays live everywhere else." },
file: { label: "Skip a file entirely", hint: "Every rule skips matching files. The widest exception; prefer the two above." },
};
/* Only the view preference persists in the browser. The state that matters
lives in the project's .impeccable/config.json and arrives from the doc
session, so the page shows what the hook will actually do, not a preview. */
const loadView = () => {
try {
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null");
return { activeFamily: parsed && FAMILY_META[parsed.activeFamily] ? parsed.activeFamily : "fingerprints" };
} catch {
return { activeFamily: "fingerprints" };
}
};
const view = loadView();
const persistView = () => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ activeFamily: view.activeFamily }));
} catch {
// The file:// preview may deny storage; the in-memory controls still work.
}
};
/* live is the last state the project confirmed; draft is what the controls
show. Apply sends the whole draft and the server's echo becomes the new
live, so the page and .impeccable/config.json can only disagree while the
Apply bar is visible and says so. */
let live = null;
let draft = null;
let liveError = "";
let applying = false;
let appliedFlash = false;
let appliedFlashTimer = 0;
let fetchStarted = false;
const docSession = () => window.dcxDocSession || null;
const hooksUrl = () => {
const session = docSession();
if (!session?.base || !session?.token) return "";
return `${session.base}/doc/hooks?token=${encodeURIComponent(session.token)}`;
};
const cloneState = (state) => JSON.parse(JSON.stringify(state));
const entryKey = (entry) => {
const files = Array.isArray(entry.files) && entry.files.length > 0 ? [...entry.files].sort().join("\u001f") : "";
return `${entry.rule}\u0000${entry.value}\u0000${files}`;
};
const changeCount = () => {
if (!live || !draft) return 0;
let count = draft.enabled !== live.enabled ? 1 : 0;
const liveRules = new Set(live.ignoreRules);
const draftRules = new Set(draft.ignoreRules);
for (const id of draftRules) if (!liveRules.has(id)) count += 1;
for (const id of liveRules) if (!draftRules.has(id)) count += 1;
const liveFiles = new Set(live.ignoreFiles);
const draftFiles = new Set(draft.ignoreFiles);
for (const glob of draftFiles) if (!liveFiles.has(glob)) count += 1;
for (const glob of liveFiles) if (!draftFiles.has(glob)) count += 1;
const liveValues = new Map(live.ignoreValues.map((entry) => [entryKey(entry), entry]));
const draftValues = new Map(draft.ignoreValues.map((entry) => [entryKey(entry), entry]));
for (const key of draftValues.keys()) if (!liveValues.has(key)) count += 1;
for (const key of liveValues.keys()) if (!draftValues.has(key)) count += 1;
return count;
};
/* Three-way rebase for an apply that lost the race: keep every edit the
visitor made (draft against the state the page read) and land it on what
the project now holds, so nothing another writer added is dropped. */
const rebaseDraft = (oldLive, oldDraft, newLive) => {
const next = cloneState(newLive);
if (oldDraft.enabled !== oldLive.enabled) next.enabled = oldDraft.enabled;
for (const key of ["ignoreRules", "ignoreFiles"]) {
const removed = new Set(oldLive[key].filter((item) => !oldDraft[key].includes(item)));
const added = oldDraft[key].filter((item) => !oldLive[key].includes(item));
next[key] = next[key].filter((item) => !removed.has(item));
for (const item of added) if (!next[key].includes(item)) next[key].push(item);
}
const oldKeys = new Set(oldLive.ignoreValues.map(entryKey));
const draftKeys = new Set(oldDraft.ignoreValues.map(entryKey));
const removedKeys = new Set([...oldKeys].filter((key) => !draftKeys.has(key)));
next.ignoreValues = next.ignoreValues.filter((entry) => !removedKeys.has(entryKey(entry)));
const presentKeys = new Set(next.ignoreValues.map(entryKey));
for (const entry of oldDraft.ignoreValues) {
const key = entryKey(entry);
if (!oldKeys.has(key) && !presentKeys.has(key)) {
next.ignoreValues.push(cloneState(entry));
presentKeys.add(key);
}
}
return next;
};
const escapeHtml = (value) => String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
const slugify = (value) => String(value)
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "") || "custom-rule";
const compactDescription = (value) => {
const text = String(value).trim();
const firstSentence = text.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim();
return firstSentence || text;
};
const ruleName = (id) => RULES.find((rule) => rule.id === id)?.name || id;
const templateMarkup = () => `
<article class="dcx-article">
<header>
<h2 class="dcx-title">Hooks</h2>
<p class="dcx-lede">Checks that catch design regressions while you work.</p>
</header>
<section class="dcx-block" data-label="How it runs">
<span class="dcx-block-label">How it runs</span>
<div class="dcx-hooks-intro">
<p class="dcx-hooks-definition">Hooks watch interface changes and surface problems before they spread.</p>
<div class="dcx-hooks-status" data-hooks-status>
<div class="dcx-hooks-status-copy">
<strong data-hooks-master-copy>Enable hooks</strong>
<p data-hooks-master-detail>Reading this project&rsquo;s settings&hellip;</p>
</div>
<div class="dcx-hooks-status-control">
<span class="dcx-hooks-status-state" data-hooks-master-state>On</span>
<label class="dcx-hooks-switch dcx-hooks-switch--master">
<input type="checkbox" role="switch" data-hooks-master aria-label="Enable design hooks">
<span aria-hidden="true"></span>
</label>
</div>
</div>
<dl class="dcx-hooks-flow">
<div>
<dt>On each edit</dt>
<dd>Checks the changed UI.</dd>
</div>
<div>
<dt>At session end</dt>
<dd>Runs a full pass on touched UI files.</dd>
</div>
</dl>
</div>
</section>
<section class="dcx-block" data-label="Built-in rules">
<span class="dcx-block-label">Built-in rules</span>
<div class="dcx-hooks-browser" data-hooks-browser>
<div class="dcx-hooks-families" role="tablist" aria-label="Rule families" data-hooks-families></div>
<div class="dcx-hooks-rule-panel" id="dcx-hooks-rule-panel" role="tabpanel">
<div class="dcx-hooks-toolbar">
<label class="dcx-hooks-search">
<input type="search" autocomplete="off" aria-label="Search rules" placeholder="Search rules" data-hooks-search>
</label>
<p class="dcx-hooks-summary" data-hooks-summary aria-live="polite"></p>
</div>
<div class="dcx-hooks-rule-groups" data-hooks-rule-groups></div>
</div>
</div>
</section>
<section class="dcx-block" data-label="Exceptions">
<span class="dcx-block-label">Exceptions</span>
<div class="dcx-hooks-custom" data-hooks-custom>
<div class="dcx-hooks-custom-toolbar">
<p data-hooks-custom-count>No exceptions.</p>
<button class="dcx-hooks-button" type="button" data-hooks-add aria-expanded="false" aria-controls="dcx-hooks-custom-form">Add exception</button>
</div>
<form class="dcx-hooks-custom-form" id="dcx-hooks-custom-form" data-hooks-form hidden>
<label>
<span>Kind</span>
<select name="kind" data-hooks-kind>
${Object.entries(EXCEPTION_KINDS).map(([id, meta]) => `<option value="${id}">${escapeHtml(meta.label)}</option>`).join("")}
</select>
</label>
<p class="dcx-hooks-kind-hint" data-hooks-kind-hint>${escapeHtml(EXCEPTION_KINDS.value.hint)}</p>
<label data-hooks-field="rule">
<span>Rule</span>
<select name="rule">
${[...RULES].sort((a, b) => a.name.localeCompare(b.name)).map((rule) => `<option value="${escapeHtml(rule.id)}">${escapeHtml(rule.name)}</option>`).join("")}
</select>
</label>
<label data-hooks-field="value">
<span>Value</span>
<input name="value" maxlength="200" placeholder="e.g. Inter, or #7BA98F">
</label>
<label data-hooks-field="files" hidden>
<span>Files</span>
<input name="files" maxlength="400" placeholder="Globs, comma separated: src/legacy/**, docs/demo.html">
</label>
<label data-hooks-field="reason">
<span>Reason</span>
<input name="reason" maxlength="200" placeholder="Optional: who decided, and the evidence">
</label>
<div class="dcx-hooks-form-actions">
<button class="dcx-hooks-button dcx-hooks-button--quiet" type="button" data-hooks-cancel>Cancel</button>
<button class="dcx-hooks-button" type="submit">Add</button>
</div>
</form>
<div class="dcx-hooks-custom-list" data-hooks-custom-list></div>
<p class="dcx-hooks-storage-note" data-hooks-live-note>Exceptions apply to the design hook and to npx impeccable detect in this project.</p>
</div>
</section>
<div class="dcx-hooks-applybar" data-hooks-applybar hidden>
<p class="dcx-hooks-applybar-copy" data-hooks-applybar-copy aria-live="polite"></p>
<div class="dcx-hooks-applybar-actions">
<button class="dcx-hooks-button dcx-hooks-button--quiet" type="button" data-hooks-discard>Discard</button>
<button class="dcx-hooks-button" type="button" data-hooks-apply>Apply</button>
</div>
</div>
</article>
`;
const renameInterfaceToHooks = () => {
const tile = document.querySelector('.dcx-tile[data-category="interface"]');
if (tile) {
tile.dataset.category = "hooks";
tile.dataset.name = "Hooks";
tile.setAttribute("aria-label", "Open Hooks");
const title = tile.querySelector(".dcx-tile-title");
if (title) title.textContent = "Hooks";
}
const shellTemplate = document.querySelector("#dcx-shell-template");
const navItem = shellTemplate?.content.querySelector('li[data-category="interface"]');
const navLink = navItem?.querySelector(".dcx-nav-link");
if (navItem && navLink) {
navItem.dataset.category = "hooks";
navLink.href = "#hooks";
navLink.dataset.dcxNav = "hooks";
navLink.textContent = "Hooks";
}
};
const installTemplate = () => {
if (document.querySelector("#dcx-detail-hooks")) return;
const template = document.createElement("template");
template.id = "dcx-detail-hooks";
template.innerHTML = templateMarkup();
document.querySelector("#dcx-detail-interface")?.after(template);
};
const familyRules = (family) => RULES.filter((rule) => rule.group === family);
const isEnabled = (id) => !(draft ? draft.ignoreRules.includes(id) : false);
const interactive = () => Boolean(live && draft && !applying);
const revealSelectedFamily = (target) => {
if (!MOBILE_FAMILIES.matches) return;
const selected = target.querySelector('[data-hooks-family][aria-selected="true"]');
if (!selected) return;
const viewport = target.getBoundingClientRect();
const item = selected.getBoundingClientRect();
let delta = 0;
if (item.left < viewport.left + 3) delta = item.left - viewport.left - 3;
else if (item.right > viewport.right - 3) delta = item.right - viewport.right + 3;
if (Math.abs(delta) < 1) return;
target.scrollTo({
left: Math.max(0, target.scrollLeft + delta),
behavior: REDUCED_MOTION.matches ? "auto" : "smooth",
});
};
const disciplineAnimations = new WeakMap();
const customFormAnimations = new WeakMap();
let syncFrame = 0;
const setDisciplineOpen = (details, expanded) => {
const panel = details.querySelector(":scope > .dcx-hooks-disclosure");
const inner = panel?.querySelector(":scope > .dcx-hooks-disclosure-inner");
if (!panel || !inner) {
details.open = expanded;
return;
}
const previous = disciplineAnimations.get(details);
const currentHeight = panel.getBoundingClientRect().height;
const currentOpacity = Number.parseFloat(getComputedStyle(panel).opacity) || 0;
previous?.cancel();
if (REDUCED_MOTION.matches) {
disciplineAnimations.delete(details);
details.classList.remove("is-closing");
details.open = expanded;
panel.style.removeProperty("height");
panel.style.removeProperty("opacity");
return;
}
details.open = true;
details.classList.toggle("is-closing", !expanded);
const fromHeight = previous ? currentHeight : expanded ? 0 : currentHeight;
const fromOpacity = previous ? currentOpacity : expanded ? 0 : 1;
const toHeight = expanded ? inner.scrollHeight : 0;
const toOpacity = expanded ? 1 : 0;
const animation = panel.animate([
{ height: `${fromHeight}px`, opacity: fromOpacity },
{ height: `${toHeight}px`, opacity: toOpacity },
], {
duration: 360,
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
fill: "both",
});
disciplineAnimations.set(details, animation);
animation.finished.then(() => {
if (disciplineAnimations.get(details) !== animation) return;
disciplineAnimations.delete(details);
details.classList.remove("is-closing");
details.open = expanded;
animation.cancel();
panel.style.removeProperty("height");
panel.style.removeProperty("opacity");
}).catch(() => {});
};
const setCustomFormOpen = (form, expanded) => {
const previous = customFormAnimations.get(form);
const currentHeight = form.hidden ? 0 : form.getBoundingClientRect().height;
const currentOpacity = form.hidden ? 0 : Number.parseFloat(getComputedStyle(form).opacity) || 1;
previous?.cancel();
if (REDUCED_MOTION.matches) {
customFormAnimations.delete(form);
form.hidden = !expanded;
return;
}
if (expanded) form.hidden = false;
const toHeight = expanded ? form.scrollHeight : 0;
form.style.overflow = "clip";
const animation = form.animate([
{ height: `${currentHeight}px`, opacity: currentOpacity },
{ height: `${toHeight}px`, opacity: expanded ? 1 : 0 },
], {
duration: 320,
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
fill: "both",
});
customFormAnimations.set(form, animation);
animation.finished.then(() => {
if (customFormAnimations.get(form) !== animation) return;
customFormAnimations.delete(form);
form.hidden = !expanded;
animation.cancel();
form.style.removeProperty("height");
form.style.removeProperty("opacity");
form.style.removeProperty("overflow");
}).catch(() => {});
};
const toggleDiscipline = (article, summary) => {
const disclosure = summary.parentElement;
const expanded = !disclosure.open || disclosure.classList.contains("is-closing");
const query = article.querySelector("[data-hooks-search]")?.value.trim();
if (expanded && !query) {
disclosure.parentElement?.querySelectorAll(":scope > .dcx-hooks-discipline[open]").forEach((sibling) => {
if (sibling !== disclosure) setDisciplineOpen(sibling, false);
});
}
setDisciplineOpen(disclosure, expanded);
};
const renderFamilies = (article) => {
const target = article.querySelector("[data-hooks-families]");
if (!target) return;
target.innerHTML = Object.entries(FAMILY_META).map(([id, meta]) => {
const rules = familyRules(id);
const enabled = rules.filter((rule) => isEnabled(rule.id)).length;
const selected = view.activeFamily === id;
return `
<button
id="dcx-hooks-family-${id}"
class="dcx-hooks-family${selected ? " is-active" : ""}"
type="button"
role="tab"
tabindex="${selected ? "0" : "-1"}"
aria-selected="${selected}"
aria-label="${escapeHtml(meta.label)}, ${enabled} of ${rules.length} selected"
aria-controls="dcx-hooks-rule-panel"
data-hooks-family="${id}"
>
<span class="dcx-hooks-family-name">${escapeHtml(meta.label)}</span>
<span class="dcx-hooks-family-count">${enabled}/${rules.length}</span>
</button>
`;
}).join("");
const panel = article.querySelector("#dcx-hooks-rule-panel");
panel?.setAttribute("aria-labelledby", `dcx-hooks-family-${view.activeFamily}`);
requestAnimationFrame(() => revealSelectedFamily(target));
};
const renderRules = (article) => {
const target = article.querySelector("[data-hooks-rule-groups]");
const summary = article.querySelector("[data-hooks-summary]");
const search = article.querySelector("[data-hooks-search]");
if (!target || !summary) return;
const query = (search?.value || "").trim().toLowerCase();
const rules = familyRules(view.activeFamily);
const filtered = rules.filter((rule) => !query
|| `${rule.id} ${rule.name} ${rule.description} ${rule.discipline}`.toLowerCase().includes(query));
const enabled = rules.filter((rule) => isEnabled(rule.id)).length;
summary.textContent = query
? `${filtered.length} matching ${filtered.length === 1 ? "rule" : "rules"}`
: `${enabled} of ${rules.length} selected`;
const groups = new Map();
filtered.forEach((rule) => {
if (!groups.has(rule.discipline)) groups.set(rule.discipline, []);
groups.get(rule.discipline).push(rule);
});
const orderedGroups = [...groups.entries()].sort(([a], [b]) => {
const ai = DISCIPLINE_ORDER.indexOf(a);
const bi = DISCIPLINE_ORDER.indexOf(b);
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi) || a.localeCompare(b);
});
if (!orderedGroups.length) {
target.innerHTML = '<p class="dcx-hooks-empty">No rules match this search.</p>';
return;
}
const disabledUi = interactive() ? "" : "disabled";
target.innerHTML = orderedGroups.map(([discipline, entries], index) => {
const disclosureId = `dcx-hooks-${view.activeFamily}-${slugify(discipline)}`;
const summaryId = `${disclosureId}-summary`;
return `
<details class="dcx-hooks-discipline" ${query || index === 0 ? "open" : ""}>
<summary id="${summaryId}">
<span>${escapeHtml(discipline)}</span>
<span>${entries.length}</span>
</summary>
<div class="dcx-hooks-disclosure" id="${disclosureId}" role="region" aria-labelledby="${summaryId}">
<div class="dcx-hooks-disclosure-inner">
<ul class="dcx-hooks-rules">
${entries.map((rule) => `
<li class="dcx-hooks-rule" data-rule-id="${escapeHtml(rule.id)}">
<div class="dcx-hooks-rule-copy">
<strong>${escapeHtml(rule.name)}</strong>
<p>${escapeHtml(compactDescription(rule.description))}</p>
</div>
<label class="dcx-hooks-switch">
<input
type="checkbox"
role="switch"
data-hooks-rule="${escapeHtml(rule.id)}"
aria-label="Enable ${escapeHtml(rule.name)}"
${isEnabled(rule.id) ? "checked" : ""}
${disabledUi}
>
<span aria-hidden="true"></span>
</label>
</li>
`).join("")}
</ul>
</div>
</div>
</details>
`;
}).join("");
};
/* One row per exception the project holds, whatever wrote it: entries added
here, by an agent's triage, or by npx impeccable ignores all render the
same, and Remove queues a real removal for Apply. */
const renderExceptions = (article) => {
const target = article.querySelector("[data-hooks-custom-list]");
const count = article.querySelector("[data-hooks-custom-count]");
const note = article.querySelector("[data-hooks-live-note]");
const add = article.querySelector("[data-hooks-add]");
if (!target) return;
const values = draft ? draft.ignoreValues : [];
const files = draft ? draft.ignoreFiles : [];
const total = values.length + files.length;
if (count) {
count.textContent = total
? `${total} ${total === 1 ? "exception" : "exceptions"}`
: "No exceptions.";
}
if (add) add.disabled = !interactive();
if (note) {
note.textContent = interactive() || applying
? "Exceptions apply to the design hook and to npx impeccable detect in this project."
: "The editing session has ended. /impeccable design-context reopens it.";
}
target.innerHTML = "";
values.forEach((entry, index) => {
const row = document.createElement("article");
row.className = "dcx-hooks-custom-rule";
const copy = document.createElement("div");
copy.className = "dcx-hooks-rule-copy";
const kind = document.createElement("span");
kind.className = "dcx-hooks-custom-discipline";
kind.textContent = entry.value === "*" ? "Rule, in files" : "Value";
const name = document.createElement("strong");
name.textContent = entry.value === "*"
? ruleName(entry.rule)
: `${ruleName(entry.rule)}: ${entry.value}`;
const detail = document.createElement("p");
detail.textContent = entry.files?.length
? `In ${entry.files.join(", ")}`
: "Everywhere in this project.";
copy.append(kind, name, detail);
if (entry.reason) {
const reason = document.createElement("p");
reason.className = "dcx-hooks-custom-reason";
reason.textContent = entry.reason;
copy.append(reason);
}
const controls = document.createElement("div");
controls.className = "dcx-hooks-custom-controls";
const remove = document.createElement("button");
remove.className = "dcx-hooks-remove";
remove.type = "button";
remove.dataset.hooksRemoveValue = String(index);
remove.setAttribute("aria-label", `Remove exception for ${ruleName(entry.rule)}`);
remove.textContent = "Remove";
remove.disabled = !interactive();
controls.append(remove);
row.append(copy, controls);
target.appendChild(row);
});
files.forEach((glob, index) => {
const row = document.createElement("article");
row.className = "dcx-hooks-custom-rule";
const copy = document.createElement("div");
copy.className = "dcx-hooks-rule-copy";
const kind = document.createElement("span");
kind.className = "dcx-hooks-custom-discipline";
kind.textContent = "Skipped files";
const name = document.createElement("strong");
name.textContent = glob;
const detail = document.createElement("p");
detail.textContent = "Every rule skips matching files.";
copy.append(kind, name, detail);
const controls = document.createElement("div");
controls.className = "dcx-hooks-custom-controls";
const remove = document.createElement("button");
remove.className = "dcx-hooks-remove";
remove.type = "button";
remove.dataset.hooksRemoveFile = String(index);
remove.setAttribute("aria-label", `Stop skipping ${glob}`);
remove.textContent = "Remove";
remove.disabled = !interactive();
controls.append(remove);
row.append(copy, controls);
target.appendChild(row);
});
};
const syncMaster = (article) => {
const input = article.querySelector("[data-hooks-master]");
const status = article.querySelector("[data-hooks-status]");
const copy = article.querySelector("[data-hooks-master-copy]");
const detail = article.querySelector("[data-hooks-master-detail]");
const stateText = article.querySelector("[data-hooks-master-state]");
if (!input || !status || !copy || !detail || !stateText) return;
const enabled = draft ? draft.enabled : true;
input.checked = enabled;
input.disabled = !interactive();
status.classList.toggle("is-paused", !enabled);
copy.textContent = "Enable hooks";
if (draft) {
detail.textContent = "Runs in this project; changes land in .impeccable/config.json when you press Apply.";
} else if (liveError) {
detail.textContent = liveError;
} else if (!docSession()) {
detail.textContent = "The editing session has ended. /impeccable design-context reopens it.";
} else {
detail.textContent = "Reading this projects settings…";
}
stateText.textContent = enabled ? "On" : "Off";
};
const syncApplyBar = (article) => {
const bar = article.querySelector("[data-hooks-applybar]");
const copy = article.querySelector("[data-hooks-applybar-copy]");
const apply = article.querySelector("[data-hooks-apply]");
const discard = article.querySelector("[data-hooks-discard]");
if (!bar || !copy || !apply || !discard) return;
const changes = changeCount();
const show = Boolean(live && draft) && (changes > 0 || applying || appliedFlash);
bar.hidden = !show;
if (!show) return;
apply.disabled = applying || changes === 0;
discard.disabled = applying || changes === 0;
if (applying) {
copy.textContent = "Applying…";
} else if (changes > 0) {
copy.textContent = `${changes} ${changes === 1 ? "change" : "changes"} not applied yet.`;
} else {
copy.textContent = "Applied to .impeccable/config.json.";
}
};
const renderArticle = (article) => {
syncMaster(article);
renderFamilies(article);
renderRules(article);
renderExceptions(article);
syncApplyBar(article);
};
const hooksArticles = () => [...document.querySelectorAll('.dcx-article[data-dcx-category="hooks"]')];
const renderAll = () => hooksArticles().forEach(renderArticle);
const fetchLiveState = async () => {
const url = hooksUrl();
if (!url || fetchStarted) return;
fetchStarted = true;
try {
const response = await fetch(url);
const body = await response.json().catch(() => null);
if (!response.ok || !body?.ok || !body.state) {
throw new Error(body?.error || `The doc session answered ${response.status}.`);
}
live = body.state;
draft = cloneState(live);
liveError = "";
} catch {
liveError = "Could not read this projects hook settings; the controls stay read-only.";
/* A later remount retries: the session may only now be announced. */
fetchStarted = false;
}
renderAll();
};
const applyDraft = async (article) => {
const url = hooksUrl();
const session = docSession();
if (!url || !session || !draft || applying) return;
applying = true;
appliedFlash = false;
if (appliedFlashTimer) {
window.clearTimeout(appliedFlashTimer);
appliedFlashTimer = 0;
}
renderAll();
try {
const response = await fetch(`${session.base}/doc/hooks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: session.token, state: { ...draft, baseline: live } }),
});
const body = await response.json().catch(() => null);
if (!response.ok || !body?.ok || !body.state) {
throw new Error(body?.error || `The doc session answered ${response.status}.`);
}
live = body.state;
draft = cloneState(live);
appliedFlash = true;
appliedFlashTimer = window.setTimeout(() => {
appliedFlashTimer = 0;
appliedFlash = false;
renderAll();
}, 2600);
} catch (error) {
const message = String(error.message || error);
applying = false;
if (message.includes("hook config changed on disk") && live && draft) {
const oldLive = live;
const oldDraft = draft;
try {
const refreshed = await fetch(url);
const refreshedBody = await refreshed.json().catch(() => null);
if (refreshed.ok && refreshedBody?.ok && refreshedBody.state) {
live = refreshedBody.state;
draft = rebaseDraft(oldLive, oldDraft, live);
renderAll();
/* Written after renderAll so the bar's own copy cannot eat it. */
const copy = article.querySelector("[data-hooks-applybar-copy]");
if (copy) copy.textContent = "Another run changed this project's hook settings while you edited. Your changes were re-applied on top; review and press Apply again.";
return;
}
} catch {
/* The session dropped mid-conflict; fall through to the plain error. */
}
}
syncApplyBar(article);
/* After the bar re-renders its change count, the reason overwrites it;
written after the sync so the sync cannot eat it. */
const copy = article.querySelector("[data-hooks-applybar-copy]");
if (copy) copy.textContent = `Not applied: ${message}`;
return;
}
applying = false;
renderAll();
};
const initializeMountedArticles = () => {
syncFrame = 0;
hooksArticles().forEach((article) => {
if (article.dataset.dcxHooksReady === "true") return;
article.dataset.dcxHooksReady = "true";
renderArticle(article);
});
if (live === null) fetchLiveState();
};
const scheduleSync = () => {
if (syncFrame) return;
syncFrame = requestAnimationFrame(initializeMountedArticles);
};
const syncKindFields = (form) => {
const kind = form.querySelector("[data-hooks-kind]")?.value || "value";
const hint = form.querySelector("[data-hooks-kind-hint]");
if (hint) hint.textContent = EXCEPTION_KINDS[kind]?.hint || "";
form.querySelector('[data-hooks-field="rule"]')?.toggleAttribute("hidden", kind === "file");
form.querySelector('[data-hooks-field="value"]')?.toggleAttribute("hidden", kind !== "value");
form.querySelector('[data-hooks-field="files"]')?.toggleAttribute("hidden", kind === "value");
form.querySelector('[data-hooks-field="reason"]')?.toggleAttribute("hidden", kind === "file");
};
const parseGlobList = (value) => [...new Set(String(value)
.split(/[\n,]/)
.map((glob) => glob.trim())
.filter(Boolean))];
document.addEventListener("click", (event) => {
const article = event.target.closest('.dcx-article[data-dcx-category="hooks"]');
if (!article) return;
const summary = event.target.closest(".dcx-hooks-discipline > summary");
if (summary) {
event.preventDefault();
toggleDiscipline(article, summary);
return;
}
const family = event.target.closest("[data-hooks-family]");
if (family) {
const restoreFocus = family === document.activeElement;
view.activeFamily = family.dataset.hooksFamily;
persistView();
renderFamilies(article);
renderRules(article);
if (restoreFocus) {
article.querySelector(`[data-hooks-family="${view.activeFamily}"]`)?.focus({ preventScroll: true });
}
return;
}
const add = event.target.closest("[data-hooks-add]");
if (add && interactive()) {
const form = article.querySelector("[data-hooks-form]");
if (!form) return;
syncKindFields(form);
setCustomFormOpen(form, true);
add.setAttribute("aria-expanded", "true");
form.querySelector("[data-hooks-kind]")?.focus();
return;
}
const cancel = event.target.closest("[data-hooks-cancel]");
if (cancel) {
const form = article.querySelector("[data-hooks-form]");
form?.reset();
if (form) setCustomFormOpen(form, false);
const addButton = article.querySelector("[data-hooks-add]");
addButton?.setAttribute("aria-expanded", "false");
addButton?.focus({ preventScroll: true });
return;
}
const removeValue = event.target.closest("[data-hooks-remove-value]");
if (removeValue && interactive()) {
draft.ignoreValues.splice(Number(removeValue.dataset.hooksRemoveValue), 1);
renderAll();
(article.querySelector(".dcx-hooks-remove") || article.querySelector("[data-hooks-add]"))
?.focus({ preventScroll: true });
return;
}
const removeFile = event.target.closest("[data-hooks-remove-file]");
if (removeFile && interactive()) {
draft.ignoreFiles.splice(Number(removeFile.dataset.hooksRemoveFile), 1);
renderAll();
(article.querySelector(".dcx-hooks-remove") || article.querySelector("[data-hooks-add]"))
?.focus({ preventScroll: true });
return;
}
const discard = event.target.closest("[data-hooks-discard]");
if (discard && live) {
draft = cloneState(live);
renderAll();
return;
}
const apply = event.target.closest("[data-hooks-apply]");
if (apply) applyDraft(article);
});
document.addEventListener("input", (event) => {
if (!event.target.matches("[data-hooks-search]")) return;
const article = event.target.closest('.dcx-article[data-dcx-category="hooks"]');
if (article) renderRules(article);
});
document.addEventListener("keydown", (event) => {
const summary = event.target.closest(".dcx-hooks-discipline > summary");
if (summary && ["Enter", " "].includes(event.key)) {
event.preventDefault();
if (!event.repeat) {
const article = summary.closest('.dcx-article[data-dcx-category="hooks"]');
if (article) toggleDiscipline(article, summary);
}
return;
}
const family = event.target.closest("[data-hooks-family]");
if (!family || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
const article = family.closest('.dcx-article[data-dcx-category="hooks"]');
const buttons = [...article.querySelectorAll("[data-hooks-family]")];
const index = buttons.indexOf(family);
if (index < 0) return;
event.preventDefault();
const direction = ["ArrowRight", "ArrowDown"].includes(event.key) ? 1 : -1;
const nextIndex = event.key === "Home"
? 0
: event.key === "End"
? buttons.length - 1
: (index + direction + buttons.length) % buttons.length;
buttons[nextIndex].click();
article.querySelector(`[data-hooks-family="${view.activeFamily}"]`)?.focus();
});
document.addEventListener("change", (event) => {
const article = event.target.closest('.dcx-article[data-dcx-category="hooks"]');
if (!article) return;
if (event.target.matches("[data-hooks-kind]")) {
const form = event.target.closest("[data-hooks-form]");
if (form) syncKindFields(form);
return;
}
if (!interactive()) return;
if (event.target.matches("[data-hooks-master]")) {
draft.enabled = event.target.checked;
syncMaster(article);
syncApplyBar(article);
return;
}
if (event.target.matches("[data-hooks-rule]")) {
const id = event.target.dataset.hooksRule;
if (event.target.checked) draft.ignoreRules = draft.ignoreRules.filter((entry) => entry !== id);
else if (!draft.ignoreRules.includes(id)) draft.ignoreRules.push(id);
renderFamilies(article);
syncApplyBar(article);
const query = article.querySelector("[data-hooks-search]")?.value.trim();
const summary = article.querySelector("[data-hooks-summary]");
if (!query && summary) {
const rules = familyRules(view.activeFamily);
summary.textContent = `${rules.filter((rule) => isEnabled(rule.id)).length} of ${rules.length} selected`;
}
}
});
document.addEventListener("submit", (event) => {
const form = event.target.closest("[data-hooks-form]");
if (!form) return;
event.preventDefault();
const article = form.closest('.dcx-article[data-dcx-category="hooks"]');
if (!article || !interactive()) return;
const data = new FormData(form);
const kind = String(data.get("kind") || "value");
const rule = String(data.get("rule") || "").trim();
const value = String(data.get("value") || "").trim();
const files = parseGlobList(data.get("files") || "");
const reason = String(data.get("reason") || "").trim();
if (kind === "file") {
if (!files.length) return;
files.forEach((glob) => {
if (!draft.ignoreFiles.includes(glob)) draft.ignoreFiles.push(glob);
});
} else {
const entry = kind === "value"
? { rule, value }
: { rule, value: "*", files };
if (!entry.rule || (kind === "value" && !entry.value)) return;
if (kind === "rule-in-files" && !files.length) return;
if (reason) entry.reason = reason;
const keys = new Set(draft.ignoreValues.map(entryKey));
if (!keys.has(entryKey(entry))) draft.ignoreValues.push(entry);
}
form.reset();
syncKindFields(form);
setCustomFormOpen(form, false);
const addButton = article.querySelector("[data-hooks-add]");
addButton?.setAttribute("aria-expanded", "false");
renderAll();
addButton?.focus({ preventScroll: true });
});
renameInterfaceToHooks();
installTemplate();
const observer = new MutationObserver(scheduleSync);
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener("pageshow", scheduleSync);
scheduleSync();
})();
File diff suppressed because it is too large Load Diff
+223
View File
@@ -0,0 +1,223 @@
(() => {
"use strict";
if (window.__dcxSettingsInstalled) return;
window.__dcxSettingsInstalled = true;
const REDUCED_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)");
const COMMANDS = [
{
label: "Open",
description: "Pick up where you left off.",
command: "/impeccable design-context open",
},
{
label: "Edit",
description: "Make a few changes to this design context.",
command: "/impeccable design-context edit",
},
{
label: "Export",
description: "Save a copy to share or keep elsewhere.",
command: "/impeccable design-context export",
},
{
label: "Import",
description: "Bring a saved design context into this project.",
command: "/impeccable design-context import",
},
];
const settingsIcon = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>`;
const closeIcon = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"></path>
</svg>`;
const installTopbarActions = (root) => {
const topbar = root?.querySelector?.(".dcx-topbar");
const templateClose = topbar?.querySelector(".dcx-close");
if (!topbar || !templateClose || topbar.querySelector("[data-dcx-settings-open]")) return;
const actions = document.createElement("div");
actions.className = "dcx-topbar-actions";
const trigger = document.createElement("button");
trigger.className = "dcx-settings-trigger";
trigger.type = "button";
trigger.dataset.dcxSettingsOpen = "";
trigger.setAttribute("aria-label", "Design context settings");
trigger.setAttribute("aria-haspopup", "dialog");
trigger.setAttribute("aria-controls", "dcx-context-settings");
trigger.setAttribute("aria-expanded", "false");
trigger.innerHTML = settingsIcon;
templateClose.before(actions);
actions.append(trigger, templateClose);
};
const shellTemplate = document.querySelector("#dcx-shell-template");
installTopbarActions(shellTemplate?.content);
document.querySelectorAll(".dcx-expander").forEach(installTopbarActions);
let modal = document.querySelector("#dcx-context-settings");
if (!modal) {
modal = document.createElement("dialog");
modal.id = "dcx-context-settings";
modal.className = "picker-modal dcx-settings-modal";
modal.setAttribute("aria-labelledby", "dcx-context-settings-title");
modal.setAttribute("aria-describedby", "dcx-context-settings-lede");
modal.innerHTML = `
<div class="picker-modal-inner dcx-settings-panel" data-dcx-command-context>
<header class="picker-modal-head dcx-settings-head">
<div>
<h2 id="dcx-context-settings-title">Design context commands</h2>
<p id="dcx-context-settings-lede">Choose what youd like to do with this design context.</p>
</div>
<button class="dcx-settings-close" type="button" data-dcx-settings-close aria-label="Close settings">
${closeIcon}
</button>
</header>
<div class="dcx-settings-commands">
${COMMANDS.map(({ label, description, command }) => `
<section class="dcx-settings-command">
<div class="dcx-settings-command-copy">
<h3>${label}</h3>
<p>${description}</p>
</div>
<div class="dcx-command-copy">
<span class="dcx-command-copy__prompt" aria-hidden="true">$</span>
<code>${command}</code>
<button class="dcx-command-copy__button" type="button" data-dcx-copy-command="${command}" aria-label="Copy ${label.toLowerCase()} command">
<svg class="dcx-command-copy__copy-icon" viewBox="0 0 24 24" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="dcx-command-copy__check-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
<span class="dcx-command-copy__label">Copy</span>
</button>
</div>
</section>`).join("")}
</div>
<p class="dcx-command-status" role="status" aria-live="polite"></p>
</div>`;
document.body.appendChild(modal);
}
let invoker = null;
let closeTimer = 0;
const resetCopyState = () => {
modal.querySelectorAll("[data-dcx-copy-command]").forEach((button) => {
window.clearTimeout(button._dcxCopyTimer);
button.classList.remove("copied");
button.removeAttribute("data-copied");
});
const status = modal.querySelector(".dcx-command-status");
if (status) status.textContent = "";
};
const openSettings = (trigger) => {
window.clearTimeout(closeTimer);
resetCopyState();
invoker = trigger;
trigger.setAttribute("aria-expanded", "true");
if (!modal.open) modal.showModal();
requestAnimationFrame(() => {
modal.classList.add("is-visible");
modal.querySelector("[data-dcx-settings-close]")?.focus({ preventScroll: true });
});
};
const closeSettings = ({ restoreFocus = true, instant = false } = {}) => {
if (!modal.open) return;
window.clearTimeout(closeTimer);
modal.classList.remove("is-visible");
const finish = () => {
if (!modal.open) return;
modal.close();
resetCopyState();
invoker?.setAttribute?.("aria-expanded", "false");
if (restoreFocus && invoker instanceof HTMLElement && invoker.isConnected) {
invoker.focus({ preventScroll: true });
}
invoker = null;
};
if (instant || REDUCED_MOTION.matches) finish();
else closeTimer = window.setTimeout(finish, 200);
};
window.closeDcxSettings = closeSettings;
document.addEventListener("click", async (event) => {
const openButton = event.target.closest?.("[data-dcx-settings-open]");
if (openButton) {
event.preventDefault();
openSettings(openButton);
return;
}
if (event.target.closest?.("[data-dcx-settings-close]")) {
closeSettings();
return;
}
});
modal.addEventListener("click", (event) => {
if (event.target !== modal) return;
const rect = modal.getBoundingClientRect();
const outside = event.clientX < rect.left
|| event.clientX > rect.right
|| event.clientY < rect.top
|| event.clientY > rect.bottom;
if (outside) closeSettings();
});
modal.addEventListener("cancel", (event) => {
event.preventDefault();
closeSettings();
});
document.addEventListener("keydown", (event) => {
if (!modal.open) return;
if (event.key === "Escape") {
event.preventDefault();
event.stopImmediatePropagation();
closeSettings();
return;
}
if (event.key !== "Tab") return;
const focusable = [...modal.querySelectorAll("button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])")]
.filter((element) => element.getClientRects().length > 0 && !element.hidden);
if (!focusable.length) {
event.preventDefault();
modal.focus({ preventScroll: true });
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement;
if (event.shiftKey && (active === first || !modal.contains(active))) {
event.preventDefault();
last.focus({ preventScroll: true });
} else if (!event.shiftKey && (active === last || !modal.contains(active))) {
event.preventDefault();
first.focus({ preventScroll: true });
}
}, true);
document.addEventListener("dcx:document-mounted", (event) => {
installTopbarActions(event.target);
});
})();
File diff suppressed because it is too large Load Diff
+189
View File
@@ -0,0 +1,189 @@
/** Restoring a previous run into the questionnaire.
*
* The one rule this file is built around: setting `.checked` or `.value` from
* script fires no event, and every committer on this page runs off change
* events. A bare write therefore leaves the radio saying one thing and the
* hidden field the rest of the run reads saying another. So every answer is
* restored by clicking its control, or by calling the function the control
* would have called.
*
* The order matters too. Clicking a radio runs record(), which stamps
* data-chosen on every field it writes, so which answers a person actually
* visited is restored last, after every click has had its say.
*
* The questionnaire owns the state being restored, so palette-picker.js passes
* the handles this needs rather than this file reaching into it.
*/
const ROLES = ['primary', 'secondary', 'tertiary', 'neutral'];
/* Groups whose answer is one radio the whole run reads. Absent keys are left
alone: a run that was never asked about movement must come back without a
movement key, not with a default standing in for a decision. */
const RADIO_GROUPS = [
'color-strategy',
'motion-energy',
'layout-structure',
'boundary-style',
'corner-style',
'depth-style',
'type-scale',
'icon-pack',
];
const asText = (value) => (typeof value === 'string' ? value : '');
/* Clicking is the whole point: activation runs the change handlers that commit
the answer, which a bare `checked = true` would skip. */
function clickOption(name, value) {
if (!value) return false;
const input = document.querySelector(`input[name="${name}"][value="${CSS.escape(value)}"]`);
if (!input || input.disabled) return false;
input.click();
return true;
}
function surfaceFields() {
return [...document.querySelectorAll('input[type="hidden"][data-surface-field]')];
}
export function hydrateAnswers(prior, ctx) {
if (!prior) return;
restoreSurfaces(prior, ctx);
for (const group of RADIO_GROUPS) {
if (group in prior) clickOption(group, asText(prior[group]));
}
restoreSurfaceFields(prior);
restorePalette(prior, ctx);
restoreFonts(prior, ctx);
restoreIconMeta(prior);
restoreChosen(prior, ctx);
}
/* Which surfaces the run covers, first, because every per-surface field is
filled with its default the moment a tile is checked. */
function restoreSurfaces(prior, ctx) {
const raw = prior['surface-modes'];
const wanted = new Set(Array.isArray(raw) ? raw : (asText(raw) ? [raw] : []));
if (!wanted.size) return;
// The same guard the agent's own hint carries: a set naming no real tile
// would otherwise clear the run's only required answer.
if (!ctx.modeInputs.some((input) => wanted.has(input.value))) return;
for (const input of ctx.modeInputs) input.checked = wanted.has(input.value);
ctx.syncModes();
}
function restoreSurfaceFields(prior) {
for (const field of surfaceFields()) {
const key = field.dataset.surfaceField;
if (field.disabled || !(key in prior)) continue;
const value = asText(prior[key]);
if (value) field.value = value;
}
}
/* The deck has no programmatic selection path: the committing click reads the
card the scroller is parked on. So the card's own state is written first,
which is also what stops a later reorder or reset from reverting the fields
to the colors the cue was dealt with. */
function restorePalette(prior, ctx) {
const source = asText(prior['palette-source']);
const colors = {};
for (const role of ROLES) {
const hex = asText(prior[`palette-${role}`]);
if (hex) colors[role] = hex;
}
if (!source && !Object.keys(colors).length) return;
/* The fields come first, and unconditionally: the colors are the answer, the
source is what the document names them by, and neither depends on the deck
still being able to show the card they came from. A document reopened after
the generation workspace was cleaned has no deck at all, and it still has a
palette. */
const sourceField = document.querySelector('[name="palette-source"]');
if (sourceField && source) sourceField.value = source;
for (const role of ROLES) {
const field = document.querySelector(`[name="palette-${role}"]`);
if (field && colors[role]) field.value = colors[role];
}
// Nothing was dealt, so there is no card to park on and nothing to repaint.
if (!ctx.cards.length) return;
const index = ctx.cards.findIndex((item) => item.id === source);
const target = index === -1 ? 0 : index;
/* Writing the card's own state is also what stops a later reorder or reset
from reverting the fields to the colors the cue was dealt with. */
const state = ctx.states.get(ctx.cards[target]?.id);
if (state?.colors) Object.assign(state.colors, colors);
ctx.setCurrent(target);
ctx.render();
ctx.syncDeckScroll();
}
/* A pair still on the rail is chosen by clicking it, which runs syncFontPair.
A pair that is not, an upload or a set of faces this run was not dealt, is
rebuilt as the custom pair and registered before its card is added: the rail
resolves a click through the manifest, so a card the manifest does not know
cannot be chosen a second time. */
function restoreFonts(prior, ctx) {
const wanted = asText(prior['font-pair']);
const heading = asText(prior['font-heading']);
const body = asText(prior['font-body']);
const manifest = ctx.fontManifest();
if (wanted && wanted !== 'custom' && manifest.pairs.some(({ id }) => id === wanted)) {
if (clickOption('font-pair', wanted)) return;
}
if (!heading || !body) return;
const pair = {
id: 'custom',
name: 'Custom',
heading: { family: heading, weight: 600, source: asText(prior['font-heading-source']) },
body: { family: body, weight: 400, source: asText(prior['font-body-source']) },
why: 'From your last run',
};
manifest.pairs = [pair, ...manifest.pairs.filter(({ id }) => id !== 'custom')];
for (const node of ctx.pairNodes()) {
if (node.querySelector('input')?.value === 'custom') ctx.removePairCard(node);
}
ctx.addPairCard(pair, { checked: true, first: true });
ctx.loadCustomFace(pair);
ctx.syncFontPair(pair);
ctx.applyHoist({ force: true });
}
/* The pack's own radio carries the license and URL when it is still on offer;
these are the record of one that is not. */
function restoreIconMeta(prior) {
for (const key of ['icon-pack-name', 'icon-pack-license', 'icon-pack-url']) {
const field = document.querySelector(`[name="${key}"]`);
const value = asText(prior[key]);
if (field && !field.value && value) field.value = value;
}
}
/* Last, because every click above stamped its own. A default nobody opened and
a default someone confirmed hold the same value, so only this list tells them
apart, and the document says which is which. */
function restoreChosen(prior, ctx) {
let chosen = null;
try {
const parsed = JSON.parse(asText(prior._chosen) || 'null');
if (Array.isArray(parsed)) chosen = new Set(parsed);
} catch {
/* Written by a run that did not keep the distinction. */
}
for (const field of surfaceFields()) {
const key = field.dataset.surfaceField;
if (field.disabled) continue;
// A run that kept no list was confirmed wholesale at submit, so every
// answer it carries counts as visited.
const visited = chosen ? chosen.has(key) : key in prior;
if (visited) field.dataset.chosen = 'yes';
else delete field.dataset.chosen;
}
ctx.syncChosenField();
}
File diff suppressed because it is too large Load Diff
+205
View File
@@ -0,0 +1,205 @@
/* Audience detail treatment shared with demos/audience-scroll/docs.html. */
.dcx-audience {
--dcx-audience-icon-size: 100px;
}
.dcx-audience > .dcx-audience-hero {
margin-bottom: clamp(48px, 6vw, 76px);
}
.dcx-audience .dcx-audience-section {
margin-top: 0;
margin-bottom: clamp(48px, 6vw, 76px);
scroll-margin-top: 18px;
}
.dcx-audience .dcx-audience-section:last-child {
margin-bottom: 0;
}
.dcx-audience .dcx-audience-section-head {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--dcx-audience-icon-size);
gap: clamp(24px, 4vw, 44px);
align-items: center;
margin-bottom: 18px;
}
.dcx-audience .dcx-audience-section-heading {
min-width: 0;
}
.dcx-audience .dcx-audience-section-title {
margin: 0;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 1.5rem;
font-weight: 650;
letter-spacing: 0;
line-height: 1.25;
text-wrap: balance;
}
.dcx-audience .dcx-audience-section-lede {
max-width: 52ch;
margin: 10px 0 0;
color: var(--ks-text-muted);
font-size: 0.95rem;
line-height: 1.6;
}
.dcx-audience .dcx-audience-section-icon {
width: var(--dcx-audience-icon-size);
height: var(--dcx-audience-icon-size);
margin: 0;
padding: 0;
border: 0;
background: transparent;
}
.dcx-audience .dcx-audience-section-icon img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.dcx-audience .dcx-audience-section--people .dcx-defs {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
border-top: 0;
}
.dcx-audience .dcx-audience-section--people .dcx-def {
display: block;
min-width: 0;
padding: 18px 20px 20px;
border: 1px solid transparent;
border-radius: 8px;
background: var(--panel-bg);
}
.dcx-audience .dcx-audience-section--people .dcx-def dt {
color: var(--ks-champagne);
font-size: 1.05rem;
font-weight: 600;
line-height: 1.35;
}
.dcx-audience .dcx-audience-section--people .dcx-def dd {
margin-top: 9px;
color: var(--ks-text-muted);
font-size: 0.92rem;
line-height: 1.58;
text-wrap: pretty;
}
.dcx-audience .dcx-audience-journey {
display: grid;
grid-template-columns: minmax(0, 1fr) 30px minmax(0, 1fr);
gap: 12px;
align-items: stretch;
}
.dcx-audience .dcx-audience-journey .dcx-callout {
min-width: 0;
padding: 18px 20px 20px;
border: 1px solid transparent;
border-radius: 8px;
background: var(--panel-bg);
}
.dcx-audience .dcx-audience-journey .dcx-callout + .dcx-callout {
margin-top: 0;
}
.dcx-audience .dcx-audience-journey-arrow {
display: grid;
place-items: center;
color: var(--accent);
font-family: var(--ks-mono);
font-size: 1rem;
}
.dcx-audience .dcx-audience-list {
counter-reset: dcx-audience-item;
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.dcx-audience .dcx-audience-list li {
counter-increment: dcx-audience-item;
position: static;
display: grid;
grid-template-columns: 32px minmax(0, 1fr);
gap: 12px;
align-items: baseline;
min-width: 0;
padding: 18px 20px;
border: 1px solid transparent;
border-radius: 8px;
background: var(--panel-bg);
color: var(--ks-text);
font-size: 0.96rem;
line-height: 1.6;
text-wrap: pretty;
}
.dcx-audience .dcx-audience-list li::before {
content: counter(dcx-audience-item, decimal-leading-zero);
position: static;
width: auto;
height: auto;
background: transparent;
color: var(--accent);
font-family: var(--ks-mono);
font-size: 0.72rem;
letter-spacing: 0.1em;
}
@media (max-width: 620px) {
.dcx-audience .dcx-audience-section-head {
gap: 18px;
}
.dcx-audience .dcx-audience-section--people .dcx-defs,
.dcx-audience .dcx-audience-journey {
grid-template-columns: 1fr;
}
.dcx-audience .dcx-audience-journey-arrow {
height: 22px;
justify-content: start;
padding-left: 20px;
transform: rotate(90deg);
}
}
@media (max-width: 430px) {
.dcx-audience .dcx-audience-section-head {
gap: 14px;
}
.dcx-audience .dcx-audience-section-title {
font-size: 1.3rem;
}
.dcx-audience .dcx-audience-list li {
grid-template-columns: 26px minmax(0, 1fr);
padding: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.dcx-audience *,
.dcx-audience *::before,
.dcx-audience *::after {
transition-duration: 0.001ms !important;
transition-delay: 0ms !important;
}
}
+165
View File
@@ -0,0 +1,165 @@
/* Compact command-copy control, matched to the homepage Get started treatment. */
.dcx-command-copy {
--dcx-command-accent: var(--ks-kinpaku);
--dcx-command-ground: var(--ks-code-block-bg, var(--ks-lacquer-deep));
--dcx-command-ink: var(--ks-code-block-fg, var(--ks-text));
--dcx-command-copy-border: var(--ks-rule);
--dcx-command-copy-hover: oklch(77% 0.14 82 / 0.08);
box-sizing: border-box;
display: flex;
align-items: center;
gap: 12px;
width: 100%;
min-width: 0;
min-height: 52px;
padding: 9px 9px 9px 18px;
border: 1px solid var(--dcx-command-accent);
border-radius: 2px;
background: var(--dcx-command-ground);
}
.dcx-command-copy__prompt {
flex: none;
color: var(--dcx-command-accent);
font: 500 1.125rem/1 var(--ks-mono);
user-select: none;
}
.dcx-command-copy > code {
flex: 1 1 auto;
min-width: 0;
margin: 0;
padding: 0;
overflow-x: auto;
border: 0;
background: transparent;
color: var(--dcx-command-ink);
font: 400 0.98rem/1.35 var(--ks-mono);
letter-spacing: 0;
scrollbar-width: thin;
user-select: all;
white-space: nowrap;
}
.dcx-command-copy__button {
position: relative;
display: inline-grid;
flex: none;
place-items: center;
width: 34px;
height: 34px;
padding: 0;
border: 1px solid var(--dcx-command-copy-border);
border-radius: 2px;
background: transparent;
color: var(--dcx-command-accent);
cursor: pointer;
transition:
background-color 180ms var(--ks-ease),
border-color 180ms var(--ks-ease),
color 180ms var(--ks-ease),
transform 120ms var(--ks-ease);
}
.dcx-command-copy__button:hover,
.dcx-command-copy__button.copied {
border-color: currentColor;
background: var(--dcx-command-copy-hover);
}
.dcx-command-copy__button:active {
transform: scale(0.96);
}
.dcx-command-copy__button:focus-visible {
outline: 2px solid var(--ks-patina);
outline-offset: 4px;
}
.dcx-command-copy__button svg {
grid-area: 1 / 1;
fill: none;
stroke: currentColor;
transition:
opacity 140ms var(--ks-ease),
transform 180ms var(--ks-ease);
}
.dcx-command-copy__copy-icon {
width: 14px;
height: 14px;
opacity: 1;
stroke-width: 1.6;
transform: scale(1);
}
.dcx-command-copy__check-icon {
width: 16px;
height: 16px;
opacity: 0;
stroke-width: 2.1;
stroke-linecap: round;
stroke-linejoin: round;
transform: scale(0.86);
}
.dcx-command-copy__check-icon path {
stroke-dasharray: 24;
stroke-dashoffset: 24;
}
.dcx-command-copy__button.copied .dcx-command-copy__copy-icon {
opacity: 0;
transform: scale(0.82);
}
.dcx-command-copy__button.copied .dcx-command-copy__check-icon {
opacity: 1;
transform: scale(1);
}
.dcx-command-copy__button.copied .dcx-command-copy__check-icon path {
animation: dcx-command-check-draw 260ms var(--ks-ease) forwards;
}
.dcx-command-copy__label,
.dcx-command-status {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
border: 0;
white-space: nowrap;
}
@keyframes dcx-command-check-draw {
to { stroke-dashoffset: 0; }
}
@media (max-width: 520px) {
.dcx-command-copy {
gap: 10px;
padding: 9px 8px 9px 14px;
}
.dcx-command-copy > code {
font-size: 0.72rem;
}
}
@media (prefers-reduced-motion: reduce) {
.dcx-command-copy__button,
.dcx-command-copy__button svg {
transition: none;
}
.dcx-command-copy__button.copied .dcx-command-copy__check-icon path {
animation: none;
stroke-dashoffset: 0;
}
}
+655
View File
@@ -0,0 +1,655 @@
/* A visual component inventory: specimens first, documentation chrome kept quiet. */
.dcx-detail-article--components .dcx-component-stack {
display: grid;
gap: clamp(42px, 5vw, 64px);
min-width: 0;
}
.dcx-detail-article--components .dcx-component-example {
min-width: 0;
margin: 0;
}
.dcx-detail-article--components .dcx-component-example > figcaption {
margin: 0 0 14px;
}
.dcx-detail-article--components .dcx-component-example h4 {
margin: 0;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 1.08rem;
font-weight: 600;
line-height: 1.4;
}
.dcx-detail-article--components .dcx-component-canvas {
box-sizing: border-box;
width: 100%;
min-width: 0;
min-height: clamp(230px, 27vw, 300px);
padding: clamp(30px, 5vw, 58px);
border: 1px solid var(--ks-rule);
border-radius: 8px;
background: var(--ks-lacquer-deep);
}
.dcx-detail-article--components .dcx-component-canvas--center {
display: grid;
place-items: center;
}
.dcx-detail-article--components .dcx-component-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 12px;
max-width: 100%;
}
.dcx-detail-article--components .dcx-component-row--baseline {
align-items: baseline;
}
.dcx-detail-article--components .dcx-demo-button {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
min-height: 44px;
padding: 0 20px;
border: 1px solid transparent;
border-radius: 3px;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 0.88rem;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.dcx-detail-article--components .dcx-demo-button svg,
.dcx-detail-article--components .dcx-demo-icon-button svg,
.dcx-detail-article--components .dcx-demo-input svg {
flex: none;
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
.dcx-detail-article--components .dcx-demo-button--primary {
border-color: var(--ks-kinpaku);
background: var(--ks-kinpaku);
color: var(--ks-lacquer-deep);
}
.dcx-detail-article--components .dcx-demo-button--secondary {
border-color: var(--ks-graphite-2);
background: var(--ks-graphite-2);
color: var(--ks-champagne);
}
.dcx-detail-article--components .dcx-demo-button--tertiary {
border-color: color-mix(in oklch, var(--ks-patina) 55%, var(--ks-rule));
background: color-mix(in oklch, var(--ks-patina) 13%, transparent);
color: var(--ks-patina-pale);
}
.dcx-detail-article--components .dcx-demo-button--outline {
border-color: var(--ks-rule);
background: transparent;
color: var(--ks-champagne);
}
.dcx-detail-article--components .dcx-demo-button--ghost {
padding-inline: 12px;
background: transparent;
color: var(--ks-text);
}
.dcx-detail-article--components .dcx-demo-button--danger {
border-color: var(--ks-vermilion);
background: var(--ks-vermilion);
color: var(--ks-champagne);
}
.dcx-detail-article--components .dcx-demo-button--danger-soft {
border-color: transparent;
background: color-mix(in oklch, var(--ks-vermilion) 20%, transparent);
color: color-mix(in oklch, var(--ks-vermilion) 74%, var(--ks-champagne));
}
.dcx-detail-article--components .dcx-demo-button--small {
min-height: 36px;
padding-inline: 16px;
font-size: 0.78rem;
}
.dcx-detail-article--components .dcx-demo-button--large {
min-height: 54px;
padding-inline: 26px;
font-size: 0.96rem;
}
.dcx-detail-article--components .dcx-demo-icon-button {
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
border: 1px solid transparent;
border-radius: 50%;
color: var(--ks-champagne);
}
.dcx-detail-article--components .dcx-demo-icon-button--secondary {
background: var(--ks-graphite-2);
}
.dcx-detail-article--components .dcx-demo-icon-button--outline {
border-color: var(--ks-rule);
color: var(--ks-patina);
}
.dcx-detail-article--components .dcx-demo-icon-button--danger {
background: var(--ks-vermilion);
}
.dcx-detail-article--components .dcx-demo-button--loading {
opacity: 0.72;
}
.dcx-detail-article--components .dcx-demo-spinner {
width: 14px;
height: 14px;
border: 2px solid color-mix(in oklch, var(--ks-lacquer-deep) 35%, transparent);
border-top-color: var(--ks-lacquer-deep);
border-radius: 50%;
animation: dcx-component-spin 900ms linear infinite;
}
@keyframes dcx-component-spin {
to {
transform: rotate(360deg);
}
}
.dcx-detail-article--components .dcx-component-canvas--fields,
.dcx-detail-article--components .dcx-component-canvas--cards {
display: grid;
place-items: center;
}
.dcx-detail-article--components .dcx-component-demo-grid {
display: grid;
align-items: stretch;
width: min(100%, 900px);
min-width: 0;
gap: clamp(16px, 2.2vw, 22px);
}
.dcx-detail-article--components .dcx-component-demo-grid--three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.dcx-detail-article--components .dcx-component-demo-grid--two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dcx-detail-article--components .dcx-component-demo-grid--single {
grid-template-columns: minmax(0, 1fr);
width: min(100%, 680px);
}
.dcx-detail-article--components .dcx-component-demo-grid--baseline {
align-items: end;
}
.dcx-detail-article--components .dcx-demo-field {
display: grid;
align-content: start;
gap: 8px;
min-width: 0;
}
.dcx-detail-article--components .dcx-demo-field-label {
color: var(--ks-text-muted);
font-family: var(--ks-font);
font-size: 0.8rem;
font-weight: 500;
line-height: 1.4;
}
.dcx-detail-article--components .dcx-demo-input {
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
min-width: 0;
min-height: 48px;
padding: 0 14px;
border: 1px solid var(--ks-rule);
border-radius: 3px;
background: var(--ks-lacquer-raised);
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 0.88rem;
line-height: 1.4;
overflow-wrap: anywhere;
}
.dcx-detail-article--components .dcx-demo-input.is-placeholder {
color: var(--ks-text-mute-deep);
}
.dcx-detail-article--components .dcx-demo-field small {
color: var(--ks-text-muted);
font-size: 0.72rem;
line-height: 1.45;
}
.dcx-detail-article--components .dcx-demo-field.is-invalid .dcx-demo-input {
border-color: var(--ks-vermilion);
}
.dcx-detail-article--components .dcx-demo-field.is-invalid small {
color: color-mix(in oklch, var(--ks-vermilion) 76%, var(--ks-champagne));
}
.dcx-detail-article--components .dcx-demo-field--small .dcx-demo-input {
min-height: 38px;
font-size: 0.78rem;
}
.dcx-detail-article--components .dcx-demo-field--large .dcx-demo-input {
min-height: 58px;
font-size: 0.96rem;
}
.dcx-detail-article--components .dcx-demo-input--affix {
gap: 10px;
}
.dcx-detail-article--components .dcx-demo-input--affix > span {
flex: none;
color: var(--ks-patina);
font-family: var(--ks-mono);
font-size: 0.74rem;
}
.dcx-detail-article--components .dcx-demo-input--suffix {
justify-content: space-between;
}
.dcx-detail-article--components .dcx-demo-input--textarea {
align-items: flex-start;
min-height: 132px;
padding-block: 14px;
line-height: 1.6;
}
.dcx-detail-article--components .dcx-showcase-card {
box-sizing: border-box;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 268px;
overflow: hidden;
border: 1px solid var(--ks-rule);
border-radius: 8px;
background: var(--panel-bg);
}
.dcx-detail-article--components .dcx-showcase-card-body {
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
min-width: 0;
padding: clamp(20px, 2.6vw, 28px);
}
.dcx-detail-article--components .dcx-showcase-card h5 {
margin: 14px 0 8px;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 1.04rem;
font-weight: 600;
line-height: 1.38;
}
.dcx-detail-article--components .dcx-showcase-card p,
.dcx-detail-article--components .dcx-showcase-card blockquote {
margin: 0;
color: var(--ks-text);
font-family: var(--ks-font);
font-size: 0.88rem;
line-height: 1.62;
text-wrap: pretty;
}
.dcx-detail-article--components .dcx-showcase-card blockquote {
margin-top: 24px;
color: var(--ks-champagne);
font-size: 1.02rem;
}
.dcx-detail-article--components .dcx-showcase-card-meta {
margin-top: auto;
padding-top: 22px;
color: var(--ks-text-muted);
font-family: var(--ks-mono);
font-size: 0.7rem;
}
.dcx-detail-article--components .dcx-showcase-card dl {
display: grid;
gap: 10px;
width: 100%;
margin: 10px 0 0;
}
.dcx-detail-article--components .dcx-showcase-card dl > div {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 12px;
}
.dcx-detail-article--components .dcx-showcase-card dt {
color: var(--ks-text-faint);
font-family: var(--ks-mono);
font-size: 0.66rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.dcx-detail-article--components .dcx-showcase-card dd {
margin: 0;
color: var(--ks-text);
font-size: 0.82rem;
}
.dcx-detail-article--components .dcx-showcase-card-link {
margin-top: auto;
padding-top: 22px;
color: var(--ks-patina);
font-size: 0.84rem;
font-weight: 600;
}
.dcx-detail-article--components .dcx-showcase-card--media img {
width: 100%;
height: clamp(190px, 23vw, 240px);
object-fit: cover;
object-position: 50% 38%;
}
.dcx-detail-article--components .dcx-showcase-card--media-close img {
object-position: 50% 66%;
}
.dcx-detail-article--components .dcx-component-canvas--single-card {
min-height: clamp(300px, 34vw, 380px);
}
.dcx-detail-article--components .dcx-showcase-card--horizontal {
display: grid;
grid-template-columns: minmax(220px, 0.85fr) minmax(0, 1.15fr);
width: min(100%, 780px);
min-height: 290px;
}
.dcx-detail-article--components .dcx-showcase-card--horizontal > img {
width: 100%;
height: 100%;
min-height: 290px;
object-fit: cover;
object-position: 50% 42%;
}
.dcx-detail-article--components .dcx-showcase-card--action .dcx-showcase-card-body {
min-height: 286px;
}
.dcx-detail-article--components .dcx-showcase-card--action .dcx-demo-button {
margin-top: auto;
}
@media (max-width: 1020px) {
.dcx-detail-article--components .dcx-component-demo-grid--three {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dcx-detail-article--components .dcx-component-demo-grid--three > :last-child:nth-child(odd) {
grid-column: 1 / -1;
}
}
@media (max-width: 640px) {
.dcx-detail-article--components .dcx-component-stack {
gap: 36px;
}
.dcx-detail-article--components .dcx-component-canvas {
min-height: 210px;
padding: 24px 20px;
}
.dcx-detail-article--components .dcx-component-demo-grid--three,
.dcx-detail-article--components .dcx-component-demo-grid--two {
grid-template-columns: minmax(0, 1fr);
}
.dcx-detail-article--components .dcx-component-demo-grid--three > :last-child:nth-child(odd) {
grid-column: auto;
}
.dcx-detail-article--components .dcx-showcase-card--horizontal {
grid-template-columns: minmax(0, 1fr);
}
.dcx-detail-article--components .dcx-showcase-card--horizontal > img {
height: 230px;
min-height: 0;
}
}
.dcx-detail-article--components .dcx-component-kind {
color: var(--ks-text-faint);
font-family: var(--ks-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.14em;
line-height: 1.45;
text-transform: uppercase;
}
.dcx-detail-article--components .dcx-components-command {
box-sizing: border-box;
display: grid;
gap: 12px;
width: min(100%, 620px);
min-width: 0;
margin-top: clamp(28px, 3.5vw, 40px);
padding: 0;
border: 0;
background: transparent;
}
.dcx-detail-article--components .dcx-components-command-copy h4 {
margin: 0 0 6px;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 1rem;
font-weight: 600;
line-height: 1.4;
}
.dcx-detail-article--components .dcx-components-command-copy p {
max-width: 54ch;
margin: 0;
color: var(--ks-text-muted);
font-size: 0.86rem;
line-height: 1.55;
}
/* ============================================================
Specimens in the committed palette: when the run committed one,
the inventory renders in it. design-context.js sets
.dcx-palette-live on <body> and paints --pkc-* there (the body
survives document remounts; the article does not). Without the
gate the vendored kinpaku colors above stand. Danger stays
--ks-vermilion: semantic, not brand. Geometry untouched.
============================================================ */
.dcx-palette-live .dcx-detail-article--components .dcx-component-canvas {
background: var(--pkc-neutral);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--primary {
border-color: var(--pkc-primary);
background: var(--pkc-primary);
color: var(--pkc-p-ink);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--secondary {
border-color: color-mix(in oklab, var(--pkc-n-ink) 14%, var(--pkc-neutral));
background: color-mix(in oklab, var(--pkc-n-ink) 14%, var(--pkc-neutral));
color: var(--pkc-n-ink);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--tertiary {
border-color: color-mix(in oklab, var(--pkc-tertiary) 55%, var(--pkc-neutral));
background: color-mix(in oklab, var(--pkc-tertiary) 13%, transparent);
color: var(--pkc-t-on-n);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--outline {
border-color: color-mix(in oklab, var(--pkc-n-ink) 22%, transparent);
color: var(--pkc-n-ink);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--ghost {
color: color-mix(in oklab, var(--pkc-n-ink) 92%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-icon-button--secondary {
background: color-mix(in oklab, var(--pkc-n-ink) 14%, var(--pkc-neutral));
color: var(--pkc-n-ink);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-icon-button--outline {
border-color: color-mix(in oklab, var(--pkc-n-ink) 22%, transparent);
color: var(--pkc-s-on-n);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-spinner {
border-color: color-mix(in oklab, var(--pkc-p-ink) 35%, transparent);
border-top-color: var(--pkc-p-ink);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-field-label {
color: color-mix(in oklab, var(--pkc-n-ink) 75%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-input {
border-color: color-mix(in oklab, var(--pkc-n-ink) 22%, transparent);
background: color-mix(in oklab, var(--pkc-n-ink) 7%, var(--pkc-neutral));
color: var(--pkc-n-ink);
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-input.is-placeholder {
color: color-mix(in oklab, var(--pkc-n-ink) 55%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-field:not(.is-invalid) small {
color: color-mix(in oklab, var(--pkc-n-ink) 75%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-demo-input--affix > span {
color: var(--pkc-s-on-n);
}
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card {
border-color: color-mix(in oklab, var(--pkc-n-ink) 18%, transparent);
background: color-mix(in oklab, var(--pkc-n-ink) 5%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card h5,
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card blockquote {
color: var(--pkc-n-ink);
}
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card p,
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card dd {
color: color-mix(in oklab, var(--pkc-n-ink) 92%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card-meta {
color: color-mix(in oklab, var(--pkc-n-ink) 75%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card dt,
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card .dcx-component-kind {
color: color-mix(in oklab, var(--pkc-n-ink) 64%, var(--pkc-neutral));
}
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card-link {
color: var(--pkc-s-on-n);
}
/* ============================================================
Specimens in the committed kit: faces, corners, depth, and the
scale step, the same contract .dcx-palette-live holds for color.
design-context.js paintKit publishes --pkc-font-*, --pkc-radius-*,
--pkc-shadow-*, and --pkc-scale-ratio on <body> and gates them
behind .dcx-kit-live; without the gate the vendored kinpaku kit
above stands. Documentation chrome (the canvas frame, figcaption
headings, the mono eyebrows and metadata) stays the document's
own furniture and is deliberately left alone.
============================================================ */
.dcx-kit-live .dcx-detail-article--components .dcx-component-canvas {
font-family: var(--pkc-font-body);
}
.dcx-kit-live .dcx-detail-article--components .dcx-demo-button,
.dcx-kit-live .dcx-detail-article--components .dcx-demo-field-label,
.dcx-kit-live .dcx-detail-article--components .dcx-demo-input,
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card p,
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card blockquote {
font-family: var(--pkc-font-body);
}
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card h5 {
font-family: var(--pkc-font-heading);
font-weight: var(--pkc-font-heading-weight);
font-size: calc(0.88rem * var(--pkc-scale-ratio, 1.18));
}
.dcx-kit-live .dcx-detail-article--components .dcx-demo-button,
.dcx-kit-live .dcx-detail-article--components .dcx-demo-input {
border-radius: var(--pkc-radius-control);
}
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card {
border-radius: var(--pkc-radius-surface);
box-shadow: var(--pkc-shadow-card);
}
.dcx-kit-live .dcx-detail-article--components :is(
.dcx-demo-button--primary,
.dcx-demo-button--secondary,
.dcx-demo-button--danger
) {
box-shadow: var(--pkc-shadow-control);
}
.dcx-kit-live .dcx-detail-article--components .dcx-demo-input {
box-shadow: var(--pkc-shadow-surface);
}
+710
View File
@@ -0,0 +1,710 @@
/* Audience-led editorial treatment for the remaining Design Context screens. */
.dcx-detail-article {
--dcx-detail-icon-size: 100px;
}
.dcx-detail-article > .dcx-detail-hero {
margin-bottom: clamp(48px, 6vw, 76px);
}
.dcx-detail-article .dcx-detail-section {
margin-top: 0;
margin-bottom: clamp(56px, 7vw, 88px);
scroll-margin-top: 18px;
}
.dcx-detail-article .dcx-detail-section:last-child {
margin-bottom: 0;
}
.dcx-detail-article .dcx-detail-section-head {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(80px, 150px);
gap: clamp(24px, 4vw, 44px);
align-items: center;
margin-bottom: 20px;
}
.dcx-detail-article .dcx-detail-section-head--with-icon {
grid-template-columns: minmax(0, 1fr) var(--dcx-detail-icon-size);
}
.dcx-detail-article .dcx-detail-section-head:not(.dcx-detail-section-head--with-icon)::after {
content: "";
width: 100%;
height: 1px;
border-inline-end: 46px solid var(--accent);
background: color-mix(in oklch, var(--ks-patina) 72%, transparent);
opacity: 0.72;
}
.dcx-detail-article .dcx-detail-section-heading {
min-width: 0;
}
.dcx-detail-article .dcx-detail-section-title {
margin: 0;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 1.5rem;
font-weight: 650;
letter-spacing: 0;
line-height: 1.25;
text-wrap: balance;
}
.dcx-detail-article .dcx-detail-section-lede {
max-width: 54ch;
margin: 10px 0 0;
color: var(--ks-text-muted);
font-size: 0.95rem;
line-height: 1.6;
text-wrap: pretty;
}
.dcx-detail-article .dcx-detail-section-icon {
width: var(--dcx-detail-icon-size);
height: var(--dcx-detail-icon-size);
margin: 0;
padding: 0;
border: 0;
background: transparent;
}
.dcx-detail-article .dcx-detail-section-icon img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.dcx-detail-article .dcx-detail-section-body {
min-width: 0;
}
.dcx-detail-article :is(
.dcx-callout,
.dcx-voice-cell,
.dcx-ref-card,
.dcx-pick,
.dcx-pair-card,
.dcx-proof,
.dcx-cue-card,
.dcx-empty
) {
border-color: transparent;
border-radius: 8px;
background: var(--panel-bg);
}
.dcx-detail-article .dcx-purpose {
grid-template-columns: minmax(0, 1fr);
gap: 0;
}
.dcx-detail-article .dcx-purpose-main {
min-width: 0;
}
.dcx-detail-article .dcx-purpose .dcx-callout {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: baseline;
column-gap: 18px;
}
.dcx-detail-article .dcx-purpose .dcx-callout > :not(.dcx-callout-name, .dcx-platform-pill) {
grid-column: 1 / -1;
}
.dcx-detail-article .dcx-platform-pill {
justify-self: end;
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--ks-text-faint);
}
.dcx-detail-article .dcx-detail-list {
counter-reset: dcx-detail-item;
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.dcx-detail-article .dcx-detail-list li {
counter-increment: dcx-detail-item;
position: static;
display: grid;
grid-template-columns: 32px minmax(0, 1fr);
gap: 12px;
align-items: baseline;
min-width: 0;
padding: 18px 20px;
border: 0;
border-radius: 8px;
background: var(--panel-bg);
color: var(--ks-text);
font-size: 0.96rem;
line-height: 1.6;
text-wrap: pretty;
}
.dcx-detail-article .dcx-detail-list li::before {
content: counter(dcx-detail-item, decimal-leading-zero);
position: static;
width: auto;
height: auto;
background: transparent;
color: var(--accent);
font-family: var(--ks-mono);
font-size: 0.72rem;
letter-spacing: 0.1em;
}
.dcx-detail-article .dcx-principles,
.dcx-detail-article .dcx-principles--cols {
counter-reset: principle;
display: grid;
grid-template-columns: 1fr;
gap: 12px;
margin: 0;
padding: 0;
border: 0;
}
.dcx-detail-article .dcx-principles--cols {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dcx-detail-article .dcx-principles li {
display: grid;
grid-template-columns: 32px minmax(0, 1fr);
gap: 12px;
align-items: start;
min-width: 0;
padding: 18px 20px;
border: 0;
border-radius: 8px;
background: var(--panel-bg);
color: var(--ks-text-muted);
font-size: 0.94rem;
line-height: 1.6;
text-wrap: pretty;
}
.dcx-detail-article .dcx-principles li::before {
padding-top: 0.1em;
color: var(--accent);
font-size: 0.72rem;
letter-spacing: 0.1em;
}
.dcx-detail-article .dcx-detail-principle-copy {
min-width: 0;
}
.dcx-detail-article .dcx-detail-principle-copy strong {
display: block;
margin-bottom: 5px;
color: var(--ks-champagne);
font-size: 1rem;
line-height: 1.4;
}
.dcx-detail-article .dcx-detail-defs {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin: 0;
border: 0;
}
.dcx-detail-article .dcx-detail-defs .dcx-def {
display: block;
min-width: 0;
padding: 18px 20px 20px;
border: 0;
border-radius: 8px;
background: var(--panel-bg);
}
.dcx-detail-article .dcx-detail-defs .dcx-def dt {
color: var(--ks-champagne);
font-size: 1rem;
}
.dcx-detail-article .dcx-detail-defs .dcx-def dd {
margin-top: 8px;
text-wrap: pretty;
}
.dcx-detail-article .dcx-voice,
.dcx-detail-article .dcx-voice-pair {
gap: 12px;
}
.dcx-detail-article .dcx-ref-cards,
.dcx-detail-article .dcx-picks,
.dcx-detail-article .dcx-pair,
.dcx-detail-article .dcx-surfaces {
gap: clamp(16px, 2.5vw, 26px);
}
.dcx-detail-article--brand .dcx-brand-assets-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(18px, 2.5vw, 26px);
margin: 0;
padding: 0;
list-style: none;
}
.dcx-detail-article--brand .dcx-brand-asset,
.dcx-detail-article--brand .dcx-brand-asset figure {
min-width: 0;
margin: 0;
}
.dcx-detail-article--brand .dcx-brand-asset figure {
display: grid;
gap: 12px;
}
.dcx-detail-article--brand .dcx-brand-asset-frame {
position: relative;
display: grid;
aspect-ratio: 4 / 3;
min-width: 0;
overflow: hidden;
place-items: center;
border: 1px solid var(--ks-rule);
border-radius: 8px;
background: var(--panel-bg);
}
.dcx-detail-article--brand .dcx-brand-asset[data-asset-kind="logo"] .dcx-brand-asset-frame {
background-color: var(--panel-bg);
background-image:
linear-gradient(to right, var(--ks-rule) 1px, transparent 1px),
linear-gradient(to bottom, var(--ks-rule) 1px, transparent 1px);
background-size: 40px 40px;
}
.dcx-detail-article--brand .dcx-brand-asset-frame img {
position: relative;
z-index: 1;
display: block;
}
.dcx-detail-article--brand .dcx-brand-asset[data-asset-kind="logo"] img {
width: 72%;
height: 72%;
object-fit: contain;
}
.dcx-detail-article--brand .dcx-brand-asset:not([data-asset-kind="logo"]) img {
width: 100%;
height: 100%;
object-fit: cover;
}
.dcx-detail-article--brand .dcx-brand-asset figcaption {
display: flex;
min-width: 0;
align-items: baseline;
justify-content: space-between;
gap: 16px;
padding: 0 2px;
}
.dcx-detail-article--brand .dcx-brand-asset figcaption strong {
min-width: 0;
color: var(--ks-text);
font-family: var(--ks-font);
font-size: 1rem;
font-weight: 600;
line-height: 1.4;
overflow-wrap: anywhere;
}
.dcx-detail-article--brand .dcx-brand-asset figcaption span {
flex: none;
color: var(--ks-text-faint);
font-family: var(--ks-mono);
font-size: 0.66rem;
letter-spacing: 0.14em;
line-height: 1.45;
text-transform: uppercase;
}
.dcx-detail-article .dcx-ref-card,
.dcx-detail-article .dcx-pick,
.dcx-detail-article .dcx-pair-card {
padding: clamp(18px, 2.5vw, 26px);
}
.dcx-detail-article .dcx-pick {
display: grid;
grid-template-columns: minmax(7rem, 0.65fr) minmax(0, 1.35fr);
grid-template-areas:
"surface title"
"surface description";
align-content: start;
column-gap: clamp(18px, 2.5vw, 28px);
row-gap: 7px;
}
.dcx-detail-article .dcx-pick--proof {
grid-template-areas:
"surface title"
"surface description"
"proof proof";
}
.dcx-detail-article .dcx-pick--proof > .dcx-proof {
grid-area: proof;
margin-top: 14px;
}
.dcx-detail-article .dcx-pick-surface {
grid-area: surface;
align-self: start;
margin: 4px 0 0;
}
.dcx-detail-article .dcx-pick-title {
grid-area: title;
font-family: var(--ks-font);
font-size: 1.05rem;
font-weight: 600;
line-height: 1.35;
margin: 0;
}
.dcx-detail-article .dcx-pick-desc {
grid-area: description;
color: var(--ks-text);
font-size: 0.95rem;
line-height: 1.58;
text-wrap: pretty;
}
@media (width <= 620px) {
.dcx-detail-article .dcx-pick {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"surface"
"title"
"description";
row-gap: 8px;
}
.dcx-detail-article .dcx-pick--proof {
grid-template-areas:
"surface"
"title"
"description"
"proof";
}
.dcx-detail-article .dcx-pick-surface {
margin-top: 0;
}
}
.dcx-detail-article .dcx-surface-card {
gap: 18px;
}
.dcx-detail-article .dcx-surface-copy {
padding: 0 clamp(14px, 2vw, 22px) 4px;
}
.dcx-detail-article .dcx-surface-name {
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 1.15rem;
font-weight: 600;
line-height: 1.4;
}
.dcx-detail-article .dcx-proof--board,
.dcx-detail-article .dcx-cue-frame {
overflow: hidden;
border-color: transparent;
border-radius: 8px;
}
.dcx-detail-article .dcx-cue {
gap: clamp(16px, 2.5vw, 26px);
}
.dcx-detail-article .dcx-cue-strip {
gap: 12px;
}
.dcx-detail-article .dcx-fan {
overflow: hidden;
border-radius: 8px 8px 0 0;
}
.dcx-detail-article--color .dcx-cue {
align-items: stretch;
}
.dcx-detail-article--color .dcx-cue-card {
container-name: dcx-cue-palette;
container-type: inline-size;
display: grid;
grid-template-rows: minmax(0, 1fr);
gap: 0;
min-width: 0;
padding: 0;
overflow: hidden;
background: var(--panel-bg);
}
.dcx-detail-article--color .dcx-cue-bands {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1px;
min-width: 0;
min-height: 100%;
margin: 0;
padding: 0;
background: var(--ks-rule);
list-style: none;
}
.dcx-detail-article--color .dcx-cue-band-item {
display: grid;
grid-template-rows: minmax(0, 1fr) 46px;
min-width: 0;
background: var(--panel-bg);
}
.dcx-detail-article--color .dcx-cue-band {
background: var(--band-color);
color: var(--band-ink);
min-width: 0;
min-height: 175px;
position: relative;
}
.dcx-detail-article--color .dcx-cue-band-value {
position: absolute;
right: 12px;
bottom: 14px;
left: 12px;
overflow-wrap: anywhere;
padding: 0;
background: transparent;
color: inherit;
font-family: var(--ks-mono);
font-size: clamp(0.58rem, 0.85vw, 0.7rem);
font-weight: 500;
letter-spacing: 0.04em;
line-height: 1;
}
.dcx-detail-article--color .dcx-cue-band-label {
display: grid;
place-items: center;
min-width: 0;
padding: 0 6px;
overflow-wrap: anywhere;
color: var(--ks-champagne);
font-size: clamp(0.76rem, 1.1vw, 0.92rem);
font-weight: 500;
line-height: 1.15;
text-align: center;
}
@container dcx-cue-palette (max-width: 439px) {
.dcx-detail-article--color .dcx-cue-bands {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dcx-detail-article--color .dcx-cue-band {
min-height: 124px;
}
}
.dcx-detail-article .dcx-swatches {
gap: 12px;
}
.dcx-detail-article .dcx-swatch {
padding: 12px;
border-radius: 8px;
background: var(--panel-bg);
}
.dcx-detail-article .dcx-swatch-chip {
min-height: 140px;
overflow: hidden;
border: 0;
border-radius: 6px;
}
.dcx-detail-article .dcx-swatch-meta h4 {
margin: 0 0 6px;
color: var(--ks-text);
font-family: var(--ks-font-display);
font-size: 1.3rem;
font-weight: 300;
}
.dcx-detail-article .dcx-pair-name {
color: var(--ks-champagne);
overflow-wrap: anywhere;
}
.dcx-detail-article .dcx-proof--scale,
.dcx-detail-article .dcx-proof--specimen,
.dcx-detail-article .dcx-proof--icons {
padding: clamp(18px, 2.5vw, 28px);
}
.dcx-detail-article .dcx-proof--scale > .picker-scale-sheet {
row-gap: 0;
padding-block: clamp(36px, 4vw, 52px);
}
.dcx-detail-article .dcx-proof--scale .ts-row {
padding-block: clamp(14px, 2vw, 22px);
}
.dcx-detail-article .dcx-proof--specimen {
min-width: 0;
max-width: 100%;
overflow: hidden;
}
.dcx-detail-article .dcx-proof--specimen > .picker-scale-specimen {
gap: clamp(28px, 3vw, 36px);
padding-block: clamp(36px, 4vw, 52px);
}
.dcx-detail-article .dcx-proof--specimen .sp-h1 {
font-size: clamp(2.5rem, 4.25vw, 3.25rem);
}
.dcx-detail-article .dcx-proof--specimen .sp-h2 {
font-size: clamp(2rem, 3vw, 2.5rem);
}
.dcx-detail-article .dcx-proof--specimen .sp-h3 {
font-size: 1.65rem;
}
.dcx-detail-article .dcx-proof--specimen .sp-h4 {
font-size: 1.35rem;
}
.dcx-detail-article .dcx-proof--specimen .sp-lede {
font-size: 1.125rem;
line-height: 1.65;
}
.dcx-detail-article .dcx-proof--specimen :is(.sp-p, .sp-list) {
font-size: 1rem;
line-height: 1.7;
}
.dcx-detail-article .dcx-proof--specimen .sp-quote p {
font-size: 1.2rem;
line-height: 1.55;
}
.dcx-detail-article .dcx-proof--specimen .sp-cite,
.dcx-detail-article .dcx-proof--specimen :is(.sp-code, .sp-pre) {
font-size: 0.9rem;
}
.dcx-detail-article .dcx-proof--specimen .sp-pre {
line-height: 1.65;
}
.dcx-detail-article .sp-pre {
max-width: 100%;
overflow-x: visible;
white-space: pre-wrap;
}
.dcx-detail-article .sp-pre code {
white-space: inherit;
overflow-wrap: anywhere;
}
.dcx-detail-article .dcx-fan-note,
.dcx-detail-article .dcx-default-note {
max-width: 72ch;
margin-top: 12px;
color: var(--ks-text-faint);
line-height: 1.55;
}
@media (max-width: 720px) {
.dcx-detail-article .dcx-principles--cols,
.dcx-detail-article .dcx-detail-defs,
.dcx-detail-article--brand .dcx-brand-assets-grid {
grid-template-columns: 1fr;
}
.dcx-detail-article .dcx-swatch {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.dcx-detail-article .dcx-detail-section-head {
gap: 18px;
}
.dcx-detail-article .dcx-detail-section-head:not(.dcx-detail-section-head--with-icon) {
grid-template-columns: minmax(0, 1fr) 72px;
}
.dcx-detail-article .dcx-purpose {
grid-template-columns: 1fr;
}
}
@media (max-width: 430px) {
.dcx-detail-article .dcx-detail-section-head {
gap: 14px;
}
.dcx-detail-article .dcx-detail-section-title {
font-size: 1.3rem;
}
.dcx-detail-article .dcx-detail-list li,
.dcx-detail-article .dcx-principles li {
grid-template-columns: 26px minmax(0, 1fr);
padding: 16px;
}
.dcx-detail-article--color .dcx-cue-band-value {
right: 8px;
bottom: 12px;
left: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.dcx-detail-article *,
.dcx-detail-article *::before,
.dcx-detail-article *::after {
transition-duration: 0.001ms !important;
transition-delay: 0ms !important;
}
}
+50
View File
@@ -0,0 +1,50 @@
/* Continuous expanded document. The opening category grid is intentionally untouched. */
.dcx-main[data-dcx-continuous="true"] {
overflow-x: hidden;
}
.dcx-document-title {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.dcx-main[data-dcx-continuous="true"] > .dcx-document-article {
box-sizing: border-box;
scroll-margin-top: 18px;
}
.dcx-main[data-dcx-continuous="true"] > .dcx-document-article + .dcx-document-article {
border-top: 1px solid var(--ks-rule);
margin-top: clamp(48px, 6vw, 80px);
padding-top: clamp(36px, 5vw, 64px);
}
.dcx-expander[data-dcx-document="true"] .dcx-sub-link {
text-decoration: none;
}
.dcx-expander[data-dcx-document="true"] .dcx-nav-link[aria-expanded="true"] {
color: var(--accent);
font-weight: 600;
}
@media (max-width: 920px) {
.dcx-main[data-dcx-continuous="true"] > .dcx-document-article + .dcx-document-article {
margin-top: 42px;
padding-top: 30px;
}
}
@media (prefers-reduced-motion: reduce) {
.dcx-main[data-dcx-continuous="true"] {
scroll-behavior: auto;
}
}
+135
View File
@@ -0,0 +1,135 @@
/* Shared information hierarchy for every Design Context article.
Labels identify a field; values carry the decision; supporting copy explains it. */
.dcx-expander {
--dcx-hierarchy-title: var(--ks-champagne);
--dcx-hierarchy-value: var(--ks-text);
--dcx-hierarchy-support: var(--ks-text-muted);
--dcx-hierarchy-label: var(--ks-text-faint);
--dcx-hierarchy-label-size: 0.68rem;
--dcx-hierarchy-value-size: 0.96rem;
}
/* Definition lists: labels recede, committed values lead. */
.dcx-detail-article .dcx-detail-defs .dcx-def dt,
.dcx-audience .dcx-audience-section--people .dcx-def dt {
color: var(--dcx-hierarchy-label);
font-family: var(--ks-mono);
font-size: var(--dcx-hierarchy-label-size);
font-weight: 500;
letter-spacing: 0.14em;
line-height: 1.45;
text-transform: uppercase;
}
.dcx-detail-article .dcx-detail-defs .dcx-def dd,
.dcx-audience .dcx-audience-section--people .dcx-def dd {
margin-top: 9px;
color: var(--dcx-hierarchy-value);
font-size: var(--dcx-hierarchy-value-size);
font-weight: 400;
line-height: 1.6;
}
.dcx-detail-article .dcx-detail-defs .dcx-def dd strong {
color: inherit;
font-weight: 600;
}
/* The prose inside a decision component is content, not metadata. */
.dcx-detail-article .dcx-callout > p:not(.dcx-callout-name),
.dcx-audience .dcx-callout > p:not(.dcx-callout-name),
.dcx-detail-article .dcx-principles li,
.dcx-detail-article .dcx-voice-cell > p,
.dcx-detail-article .dcx-ref-takeaway,
.dcx-detail-article .dcx-surface-goal,
.dcx-detail-article .dcx-swatch-meta > p,
.dcx-detail-article .dcx-scale-head,
.dcx-detail-article .dcx-pick-desc,
.dcx-detail-article .dcx-asset-caption > p,
.dcx-detail-article .dcx-prose {
color: var(--dcx-hierarchy-value);
}
/* These callout headings describe the field; the following paragraph is the answer. */
.dcx-audience .dcx-audience-journey .dcx-callout-name,
.dcx-detail-article .dcx-detail-section--positioning .dcx-callout-name,
.dcx-detail-article .dcx-detail-section--primary-conversion .dcx-callout-name,
.dcx-detail-article .dcx-detail-section--anti-reference .dcx-callout-name {
margin-bottom: 9px;
color: var(--dcx-hierarchy-label);
font-family: var(--ks-mono);
font-size: var(--dcx-hierarchy-label-size);
font-weight: 500;
letter-spacing: 0.14em;
line-height: 1.45;
text-transform: uppercase;
}
.dcx-audience .dcx-audience-journey .dcx-callout-name + p,
.dcx-detail-article .dcx-detail-section--positioning .dcx-callout-name + p,
.dcx-detail-article .dcx-detail-section--primary-conversion .dcx-callout-name + p,
.dcx-detail-article .dcx-detail-section--anti-reference .dcx-callout-name + p {
color: var(--dcx-hierarchy-value);
font-size: 1rem;
font-weight: 500;
line-height: 1.6;
}
/* Preserve prominent answer titles inside components. */
.dcx-detail-article :is(
.dcx-detail-principle-copy strong,
.dcx-ref-name,
.dcx-surface-name,
.dcx-pair-name,
.dcx-pick-title,
.dcx-empty-title
) {
color: var(--dcx-hierarchy-title);
}
.dcx-detail-article .dcx-ref-name {
font-family: var(--ks-font);
}
/* In the palette, the role is the label and the hex is the committed value. */
.dcx-detail-article--color .dcx-cue-band-label {
color: var(--dcx-hierarchy-label);
font-family: var(--ks-mono);
font-size: var(--dcx-hierarchy-label-size);
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.dcx-detail-article--color .dcx-cue-band-value {
font-size: clamp(0.7rem, 1vw, 0.78rem);
font-weight: 600;
}
/* Type-scale rows follow the same label / value / supporting-value order. */
.dcx-detail-article--typography .ts-sample {
color: var(--ks-champagne) !important;
}
.dcx-detail-article--typography .ts-tag {
color: var(--dcx-hierarchy-label);
font-family: var(--ks-mono);
font-size: 16px !important;
font-weight: 500;
letter-spacing: 0.1em;
}
.dcx-detail-article--typography .ts-values {
gap: 12px;
}
.dcx-detail-article--typography .ts-values b {
color: var(--dcx-hierarchy-value);
font-size: 16px;
font-weight: 600;
}
.dcx-detail-article--typography .ts-values i {
color: var(--dcx-hierarchy-support);
}
+707
View File
@@ -0,0 +1,707 @@
/* Quiet, readable Hooks controls for the continuous Design Context document. */
.dcx-detail-article--hooks {
--dcx-hooks-rule: color-mix(in oklch, var(--ks-text) 18%, transparent);
--dcx-hooks-rule-strong: color-mix(in oklch, var(--ks-text) 32%, transparent);
--dcx-hooks-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
.dcx-detail-article--hooks .dcx-detail-section {
margin-bottom: clamp(72px, 8vw, 104px);
}
.dcx-detail-article--hooks .dcx-detail-section-head {
margin-bottom: clamp(24px, 3vw, 32px);
}
.dcx-detail-article--hooks .dcx-detail-section-lede {
color: var(--ks-text-muted);
}
.dcx-detail-article--hooks .dcx-hooks-definition {
max-width: 58ch;
margin: 0;
color: var(--ks-text-muted);
font-size: 1rem;
line-height: 1.72;
text-wrap: pretty;
}
.dcx-detail-article--hooks .dcx-hooks-status {
display: flex;
align-items: center;
justify-content: space-between;
gap: 28px;
margin-top: 24px;
padding: 20px 0;
border-block: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-status-copy {
min-width: 0;
}
.dcx-detail-article--hooks .dcx-hooks-status strong {
display: block;
color: var(--ks-champagne);
font: 600 1rem/1.4 var(--ks-font);
}
.dcx-detail-article--hooks .dcx-hooks-status p {
margin: 5px 0 0;
color: var(--ks-text-muted);
font-size: 0.9rem;
line-height: 1.55;
}
.dcx-detail-article--hooks .dcx-hooks-status-control {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 12px;
}
.dcx-detail-article--hooks .dcx-hooks-status-state {
min-width: 22px;
color: var(--ks-text);
font: 500 0.7rem/1 var(--ks-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.dcx-detail-article--hooks .dcx-hooks-status.is-paused .dcx-hooks-status-state {
color: var(--ks-text-muted);
}
.dcx-detail-article--hooks .dcx-hooks-flow {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin: 0;
padding: 0;
}
.dcx-detail-article--hooks .dcx-hooks-flow > div {
min-width: 0;
padding: 20px 0 2px;
}
.dcx-detail-article--hooks .dcx-hooks-flow > div + div {
padding-left: 28px;
border-left: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-flow dt,
.dcx-detail-article--hooks .dcx-hooks-search > span,
.dcx-detail-article--hooks .dcx-hooks-custom-form label > span,
.dcx-detail-article--hooks .dcx-hooks-custom-discipline {
color: var(--ks-text-muted);
font: 500 0.68rem/1.4 var(--ks-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.dcx-detail-article--hooks .dcx-hooks-flow dd {
margin: 7px 0 0;
color: var(--ks-text);
font-size: 0.94rem;
line-height: 1.58;
}
.dcx-detail-article--hooks .dcx-hooks-switch {
position: relative;
display: inline-grid;
flex: 0 0 auto;
width: 46px;
height: 44px;
place-items: center;
cursor: pointer;
}
.dcx-detail-article--hooks .dcx-hooks-switch input {
position: absolute;
width: 44px;
height: 44px;
margin: 0;
opacity: 0;
cursor: inherit;
}
.dcx-detail-article--hooks .dcx-hooks-switch > span {
position: relative;
display: block;
width: 38px;
height: 20px;
border: 1px solid var(--dcx-hooks-rule-strong);
border-radius: 999px;
background: transparent;
transition: border-color 180ms ease, background-color 180ms ease;
}
.dcx-detail-article--hooks .dcx-hooks-switch > span::after {
position: absolute;
top: 3px;
left: 3px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--ks-text-muted);
content: "";
transition: transform 260ms var(--dcx-hooks-ease), background-color 180ms ease;
}
.dcx-detail-article--hooks .dcx-hooks-switch input:checked + span {
border-color: var(--ks-text-muted);
background: var(--ks-graphite-2);
}
.dcx-detail-article--hooks .dcx-hooks-switch input:checked + span::after {
background: var(--ks-kinpaku);
transform: translateX(18px);
}
.dcx-detail-article--hooks .dcx-hooks-switch input:focus-visible + span {
outline: 2px solid var(--ks-kinpaku);
outline-offset: 3px;
}
.dcx-detail-article--hooks .dcx-hooks-browser {
min-width: 0;
border-block: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-families {
display: flex;
align-items: stretch;
gap: clamp(24px, 4vw, 42px);
min-width: 0;
padding-inline: 3px;
overflow-x: auto;
border-bottom: 1px solid var(--dcx-hooks-rule);
scrollbar-width: thin;
}
.dcx-detail-article--hooks .dcx-hooks-family {
position: relative;
display: inline-flex;
flex: 0 0 auto;
min-height: 62px;
align-items: center;
gap: 9px;
padding: 0;
border: 0;
color: var(--ks-text-muted);
background: transparent;
text-align: left;
cursor: pointer;
}
.dcx-detail-article--hooks .dcx-hooks-family::after {
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 1px;
background: transparent;
content: "";
}
.dcx-detail-article--hooks .dcx-hooks-family:hover {
color: var(--ks-patina);
}
.dcx-detail-article--hooks .dcx-hooks-family.is-active {
color: var(--ks-kinpaku);
}
.dcx-detail-article--hooks .dcx-hooks-family.is-active::after {
background: var(--ks-kinpaku);
}
.dcx-detail-article--hooks .dcx-hooks-family:focus-visible {
outline: 2px solid var(--ks-kinpaku);
outline-offset: -3px;
}
.dcx-detail-article--hooks .dcx-hooks-family-name {
font: 600 0.92rem/1.35 var(--ks-font);
white-space: nowrap;
}
.dcx-detail-article--hooks .dcx-hooks-family-count {
color: var(--ks-text-muted);
font: 500 0.68rem/1.35 var(--ks-mono);
white-space: nowrap;
}
.dcx-detail-article--hooks .dcx-hooks-rule-panel {
min-width: 0;
}
.dcx-detail-article--hooks .dcx-hooks-toolbar {
display: flex;
min-height: 68px;
align-items: end;
justify-content: space-between;
gap: 24px;
padding: 12px 0 14px;
border-bottom: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-search {
display: grid;
flex: 1 1 auto;
gap: 6px;
min-width: 0;
}
.dcx-detail-article--hooks .dcx-hooks-search input,
.dcx-detail-article--hooks .dcx-hooks-custom-form input,
.dcx-detail-article--hooks .dcx-hooks-custom-form select,
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea {
box-sizing: border-box;
width: 100%;
min-width: 0;
border: 1px solid var(--dcx-hooks-rule-strong);
border-radius: 0;
color: var(--ks-text);
background: transparent;
font: inherit;
}
.dcx-detail-article--hooks .dcx-hooks-search input {
height: 40px;
padding: 0 12px;
font-size: 0.88rem;
}
.dcx-detail-article--hooks .dcx-hooks-search input::placeholder,
.dcx-detail-article--hooks .dcx-hooks-custom-form input::placeholder,
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea::placeholder {
color: var(--ks-text-muted);
}
.dcx-detail-article--hooks .dcx-hooks-search input:focus,
.dcx-detail-article--hooks .dcx-hooks-custom-form input:focus,
.dcx-detail-article--hooks .dcx-hooks-custom-form select:focus,
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea:focus {
border-color: var(--ks-patina);
outline: 1px solid var(--ks-patina);
outline-offset: 0;
}
.dcx-detail-article--hooks .dcx-hooks-summary {
flex: 0 0 auto;
margin: 0 0 10px;
color: var(--ks-text-muted);
font: 500 0.68rem/1.4 var(--ks-mono);
letter-spacing: 0.04em;
}
.dcx-detail-article--hooks .dcx-hooks-discipline {
border-bottom: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-discipline > summary {
display: flex;
min-height: 56px;
align-items: center;
justify-content: space-between;
gap: 18px;
color: var(--ks-text);
list-style: none;
cursor: pointer;
}
.dcx-detail-article--hooks .dcx-hooks-discipline > summary::-webkit-details-marker {
display: none;
}
.dcx-detail-article--hooks .dcx-hooks-discipline > summary::before {
width: 7px;
height: 7px;
margin-right: 10px;
border-right: 1px solid currentColor;
border-bottom: 1px solid currentColor;
content: "";
transform: rotate(-45deg);
transition: transform 360ms var(--dcx-hooks-ease);
}
.dcx-detail-article--hooks .dcx-hooks-discipline[open]:not(.is-closing) > summary::before {
color: var(--ks-patina);
transform: rotate(45deg) translate(-2px, -2px);
}
.dcx-detail-article--hooks .dcx-hooks-discipline > summary > span:first-child {
flex: 1;
font: 600 0.94rem/1.4 var(--ks-font);
}
.dcx-detail-article--hooks .dcx-hooks-discipline > summary > span:last-child {
color: var(--ks-text-muted);
font: 500 0.68rem/1.4 var(--ks-mono);
}
.dcx-detail-article--hooks .dcx-hooks-discipline > summary:focus-visible {
outline: 2px solid var(--ks-kinpaku);
outline-offset: -2px;
}
.dcx-detail-article--hooks .dcx-hooks-disclosure {
display: block;
overflow: clip;
opacity: 1;
}
.dcx-detail-article--hooks .dcx-hooks-disclosure-inner {
min-height: 0;
}
.dcx-detail-article--hooks .dcx-hooks-rules {
margin: 0;
padding: 0 0 10px;
list-style: none;
}
.dcx-detail-article--hooks .dcx-hooks-rule {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 24px;
padding: 16px 0;
border-top: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-rule-copy {
display: grid;
min-width: 0;
}
.dcx-detail-article--hooks .dcx-hooks-rule-copy code {
overflow: hidden;
color: var(--ks-text-muted);
font: 500 0.68rem/1.45 var(--ks-mono);
text-overflow: ellipsis;
white-space: nowrap;
}
.dcx-detail-article--hooks .dcx-hooks-rule-copy strong {
color: var(--ks-champagne);
font: 600 0.96rem/1.4 var(--ks-font);
}
.dcx-detail-article--hooks .dcx-hooks-rule-copy p {
max-width: 64ch;
margin: 5px 0 0;
color: var(--ks-text-muted);
font-size: 0.88rem;
line-height: 1.58;
text-wrap: pretty;
}
.dcx-detail-article--hooks .dcx-hooks-rule > .dcx-hooks-switch {
margin-inline: 2px;
}
.dcx-detail-article--hooks .dcx-hooks-empty {
margin: 0;
padding: 32px 0;
color: var(--ks-text);
font-size: 0.92rem;
}
.dcx-detail-article--hooks .dcx-hooks-custom {
border-top: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-custom-toolbar {
display: flex;
min-height: 64px;
align-items: center;
justify-content: space-between;
gap: 24px;
border-bottom: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-custom-toolbar p {
margin: 0;
color: var(--ks-text);
font-size: 0.92rem;
line-height: 1.55;
}
.dcx-detail-article--hooks .dcx-hooks-button,
.dcx-detail-article--hooks .dcx-hooks-remove {
min-height: 42px;
border: 1px solid var(--dcx-hooks-rule-strong);
border-radius: 0;
color: var(--ks-text);
background: transparent;
font: 600 0.82rem/1 var(--ks-font);
cursor: pointer;
transition: border-color 180ms ease, background-color 180ms ease, color 180ms ease;
}
.dcx-detail-article--hooks .dcx-hooks-button {
padding: 0 17px;
}
.dcx-detail-article--hooks .dcx-hooks-button:hover {
border-color: var(--ks-text);
background: color-mix(in oklch, var(--ks-text) 8%, transparent);
}
.dcx-detail-article--hooks .dcx-hooks-button:focus-visible,
.dcx-detail-article--hooks .dcx-hooks-remove:focus-visible {
outline: 2px solid var(--ks-kinpaku);
outline-offset: 3px;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(170px, 0.75fr);
gap: 20px;
padding: 26px 0;
border-bottom: 1px solid var(--dcx-hooks-rule);
background: transparent;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form[hidden] {
display: none;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form label {
display: grid;
gap: 8px;
min-width: 0;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form input,
.dcx-detail-article--hooks .dcx-hooks-custom-form select {
height: 44px;
padding: 0 12px;
font-size: 0.9rem;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea {
min-height: 104px;
padding: 11px 12px;
font-size: 0.9rem;
line-height: 1.55;
resize: vertical;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form-description,
.dcx-detail-article--hooks .dcx-hooks-form-actions {
grid-column: 1 / -1;
}
.dcx-detail-article--hooks .dcx-hooks-form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.dcx-detail-article--hooks .dcx-hooks-custom-list {
border-bottom: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-custom-list:empty {
display: none;
}
.dcx-detail-article--hooks .dcx-hooks-custom-rule {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 24px;
padding: 18px 0;
border-bottom: 1px solid var(--dcx-hooks-rule);
}
.dcx-detail-article--hooks .dcx-hooks-custom-rule:last-child {
border-bottom: 0;
}
.dcx-detail-article--hooks .dcx-hooks-custom-discipline {
margin-top: 8px;
}
.dcx-detail-article--hooks .dcx-hooks-custom-controls {
display: flex;
align-items: center;
gap: 10px;
}
.dcx-detail-article--hooks .dcx-hooks-remove {
min-height: 36px;
padding: 0 11px;
color: var(--ks-text-muted);
font-size: 0.74rem;
}
.dcx-detail-article--hooks .dcx-hooks-storage-note {
margin: 14px 0 0;
color: var(--ks-text-muted);
font-size: 0.82rem;
line-height: 1.5;
}
@media (max-width: 780px) {
.dcx-detail-article--hooks .dcx-hooks-families {
gap: 24px;
}
.dcx-detail-article--hooks .dcx-hooks-family {
min-height: 58px;
}
}
@media (max-width: 560px) {
.dcx-detail-article--hooks .dcx-detail-section {
margin-bottom: 64px;
}
.dcx-detail-article--hooks .dcx-hooks-status {
gap: 18px;
}
.dcx-detail-article--hooks .dcx-hooks-flow {
grid-template-columns: minmax(0, 1fr);
}
.dcx-detail-article--hooks .dcx-hooks-flow > div {
padding-top: 18px;
}
.dcx-detail-article--hooks .dcx-hooks-flow > div + div {
padding-left: 0;
border-left: 0;
}
.dcx-detail-article--hooks .dcx-hooks-families {
gap: 22px;
overscroll-behavior-inline: contain;
}
.dcx-detail-article--hooks .dcx-hooks-toolbar,
.dcx-detail-article--hooks .dcx-hooks-custom-toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: stretch;
justify-content: stretch;
gap: 12px;
padding-block: 16px;
}
.dcx-detail-article--hooks .dcx-hooks-summary {
margin: 0;
}
.dcx-detail-article--hooks .dcx-hooks-rule,
.dcx-detail-article--hooks .dcx-hooks-custom-rule {
gap: 14px;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form {
grid-template-columns: minmax(0, 1fr);
padding: 22px 0;
}
.dcx-detail-article--hooks .dcx-hooks-custom-form label,
.dcx-detail-article--hooks .dcx-hooks-custom-form-description,
.dcx-detail-article--hooks .dcx-hooks-form-actions {
grid-column: 1;
}
.dcx-detail-article--hooks .dcx-hooks-form-actions {
justify-content: stretch;
}
.dcx-detail-article--hooks .dcx-hooks-form-actions .dcx-hooks-button {
flex: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.dcx-detail-article--hooks .dcx-hooks-switch > span,
.dcx-detail-article--hooks .dcx-hooks-switch > span::after,
.dcx-detail-article--hooks .dcx-hooks-discipline > summary::before {
transition: none;
}
}
/* ============================================================
Live wiring: the exceptions form's kind-driven fields and the
Apply bar. State loads from and lands in the project's
.impeccable/config.json through the doc session; the bar shows
only while the draft and the project disagree.
============================================================ */
.dcx-detail-article--hooks .dcx-hooks-custom-form label[hidden] {
display: none;
}
.dcx-detail-article--hooks .dcx-hooks-kind-hint {
grid-column: 1 / -1;
margin: -6px 0 0;
font-size: 0.82rem;
line-height: 1.5;
color: var(--ks-text-muted);
}
.dcx-detail-article--hooks .dcx-hooks-custom-reason {
margin: 2px 0 0;
font-size: 0.82rem;
font-style: italic;
color: var(--ks-text-faint);
}
.dcx-detail-article--hooks .dcx-hooks-switch input:disabled + span,
.dcx-detail-article--hooks .dcx-hooks-button:disabled {
opacity: 0.45;
cursor: default;
}
.dcx-detail-article--hooks .dcx-hooks-applybar {
position: sticky;
bottom: 18px;
z-index: 4;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 26px;
padding: 14px 18px;
border: 1px solid var(--ks-rule);
border-radius: 12px;
background: var(--ks-graphite-2);
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.35);
}
.dcx-detail-article--hooks .dcx-hooks-applybar[hidden] {
display: none;
}
.dcx-detail-article--hooks .dcx-hooks-applybar-copy {
margin: 0;
font-size: 0.88rem;
color: var(--ks-text);
}
.dcx-detail-article--hooks .dcx-hooks-applybar-actions {
display: flex;
gap: 10px;
flex-shrink: 0;
}
@media (max-width: 560px) {
.dcx-detail-article--hooks .dcx-hooks-applybar {
flex-direction: column;
align-items: stretch;
text-align: center;
}
.dcx-detail-article--hooks .dcx-hooks-applybar-actions .dcx-hooks-button {
flex: 1;
}
}
+261
View File
@@ -0,0 +1,261 @@
/* Shared material rail for every Design Context category. */
.dcx-nav.dcx-material-rail-host {
--dcx-rail-text-gap: 18px;
--dcx-rail-child-indent: 14px;
--dcx-rail-item-padding-block: 8px;
--dcx-rail-motion-duration: 520ms;
--dcx-rail-motion-easing: cubic-bezier(0.4, 0, 0.2, 1);
--dcx-rail-category-duration: 520ms;
--dcx-rail-category-easing: cubic-bezier(0.4, 0, 0.2, 1);
--dcx-rail-branch-duration: 320ms;
position: relative;
isolation: isolate;
}
.dcx-material-rail-host .dcx-nav-label,
.dcx-material-rail-host .dcx-nav-list {
position: relative;
z-index: 1;
}
.dcx-material-rail-host .dcx-nav-link {
padding-inline-start: var(--dcx-rail-text-gap);
border-left-color: color-mix(in oklch, var(--ks-patina) 42%, transparent);
}
.dcx-material-rail-host .dcx-subnav {
padding: 2px 0 8px var(--dcx-rail-child-indent);
}
@media (min-width: 921px) {
.dcx-expander[data-dcx-document="true"] .dcx-material-rail-host .dcx-subnav {
display: grid;
gap: 2px;
max-height: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(-4px);
animation: none;
transition:
max-height var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
padding-top var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
padding-bottom var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
opacity 240ms var(--dcx-rail-category-easing),
transform 360ms var(--dcx-rail-category-easing),
visibility 0s linear var(--dcx-rail-category-duration);
}
.dcx-expander[data-dcx-document="true"] .dcx-material-rail-host li.is-active > .dcx-subnav {
max-height: var(--dcx-subnav-height, 24rem);
padding-top: 2px;
padding-bottom: 8px;
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateY(0);
transition-delay: 0s;
}
.dcx-expander[data-dcx-document="true"] .dcx-material-rail-host li.is-active > .dcx-subnav--summary {
gap: 0;
padding-top: 4px;
padding-bottom: 10px;
}
}
.dcx-material-rail-host .dcx-sub-link {
padding: var(--dcx-rail-item-padding-block) 0;
padding-inline-start: var(--dcx-rail-text-gap);
border: 0;
border-left: 2px solid color-mix(in oklch, var(--ks-patina) 42%, transparent);
line-height: 1.45;
transition:
color 160ms var(--ks-ease),
border-color 160ms var(--ks-ease);
}
.dcx-material-rail-host .dcx-sub-link[aria-current="location"] {
color: var(--accent);
font-weight: 600;
}
.dcx-material-rail-host .dcx-subnav--summary {
gap: 0;
padding-top: 4px;
padding-bottom: 10px;
}
.dcx-material-rail-host .dcx-subnav--summary .dcx-sub-link {
color: var(--ks-text-muted);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.dcx-material-rail-host .dcx-subnav--summary .dcx-sub-link[aria-current="location"] {
color: var(--accent);
}
.dcx-material-rail {
position: absolute;
z-index: 0;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
}
.dcx-material-rail__patina-base {
fill: var(--ks-patina);
}
.dcx-material-rail__gold-base {
fill: var(--ks-kinpaku);
}
.dcx-material-rail__patina-image {
opacity: 0.68;
filter: saturate(1.18) contrast(1.06);
}
.dcx-material-rail__gold-image {
opacity: 0.7;
filter: saturate(1.15) contrast(1.08);
}
.dcx-material-rail__track,
.dcx-material-rail__active,
.dcx-material-rail__transition-track,
.dcx-material-rail__transition-active {
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.dcx-material-rail__track {
stroke: var(--ks-patina);
stroke: url("#dcx-material-rail-patina") var(--ks-patina);
opacity: 0;
transition: opacity var(--dcx-rail-category-duration) var(--dcx-rail-category-easing);
}
.dcx-material-rail-host[data-rail-ready] .dcx-material-rail__track {
opacity: 0.58;
}
.dcx-material-rail__track[data-entering="true"] {
opacity: 0 !important;
transition: none !important;
}
.dcx-material-rail__active {
stroke: var(--accent);
stroke: url("#dcx-material-rail-gold") var(--accent);
opacity: 0;
transition:
stroke-dasharray var(--dcx-rail-motion-duration) var(--dcx-rail-motion-easing),
stroke-dashoffset var(--dcx-rail-motion-duration) var(--dcx-rail-motion-easing),
opacity 180ms ease;
}
.dcx-material-rail__active[data-ready="true"] {
opacity: 1;
}
.dcx-material-rail__active[data-initial="true"] {
transition-property: opacity;
}
.dcx-material-rail__active[data-entering="true"] {
opacity: 0 !important;
transition: none !important;
}
.dcx-material-rail__transition-track {
opacity: 0;
transition: opacity var(--dcx-rail-branch-duration) var(--dcx-rail-category-easing);
}
.dcx-material-rail__transition-active {
opacity: 0;
}
.dcx-material-rail__transition-track[data-visible="true"] {
opacity: 0.58;
}
.dcx-material-rail__transition-active[data-visible="true"] {
opacity: 1;
}
.dcx-material-rail__transition-track[data-initial="true"],
.dcx-material-rail__transition-active[data-initial="true"] {
transition: none !important;
}
.dcx-material-rail__transition-active {
transform-box: fill-box;
transform-origin: center;
transition:
transform var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
opacity 180ms ease;
}
.dcx-material-rail__track[data-morphing="true"] {
opacity: 0.58 !important;
transition: none !important;
}
.dcx-material-rail__transition-active[data-settling="true"] {
transition:
transform var(--dcx-rail-branch-duration) var(--dcx-rail-category-easing),
opacity 180ms ease;
}
.dcx-material-rail__active[data-handoff="true"],
.dcx-material-rail__transition-active[data-handoff="true"] {
transition: none !important;
}
.dcx-material-rail-host[data-rail-ready] .dcx-nav-link,
.dcx-material-rail-host[data-rail-ready] .dcx-sub-link,
.dcx-material-rail-host[data-rail-ready] .dcx-nav-link:hover,
.dcx-material-rail-host[data-rail-ready] .dcx-nav-link:focus-visible,
.dcx-material-rail-host[data-rail-ready] .dcx-sub-link:hover,
.dcx-material-rail-host[data-rail-ready] .dcx-sub-link:focus-visible {
border-left-color: transparent;
}
@media (max-width: 920px) {
.dcx-material-rail {
display: none;
}
.dcx-material-rail-host .dcx-nav-link {
padding: 8px 10px;
border-left: 0;
}
.dcx-material-rail-host .dcx-subnav {
display: none !important;
}
}
@media (prefers-reduced-motion: reduce) {
.dcx-material-rail-host *,
.dcx-material-rail-host *::before,
.dcx-material-rail-host *::after {
scroll-behavior: auto !important;
transition-duration: 0.001ms !important;
transition-delay: 0ms !important;
}
}
+263
View File
@@ -0,0 +1,263 @@
.dcx-topbar-actions {
display: flex;
align-items: center;
flex: none;
gap: 10px;
margin-left: auto;
}
.dcx-topbar-actions .dcx-close:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.dcx-settings-trigger,
.dcx-settings-close {
width: 40px;
height: 40px;
border: 1px solid var(--ks-rule);
border-radius: 999px;
background: transparent;
color: var(--ks-text);
cursor: pointer;
display: grid;
flex: none;
place-items: center;
transition:
color 160ms var(--ks-ease),
border-color 160ms var(--ks-ease),
transform 220ms var(--ks-ease);
}
.dcx-settings-trigger svg,
.dcx-settings-close svg {
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.45;
}
.dcx-settings-trigger:hover,
.dcx-settings-trigger:focus-visible,
.dcx-settings-close:hover,
.dcx-settings-close:focus-visible {
border-color: var(--accent-line);
color: var(--accent);
outline: none;
}
.dcx-settings-trigger:hover svg,
.dcx-settings-trigger:focus-visible svg {
transform: rotate(24deg);
}
.dcx-settings-trigger svg {
transition: transform 260ms var(--ks-ease);
}
.dcx-settings-close:hover,
.dcx-settings-close:focus-visible {
transform: rotate(90deg);
}
.dcx-settings-trigger:focus-visible,
.dcx-settings-close:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.dcx-settings-modal {
width: min(680px, calc(100vw - 24px));
max-width: none;
max-height: calc(100svh - 24px);
margin: auto;
padding: 0;
overflow: hidden;
border: 1px solid var(--ks-rule);
border-radius: 6px;
background: var(--ks-lacquer);
color: var(--ks-text);
box-shadow: 0 32px 90px oklch(0% 0 0 / 0.64);
opacity: 0;
transform: translateY(12px) scale(0.985);
transition:
opacity 180ms ease,
transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
}
.dcx-settings-modal::backdrop {
background: oklch(0% 0 0 / 0);
backdrop-filter: blur(0);
transition:
background-color 180ms ease,
backdrop-filter 240ms ease;
}
.dcx-settings-modal.is-visible {
opacity: 1;
transform: translateY(0) scale(1);
}
.dcx-settings-modal.is-visible::backdrop {
background: oklch(0% 0 0 / 0.76);
backdrop-filter: blur(5px);
}
.dcx-settings-panel {
box-sizing: border-box;
min-width: 0;
padding: clamp(24px, 3vw, 30px);
}
.dcx-settings-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 24px;
padding-bottom: clamp(20px, 2.6vw, 26px);
}
.dcx-settings-head h2 {
margin: 0;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: clamp(1.45rem, 2.6vw, 1.75rem);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.015em;
}
.dcx-settings-head p {
max-width: 52ch;
margin: 10px 0 0;
color: var(--ks-text-muted);
font-size: 0.94rem;
line-height: 1.6;
}
.dcx-settings-commands {
display: grid;
gap: 20px;
margin: 0;
padding-top: 24px;
border-top: 1px solid var(--ks-rule);
}
.dcx-settings-modal .dcx-command-copy {
border-color: var(--ks-rule);
}
.dcx-settings-command {
display: grid;
gap: 12px;
padding: 0 0 20px;
border-bottom: 1px solid var(--ks-rule);
}
.dcx-settings-command-copy {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: baseline;
gap: 5px;
min-width: 0;
}
.dcx-settings-command h3 {
margin: 0;
color: var(--ks-champagne);
font-family: var(--ks-font);
font-size: 1.05rem;
font-weight: 600;
line-height: 1.35;
}
.dcx-settings-command p {
margin: 0;
color: var(--ks-text-muted);
font-size: 0.86rem;
line-height: 1.45;
}
.dcx-settings-command:last-child {
padding-bottom: 0;
border-bottom: 0;
}
@media (max-height: 820px) {
.dcx-settings-panel {
padding-block: 18px;
}
.dcx-settings-head {
padding-bottom: 16px;
}
.dcx-settings-head p {
margin-top: 6px;
line-height: 1.45;
}
.dcx-settings-commands {
gap: 12px;
padding-top: 16px;
}
.dcx-settings-command {
gap: 8px;
padding-bottom: 12px;
}
.dcx-settings-command-copy {
gap: 2px;
}
}
@media (max-width: 620px) {
.dcx-topbar-actions {
gap: 8px;
}
.dcx-settings-modal {
width: calc(100vw - 24px);
max-height: calc(100svh - 24px);
border-radius: 4px;
}
.dcx-settings-panel {
padding: 22px 20px 18px;
}
.dcx-settings-head {
gap: 18px;
padding-bottom: 22px;
}
.dcx-settings-head h2 {
font-size: 1.4rem;
}
.dcx-settings-head p {
font-size: 0.88rem;
}
.dcx-settings-command {
padding-bottom: 18px;
}
.dcx-settings-command-copy {
gap: 5px;
}
}
@media (prefers-reduced-motion: reduce) {
.dcx-settings-modal,
.dcx-settings-modal::backdrop,
.dcx-settings-trigger,
.dcx-settings-trigger svg,
.dcx-settings-close {
transition: none;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+531
View File
@@ -0,0 +1,531 @@
/* ============================================================
Screen 08: boundary style.
What tells the reader one section has ended and the next has begun. Four
answers, in order of how much material they spend: nothing, a line, a
change of ground, an edge you can see.
Bands are mixed from the artboard's own ink and ground rather than named
colors, so a drenched page separates with a lighter wash of its primary
instead of turning grey.
The tokens moved here are section-scale, and they are the grounds and edges
themselves. Depth works at element scale and only ever adds to what is set
here, as a shadow or as a tonal overlay, so the two answers compose rather
than overwrite each other.
============================================================ */
/* The four answers differ in material, not in vertical rhythm: a tint, a
hairline, an inset, an edge. None of them pads the proof band, because on the
desktop artboard that band is a fixed grid row and the row heights belong to
the layout answer on screen 07. Padding it was not buying air, it was
claiming room the row did not have: the copy overflowed the padding box
downward, which is why a tinted band read as squeezed and sat low in its own
tint, and why the open answer quietly stretched the row and shifted every
section under it. */
/* Open space
Every rule off and every ground the same, so the only thing left holding
the sections apart is the air between them. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-preview-boundaries,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked)) [data-carry] {
--pvs-divider-w: 0px;
--pvs-band: transparent;
--pvs-band-alt: transparent;
--pvs-band-top: transparent;
--pvs-panel: transparent;
--pvs-panel-edge-w: 0px;
--pvs-panel-pad: 0px;
--pvs-proof-inset: 0px;
}
/* Thin dividers
One hairline per boundary and nothing else: at the nav, above and below
the proof band, between its columns, and over the footer. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-preview-boundaries,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked)) [data-carry] {
--pvs-divider-w: 1px;
--pvs-divider-c: color-mix(in oklab, var(--pvs-ink) 24%, var(--pvs-ground));
--pvs-band: transparent;
--pvs-band-alt: transparent;
--pvs-band-top: transparent;
--pvs-panel: transparent;
--pvs-panel-edge-w: 0px;
--pvs-panel-pad: 0px;
--pvs-proof-inset: 0px;
}
/* Surface changes
No lines at all. Chrome sits on one tone, the proof band on another, and
the boundary is wherever the ground changes under your eye. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-preview-boundaries,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] {
--pvs-divider-w: 0px;
--pvs-band: color-mix(in oklab, var(--pvs-ink) 7%, var(--pvs-ground));
--pvs-band-alt: color-mix(in oklab, var(--pvs-ink) 13%, var(--pvs-ground));
--pvs-band-top: color-mix(in oklab, var(--pvs-ink) 7%, var(--pvs-ground));
--pvs-panel: transparent;
--pvs-panel-edge-w: 0px;
--pvs-panel-pad: 0px;
--pvs-proof-inset: 0px;
}
/* Cards and panels
Content moves inside containers. The proof band pulls in off the page
margins and grows an edge, the cards do the same, and the page reads as a
set of objects on a ground rather than a run of sections. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-preview-boundaries,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] {
--pvs-divider-w: 0px;
--pvs-band: transparent;
--pvs-band-alt: color-mix(in oklab, var(--pvs-ink) 5%, var(--pvs-ground));
--pvs-band-top: transparent;
--pvs-panel: color-mix(in oklab, var(--pvs-ink) 5%, var(--pvs-ground));
--pvs-panel-edge: color-mix(in oklab, var(--pvs-ink) 20%, var(--pvs-ground));
--pvs-panel-edge-w: 1px;
--pvs-panel-pad: 5%;
--pvs-proof-inset: var(--pvs-gutter);
}
/* The proof band is the one section that can become a container without the
page being restructured, so it carries the difference between a tinted
band and a panel: the inset pulls it off the page margins and the edge
only exists where an inset was asked for. */
[data-carry] .ps-desktop > .ps-proof {
margin-inline: var(--pvs-proof-inset, 0px);
border: var(--pvs-panel-edge-w) solid var(--pvs-panel-edge);
border-radius: var(--pvs-radius-surface);
}
/* The handset's chrome carries its own tint in the base artboard, which is a
boundary the open answers did not ask for. Both ends take the band. */
[data-carry] .ps-phone-top,
[data-carry] .ps-phone-footer {
background-color: var(--pvs-band-top, var(--pvs-surface));
}
/* Percentage padding resolves against the card's own width, so one value
holds at both artboard sizes instead of swamping the handset. */
[data-carry] .ps-gallery-item {
padding: var(--pvs-panel-pad, 0px);
}
/* The handset's card row is already the block closest to the footer, so it
takes a shorter version of the same padding rather than growing into it. */
[data-carry] .ps-phone-body .ps-gallery-item {
padding: calc(var(--pvs-panel-pad, 0px) * 0.5);
}
/* The three region bodies
A shell, a document, and an index are separated in different places from a
landing page, so the same four answers are spent on different parts. What is
shared is the material: the answers above set a hairline, a ground, or an edge,
and each body below reads the one it has a use for.
The dividing rules on these bodies were hardcoded hairlines, which is the
thing this screen is asking about. A shell with a permanent rule down its rail
answers Open space with a line still on screen. */
[data-carry] :is(.ps-ops-rail, .ps-ops-main, .ps-docs-rail) {
border-right: var(--pvs-divider-w) solid var(--pvs-divider-c);
}
/* A table is a run of sections stacked, so the hairline answer rules it. The
plot's own baseline is not in here: an axis is part of the chart's drawing and
not a boundary between two things. */
[data-carry] .ps-ops-table {
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
}
[data-carry] .ps-ops-row + .ps-ops-row {
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
}
[data-carry] .ps-index-row + .ps-index-row,
[data-carry] .ps-index-rail {
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
}
[data-carry] :is(.ps-index-row + .ps-index-row, .ps-index-rail) {
padding-top: calc(var(--pvs-divider-w) * 8);
}
/* Grounds. Each one fills its own region and stops at the page margin rather than
bleeding to the frame edge. That is the same claim the layout screen's header
makes about these boards: their regions sit on the page grid, so the rail's
ground starts where the rail starts, on the first column line. Reaching the
edge would need the margin restated as a share of a different box, and the two
percentages do not resolve against the same width, so the tone would land a
point or two off the very line the board is drawn to check. */
[data-carry] :is(.ps-ops-rail, .ps-ops-panel, .ps-docs-rail) {
background-color: var(--pvs-band);
}
/* Alternating grounds are what a run of entries has instead of bands: the second
entry sits on a different tone from the first, and the boundary is where the
tone changes. */
[data-carry] .ps-index-row--flip {
background-color: var(--pvs-band-alt);
}
/* A table's heading row is the one place on a shell where a change of ground
does the whole job a rule would have done. */
[data-carry] .ps-ops-row--head {
background-color: var(--pvs-band-alt);
}
/* The callout is the document's only container, so it is where this screen's
answer lands. Its accent edge is not part of the answer: that is what marks
the block as a callout, and it stays under all three. */
[data-carry] .ps-docs-note {
padding: calc(var(--pt-base) * 0.7) calc(var(--pt-base) * 0.9);
background-color: var(--pvs-band-alt);
border-left: calc(var(--pt-base) * 0.25) solid var(--pvs-accent-c);
}
/* Cards, on a tool. Offered to Persuade and Operate only, so a shell is the one
region body that draws it: the chart, the table, and the side panel become
objects on a ground, and the rules that had been dividing them go, because an
edge and a hairline saying the same thing twice is the fault the answer after
Open space is meant to avoid. */
[data-carry] :is(.ps-ops-chart, .ps-ops-table, .ps-ops-panel) {
background-color: var(--pvs-panel);
border: var(--pvs-panel-edge-w) solid var(--pvs-panel-edge);
}
[data-carry] :is(.ps-ops-chart, .ps-ops-table) {
padding: var(--pvs-panel-pad, 0px);
}
/* An open rail plate is a state and reads as one already, so under the card
answer the rest of the rail joins it rather than the plate growing an edge. */
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-ops-item,
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-preview-boundaries .ps-ops-item {
background-color: var(--pvs-panel);
}
/* Gallery parity (plan-18): the persuade artboard kept on the boundaries and
corners screens flattens its shadows and re-grids the proof strip, ported
from the gallery's previews-38-41-proof-spacing block. Scoped to the two
screens' own stages; document proofs and hub clones keep carried shadows
through the dcx mirrors. */
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] :is(*, *::before, *::after) {
box-shadow: none !important;
text-shadow: none !important;
}
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-desktop .ps-proof {
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
}
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-proof {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
position: relative;
top: 10px;
}
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-editorial-copy {
margin-top: 16px !important;
}
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-desktop .ps-proof-item {
padding-right: 24px !important;
}
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof-divider {
display: none !important;
}
/* Cells 40 and 41 only: band washes under surface-changes, and both band
rules under cards-and-panels, exactly the gallery's cellNum conditionals. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof,
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof {
background-color: color-mix(in oklab, var(--pvs-band-alt) 65%, white) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer,
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer {
background-color: color-mix(in oklab, var(--pvs-band) 65%, white) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-proof,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-proof {
column-gap: 16px !important;
padding-right: var(--pvs-gutter) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-gallery-item,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-gallery-item {
background: transparent !important;
}
/* ==== Derived stage (plan-18): screens 08, 09, 10 mount strategy-preview
clones for the surfaces the gallery derives. Geometry mirrors the artboard
footprint; colors are locked to the restrained base for operate, read and
experience clones, matching the gallery's baseCellId map. ==== */
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview {
grid-row: 2;
aspect-ratio: 1.865 / 1;
width: 100%;
--pv-primary: var(--pkc-primary, var(--ks-champagne));
--pv-secondary: var(--pkc-secondary, var(--ks-patina));
--pv-tertiary: var(--pkc-tertiary, var(--ks-kinpaku));
--pv-neutral: var(--pkc-neutral, var(--ks-lacquer-raised));
--pv-n-ink: var(--pkc-n-ink, var(--ks-champagne));
--pv-p-ink: var(--pkc-p-ink, var(--ks-champagne));
--derived-rule: color-mix(in oklab, var(--pv-strong) 24%, var(--pv-neutral));
--derived-band: color-mix(in oklab, var(--pv-strong) 7%, var(--pv-neutral));
--derived-band-alt: color-mix(in oklab, var(--pv-strong) 13%, var(--pv-neutral));
--derived-panel: color-mix(in oklab, var(--pv-strong) 5%, var(--pv-neutral));
--derived-panel-edge: color-mix(in oklab, var(--pv-strong) 20%, var(--pv-neutral));
}
/* Restrained lock for the non-persuade clones, mirrored from the strategy
remap so the derived boards hold the gallery's base regardless of the
run's strategy answer. */
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview:not([data-surface="persuade"]) {
--pv-secondary: color-mix(in oklab, var(--pkc-neutral) 78%, var(--pkc-n-ink));
--pv-tertiary: color-mix(in oklab, var(--pkc-neutral) 86%, var(--pkc-n-ink));
--pv-t-ink: var(--pkc-n-ink);
--pv-t-on-t: var(--pkc-n-ink);
--pv-t-on-n: color-mix(in oklab, var(--pkc-n-ink) 62%, var(--pkc-neutral));
--pv-t-on-p: var(--pkc-p-ink);
}
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview.picker-preview--docs {
--pd-quote: var(--pd-ink);
}
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview.picker-preview--docs :is(.pd-note-dot, .pd-note-lines i) {
background: var(--pd-ink);
}
/* Wireframe weights. The gallery derives these boards from the strategy
stage, so the ops, docs and portfolio clones carry that stage's 1.35 desktop
step and nothing else; their bar geometry stays the authored base. */
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview:is(.picker-preview--ops, .picker-preview--docs, .picker-preview--gallery) .pv-desktop {
--pv-text: 1.35;
}
/* Boundaries read (gallery cells 45 to 47) is the one derived group the
gallery reweights: both viewports step to 1.65 and the rail dot and note dot
widen. Corners read and depth read draw at the base weight. */
.picker-screen[data-screen="08"] .picker-board-stage > .picker-preview.picker-preview--docs :is(.pv-desktop, .pv-phone) {
--pv-text: 1.65;
}
.picker-screen[data-screen="08"] .picker-board-stage > .picker-preview.picker-preview--docs .pd-item i { width: calc(4.07 * var(--pv-x)); }
.picker-screen[data-screen="08"] .picker-board-stage > .picker-preview.picker-preview--docs .pd-note-dot { width: calc(4.07 * var(--pv-x)); }
/* Experience cap avatars on derived boards, from the gallery derived sheet. */
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview--gallery .pv-desktop .pg-cap { padding-top: 5.2cqh; gap: 2.6cqh; }
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview--gallery .pv-desktop .pg-cap::before {
content: "";
display: block;
width: calc(11.5 * var(--pv-x));
aspect-ratio: 1;
border: 1px solid color-mix(in oklab, var(--pv-secondary) 55%, var(--pv-neutral));
border-radius: 50%;
background: linear-gradient(135deg, color-mix(in oklab, var(--pv-secondary) 24%, var(--pv-neutral)), color-mix(in oklab, var(--pv-secondary) 46%, var(--pv-neutral)));
}
/* Phone field chevron alignment on ops clones, from the gallery derived sheet
(cells 44 and 55-58; the corners screen applies it under every option). */
:is(.picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview .pv-phone .po-field > .po-chev { align-self: center; transform: translateY(-25%) rotate(45deg); }
/* Boundary additions on derived clones: the base geometry stays fixed; only
separation changes. Ported from the gallery derived sheet. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side) {
border-color: transparent !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-divider, .pg-row--flip, .pg-rail),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-divider, .pg-row--flip, .pg-rail) {
border-color: transparent !important;
box-shadow: none !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
background: transparent !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:is(.picker-preview--ops, .picker-preview--docs, .picker-preview--gallery) .pv-nav,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:is(.picker-preview--ops, .picker-preview--docs, .picker-preview--gallery) .pv-nav {
border-bottom: 1px solid var(--derived-rule) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-main, .pd-side),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-main, .pd-side) {
border-right: 1px solid var(--derived-rule) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
background: var(--derived-rule) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-row + .po-row,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-row + .po-row {
box-shadow: inset 0 1px 0 var(--derived-rule);
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-row--flip,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-row--flip {
box-shadow: inset 0 1px 0 var(--derived-rule);
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-rail,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-rail {
border-top: 1px solid var(--derived-rule);
}
/* Cell 49: experience thin-dividers redraws its flip row and phone image
separations by pseudo-element rules. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip {
position: relative;
margin-top: 10cqh !important;
box-shadow: none !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip::before,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip::before {
content: "";
position: absolute;
top: -5cqh;
right: 0;
left: 0;
height: 1px;
background: var(--derived-rule);
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail,
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail {
border-top: 0 !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image {
margin-top: 8cqh !important;
overflow: visible;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image::before,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image::before {
content: "";
position: absolute;
top: -4cqh;
right: 0;
left: 0;
height: 1px;
background: var(--derived-rule);
}
/* Cell 50: experience surface-changes. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip {
margin-top: 5cqh !important;
padding-block: 5cqh !important;
box-shadow: none !important;
background: color-mix(in oklab, var(--derived-band-alt) 60%, white 40%) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image {
margin-top: 8cqh !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body {
background: linear-gradient(to bottom, transparent 0 50%, color-mix(in oklab, var(--derived-band-alt) 60%, white 40%) 50% 100%) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pv-phone .pv-tabbar,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pv-phone .pv-tabbar {
background: color-mix(in oklab, var(--derived-band-alt) 60%, white 40%) !important;
}
/* Surface changes on every derived clone. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side) {
border-color: transparent !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .pv-phone-top, .pv-tabbar),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .pv-phone-top, .pv-tabbar) {
background: var(--derived-band) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-panel, .pd-side),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-panel, .pd-side) {
background: var(--derived-band) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) :is(.pd-note, .pg-row--flip),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) :is(.pd-note, .pg-row--flip) {
background: var(--derived-band-alt) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
background: transparent !important;
}
/* Cards and panels. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main) {
border-color: transparent !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
background: transparent !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-chart, .po-rows, .po-panel, .po-phone-body .po-chart, .po-phone-body .po-field, .po-phone-body .po-prow),
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-chart, .po-rows, .po-panel, .po-phone-body .po-chart, .po-phone-body .po-field, .po-phone-body .po-prow) {
background: var(--derived-panel) !important;
border: 1px solid var(--derived-panel-edge) !important;
border-radius: 3px;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-chart,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-chart {
padding-inline: calc(3 * var(--pv-x));
}
/* Cell 44: operate cards-and-panels details. */
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-desktop .po-chart,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-desktop .po-chart {
padding: calc(3 * var(--pv-x)) calc(5 * var(--pv-x)) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-toggle,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-toggle {
margin-left: auto !important;
margin-right: 12px !important;
padding-right: 12px !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-prow,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-prow {
padding-left: 12px !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-field > .po-chev,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-field > .po-chev {
align-self: center !important;
transform: translateY(-25%) rotate(45deg) !important;
}
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-rows,
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-rows {
padding-inline: calc(2 * var(--pv-x));
}
+81
View File
@@ -0,0 +1,81 @@
/* ============================================================
Screen 09: corner style.
One question, four radii, and a deliberate limit: the text bars barely move
and the dots do not move at all.
A bar in this wireframe stands for a line of type. Round it like a button
and it stops reading as text and starts reading as a tag, which would make
the pill answer look like a different page rather than the same page with
rounder corners. So bars top out a few pixels short of their own height.
The dots stay circular in every answer, Sharp included. A dot is a mark
rather than a shape with corners; squaring it changes what it is, not how
round it is, and the Sharp page reads as sharp without it.
============================================================ */
/* Sharp
Nothing rounded. Every corner is drafted. */
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="sharp"]) .picker-preview-corners,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="sharp"]:checked)) [data-carry] {
--pvs-radius-frame: 0px;
--pvs-radius-frame-phone: 0px;
--pvs-radius-surface: 0px;
--pvs-radius-control: 0px;
--pvs-radius-bar: 0px;
}
/* Slightly soft
Enough radius to take the edge off and not enough to be remarked on. */
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="slightly-soft"]) .picker-preview-corners,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="slightly-soft"]:checked)) [data-carry] {
--pvs-radius-frame: 4px;
--pvs-radius-frame-phone: 5px;
--pvs-radius-surface: 3px;
--pvs-radius-control: 3px;
--pvs-radius-bar: 2px;
}
/* Friendly
Cards, pictures, and buttons are clearly rounded, and the handset rounds
with them. */
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="friendly"]) .picker-preview-corners,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="friendly"]:checked)) [data-carry] {
--pvs-radius-frame: 7px;
--pvs-radius-frame-phone: 11px;
--pvs-radius-surface: 8px;
--pvs-radius-control: 8px;
--pvs-radius-bar: 3px;
}
/* Pill-like
Controls go fully round. Surfaces round hard but stay rectangles, because
a card at half its own height is no longer a card. */
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="pill"]) .picker-preview-corners,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="pill"]:checked)) [data-carry] {
--pvs-radius-frame: 9px;
--pvs-radius-frame-phone: 15px;
--pvs-radius-surface: 12px;
--pvs-radius-control: 999px;
--pvs-radius-bar: 3px;
}
/* Corner radii on the derived clones. Dots remain dots while frames,
surfaces, controls, and text bars take the selected radius. Ported from
the gallery derived sheet; values match the artboard radius ramp above. */
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="sharp"]) .picker-screen[data-screen="09"] .picker-board-stage,
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="sharp"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 0px; --derived-phone-radius: 0px; --derived-surface-radius: 0px; --derived-control-radius: 0px; --derived-bar-radius: 0px; }
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="slightly-soft"]) .picker-screen[data-screen="09"] .picker-board-stage,
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="slightly-soft"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 4px; --derived-phone-radius: 5px; --derived-surface-radius: 3px; --derived-control-radius: 3px; --derived-bar-radius: 2px; }
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="friendly"]) .picker-screen[data-screen="09"] .picker-board-stage,
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="friendly"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 7px; --derived-phone-radius: 11px; --derived-surface-radius: 8px; --derived-control-radius: 8px; --derived-bar-radius: 3px; }
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="pill"]) .picker-screen[data-screen="09"] .picker-board-stage,
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="pill"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 9px; --derived-phone-radius: 15px; --derived-surface-radius: 12px; --derived-control-radius: 999px; --derived-bar-radius: 3px; }
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .pv-desktop { border-radius: var(--derived-frame-radius) !important; }
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .pv-phone { border-radius: var(--derived-phone-radius) !important; }
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview :is(.pv-image, .pv-mini-card, .po-chart, .po-rows, .po-panel, .pg-row) { border-radius: var(--derived-surface-radius) !important; }
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .pd-note { border-radius: 0 var(--derived-surface-radius) var(--derived-surface-radius) 0 !important; }
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview :is(.pv-pill, .pv-actions i, .po-item--on, .po-field, .po-row, .po-toggle) { border-radius: var(--derived-control-radius) !important; }
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .po-toggle::after { border-radius: var(--derived-control-radius) !important; }
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview :is(.pv-nav-bars i, .pv-pill::after, .pv-eyebrow, .pv-headline i, .pv-copy-bar, .po-line, .po-cell, .po-heading, .po-label, .pd-item span, .pd-heading, .pd-para i, .pd-note-lines i, .pg-cap i, .pg-track i) { border-radius: var(--derived-bar-radius) !important; }
+173
View File
@@ -0,0 +1,173 @@
/* ============================================================
Screen 10: depth style.
How far off the page an element sits. Boundaries fills section grounds;
this file lifts individual elements, and the two answers compose instead
of arguing.
Every answer here is a cast shadow. They go in the --pvs-shadow-* slots and
need something solid under them, which is why the block at the bottom moves
the card's lift onto the picture inside it wherever the boundary answer left
the card transparent: a shadow around an invisible rectangle reads as a bug.
Shadows are sized for the artboard rather than for a real page. The frame
is about a third of browser width, so a blur that would read as gentle at
full size disappears here; the offsets and blurs below are already scaled.
They are also black rather than mixed from the artboard's ink. Ink follows
the ground, so on a drenched page an ink shadow turns pale and the lift
comes out as a glow. Light falls the same way whatever color the page is.
============================================================ */
/* Flat
One plane. Nothing casts. */
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="flat"]) .picker-preview-depth,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="flat"]:checked)) [data-carry] {
--pvs-shadow-chrome: none;
--pvs-shadow-card: none;
--pvs-shadow-control: none;
--pvs-shadow-surface: none;
}
/* Soft lift
Cards and controls come off the page by a millimetre. Two shadows on the
cards: a tight one for the contact edge, a wider one for the ambient. */
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="soft-lift"]) .picker-preview-depth,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="soft-lift"]:checked)) [data-carry] {
--pvs-shadow-chrome: 0 1px 2px rgb(0 0 0 / 0.1);
--pvs-shadow-card:
0 1px 1px rgb(0 0 0 / 0.15),
0 3px 7px rgb(0 0 0 / 0.14);
--pvs-shadow-control: 0 1px 2px rgb(0 0 0 / 0.24);
--pvs-shadow-surface: 0 2px 5px rgb(0 0 0 / 0.14);
}
/* Floating
A clear gap between the panels and the page. Offsets grow faster than
blurs, so the lift reads as height rather than as fog. */
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="floating"]) .picker-preview-depth,
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="floating"]:checked)) [data-carry] {
--pvs-shadow-chrome: 0 2px 6px rgb(0 0 0 / 0.16);
--pvs-shadow-card:
0 2px 3px rgb(0 0 0 / 0.2),
0 9px 18px rgb(0 0 0 / 0.24);
--pvs-shadow-control: 0 3px 6px rgb(0 0 0 / 0.32);
--pvs-shadow-surface: 0 6px 14px rgb(0 0 0 / 0.22);
}
/* Where a cast shadow is allowed to land
Only off something solid. Under the two boundary answers that give the
card and the proof band a ground, the section itself lifts. Under the two
that leave them transparent, the lift moves inward to the picture, which
is the only part of the card that is a surface in its own right. */
#picker-form:has(input[name="boundary-style"][value="cards-and-panels"]:checked) [data-carry] :is(.ps-gallery-item, .ps-desktop > .ps-proof),
#picker-form:has(input[name="boundary-style"][value="surface-changes"]:checked) [data-carry] .ps-desktop > .ps-proof {
box-shadow: var(--pvs-shadow-card);
}
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-gallery-item {
box-shadow: none;
}
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-gallery-item > i {
box-shadow: var(--pvs-shadow-card);
}
/* Chrome is a ground only under the surface answer, so that is the only one
where the bar at the top of the page casts. The footers are left out: a
footer's shadow would fall out of the frame and be clipped away, so the
only trace of it would be a hard line. */
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] :is(.ps-nav, .ps-phone-top) {
box-shadow: none;
}
/* The same rule, on the three region bodies
Each part is asked the same question the card above was: does the boundary
answer leave a ground under it. The answer differs part by part, because a
shell's regions, a document's callout, and an index's alternating band are
each filled by a different one of the four answers.
A rail and a document's rail take a ground only where sections are told apart
by tone, so that is the only answer they lift under. The side panel is filled
by that answer and by the card answer, so it lifts under both. */
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] :is(.ps-ops-rail, .ps-docs-rail) {
box-shadow: none;
}
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)):not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-ops-panel {
box-shadow: none;
}
/* The chart and the table are containers only under the card answer. Under the
others the lift moves inward to the plot's bars, which are the one part of a
chart that is a surface in its own right, exactly as the landing page's card
hands its lift to the picture inside it. */
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] :is(.ps-ops-chart, .ps-ops-table) {
box-shadow: none;
}
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-ops-plot > i {
box-shadow: var(--pvs-shadow-card);
}
/* The callout and the index's second entry are both filled by the tone answer
alone, so both are held to it. */
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] .ps-docs-note {
box-shadow: none;
}
#picker-form:has(input[name="boundary-style"][value="surface-changes"]:checked) [data-carry] .ps-index-row--flip {
box-shadow: var(--pvs-shadow-card);
}
/* Depth additions on derived clones. Flat is the base; the other answers
lift only solid, meaningful objects. Ported from the gallery derived
sheet; shadow alphas stay literal because they are shadows, not palette
roles. */
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="flat"]) .picker-screen[data-screen="10"] .picker-board-stage,
#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="flat"]:checked) .picker-screen[data-screen="10"] .picker-board-stage { --derived-shadow-chrome: none; --derived-shadow-card: none; --derived-shadow-control: none; --derived-shadow-surface: none; }
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="soft-lift"]) .picker-screen[data-screen="10"] .picker-board-stage,
#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="soft-lift"]:checked) .picker-screen[data-screen="10"] .picker-board-stage { --derived-shadow-chrome: 0 1px 2px rgb(0 0 0 / 0.08); --derived-shadow-card: 0 2px 4px rgb(0 0 0 / 0.1); --derived-shadow-control: 0 1px 2px rgb(0 0 0 / 0.1); --derived-shadow-surface: 0 2px 4px rgb(0 0 0 / 0.1); }
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="floating"]) .picker-screen[data-screen="10"] .picker-board-stage,
#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="floating"]:checked) .picker-screen[data-screen="10"] .picker-board-stage { --derived-shadow-chrome: 0 1px 3px rgb(0 0 0 / 0.1); --derived-shadow-card: 0 4px 8px rgb(0 0 0 / 0.14); --derived-shadow-control: 0 2px 4px rgb(0 0 0 / 0.12); --derived-shadow-surface: 0 4px 8px rgb(0 0 0 / 0.14); }
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.pv-nav, .pv-phone-top) { box-shadow: var(--derived-shadow-chrome) !important; }
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.pv-pill, .pv-actions i, .po-item--on, .po-field, .po-toggle) { box-shadow: var(--derived-shadow-control) !important; }
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.pv-image, .pv-mini-card, .pd-note) { box-shadow: var(--derived-shadow-card) !important; }
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview .pg-cap::before { box-shadow: var(--derived-shadow-card) !important; }
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.po-chart i, .po-side, .po-panel) { box-shadow: var(--derived-shadow-surface) !important; }
/* Depth persuade rode the gallery's drenched base cell unconditionally, which
painted the board in the primary for every run. The swap now applies only
when the run actually chose the drenched strategy; otherwise the committed
base from the shared 08/09/10 map (boundaries.css) carries the board. */
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] {
--pv-neutral: var(--pkc-primary);
--pv-n-ink: var(--pkc-p-ink);
--pv-primary: var(--pkc-p-ink);
--pv-p-ink: var(--pkc-primary);
--pv-p-on-n: var(--pkc-p-ink);
--pv-p-on-p: var(--pkc-primary);
--pv-t-on-n: var(--pkc-t-on-p);
--pv-secondary: var(--pkc-secondary);
--pv-tertiary: var(--pkc-tertiary);
--pv-bone: color-mix(in oklab, var(--pkc-p-ink) 55%, var(--pkc-primary));
--pv-strong: var(--pkc-p-ink);
--pv-secondary-wash: color-mix(in oklab, var(--pkc-p-ink) 16%, var(--pkc-primary));
}
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-image {
background: linear-gradient(135deg, color-mix(in oklab, var(--pkc-p-ink) 16%, var(--pkc-primary)), color-mix(in oklab, var(--pkc-secondary) 34%, var(--pkc-primary))) !important;
border-color: color-mix(in oklab, var(--pkc-secondary) 52%, var(--pkc-primary)) !important;
}
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-image::after { background: var(--pkc-tertiary) !important; }
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-actions i:last-child {
border-color: color-mix(in oklab, var(--pkc-secondary) 68%, var(--pkc-p-ink)) !important;
background: color-mix(in oklab, var(--pkc-secondary) 18%, var(--pkc-primary)) !important;
}
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-nav-bars i:nth-child(2) {
background: color-mix(in oklab, var(--pkc-secondary) 72%, var(--pkc-p-ink)) !important;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+213
View File
@@ -0,0 +1,213 @@
/*
* impeccable.style Neo kinpaku tokens (page-global, single source of truth)
*
* Every brand-bearing color, surface, text color, hairline rule, type-scale
* value, and motion easing used on the site lives here. New pages should read
* these tokens via var(--ks-*) rather than hand-typing oklch values.
*
* Pages may override individual tokens locally when there's a documented
* reason (e.g. .home-kinpaku bumps --ks-rule alpha for sharper UI borders
* against bento tiles). When in doubt, do not override match.
*
* Imported by design-system.css, home-kinpaku.css, and designing-kinpaku.css.
* If a new page-level stylesheet needs these tokens, @import this file at the
* top so the values are available at :root.
*/
:root {
/* color-scheme follows html.light / html.dark class on <html> */
/* ============================================================
Brand anchors. Two anchors carry the brand kinpaku gold for
commitment and brand voice, verdigris patina for state and
contrast. Everything else is supporting role.
============================================================ */
/* Kinpaku gold. The rich, warm, vibrant primary. */
--ks-kinpaku: oklch(84% 0.19 80.46); /* primary accent, wordmark, icon */
--ks-kinpaku-vivid: oklch(87% 0.20 85); /* hover on gold fills: lit, not washed */
--ks-kinpaku-pale: oklch(86% 0.07 84); /* pale fills, tints on dark */
--ks-kinpaku-rich: oklch(77% 0.13 82); /* active CTA, severity-medium */
--ks-kinpaku-deep: oklch(61% 0.085 78); /* borders against the brand */
--ks-dark-ink: oklch(14% 0.018 95); /* foreground on gold; never theme-remapped */
/* Verdigris patina. The secondary accent — state, signal, selection. */
--ks-patina: oklch(70% 0.12 188); /* secondary accent */
--ks-patina-pale: oklch(82% 0.07 188); /* hover lift on patina */
--ks-patina-deep: oklch(49% 0.08 188); /* deep oxide, dark variants */
/* Semantic foregrounds for state, selection, and interactive labels. */
--ks-state-ink: oklch(70% 0.12 188);
/* Warning — vermilion. Used sparingly, only for failures and warnings. */
--ks-vermilion: oklch(58% 0.15 35);
/* ============================================================
Surfaces. Warm-black lacquer carries everything.
============================================================ */
--ks-lacquer: oklch(7% 0.006 95); /* page ground */
--ks-lacquer-deep: oklch(4% 0.004 95); /* deepest inset */
--ks-lacquer-raised: oklch(11% 0.006 95); /* raised panels, inputs */
--ks-graphite: oklch(15% 0.008 95); /* inactive surfaces */
--ks-graphite-2: oklch(19% 0.008 95); /* one step up from graphite */
/* ============================================================
Text. The whole text ramp runs near-neutral and bright so copy,
headings, and labels read crisp on lacquer instead of mushing
into the warm floor. Warmth now lives only in the gold accents
and the surfaces, not in the type. (--ks-champagne keeps its name
for compatibility but is no longer warm.)
============================================================ */
--ks-champagne: oklch(91% 0 0); /* headlines, <strong> — neutral white */
--ks-text: oklch(88% 0 0); /* body — neutral near-white */
--ks-text-muted: oklch(72% 0 0); /* captions, meta */
--ks-text-faint: oklch(62% 0 0); /* subdued */
--ks-text-mute-deep: oklch(52% 0 0); /* disabled */
/* ============================================================
Rules. The hairline is neutral so borders/dividers/labels don't
carry warmth (warmth lives in the gold accents and surfaces). For a
gold accent border, use --ks-kinpaku directly, not a readability variant.
============================================================ */
--ks-rule: oklch(78% 0 0 / 0.16);
/* ============================================================
Code. One treatment site-wide. Inline code is a neutral graphite
chip; blocks / CLI commands are a deep lacquer terminal with a
neutral hairline. Gold is only for code that is itself a command
link (--ks-code-cmd). All code rules read these don't hand-type.
============================================================ */
--ks-code-fg: oklch(91% 0 0); /* inline code text */
--ks-code-bg: var(--ks-graphite-2); /* inline code chip */
--ks-code-radius: 3px;
--ks-code-pad: 0.3em 0.5em; /* inline code chip padding */
--ks-code-block-fg: oklch(86% 0 0); /* block / CLI text */
--ks-code-block-bg: var(--ks-lacquer-deep);
--ks-code-block-border: var(--ks-rule);
--ks-code-block-radius: 2px;
--ks-code-cmd: var(--ks-kinpaku); /* code that's a command link */
/* ============================================================
Typography two faces.
- --ks-font-display: Alumni Sans. A weightable humanist grotesque;
the hero h1 runs at a thin hairline (100) for elegance, section
h2s at a heavier cut (300) to anchor each block.
- --ks-font-wordmark: Alumni Sans, same face as display, held at
weight 400 for the brand lockup.
- --ks-font: Albert Sans. Quiet geometric humanist, used for body
and UI text. Pairs cleanly with the Alumni Sans display without
competing.
All loaded via Base.astro's Google Fonts link.
============================================================ */
--ks-font-display: "Alumni Sans", "Albert Sans", Arial, sans-serif;
--ks-font-wordmark: "Alumni Sans", "Albert Sans", Arial, sans-serif;
--ks-font: "Albert Sans", "Avenir Next", "Helvetica Neue", Arial, system-ui, sans-serif;
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
/* ============================================================
Type scale. These are the actual sizes the homepage ships with
new pages should read them via var(...) rather than hand-typing.
The display vs headline weight split is deliberate: the hero h1
reads thin (100) for elegance; section h2s read heavier (300) to
anchor each block. Section h2s look heavier than the h1 on purpose.
============================================================ */
/* Display — hero h1. */
--ks-type-display-size: clamp(3.4rem, 6.5vw, 5.6rem);
--ks-type-display-weight: 100;
--ks-type-display-line: 1.02;
--ks-type-display-track: -0.01em;
/* Headline — section h2. Heavier than display. */
--ks-type-headline-size: clamp(2.6rem, 4vw, 3.4rem);
--ks-type-headline-weight: 300;
--ks-type-headline-line: 1.04;
/* Title — card and panel headings (h3). */
--ks-type-title-size: 1.18rem;
--ks-type-title-weight: 500;
--ks-type-title-line: 1.35;
/* Body. */
--ks-type-body-size: 1.02rem;
--ks-type-body-line: 1.8;
/* Eyebrow — small mono labels above titles. */
--ks-type-eyebrow-size: 0.7rem;
--ks-type-eyebrow-track: 0.18em;
/* Wordmark — IMPECCABLE in the header. */
--ks-type-wordmark-size: 1.15rem;
--ks-type-wordmark-track: 0.42em;
/* Mono — code, terminal, audit lines. */
--ks-type-mono-size: 0.72rem;
--ks-type-mono-track: 0.22em;
/* ============================================================
Motion.
============================================================ */
--ks-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}
html.dark {
color-scheme: dark;
}
/* ============================================================
Light mode pale surfaces, dark ink type, and kinpaku kept as the
real brand gold. Activated via html.light on
<html> (set by theme.js + inline FOUC guard in Base.astro).
============================================================ */
html.light {
color-scheme: light;
/* Brand gold same fills as dark mode (logo, buttons, marks).
For decorative small text on paper, use patina. Use --ks-link-on-paper
for body-sized links that need stronger contrast. */
--ks-kinpaku: oklch(84% 0.19 80.46);
--ks-kinpaku-vivid: oklch(87% 0.20 85);
--ks-kinpaku-pale: oklch(86% 0.07 84);
--ks-kinpaku-rich: oklch(77% 0.13 82);
--ks-kinpaku-deep: oklch(61% 0.085 78);
--ks-kinpaku-ink: var(--ks-patina);
/* Text links + active nav on paper — patina, not fill gold. */
--ks-link-on-paper: var(--ks-patina-deep);
--ks-link-on-paper-hover: var(--ks-patina);
--ks-link-on-paper-line: oklch(49% 0.08 188 / 0.42);
--ks-nav-active: var(--ks-patina-deep);
/* Patina — brand values for fills; deep for text on paper. */
--ks-patina: oklch(70% 0.12 188);
--ks-patina-pale: oklch(82% 0.07 188);
--ks-patina-deep: oklch(49% 0.08 188);
--ks-state-ink: var(--ks-patina-deep);
--ks-vermilion: oklch(52% 0.16 35);
/* Surfaces — warm paper and cream panels. */
--ks-lacquer: oklch(97% 0.012 95);
--ks-lacquer-deep: oklch(94% 0.014 95);
--ks-lacquer-raised: oklch(99% 0.008 95);
--ks-graphite: oklch(91% 0.012 95);
--ks-graphite-2: oklch(88% 0.014 95);
/* Text — dark ink on paper. */
--ks-champagne: oklch(18% 0.02 95);
--ks-text: oklch(25% 0.018 95);
--ks-text-muted: oklch(45% 0.015 95);
--ks-text-faint: oklch(55% 0.012 95);
--ks-text-mute-deep: oklch(65% 0.01 95);
/* Rules — dark hairlines. */
--ks-rule: oklch(25% 0.02 95 / 0.12);
/* Code — light chips, dark text. */
--ks-code-fg: oklch(28% 0.02 95);
--ks-code-bg: var(--ks-graphite);
--ks-code-block-fg: oklch(30% 0.02 95);
--ks-code-block-bg: var(--ks-lacquer-raised);
--ks-code-block-border: var(--ks-rule);
--ks-code-cmd: var(--ks-link-on-paper);
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { cp, copyFile, mkdir, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
import { composeHookRules } from './lib/hook-rule-presentation.js';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const buildDir = path.join(root, 'build-picker');
const outputDir = path.join(root, 'skill/scripts/picker');
// Static assets vendored under picker/assets/ (the site now lives in the
// private impeccable-site repo, so the picker carries its own copies). These
// are referenced from markup at runtime, so Vite never sees them.
const assetsSource = path.join(root, 'picker/assets');
const assetsOutput = path.join(outputDir, 'assets');
const runtimeAssets = ['hero-dark.jpg', 'kinpaku-gold-leaf.jpg'];
// The design context document's images, vendored from the standalone demo:
// per-section foil icons, the rail textures, the brand-asset placeholders, and
// the components photo. Whole directories, so a demo re-sync stays a plain copy.
const runtimeAssetDirs = ['audience', 'product', 'brand', 'color', 'typography', 'material', 'components'];
// The page links ./favicon.svg, so it is also served from the output root.
const faviconSource = path.join(assetsSource, 'favicon.svg');
const faviconOutput = path.join(outputDir, 'favicon.svg');
// The icon specimen is fetched at runtime rather than inlined, so it ships
// beside the page instead of going through Vite. Regenerate it with
// `node scripts/vendor-icons.mjs`.
const iconDataSource = path.join(root, 'picker/data/icon-packs.json');
const iconDataOutput = path.join(outputDir, 'icon-packs.json');
await rm(buildDir, { recursive: true, force: true });
// The Hooks page's rule list, composed from the canonical detector registry so
// the document never drifts from what the hook actually enforces. Committed and
// regenerated every build; tests/hook-rule-presentation.test.js guards the sync.
await writeFile(
path.join(root, 'picker/data/hook-rules.json'),
`${JSON.stringify(composeHookRules(ANTIPATTERNS), null, 2)}\n`,
);
execFileSync(
'bun',
['x', 'astro', 'build', '--config', 'picker/astro.config.mjs'],
{ cwd: root, stdio: 'inherit' },
);
await rm(outputDir, { recursive: true, force: true });
await cp(buildDir, outputDir, { recursive: true });
await mkdir(assetsOutput, { recursive: true });
await copyFile(faviconSource, faviconOutput);
await copyFile(iconDataSource, iconDataOutput);
for (const asset of runtimeAssets) {
await copyFile(path.join(assetsSource, asset), path.join(assetsOutput, asset));
}
for (const dir of runtimeAssetDirs) {
await cp(path.join(assetsSource, dir), path.join(assetsOutput, dir), { recursive: true });
}
await rm(buildDir, { recursive: true, force: true });
console.log(`Built ${path.relative(root, outputDir)}/`);
+94
View File
@@ -0,0 +1,94 @@
/*
Presentation metadata for the Hooks page of the design context document.
The rule ids, names, and descriptions come from the canonical registry
(cli/engine/registry/antipatterns.mjs); this module adds only how the document
groups and labels them. `FINGERPRINT_IDS` promotes a handful of slop rules into
their own "Fingerprints" family (model-specific signatures); `DISCIPLINES` gives
every rule a discipline label for the collapsible groups. Both must cover the
registry exactly tests/hook-rule-presentation.test.js fails the suite when a
rule is added without a discipline, so the document can never silently miss one.
*/
export const FINGERPRINT_IDS = new Set([
'gpt-thin-border-wide-shadow',
'repeating-stripes-gradient',
'codex-grid-background',
'theater-slop-phrase',
]);
export const DISCIPLINES = {
'side-tab': 'Visual Details',
'border-accent-on-rounded': 'Visual Details',
'overused-font': 'Typography',
'flat-type-hierarchy': 'Typography',
'gradient-text': 'Color & Contrast',
'ai-color-palette': 'Color & Contrast',
'cream-palette': 'Color & Contrast',
'nested-cards': 'Layout & Space',
'monotonous-spacing': 'Layout & Space',
'bounce-easing': 'Motion',
'pulsing-dot': 'Motion',
'blinking-cursor': 'Motion',
'shape-assembled-illustration': 'Imagery',
'organic-clip-path': 'Imagery',
'buried-raster': 'Imagery',
'dark-glow': 'Color & Contrast',
'radial-halo': 'Color & Contrast',
'radial-spotlight-glow': 'Color & Contrast',
'marquee': 'Motion',
'icon-tile-stack': 'Typography',
'italic-serif-display': 'Typography',
'hero-eyebrow-chip': 'Typography',
'kicker-above-heading': 'Typography',
'numbered-section-labels': 'Layout & Space',
'em-dash-overuse': 'Copy',
'marketing-buzzword': 'Copy',
'aphoristic-cadence': 'Copy',
'oversized-h1': 'Typography',
'extreme-negative-tracking': 'Typography',
'broken-image': 'Imagery',
'script-error': 'Quality',
'content-hidden-at-rest': 'Layout & Space',
'edge-flush-cards': 'Layout & Space',
'text-occlusion': 'Layout & Space',
'first-viewport-column-overflow': 'Layout & Space',
'gray-on-color': 'Color & Contrast',
'low-contrast': 'Quality',
'layout-transition': 'Motion',
'line-length': 'Layout & Space',
'cramped-padding': 'Layout & Space',
'body-text-viewport-edge': 'Layout & Space',
'tight-leading': 'Typography',
'skipped-heading': 'Typography',
'heading-rhythm': 'Layout & Space',
'justified-text': 'Typography',
'tiny-text': 'Typography',
'undersized-ui-text': 'Typography',
'all-caps-body': 'Typography',
'wide-tracking': 'Typography',
'text-overflow': 'Layout & Space',
'repeated-container-text': 'Quality',
'clipped-overflow-container': 'Layout & Space',
'design-system-font': 'Typography',
'design-system-color': 'Color & Contrast',
'design-system-radius': 'Visual Details',
'design-system-font-size': 'Typography',
'gpt-thin-border-wide-shadow': 'Visual Details',
'repeating-stripes-gradient': 'Visual Details',
'codex-grid-background': 'Visual Details',
'theater-slop-phrase': 'Copy',
'image-hover-transform': 'Motion',
};
/* One entry per registry rule, in registry order, in the exact shape the
document's Hooks module renders. */
export function composeHookRules(antipatterns) {
return antipatterns.map((rule) => ({
id: rule.id,
name: rule.name,
description: rule.description,
group: FINGERPRINT_IDS.has(rule.id) ? 'fingerprints' : rule.category,
discipline: DISCIPLINES[rule.id],
}));
}
+1
View File
@@ -36,6 +36,7 @@ export const SKILL_CATEGORIES = {
init: 'system',
document: 'system',
extract: 'system',
'design-context': 'system',
live: 'system',
};
+7 -3
View File
@@ -109,9 +109,11 @@ function readSkillScripts(scriptsDir) {
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(entry.name)) continue;
const relPath = path.relative(scriptsDir, entryPath).split(path.sep).join('/');
const isBinary = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2']
.includes(path.extname(entry.name).toLowerCase());
scripts.push({
name: relPath,
content: fs.readFileSync(entryPath, 'utf-8'),
content: fs.readFileSync(entryPath, isBinary ? undefined : 'utf-8'),
filePath: entryPath,
});
}
@@ -643,8 +645,9 @@ const EXCLUDED_FROM_SUGGESTIONS = new Set([
// These are the commands that audit/critique/etc. reference when suggesting next steps.
const IMPECCABLE_SUB_COMMANDS = [
'adapt', 'animate', 'audit', 'bolder', 'clarify', 'colorize',
'critique', 'delight', 'distill', 'document', 'harden', 'layout',
'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape', 'typeset',
'critique', 'delight', 'design-context', 'distill', 'document', 'harden',
'layout', 'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape',
'typeset',
];
export function replacePlaceholders(content, provider, commandNames = [], allSkillNames = []) {
@@ -700,6 +703,7 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
* here by an exact string match.
*/
export function replaceScriptProviderMarker(content, provider, buildProvider = provider) {
if (Buffer.isBuffer(content)) return content;
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS.cursor;
const commandPrefix = placeholders.command_prefix || '/';
const prefixMarker = "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix";
+27 -1
View File
@@ -8,7 +8,8 @@
// For the skill component, also reruns `bun run build:release` and refuses if the
// regenerated harness directories drift from what is committed.
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { readFileSync, readdirSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
import { execSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -88,6 +89,24 @@ function runMutating(cmd) {
execSync(cmd, { cwd: repoRoot, stdio: 'inherit' });
}
function directoryHash(directory) {
if (!existsSync(directory)) return null;
const hash = createHash('sha256');
function addDirectory(current) {
for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const entryPath = path.join(current, entry.name);
const relative = path.relative(directory, entryPath).split(path.sep).join('/');
hash.update(relative);
if (entry.isDirectory()) addDirectory(entryPath);
else if (entry.isFile()) hash.update(readFileSync(entryPath));
}
}
addDirectory(directory);
return hash.digest('hex');
}
step(`Reading version from ${cfg.manifest}`);
const manifest = JSON.parse(readFileSync(path.join(repoRoot, cfg.manifest), 'utf8'));
const version = manifest.version;
@@ -115,7 +134,14 @@ if (cfg.buildCmd) {
if (dryRun) {
console.log(` [dry-run] ${cfg.buildCmd}`);
} else {
const pickerOutput = component === 'skill'
? path.join(repoRoot, 'skill/scripts/picker')
: null;
const pickerHashBefore = pickerOutput ? directoryHash(pickerOutput) : null;
execSync(cfg.buildCmd, { cwd: repoRoot, stdio: 'inherit' });
if (pickerHashBefore && directoryHash(pickerOutput) !== pickerHashBefore) {
fail(`Picker build output was stale. Run \`bun run build:picker\`, then re-run the release check.`);
}
const postBuild = run('git status --porcelain');
if (postBuild) {
fail(`Build produced uncommitted changes. Run \`${cfg.buildCmd}\`, commit the result, then re-run.\n${postBuild}`);
+4 -1
View File
@@ -25,8 +25,9 @@ export const SUITES = {
description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.',
triggers: [
...COMMON_INFRA_PATTERNS,
/^picker\//,
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/,
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-context|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|picker|pin|surface-brief))/,
/^README(\.npm)?\.md$/,
/^cli\/bin\//,
],
@@ -47,6 +48,7 @@ export const SUITES = {
'tests/validate-plugin-versions.test.js',
'tests/validate-plugin-manifest.test.js',
'tests/plugin-paths.test.js',
'tests/hook-rule-presentation.test.js',
],
},
{
@@ -70,6 +72,7 @@ export const SUITES = {
'tests/hook.test.mjs',
'tests/impeccable-paths.test.mjs',
'tests/openai-plugin.test.mjs',
'tests/picker-server.test.mjs',
'tests/pin.test.mjs',
'tests/release.test.mjs',
'tests/doctor.test.mjs',
+538
View File
@@ -0,0 +1,538 @@
#!/usr/bin/env node
/**
* Vendor one fixed specimen from each open-source icon pack the picker offers.
*
* node scripts/vendor-icons.mjs # rebuild from the pinned versions
* node scripts/vendor-icons.mjs --latest # re-resolve each pack's latest first
* node scripts/vendor-icons.mjs --dry-run # resolve and report, write nothing
*
* Every pack draws the same 24 concepts, so the specimen grid compares packs
* rather than vocabularies. Output is picker/data/icon-packs.json: pack
* metadata plus sanitized inner SVG markup, small enough to ship with the
* picker and offline once it is there.
*/
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputPath = path.join(root, 'picker/data/icon-packs.json');
const dryRun = process.argv.includes('--dry-run');
const useLatest = process.argv.includes('--latest');
/** The specimen. Order is the reading order of the grid. */
const CONCEPTS = [
'home', 'search', 'user', 'settings', 'bell', 'heart',
'star', 'calendar', 'mail', 'message', 'trash', 'download',
'upload', 'plus', 'check', 'close', 'chevron-right', 'arrow-right',
'menu', 'filter', 'edit', 'lock', 'eye', 'external-link',
];
/**
* Candidate names per concept, best glyph first. Packs disagree on almost
* every noun here, so the resolver walks the list against each pack's real
* file listing and takes the first hit. Anything this misses gets an explicit
* entry in the pack's `overrides`.
*/
const ALIASES = {
home: ['home', 'house'],
search: ['search', 'magnifying-glass'],
user: ['user', 'person', 'account'],
settings: ['settings', 'gear', 'gear-six', 'cog'],
bell: ['bell', 'notification'],
heart: ['heart', 'favourite'],
star: ['star'],
calendar: ['calendar', 'calendar-blank', 'calendar-days', 'calendar3'],
mail: ['mail', 'envelope', 'envelope-closed', 'email'],
message: [
'message-circle', 'chat-circle', 'chat-bubble-left', 'comment',
'chat-bubble', 'message-square', 'chat-dots', 'message', 'chat',
],
trash: ['trash-2', 'trash', 'trash-can', 'delete-bin'],
download: ['download', 'download-simple', 'arrow-down-tray'],
upload: ['upload', 'upload-simple', 'arrow-up-tray'],
plus: ['plus', 'add'],
check: ['check', 'checkmark', 'tick'],
close: ['x', 'x-mark', 'cross-1', 'xmark', 'close'],
'chevron-right': ['chevron-right', 'caret-right', 'nav-arrow-right', 'arrow-right-s'],
'arrow-right': ['arrow-right'],
menu: ['menu', 'bars-3', 'hamburger-menu', 'three-bars', 'list', 'menu-2'],
filter: ['filter', 'funnel', 'funnel-simple', 'mixer-horizontal'],
edit: ['pencil', 'pencil-1', 'pencil-simple', 'edit-pencil', 'edit-2', 'edit'],
lock: ['lock', 'lock-closed', 'lock-key'],
eye: ['eye', 'eye-open', 'view'],
'external-link': [
'external-link', 'arrow-square-out', 'arrow-top-right-on-square',
'box-arrow-up-right', 'link-external', 'open-new-window',
'square-arrow-out-up-right', 'share-box',
],
};
/**
* One row per pack. `dir` and `suffix` describe where the variant we want
* lives; `count` is measured from the real listing of that variant, not
* copied from a marketing page. `note` is the row's one line of editorial.
*/
const PACKS = [
{
id: 'lucide',
name: 'Lucide',
license: 'ISC',
url: 'https://lucide.dev',
note: 'Feather\u2019s successor. Same 2px round-cap hand, far wider catalog, still shipping.',
grid: '24',
weight: '2px stroke',
source: { kind: 'npm', pkg: 'lucide-static', version: '1.27.0' },
dir: 'icons/',
viewBox: '0 0 24 24',
attrs: {
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
id: 'feather',
name: 'Feather',
license: 'MIT',
url: 'https://feathericons.com',
note: 'The 2px original most stroke sets descend from. Small, even, no longer growing.',
grid: '24',
weight: '2px stroke',
source: { kind: 'npm', pkg: 'feather-icons', version: '4.29.2' },
dir: 'dist/icons/',
viewBox: '0 0 24 24',
attrs: {
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
id: 'phosphor',
name: 'Phosphor',
license: 'MIT',
url: 'https://phosphoricons.com',
note: 'Drawn on a 256 grid with a soft geometric hand. Six weights upstream, regular here.',
grid: '256',
weight: 'regular weight',
source: { kind: 'npm', pkg: '@phosphor-icons/core', version: '2.1.1' },
dir: 'assets/regular/',
viewBox: '0 0 256 256',
attrs: { fill: 'currentColor' },
},
{
id: 'heroicons',
name: 'Heroicons',
license: 'MIT',
url: 'https://heroicons.com',
note: 'Tailwind\u2019s set. 1.5px strokes at 24, with solid and 20px mini twins for dense UI.',
grid: '24',
weight: '1.5px stroke',
source: { kind: 'npm', pkg: 'heroicons', version: '2.2.0' },
dir: '24/outline/',
viewBox: '0 0 24 24',
attrs: {
fill: 'none',
stroke: 'currentColor',
'stroke-width': '1.5',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
id: 'tabler',
name: 'Tabler',
license: 'MIT',
url: 'https://tabler.io/icons',
note: 'The widest stroke catalog here, uniform enough that mixed rows read as one hand.',
grid: '24',
weight: '2px stroke',
source: { kind: 'npm', pkg: '@tabler/icons', version: '3.45.0' },
dir: 'icons/outline/',
viewBox: '0 0 24 24',
attrs: {
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
id: 'radix',
name: 'Radix Icons',
license: 'MIT',
url: 'https://www.radix-ui.com/icons',
note: 'A 15px grid built for product chrome. Crisp at control size, thin if you scale it up.',
grid: '15',
weight: 'filled paths',
source: { kind: 'gh', repo: 'radix-ui/icons', version: '112af91ad275a63c3a29b0da2588342af74ef9bf' },
dir: 'packages/radix-icons/icons/',
viewBox: '0 0 15 15',
attrs: { fill: 'none' },
},
{
id: 'bootstrap',
name: 'Bootstrap Icons',
license: 'MIT',
url: 'https://icons.getbootstrap.com',
note: 'Filled paths on a 16 grid, so weight holds at any size. More literal than the stroke sets.',
grid: '16',
weight: 'filled paths',
source: { kind: 'npm', pkg: 'bootstrap-icons', version: '1.13.1' },
dir: 'icons/',
exclude: (name) => name.endsWith('-fill'),
viewBox: '0 0 16 16',
attrs: { fill: 'currentColor' },
},
{
id: 'iconoir',
name: 'Iconoir',
license: 'MIT',
url: 'https://iconoir.com',
note: '1.5px strokes on 24 with an open, geometric hand. Roomier than Lucide at the same size.',
grid: '24',
weight: '1.5px stroke',
source: { kind: 'npm', pkg: 'iconoir', version: '7.11.1' },
dir: 'icons/regular/',
viewBox: '0 0 24 24',
attrs: {
fill: 'none',
stroke: 'currentColor',
'stroke-width': '1.5',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
id: 'octicons',
name: 'Octicons',
license: 'MIT',
url: 'https://primer.style/octicons',
note: 'GitHub\u2019s set, redrawn per size rather than scaled. Compact and developer-fluent.',
grid: '24',
weight: 'filled paths',
source: { kind: 'npm', pkg: '@primer/octicons', version: '19.31.0' },
dir: 'build/svg/',
suffix: '-24',
viewBox: '0 0 24 24',
attrs: { fill: 'currentColor' },
},
{
id: 'hugeicons',
name: 'Hugeicons',
license: 'MIT (free core)',
url: 'https://hugeicons.com',
note: '1.5px rounded strokes on 24. The free core is one style; the other styles are paid.',
grid: '24',
weight: '1.5px stroke',
source: { kind: 'npm', pkg: '@hugeicons/core-free-icons', version: '4.2.3' },
dir: 'dist/esm/',
extension: '.js',
viewBox: '0 0 24 24',
attrs: {
fill: 'none',
stroke: 'currentColor',
'stroke-width': '1.5',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
},
{
id: 'remix',
name: 'Remix Icon',
license: 'Apache-2.0',
url: 'https://remixicon.com',
note: 'Line and fill twins across a broad catalog. Neutral, a touch heavier than the 2px sets.',
grid: '24',
weight: 'filled paths',
source: { kind: 'npm', pkg: 'remixicon', version: '4.9.1' },
dir: 'icons/',
suffix: '-line',
viewBox: '0 0 24 24',
attrs: { fill: 'currentColor' },
},
];
/** Explicit picks where the alias walk lands on a worse glyph, or nothing. */
const OVERRIDES = {
lucide: { edit: 'pencil', menu: 'menu' },
feather: { edit: 'edit-3', menu: 'menu' },
phosphor: { menu: 'list', settings: 'gear-six' },
// Tabler's menu is two rules, and Bootstrap files the funnel apart from the
// stacked-rule filter. Both rows compare better against the other nine.
tabler: { menu: 'menu-2' },
bootstrap: { menu: 'list', message: 'chat', settings: 'gear', filter: 'funnel' },
octicons: { menu: 'three-bars', settings: 'gear', message: 'comment' },
radix: { menu: 'hamburger-menu', settings: 'gear', filter: 'mixer-horizontal' },
// Iconoir's x.svg is the letter, not the close control.
iconoir: { close: 'xmark' },
// Remix files a hand bell under bell and a hex plate under settings, so the
// two rows that should read as notification and preferences do not.
remix: { bell: 'notification-2', settings: 'settings-3' },
// Hugeicons names in PascalCase and numbers its variants, so nothing here is
// guessable from the concept. Every pick is deliberate.
hugeicons: {
home: 'Home01Icon', search: 'Search01Icon', user: 'UserIcon',
settings: 'Settings01Icon', bell: 'Notification01Icon', heart: 'FavouriteIcon',
star: 'StarIcon', calendar: 'Calendar01Icon', mail: 'Mail01Icon',
message: 'BubbleChatIcon', trash: 'Delete02Icon', download: 'Download01Icon',
upload: 'Upload01Icon', plus: 'Add01Icon', check: 'Tick02Icon',
close: 'Cancel01Icon', 'chevron-right': 'ChevronRightIcon',
// ArrowRight01 is Hugeicons' chevron, and LockIcon is a keyhole disc.
'arrow-right': 'ArrowRight02Icon', menu: 'Menu01Icon', filter: 'FilterIcon',
edit: 'PencilEdit01Icon', lock: 'SquareLock01Icon', eye: 'ViewIcon',
'external-link': 'ExternalLinkIcon',
},
};
const ALLOWED_TAGS = new Set([
'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'g',
]);
const ALLOWED_ATTRS = new Set([
'd', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'x1', 'y1', 'x2', 'y2',
'width', 'height', 'points', 'transform', 'fill', 'fill-rule', 'clip-rule',
'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
'stroke-miterlimit', 'stroke-dasharray', 'opacity', 'fill-opacity',
'stroke-opacity',
]);
async function getJson(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`${response.status} ${url}`);
return response.json();
}
async function getText(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`${response.status} ${url}`);
return response.text();
}
/** Resolve the version to pin, then list every file the package ships. */
async function listFiles(pack) {
const source = useLatest ? { ...pack.source, version: null } : pack.source;
if (source.kind === 'npm') {
const version = source.version
|| (await getJson(`https://data.jsdelivr.com/v1/packages/npm/${source.pkg}`)).tags.latest;
const listing = await getJson(
`https://data.jsdelivr.com/v1/packages/npm/${source.pkg}@${version}?structure=flat`,
);
return {
version,
base: `https://cdn.jsdelivr.net/npm/${source.pkg}@${version}`,
files: listing.files.map((file) => file.name.replace(/^\//, '')),
};
}
// GitHub sources are pinned to a commit: branches move, and a specimen that
// silently redraws itself is worse than one that fails loudly.
const version = source.version
|| (await getJson(`https://api.github.com/repos/${source.repo}/commits/HEAD`)).sha;
const tree = await getJson(
`https://api.github.com/repos/${source.repo}/git/trees/${version}?recursive=1`,
);
return {
version,
base: `https://raw.githubusercontent.com/${source.repo}/${version}`,
files: tree.tree.filter((node) => node.type === 'blob').map((node) => node.path),
};
}
function basenames(pack, files) {
const extension = pack.extension || '.svg';
const map = new Map();
for (const file of files) {
if (!file.startsWith(pack.dir) || !file.endsWith(extension)) continue;
const name = file.slice(pack.dir.length, -extension.length);
// Remix nests by category (icons/system/home-line.svg); everything else is
// flat, and a nested name would only collide with itself.
map.set(name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name, file);
}
return map;
}
function variantNames(pack, names) {
const suffix = pack.suffix || '';
return [...names]
.filter((name) => name.endsWith(suffix))
.filter((name) => !pack.exclude?.(name));
}
function resolveName(pack, concept, names) {
const suffix = pack.suffix || '';
const override = OVERRIDES[pack.id]?.[concept];
const candidates = override ? [override] : ALIASES[concept];
for (const candidate of candidates) {
for (const name of [
`${candidate}${suffix}`,
candidate,
`${candidate}-01${suffix}`,
`${candidate}-1${suffix}`,
]) {
if (names.has(name) && !pack.exclude?.(name)) return name;
}
}
return null;
}
/** Keep drawing elements and drawing attributes. Drop everything else. */
function sanitize(markup) {
const body = markup
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<\?[\s\S]*?\?>/g, '')
.replace(/<(title|desc|style|script|metadata)\b[\s\S]*?<\/\1>/gi, '')
.replace(/<\/?svg\b[^>]*>/gi, '');
let dropped = 0;
const cleaned = body.replace(/<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g, (match, rawTag, rawAttrs) => {
const tag = rawTag.toLowerCase();
if (!ALLOWED_TAGS.has(tag)) {
dropped += 1;
return '';
}
if (match.startsWith('</')) return `</${tag}>`;
// Names carry digits (x1, y2), so the class cannot be letters only.
const attrs = [...rawAttrs.matchAll(/([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*"([^"]*)"/g)]
.map(([, key, value]) => [key.toLowerCase(), value.trim()])
.filter(([key]) => ALLOWED_ATTRS.has(key));
// Tabler opens every icon with an invisible bounding path, and a few packs
// carry the same trick. It draws nothing, so it only costs bytes.
const paint = Object.fromEntries(attrs);
if (paint.fill === 'none' && paint.stroke === 'none') return '';
if (paint.opacity === '0') return '';
const rendered = attrs.map(([key, value]) => `${key}="${value}"`).join(' ');
const selfClosing = match.endsWith('/>');
return `<${tag}${rendered ? ` ${rendered}` : ''}${selfClosing ? '/>' : '>'}`;
});
return { body: cleaned.replace(/>\s+</g, '><').replace(/\s+/g, ' ').trim(), dropped };
}
/**
* Hugeicons ships React-shaped tuples rather than SVG files:
* [["path", { d: "...", key: "0" }], ...]. Same drawing, one hop away.
*/
function tuplesToMarkup(module) {
const start = module.indexOf('[', module.indexOf('='));
const end = module.lastIndexOf(']');
if (start === -1 || end < start) throw new Error('No icon tuple array found');
const literal = module
.slice(start, end + 1)
.replace(/([{,])\s*([A-Za-z][A-Za-z0-9]*)\s*:/g, '$1"$2":');
const tuples = JSON.parse(literal);
return tuples
.map(([tag, props]) => {
const attrs = Object.entries(props)
.filter(([key]) => key !== 'key')
.map(([key, value]) => [key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), value])
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
return `<${tag} ${attrs}/>`;
})
.join('');
}
const rootAttr = (markup, name) => markup.match(new RegExp(`<svg[^>]*\\b${name}="([^"]*)"`, 'i'))?.[1];
async function vendorPack(pack) {
const { version, base, files } = await listFiles(pack);
const names = basenames(pack, files);
const count = variantNames(pack, names.keys()).length;
if (!count) throw new Error(`${pack.id}: no files under ${pack.dir}`);
const glyphs = [];
const misses = [];
let droppedTotal = 0;
let viewBoxMismatch = null;
for (const concept of CONCEPTS) {
const name = resolveName(pack, concept, names);
if (!name) {
misses.push(concept);
continue;
}
const raw = await getText(`${base}/${encodeURI(names.get(name))}`);
const markup = pack.extension === '.js' ? tuplesToMarkup(raw) : raw;
if (pack.extension !== '.js') {
const viewBox = rootAttr(markup, 'viewBox');
if (viewBox && viewBox !== pack.viewBox) viewBoxMismatch ??= `${name}: ${viewBox}`;
}
const { body, dropped } = sanitize(markup);
droppedTotal += dropped;
if (!body) throw new Error(`${pack.id}/${name}: sanitized to nothing`);
glyphs.push({ concept, name, body });
}
return {
pack: {
id: pack.id,
name: pack.name,
license: pack.license,
url: pack.url,
note: pack.note,
grid: pack.grid,
weight: pack.weight,
count,
version,
viewBox: pack.viewBox,
attrs: pack.attrs,
glyphs,
},
report: { misses, dropped: droppedTotal, viewBoxMismatch },
};
}
const results = await Promise.all(PACKS.map(async (pack) => {
try {
return { pack, ...(await vendorPack(pack)) };
} catch (error) {
return { pack, error };
}
}));
let failed = false;
const packs = [];
for (const result of results) {
if (result.error) {
failed = true;
console.error(`FAIL ${result.pack.id}: ${result.error.message}`);
continue;
}
const { pack, report } = result;
packs.push(pack);
const flags = [
report.misses.length ? `MISSING ${report.misses.join(', ')}` : null,
report.viewBoxMismatch ? `VIEWBOX ${report.viewBoxMismatch}` : null,
report.dropped ? `dropped ${report.dropped} tags` : null,
].filter(Boolean);
if (report.misses.length) failed = true;
console.log(
`${pack.id.padEnd(11)} v${String(pack.version).slice(0, 12).padEnd(13)}`
+ `${String(pack.count).padStart(5)} glyphs ${pack.glyphs.length}/${CONCEPTS.length}`
+ `${flags.length ? ` ${flags.join(' | ')}` : ''}`,
);
console.log(` ${pack.glyphs.map((glyph) => `${glyph.concept}=${glyph.name}`).join(' ')}`);
}
if (!dryRun && packs.length) {
const payload = {
generated: new Date().toISOString().slice(0, 10),
concepts: CONCEPTS,
packs,
};
await mkdir(path.dirname(outputPath), { recursive: true });
await writeFile(outputPath, `${JSON.stringify(payload)}\n`);
const bytes = Buffer.byteLength(JSON.stringify(payload));
console.log(`\nWrote ${path.relative(root, outputPath)} (${(bytes / 1024).toFixed(0)} KB)`);
}
process.exit(failed ? 1 : 0);
+1
View File
@@ -48,6 +48,7 @@ Choose the mode from the requested surface, not the product, and persist it only
| `init` | Build | Capture durable product context in PRODUCT.md | [reference/init.md](reference/init.md) |
| `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) |
| `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) |
| `design-context [open/edit/export/import]` | Build | Reopen, revise, export, or import the design interview and its document | [reference/design-context.md](reference/design-context.md) |
| `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) |
| `audit [target]` | Evaluate | Technical quality checks (a11y, perf, responsive) | [reference/audit.md](reference/audit.md) · native: [reference/audit.native.md](reference/audit.native.md) |
| `polish [target]` | Refine | Final quality pass before shipping | [reference/polish.md](reference/polish.md) |
+1 -1
View File
@@ -22,7 +22,7 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
## Decision Comps
When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; report a card too thin to brief a comp, never pad it from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world. A native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run.
When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields, PRODUCT.md, and on a questionnaire-seed world the packet's cue image with its **Cue, in words:** passage, nothing else; report a card too thin to brief a comp, never pad it from imagination. When the packet carries the cue, attach it to the generation call as a reference image and restate the passage's materials and light in the prompt in your own words; an attached image with no restated words leaves the model guessing what to take from it, and the cue's still-life composition never transfers, only its materials, palette, and light. A staged logo in the packet rides the same way: attach it and name it as the exact mark to reproduce, never a shape to reinterpret. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world. A native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run.
## Input Contract
+2 -2
View File
@@ -27,9 +27,9 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too: the surface brief naming the approved comp, or an `approved` flag in its sidecar. Comp-round comps with no recorded pick mean the approval point was skipped, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. When a seed or prior DESIGN.md predates this build, it matches the built world, and "matches" is evidence you grep, not an impression: custom properties DESIGN.md defines that no rule consumes, literals sitting a step from a defined token, geometry a named rule bans (a 999px pill against a Slightly Soft rule). Each hit is a material finding under the craft floor's token rule, and an approved comp excuses none of them: the comp rules composition; the world rules material. On a new world with no seed, DESIGN.md is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too: the surface brief naming the approved comp, or an `approved` flag in its sidecar. Comp-round comps with no recorded pick mean the approval point was skipped, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element; its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Three rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality (CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. A fix that requires producing an asset says so explicitly ("produce: <region> as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. A committed motion energy shows up as eased state changes in the shipped code; a recorded energy with zero transitions is a finding. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped, a material fix ahead of any craft point. Then, for each of the five blocks: does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every raster region of the spec shipped as its plate (the spec names the file; the page references it; the region's diff row is not `missing`), not a gradient, an inline SVG, or a many-vertex `clip-path` standing in for it, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind a wash is a compliance token, not a shipped material, and the detector's `buried-raster` and `organic-clip-path` findings in the packet are material fixes.
6. **Floor.** Read the craft floor's Refuse list and hold the screenshots against it: kickers and eyebrows, hard offset shadows outside a neobrutalist world, glyph icons, system display faces, gradient text, side stripes, and the rest. A banned element is a material fix even when it matches nothing in the comp: the builder loaded the same ban before writing it, and fidelity to a comp cannot authorize what the floor refuses. The parent's hook findings cover this mechanically where hooks run; this check exists because hookless harnesses reach you with none, and the last two live sessions shipped five kickers past a reviewer that never looked.
+4 -1
View File
@@ -2,6 +2,8 @@
Load this after the direction is settled, and build without announcing the checklist. A pinned brief or the committed visual world overrides anything here; your own habit does not. When the design hook is active it already enforces the mechanical checks below as you edit: act on its findings instead of re-auditing each rule. <!-- rule:skill-craft-floor -->
A wholesale file rewrite, after a hook block or an error recovery, starts by re-opening DESIGN.md and the token sheet, so the new file restates the recorded system rather than your memory of it; memory is where a display token becomes a hand-tuned clamp. The ranking lives in [new-work.md](new-work.md): the comp rules composition; the world rules material, and a rewrite changes neither. <!-- rule:skill-rebuild-from-the-record -->
## Verify
Each of these is a check on the built result, not an intention. Run them together in the batched inspection rounds, not as separate screenshot trips; the checks share one render.
@@ -11,6 +13,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
- **Spacing:** tight groups, generous separation, more space above a heading than below it. Read the computed values. <!-- rule:skill-layout-spacing-rhythm -->
- **Type:** body measure 6575ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. <!-- rule:skill-typo-floor --> <!-- rule:skill-ban-text-overflow -->
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. <!-- rule:skill-motion-floor --> <!-- rule:skill-motion-materials-palette --> <!-- rule:skill-motion-no-section-fade -->
- **Tokens:** a size or color a step from a defined token takes the token: `0.82rem` beside a `0.8rem` token is the token, not a new size, and inline SVG or JS-drawn strokes take `var(--role)` or `currentColor`. Wire or remove what nothing consumes before finish; done means every defined custom property is consumed by at least one rule. <!-- rule:skill-tokens-are-consumed -->
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. <!-- rule:skill-floor-shipping -->
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. <!-- rule:skill-craft-browser-surfaces -->
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. <!-- rule:skill-copy-design-material -->
@@ -37,7 +40,7 @@ Surface habits:
- Sparklines, progress rings, and soft-shadowed rounded rectangles standing in for content. <!-- rule:skill-reflex-decorative-chrome -->
- Monospace as a costume for "technical" rather than for code, data, or measurement. <!-- rule:skill-reflex-mono-as-technical -->
- A system display face (Impact, Arial Black, the platform sans) as the display voice of an own-world page. Source and self-host a face whose character matches the approved lettering; the closest installed font is a failure, not a fallback. <!-- rule:skill-ban-system-display-face -->
- Unicode glyphs or emoji standing in for an icon system. Icons are drawn, from a real library or authored SVG, in one consistent stroke and weight. <!-- rule:skill-ban-glyph-icons -->
- Unicode glyphs or emoji standing in for an icon system. Icons are drawn, from a real library or authored SVG, in one consistent stroke and weight. When a pack icon fails to fetch, take the nearest icon from the same pack, jsDelivr as the fallback CDN; drawing a replacement from scratch is an exception the user signs off on, so the one-pack promise survives a failed fetch. <!-- rule:skill-ban-glyph-icons -->
- Geometric masks standing in for organic contours. A circle, polygon, or radial-gradient cutout approximating a photographic subject's edge is the cheap version of the effect and reads worse than omitting it. Derive an alpha matte from the actual image, or produce a cut-out asset. <!-- rule:skill-ban-geometric-occlusion-mask -->
- Light or dark picked by category. Pick it from the use scene: who, where, under what ambient light. <!-- rule:skill-reflex-theme-by-habit -->
+93
View File
@@ -0,0 +1,93 @@
# Design Context
Loaded by `{{command_prefix}}impeccable design-context`. Owns the design interview record, the document built from it, and its portable form. The interview itself is created by `{{command_prefix}}impeccable document` seed mode; this command is everything afterwards.
## Where it lives
One store, under the project root:
```text
.impeccable/design-context/
context.json the chat half of the interview: product, audience, brand, interview
answers.json the questionnaire's decisions
assets/ brand files the user supplied
fonts/ font faces the user uploaded
cue.png the chosen cue image, copied at submit
runtime/ session.json, journal.jsonl, draft.json (local, gitignored)
exports/ the written-out forms (local, gitignored)
```
`.impeccable/visual-cues/` is separate on purpose: it is the generation workspace, regenerable and gitignored, and the document no longer depends on it. The store is the user's own record and is theirs to commit.
## No argument
Report status in two lines, then act:
- Whether `answers.json` exists, and when it was last written.
- Whether a draft is waiting (`runtime/draft.json`), whether DESIGN.md is seeded, and whether a session is live (`runtime/session.json` naming a running process).
With answers on disk, do `open`. Without them, say the design context is created by the questionnaire and offer `{{command_prefix}}impeccable document`. Never start the questionnaire unasked.
## open
Reopen the document, live for edits.
Run `node {{scripts_path}}/picker-server.mjs --doc` from the project root as a foreground command and parse its `PICKER_URL` line. Open it and wait exactly as [visual-cues.md](visual-cues.md)'s launch paragraph does: its harness-browser ladder (in-IDE browser first, then another browser tool, then the system opener, then telling the user the URL) and its wait-on-the-foreground-process rule. Skip everything earlier in its Step 7: the cue announcement and the `modes` and `context` writes belong to a run that is generating cues, and this one is not.
Then enter the document edit loop below. The process exiting is the signal:
- `DOC_SESSION_ENDED` and exit 0: the document was closed. Say so in one line; the loop is over.
- Exit 2: it timed out or was never opened. Say it can be reopened with the same command, and never relaunch unprompted.
- Exit 1: no interview exists. Route to `{{command_prefix}}impeccable document`.
## edit
Re-run the questionnaire over the previous answers.
Say in one line what it will do before launching, and settle DESIGN.md in the same breath, because a new run replaces the seed the last one produced: *"This re-runs the questionnaire with your previous answers filled in. When you finish, I will refresh DESIGN.md from the new answers. Refresh it, overwrite it, or merge by hand?"* That is the whole consent for this run; do not ask again afterwards.
Then run `node {{scripts_path}}/picker-server.mjs`, using the same launch ladder and wait rule as `open`. Prefill happens on its own: an unfinished run resumes from its draft, a finished one loads its answers, and `--fresh` starts blank. Cues and `context.json` already exist from the previous run, so do not regenerate cues and do not repeat Step 7's pre-launch writes.
On exit 0, go to [document.md](document.md) Steps 5-6 and write the seed from the new `answers.json`, honoring the choice made before launch. On exit 2, nothing was answered and nothing changed.
If `.impeccable/visual-cues/cues.json` is missing, the questionnaire cannot run: its palette screen loads the dealt cues and the built-in seeds together and neither arrives without that file. Say so and offer a full `{{command_prefix}}impeccable document --seed` run instead.
## export
```text
node {{scripts_path}}/design-context-export.mjs [--out DIR] [--no-assets]
```
Writes two files and prints an `EXPORTED` line for each. Tell the user what each is for, in one line each:
- `design-context.md` is the design context as one readable document. It is what to hand another tool, another agent, or a collaborator who needs to follow this design.
- `design-context.bundle.json` is the same context in a form `{{command_prefix}}impeccable design-context import` reads, including the files the user supplied.
Do not read the export back into the conversation; the user asked for a file, not a recitation.
## import
```text
node {{scripts_path}}/design-context-import.mjs <bundle.json> [--design skip|write] [--force]
```
It refuses a project that already has a design context unless `--force`, and refuses while a document is open either way. Report what it prints:
- `DESIGN_MD carried` with a DESIGN.md already here: ask whether to refresh it from the imported context, overwrite it, or merge by hand, then act.
- `DESIGN_MD carried` with none here: offer to write it (`--design write`) or to re-seed from the imported answers through [document.md](document.md) Steps 5-6.
- `DESIGN_MD absent`: say the bundle carried decisions but no design document, and offer to seed one.
Then offer `open`.
## The document edit loop
The document is a working surface. Follow [visual-cues.md](visual-cues.md)'s "The document edit loop" section; it is the canonical contract for polling, the event kinds, and the reply commands. Two things to hold on to while you are in it:
- **The session is the only writer of the store.** Never edit `answers.json` or `context.json` yourself while a session runs. Values you settle travel on your reply, through `--answers` or `--context`. DESIGN.md and PRODUCT.md are yours to write directly.
- **A `save_batch` is already applied.** The user's values are in the store before you hear about them. Your work is the prose those values leave stale, in whichever document the event's `downstream` names.
## Pitfalls
- Never poll `answers.json` while a server runs. The process exiting is the signal.
- Never drive the questionnaire yourself. The answers are the user's, and a run you filled in is a run they did not make.
- Editing in the document changes values that are already there. A field the interview never captured is added by asking in chat, not by this command.
+114 -21
View File
@@ -64,6 +64,7 @@ Omit irrelevant sections rather than filling them with invented rules. Put respo
## When to run
- New-work found a coherent incumbent visual system but no `DESIGN.md`.
- New-work paused before its direction roll on a project with no `DESIGN.md` and the user accepted the seed questionnaire recommendation; run seed mode.
- The first implementation of a new world is complete and its provisional decisions need to be carbonized.
- An existing `DESIGN.md` is stale (the design has drifted).
- Before a large redesign, to capture the current state as a reference.
@@ -73,9 +74,9 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user
## Two paths
- **Scan mode** (default): the project has design tokens, components, or rendered output. Extract, then confirm descriptive language. Use when there's code to analyze.
- **Seed mode**: the project is pre-implementation. Ensure PRODUCT.md exists, then reuse new-work's visual-world workshop and write its directional DESIGN.md seed. Re-run in scan mode once there's code.
- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Decide first whether the browser questionnaire can run, gather any existing brand assets, interview in chat (three named references and one anti-reference when the questionnaire will run; five high-level answers when it will not), then write a seed DESIGN.md marked `<!-- SEED -->` that carries every decision the interview and the questionnaire made. Re-run in scan mode once there's code.
Decide by scanning first (Scan mode Step 1). If the scan finds no tokens, no component files, and no rendered site, offer seed mode; don't silently switch. `/impeccable document --seed` requests new-work's world workshop, but it does not authorize replacing coherent code: when an incumbent system exists, offer scan mode or route an explicit identity-replacement request through new-work.
Decide by scanning first (Scan mode Step 1). If the scan finds no tokens, no component files, and no rendered site, offer seed mode; don't silently switch. `/impeccable document --seed` forces seed mode on a pre-implementation project, but it does not authorize replacing coherent code: when an incumbent system exists, offer scan mode or route an explicit identity-replacement request through new-work.
## Scan mode (approach C: auto-extract, then confirm descriptive language)
@@ -308,7 +309,7 @@ The `html` and `css` fields must be **self-contained, drop-in snippets** that re
1. **Tailwind expansion.** If the source uses Tailwind (className="bg-primary text-white rounded-lg px-6 py-3"), expand every utility to literal CSS properties in the `css` string. Do **not** reference Tailwind classes; do **not** assume a Tailwind CSS bundle is loaded. Each component is self-contained.
2. **Token resolution.** If the project exposes tokens as CSS custom properties on `:root` (e.g. `--color-primary`, `--radius-md`), reference them via `var(--color-primary)`; they inherit through the shadow DOM and stay live-bound. If tokens live only in JS theme objects (styled-components, CSS-in-JS), resolve to literal values at generation time.
3. **Icons.** Inline as SVG. Do not reference Lucide/Heroicons packages, icon fonts, or `<img src="...">`. A typical icon is 16-24px; copy the SVG path data directly.
3. **Icons.** Inline as SVG. Do not reference Lucide/Heroicons packages, icon fonts, or `<img src="...">`. A typical icon is 16-24px; copy the SVG path data directly. This is about how a snippet ships, not about which family the project draws from: when the design names an icon set, keep using that set's glyphs and paste their path data in.
4. **States.** Include `:hover`, `:focus-visible`, and (if meaningful) `:active` rules inline. A static default-only snapshot makes the panel feel dead. Hover + focus rules in the CSS make it feel alive.
5. **Reset bloat.** Extract only the component's *distinctive* CSS (background, color, padding, border-radius, typography, transition). Skip universal resets (`box-sizing: border-box`, `line-height: inherit`, `-webkit-font-smoothing`). The panel already has a neutral canvas; don't re-ship resets.
6. **Scoped class names.** Prefix every class with `ds-` (e.g. `ds-btn-primary`, `ds-input-search`) so component CSS doesn't collide with other components' CSS in the same shadow DOM.
@@ -349,46 +350,138 @@ Your own write is the freshest source; subsequent commands in this session don't
## Seed mode
For projects with no visual system to extract yet. Produces a user-chosen visual-world scaffold, not a fabricated token spec.
### Step 1: Route through new-work's workshop
For projects with no visual system to extract yet. Produces a minimal, user-chosen scaffold, not a fabricated token spec.
PRODUCT.md is the prerequisite. If it is missing, load [init.md](init.md) and complete its product interview first. Do not create a visual identity without durable product context.
If PRODUCT.md exists, load [new-work.md](new-work.md) and resolve visual authority. Seed mode requires a concrete first surface: use the target the user named, or ask what they want to make first. Run new-work's **Create or replace the visual world** flow, then **Commit the world**, so the visual world and its first expression are chosen together. Stop after the directional DESIGN.md seed and surface brief; do not implement. A structured simulated user counts as the user and must get the same choice.
### Step 1: Decide the path, confirm seed mode, and ask for assets
If new-work already completed the workshop in this session, use its chosen direction directly. Do not ask again.
The browser questionnaire asks color strategy and motion per surface and picks concrete typefaces and a type scale by eye, so whether it will run decides what the chat interview may ask. Decide the path **before the first question**, never after the interview:
### Step 2: Write seed DESIGN.md
- **The harness has native image generation** (Codex's `image_gen`, an equivalent MCP tool, or similar): the questionnaire path; the cues are generated directly at Step 4, no setup needed. This branch wins even when `.impeccable/.env` already holds an `IMAGE_GEN_API_KEY` or an earlier run in another harness left a wrapper script behind; those are fallbacks for keyless harnesses, not the preferred path. A native tool that **cannot generate** (zero credits, failed auth) counts as absent: fall through to the next branch without asking, and mention the swap in the final report.
- **No usable native path, key already in `.impeccable/.env`**: the questionnaire path, with no pause and no questions. Load [image-api.md](image-api.md) and use its shipped wrapper; it pre-answers everything this path has ever stopped to ask, including which provider the key belongs to.
- **No usable native path, no key**: pause. {{ask_instruction}} Ask whether the user wants generated visual cues to pick a palette by eye. *"I can generate a few small palette-and-mood images so you choose a direction visually instead of from descriptions. That needs an image-generation API key (FLUX and Google Nano Banana are supported out of the box; other providers work too), stored as `IMAGE_GEN_API_KEY` in `.impeccable/.env`. Add one, or skip straight to the chat interview?"* If a key arrives, write it to `.impeccable/.env` together with `IMAGE_GEN_PROVIDER` (`bfl` for FLUX, `gemini` for Nano Banana, the provider's own name for anything else; when the user does not say, let the wrapper infer it from the key). Confirm that file is listed in the project's `.gitignore` (add it if missing; a committed key is a leak), then load [image-api.md](image-api.md). Its shipped wrapper is the whole integration for the built-in providers; only a provider it does not know earns the project-local wrapper that file specifies. A key arriving makes this the questionnaire path.
- **The user opts out, or no key arrives**: the interview-only path. The assets ask below, the five questions in Step 3, then Steps 5-6 from the interview alone.
Use the canonical section order from Scan mode. Populate the selected workshop direction and leave unresolved implementation facts as honest placeholders. The seed commits a world and its invariants; it does not pretend implementation tokens already exist.
Then confirm seed mode and ask for assets, framed for the path:
Lead the file with:
- **Questionnaire path**: *"There's no existing visual system to scan. You'll pick the visual direction by eye in a browser questionnaire; before I generate its options, three quick things. First: if you have any visual assets (a logo, reference or product images, moodboards), drop them in or point me at the files. They're extra context that makes the first DESIGN.md seed more accurate. You can re-run `/impeccable document` once there's code, to capture the real tokens and components. OK?"*
- **Interview-only path**: *"There's no existing visual system to scan. I'll ask five quick questions to seed a starter DESIGN.md. First: if you have any visual assets (a logo, reference or product images, moodboards), drop them in or point me at the files. They'll ground the questions in what you already have. You can re-run `/impeccable document` once there's code, to capture the real tokens and components. OK?"*
Also glance for assets already in the project (`assets/`, `public/`, `brand/`, image files at the root); name anything found so the user can confirm it's relevant. Assets are optional: one ask, then proceed with whatever arrived.
If the user prefers to skip entirely, stop. No file.
### Step 2: Read the assets
Look at every asset provided (attached in chat or a file path) and record what it tells you, before writing the questions:
- **Logo**: sample the exact colors, note letterform character (geometric / humanist / serif) and temperature.
- **Reference / product images**: density, palette, type feel; what the user is drawn to.
- **Moodboards**: recurring hues, textures, era, register cues.
On the questionnaire path, the files themselves also feed the design context document the picker shows after the last question. When the user provided actual files (a logo, a mood board, a reference image), copy each one into `.impeccable/design-context/assets/`, keeping its filename. Record every staged file for Step 4's context write: it becomes an object entry in `context.json` `context.assets`, `{ "file": "<filename>", "kind": "logo" | "moodboard" | "reference", "note": "<one-line observation>" }`, where the note is what this step read off it. An observation with no file behind it stays a plain string entry, as before. On the interview-only path, stage nothing; the observations feed the questions and the seed alone.
These observations exist to sharpen Step 3. **No assets: skip straight to Step 3** with generic options.
### Step 3: The interview
Group each path's questions into one `AskUserQuestion` interaction. Options must be concrete. Keep skill vocabulary (seed, register, anti-reference) out of question text; ask for the thing in words the user would use. Ask like a magazine editor profiling the brand: curious and narrative, drawing out the feel the surface should carry.
**Questionnaire path: two questions, nothing more.** With Step 1's assets ask these are the whole chat interview; the questionnaire asks everything else by eye.
1. **Three named references.** Brands, products, printed objects. Not adjectives. When Step 2 produced observations, ground candidate names in them (references drawn from the moodboard's era).
2. **One anti-reference.** What the product should NOT feel like. Also named.
**Do not ask about color, typography, or motion here; the questionnaire owns them.** It asks color strategy and motion per surface and picks concrete typefaces and a type scale, so a chat answer would be asked again by eye and one of the two would be thrown away. Both answered, go straight to Step 4.
**Interview-only path: five questions.** When Step 2 produced observations, ground the options in them: offer the logo's sampled color as a hue anchor in Q1, a type direction that matches the letterforms in Q2, candidate named references drawn from the moodboard's era in Q4. The user should recognize their own material in the choices.
1. **Color strategy.** Pick one:
- Restrained: tinted neutrals + one accent ≤10%
- Committed: one saturated color carries 3060% of the surface
- Full palette: 34 named color roles, each deliberate
- Drenched: the surface IS the color
Then: one hue family or anchor reference ("deep teal", "mustard", "Klim #ff4500 orange").
2. **Typography direction.** Pick one (specific fonts come later):
- Serif display + sans body
- Single sans (warm / technical / geometric / humanist; pick a feel)
- Display + mono
- Mono-forward
- Editorial script + sans
3. **Motion energy.** Pick one:
- Restrained: state changes only
- Responsive: feedback + transitions, no choreography
- Choreographed: orchestrated entrances, scroll-driven sequences
4. **Three named references.** Brands, products, printed objects. Not adjectives.
5. **One anti-reference.** What it should NOT feel like. Also named.
### Step 4: Launch the questionnaire (questionnaire path only)
**Interview-only path: skip this step.** Go to Step 5 and seed from the answers alone. Step 1 already settled the capability question; do not re-open it here.
On the questionnaire path, **stop and load [visual-cues.md](visual-cues.md)** and follow its pipeline; it owns everything from the one-line user announcement and the persona palette studio through generation, `cues.json`, and the picker pause. Do not restate its mechanics here or in chat. The picker's exit is the handoff: when the server exits 0 and `.impeccable/design-context/answers.json` lands, come back here and run Steps 5-6 with that file in hand.
### Step 5: Write seed DESIGN.md
Use the canonical section order from Scan mode. Populate what the interview, the assets, and the questionnaire answer; leave the rest as honest placeholders. The seed is a scaffold, not a fabricated spec, but a decision the user actually made in the picker is real and belongs in the file at full strength.
Mark the file as a seed with this comment as the first line of the markdown body, immediately after the frontmatter's closing `---` (the frontmatter must open the file or token parsers will not see it):
```markdown
<!-- SEED: established with the user before implementation; re-run /impeccable document once there's code to capture the actual tokens and components. -->
```
Per-section guidance in seed mode:
**Two seeds exist**, and which one you write depends on whether Step 4's picker ran:
- **Overview**: the chosen design thesis, layout behavior, material character, imagery stance, motion grammar, and reusable signature. Keep the selected first-surface expression in its surface brief; do not promote its composition into the global world.
- **Colors**: the selected palette strategy and roles. Include values only when the user, an existing asset, or new-work's exploration established them; otherwise mark them `[to be resolved during implementation]`.
- **Typography**: the selected type character and role relationship. Include font names only when established; otherwise mark the pairing `[to be resolved during implementation]`.
- **Layout**: the selected spatial grammar and responsive behavior, without pretending exact measurements are settled.
- **Elevation & Depth**: the selected material and depth behavior, stated as an invariant rather than inferred from a generic preset.
- **Shapes**: the selected form and corner language.
**Interview-only seed** (the user opted out of generation, or no key arrived). Per-section guidance:
- **Overview**: Creative North Star and philosophy phrased from the answers (color strategy + motion energy + references). Reference the user's anti-reference directly.
- **Colors**: Color strategy as a Named Rule (e.g. *"The Drenched Rule. The surface IS the color."*). Hue family or anchor reference. Colors sampled from a provided logo are real; include them with exact values and note the source. Everything else stays `[to be resolved during implementation]`; those sampled anchors are the only hex this seed may carry.
- **Typography**: the direction the user picked (e.g. "Serif display + sans body"). No font names yet: `[font pairing to be chosen at implementation]`.
- **Layout** and **Shapes**: omit unless an asset or answer established a spatial or form preference; do not invent grids or corner language pre-implementation.
- **Elevation & Depth**: inferred from motion energy. Restrained/Responsive → flat by default; Choreographed → layered. One sentence.
- **Components**: omit entirely; no components exist yet.
- **Do's and Don'ts**: record the durable guardrails confirmed during the world choice, not task-local refusals.
- **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5.
Seed mode writes a minimal frontmatter with `name` and `description` only; no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `.impeccable/design.json` sidecar in seed mode for the same reason: nothing to render.
This seed writes a minimal frontmatter with `name` and `description` only; no colors, typography, rounded, spacing, or components yet.
### Step 3: Confirm
**Questionnaire seed** (`.impeccable/design-context/answers.json` exists from this run). The user answered every screen by eye, so the seed carries their answers as decisions, not directions. **`_chosen` names the fields they actually set**: it holds a JSON-encoded array of per-surface keys, and a `<key>-<mode>` field missing from that array is a **preset** the picker minted when the surface was switched on, not an answer. Read the answers file plus the picked cue's palette entry in `.impeccable/visual-cues/cues.json` (`palette-source` names it), and map:
- **Frontmatter**: `name` and `description`, plus real `colors` (the four `palette-*` hex values under descriptive slugs; these are picked, not sampled) and real `typography` (`font-heading` and `font-body` are exact family names; give each role its family and weight intent, leave sizes for implementation). Derive the two text inks and record them under `colors` too: one near-black and one near-white, the pair the picker's previews already set their text in over these exact surfaces, each holding 4.5:1 against the grounds it will carry copy on, so a builder needing body-text contrast finds ink in the system instead of inventing a fifth color. Still no `rounded`, `spacing`, or `components`: the corner and spacing answers are qualitative, and nothing is built.
- **Overview**: Creative North Star and philosophy phrased from the questionnaire's color-strategy and motion answers plus the chat references; reference the user's anti-reference directly. Name the chosen surfaces (`surface-modes`) and what each is for. Movement stays here, after the North Star, but the questionnaire asks it of a landing page and a portfolio only, so write what the keys support:
- `motion-energy-<mode>` keys present, all agreeing: one philosophy sentence for the product, as before.
- Keys present and disagreeing: one sentence per surface, named (*"The landing page moves on state change only; the portfolio stages entrances and drives sequences on scroll."*). The bare `motion-energy` is the leading one of the two.
- **No `motion-energy` key at all**: the run has neither of those surfaces, so movement was never asked. Say nothing about it, and do not fill the gap from the register; this path's chat interview never asked about motion, so there is nothing to borrow. The next Scan-mode run reads the real transitions out of the code.
- **Colors**: the four roles with their picked hex, noting the cue they came from. Name the chosen cue by its slug and name its kept image at `.impeccable/design-context/cue.png`, so a later build opens the picture the palette came from instead of imagining it; note that the unpicked cue images stay in `.impeccable/visual-cues/` for later art direction. Then open the kept image and describe it into the same section, three or four sentences under a **Cue, in words:** lead: the physical material each palette role lives on in the picture (cloth, glass, enamel, paper), the light and its temperature, the surface finish and grain, and the one material move that makes the image itself. Name each color as it appears on its material; `#C92823` as soft matte wrapping cloth instructs an image model where the bare hex only tints. Generation prompts on this world restate this passage (new-work.md and visualize.md say where), so a seed that records only the cue's file path leaves the material world to the model's imagination. The **chosen** strategy becomes the Named Rule. When surfaces differ (`color-strategy-<mode>` keys), state each surface's strategy and which surface leads (the bare key's owner).
- **Typography**: the real pair by name, the pairing's character, and the type scale as a rule: `type-scale` names it, `type-scale-ratio` is the ratio (e.g. *"Major third: each heading step is 1.25x the last"*). Base size and exact steps stay `[resolved at implementation]`. A `font-heading-source` / `font-body-source` value means a user-provided font file; record where it lives.
- **Layout**: `boundary-style` (how sections separate) per surface when the `-<mode>` keys differ, plus `layout-structure` (how pages are composed), which the questionnaire asks of a landing page and a portfolio only. No invented grids beyond what the answers state.
- `layout-structure` present: one bare key and no `-<mode>` keys, so state it as a rule for the whole product rather than per surface.
- **No `layout-structure` key at all**: the run has neither of those surfaces, so composition was never asked. Say nothing about how strict the grid is, and let `boundary-style` carry the section.
- **Elevation & Depth**: `depth-style` per surface, stated directly; the questionnaire answered this, so do not re-infer it from motion energy.
- **Shapes**: `corner-style` per surface.
- **Components**: still omit; nothing exists yet.
- **Do's and Don'ts**: the interview-only guidance, plus a Do fixing the icon source: every icon comes from the chosen pack (`icon-pack-name`, license, URL), no mixed sets. When the interview staged brand files (`context.assets` object entries in `.impeccable/design-context/context.json`), add one Do per file naming its path under `.impeccable/design-context/assets/`, its kind, and its note; a staged logo is the product's real mark and the build uses the file itself.
Per-surface answers come back for every chosen surface, presets included, and a difference between surfaces is a decision the picker enforced, not an inconsistency to smooth over (the option lists differ per surface, so a pick one surface allows can be unavailable on another and that surface falls to its preset). **Write a preset as provisional**, on the surface's own line: name the value, say it is that surface's default because the surface was never configured, and keep it out of the Named Rules and out of every product-wide sentence. Naming an untouched preset as a rule invents a law the user never chose. Where all surfaces agree **and `_chosen` shows the agreement was picked**, state the answer once for the product. `motion-energy` and `layout-structure` are the two keys that can be missing entirely, since movement and composition are asked of a landing page and a portfolio only; [visual-cues.md](visual-cues.md) has the full contract.
Both seeds skip the `.impeccable/design.json` sidecar: nothing to render yet. Real tokens for sizes, spacing, and components land on the next Scan-mode run.
### Step 6: Confirm
1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment).
2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar."
3. On the questionnaire path, add one line: the interview is kept, and `{{command_prefix}}impeccable design-context` reopens the document, re-runs the questionnaire over these answers, or writes the context out for another tool. See [design-context.md](design-context.md).
Your own write is the freshest source; no reload needed.
When the questionnaire ran, the confirm is not the end of the turn: the design context document in the user's tab is live for edits through the session the picker forked. Follow the document edit loop in [visual-cues.md](visual-cues.md): poll, apply `edit_request`s to this same DESIGN.md, reply. A color the user changed in the tab before your seed write is already in `answers.json`; one changed after arrives as a `save_batch` event, its value already in the store and its description in DESIGN.md yours to bring in line.
## Style guidelines
- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places; the frontmatter is normative.
+1
View File
@@ -102,6 +102,7 @@ node {{scripts_path}}/hook-admin.mjs ignore-file "src/legacy/Card.tsx"
- Never modify `.impeccable/config.json` or `.impeccable/config.local.json` by hand from this command. Always go through `hook-admin.mjs` so writes stay validated and the file shape stays consistent. One exception: `detector.extensions` has no admin action, so when the user asks to cover a template stack, edit that one field in `.impeccable/config.json` directly and leave the rest of the file untouched.
- Do not edit the hook scripts themselves (`hook.mjs`, `hook-lib.mjs`, `hook-before-edit.mjs`) from this flow. Those are skill plumbing.
- The design context document's Hooks page reads and writes this same config through `hook-admin.mjs` (`state` and `apply`, its machine channel, called by the doc session); those two verbs are not part of this command's routing. Entries it wrote are user decisions: the person pressed Apply in the page, so treat them like any user-made config.
- Cursor can block a proposed write when the detector finds a real issue. Claude Code, Codex, and GitHub Copilot do not block the edit; they emit a post-edit reminder instead. Disabling stops both blocking and reminders.
- The hook is bundled with the Impeccable skill and installed through project-local manifests: `.claude/settings.local.json`, `.codex/hooks.json`, `.cursor/hooks.json`, and `.github/hooks/impeccable.json`. On Codex, the user must approve the hook via `/hooks` the first time. On Cursor, confirm hooks are enabled under Settings -> Hooks. On GitHub Copilot, the CLI loads `.github/hooks/impeccable.json` once it is committed to the repository's default branch, and the cloud agent reads it from the repo directly.

Some files were not shown because too many files have changed in this diff Show More