Move theme picker out of navbar to a fixed bottom-left chip

The 12-theme popover was congested when opened inline in the
navbar. Now it lives as a floating chip in the bottom-left corner:
always reachable on desktop and mobile, popover opens upward, and
the navbar is a clean two-column wordmark + meta layout.

- index.html: themer markup moved to end of <body>, navbar simplified
- components.css: .themer is position: fixed with bottom/left anchors,
  popover opens upward (bottom: 100% + 0.5rem), responsive grid
  collapses to 2 columns under 60rem
- sections.css: removed .nav__themer rules; .nav restored to flex
  space-between
- README.md: rewritten concise + complete, mirrors the v0.2.0 surface
  (10 reference files, 12 themes, 4 verbs, 20-question slop test)
This commit is contained in:
Youssef
2026-04-27 22:38:45 +01:00
parent c49b190911
commit 3fab5ba75b
4 changed files with 265 additions and 232 deletions
+59 -57
View File
@@ -8,15 +8,54 @@ A design skill for AI coding assistants. Makes the UIs they generate look made,
## What it is
Hallmark is a single, opinionated skill — short on purpose — that teaches Claude Code / Cursor / Codex a firm, consensus-backed set of rules for typography, colour, layout, motion, interaction, responsive behaviour, and copy. It refuses to design without a confirmed audience, use case, and tone. It refuses to emit the on-distribution defaults every LLM was trained into (Inter + purple gradient + centred hero + 3-column icon tiles).
A single, opinionated skill — short on purpose — that teaches Claude Code, Cursor, or Codex a firm, consensus-backed set of rules for typography, colour, layout, motion, interaction, microinteractions, copy, responsive behaviour, and structural variety. It refuses to design without a confirmed audience, use case, and tone, and refuses to emit the on-distribution defaults every LLM was trained into.
It ships one holistic verb and two auxiliary ones:
The differentiator: Hallmark insists on **structural variety**, not just visual variety. Two pages by Hallmark for two different briefs should not share the same hero → 3-feature → CTA → footer rhythm.
| Invocation | What it does |
---
## Four verbs
| Verb | What it does |
| --- | --- |
| *(default)* | Build / edit UI. Requires design context before touching code. |
| `hallmark audit <target>` | Score existing code against the named anti-patterns. Returns a punch list. |
| `hallmark refine <target>` | Apply the ruleset to polish existing code with the smallest possible diff. |
| *(default)* | Build new UI — passes the design-context gate, picks a structural fingerprint, applies the ruleset, runs the slop test before handing back. |
| `hallmark audit <target>` | Score existing code against the named anti-patterns + structural sameness. Returns a punch list. No edits. |
| `hallmark refine <target>` | Polish in place. Smallest possible diff. Preserves structure. |
| `hallmark redesign <target> [--mood <name>]` | Throw out the structure, keep copy + IA + brand, rebuild with a deliberately different structural fingerprint. |
---
## What's inside
**Skill** — [`skill/SKILL.md`](skill/SKILL.md) plus ten reference files:
- [`typography.md`](skill/references/typography.md) — banned defaults, pairing rules, weight extremes, scale ratios
- [`color.md`](skill/references/color.md) — OKLCH palette construction, accent discipline (≤3% area), dark-mode recipe
- [`layout-and-space.md`](skill/references/layout-and-space.md) — 4pt scale, asymmetry, grid-breaks, depth via weight not shadow
- [`structure.md`](skill/references/structure.md) — six axes of structural variety (heading placement, body composition, divider, button voice, image, reveal); the per-theme structural fingerprint table
- [`motion.md`](skill/references/motion.md) — duration buckets, three named easings, no bounce on UI, reduced-motion default
- [`microinteractions.md`](skill/references/microinteractions.md) — 14 interaction recipes (button press, focus, modal, toast, optimistic update, command palette, drag, copy-to-clipboard, search-as-you-type) and the 20 named microinteraction tells
- [`interaction-and-states.md`](skill/references/interaction-and-states.md) — eight states per element, focus-visible spec, 44px hit targets, undo over confirm
- [`responsive.md`](skill/references/responsive.md) — mobile-first, content-driven breakpoints, `dvh` not `vh`, safe-area-insets
- [`copy.md`](skill/references/copy.md) — specific verbs, three-part errors, standalone link text, proper typographic punctuation
- [`anti-patterns.md`](skill/references/anti-patterns.md) — 30+ named tells with severity, why they fail, and the fix
**Slop test (20 questions)** — every output must answer *no* to all twenty before shipping. Visual (8), structural (2), microinteraction (10).
**Site** — [`site/`](site/) is a self-demonstrating landing page. Hand-written HTML + CSS + ES module, no framework, no build step. Uses Together AI's "The Future" typeface plus Google Fonts.
**Twelve themes**, four categories, each with its own structural fingerprint:
- **Editorial** — Specimen, Newsprint, Atelier
- **Soft** — Garden, Salon, Linen
- **Technical** — Midnight, Terminal, Almanac
- **Bold** — Brutal, Manifesto, Sport
Each theme overrides not only colour and typography but section-heading placement, column counts, divider style, button voice, and reveal pattern. The same landing page reorganises itself per theme, demonstrating the structural-variety principle.
**Two test specimens** — [`site/_example-bakery/`](site/_example-bakery/) (an editorial bakery announcement, output of a real `hallmark` run) and [`site/_example-cmdk/`](site/_example-cmdk/) (a keyboard-first command palette demonstrating the microinteraction canon).
**Roadmap** — [`ROADMAP.md`](ROADMAP.md) lists Tier 1, 2, and 3 work, plus an explicit "things to *not* do" list and measurement criteria.
---
@@ -28,7 +67,7 @@ It ships one holistic verb and two auxiliary ones:
npx skills add hallmark
```
### Claude Code
### Claude Code (manual)
Copy [`skill/SKILL.md`](skill/SKILL.md) and [`skill/references/`](skill/references/) into `~/.claude/skills/hallmark/`.
@@ -36,70 +75,33 @@ Copy [`skill/SKILL.md`](skill/SKILL.md) and [`skill/references/`](skill/referenc
Create `.cursor/rules/hallmark.mdc` with the body of `SKILL.md` (no frontmatter).
### Manual
`git clone` this repo; copy `skill/` into whatever config directory your agent uses.
---
## Philosophy
Five skills — [impeccable](https://github.com/pbakaus/impeccable), [kami](https://github.com/tw93/kami), [taste-skill](https://github.com/Leonxlnx/taste-skill), Anthropic's [frontend-design skill](https://github.com/anthropics/skills), and the Claude [frontend aesthetics cookbook](https://platform.claude.com/cookbook/coding-prompting-for-frontend-aesthetics) — have converged on roughly the same anti-slop ruleset. Hallmark takes that consensus, restates it in one voice, and keeps the surface small so it actually gets loaded into context.
Hallmark's distinct choices:
- **One skill, three verbs.** Not eighteen commands. Use the default; reach for `audit` or `refine` when you need them.
- **Design-context gate.** No audience + use case + tone → no design. The skill will ask.
- **Tone as a first-class decision.** "Clean and modern" is rejected. You pick an extreme: editorial, brutalist, soft, utilitarian, luxury, playful, technical, austere.
- **The slop test.** Before handing back, the skill runs ten yes/no checks. One yes fails the output.
---
## The landing page
[`site/`](site/) is a live specimen — it's the skill applied to itself. Serve it statically:
### View the landing page locally
```
cd site && python3 -m http.server 4173
# → http://localhost:4173
```
The page uses Together AI's "The Future" typeface (distributed here with permission for demonstration), an OKLCH warm-oat palette anchored at hue 80, a single signal-orange accent (`#FC4C02`, ≤ 3 % of any view), and a drafting-specimen layout language — hairline rules, small-caps section numbering, asymmetric columns.
No framework. No build step. One `index.html`, four stylesheets, one ES module.
---
## Repo layout
## Philosophy
```
hallmark/
├── README.md ← you are here
├── LICENSE ← MIT
├── package.json ← for `npx skills add`
├── skill/
│ ├── SKILL.md ← main routing + principles
│ └── references/
│ ├── typography.md
│ ├── color.md
│ ├── layout-and-space.md
│ ├── motion.md
│ ├── interaction-and-states.md
│ ├── responsive.md
│ ├── copy.md
│ └── anti-patterns.md
└── site/
├── index.html
├── css/{tokens,base,components,sections}.css
├── js/main.js
└── fonts/the-future-*.woff2
```
Five skills — [impeccable](https://github.com/pbakaus/impeccable), [kami](https://github.com/tw93/kami), [taste-skill](https://github.com/Leonxlnx/taste-skill), Anthropic's [frontend-design skill](https://github.com/anthropics/skills), and the Claude [frontend aesthetics cookbook](https://platform.claude.com/cookbook/coding-prompting-for-frontend-aesthetics) — have converged on roughly the same anti-slop ruleset. Hallmark takes that consensus, restates it in one voice, fills the gap on motion and microinteractions, and adds *structural* variety as a first-class principle.
Distinct choices:
- **One skill, four verbs.** Not eighteen commands. Use the default; reach for `audit`, `refine`, or `redesign` when you need them.
- **Design-context gate.** No audience + use case + tone → no design. The skill asks.
- **Tone as a first-class decision.** "Clean and modern" is rejected. Pick an extreme: editorial, brutalist, soft, utilitarian, luxury, playful, technical, austere.
- **Structural fingerprint.** Every page picks one option from each of six axes. Two consecutive outputs in a session should never share the same fingerprint.
- **Microinteractions are not decoration.** Silent success over celebratory toasts. Optimistic update + Undo over confirm dialogs. Hover delay 800 ms, focus delay 0 ms.
- **The slop test.** Twenty yes/no checks before handing back. One yes fails the output.
---
## Credits
Built on the open work of Paul Bakaus ([impeccable](https://github.com/pbakaus/impeccable)), tw93 ([kami](https://github.com/tw93/kami)), Leonxlnx ([taste-skill](https://github.com/Leonxlnx/taste-skill)), and Anthropic's skills team. Where their rules overlapped, Hallmark adopted them; where they diverged, Hallmark picked. See each source for its own deeper treatment of the same problem.
Built on the open work of Paul Bakaus ([impeccable](https://github.com/pbakaus/impeccable)), tw93 ([kami](https://github.com/tw93/kami)), Leonxlnx ([taste-skill](https://github.com/Leonxlnx/taste-skill)), Anthropic's skills team ([frontend-design](https://github.com/anthropics/skills) and [canvas-design](https://github.com/anthropics/skills)), [DESIGN.md](https://getdesign.md) (Google Stitch), MC Dean's [63 design skills](https://marieclairedean.substack.com/p/i-built-63-design-skills-for-claude), [PencilPlaybook](https://github.com/stevembarclay/pencilplaybook), and the [Slopless](https://slopless.design) tactile-rebellion canon. Where rules overlapped, Hallmark adopted; where they diverged, Hallmark picked.
---
+72 -30
View File
@@ -23,12 +23,15 @@
color: var(--color-accent);
}
/* — Theme picker (button + popover) ————————————————————————— */
/* — Theme picker (fixed bottom-left chip + upward popover) ————— */
.themer {
position: relative;
position: fixed;
left: max(1rem, env(safe-area-inset-left));
bottom: max(1rem, env(safe-area-inset-bottom));
z-index: var(--z-sticky);
display: inline-flex;
align-items: center;
gap: var(--space-xs);
gap: 0.5rem;
font-family: var(--font-label);
font-size: 0.6875rem;
letter-spacing: var(--tracking-label);
@@ -39,15 +42,25 @@
.themer__trigger {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.9rem;
background: var(--color-paper-2);
gap: 0.55rem;
padding: 0.55rem 0.85rem 0.55rem 0.6rem;
background: var(--color-paper);
border: var(--rule-hair) solid var(--color-rule-2);
color: var(--color-ink);
font: inherit;
letter-spacing: 0.14em;
cursor: pointer;
min-height: 36px;
/* explicit transitions, not transition-all */
transition: background-color var(--dur-micro) var(--ease-out),
border-color var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out);
box-shadow: 0 1px 2px oklch(20% 0.02 60 / 0.05),
0 8px 24px -8px oklch(20% 0.02 60 / 0.10);
}
@media (hover: hover) and (pointer: fine) {
.themer__trigger:hover { border-color: var(--color-ink); }
}
.themer__trigger:focus-visible {
@@ -55,6 +68,16 @@
outline-offset: 2px;
}
/* Tiny accent swatch on the trigger — confirms current theme at a glance */
.themer__trigger-swatch {
width: 0.55rem;
height: 0.55rem;
background: var(--color-accent);
display: inline-block;
border-radius: 50%;
flex-shrink: 0;
}
.themer__trigger-label {
color: var(--color-muted);
}
@@ -65,39 +88,45 @@
}
.themer__trigger-caret {
width: 0.45em;
height: 0.45em;
width: 0.4em;
height: 0.4em;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(45deg) translateY(-1px);
transform: rotate(-135deg) translate(-1px, 1px); /* points up — popover opens up */
transition: transform var(--dur-short) var(--ease-out);
}
.themer[aria-expanded="true"] .themer__trigger-caret {
transform: rotate(225deg) translateY(2px);
transform: rotate(45deg) translate(-2px, -1px); /* points down when open */
}
/* Keyboard-shortcut hint — delay-reveals on hover (Linear / Notion pattern) */
/* Keyboard-shortcut hint — delay-reveals on hover, instant on focus */
.themer__kbd {
display: inline-flex;
align-items: center;
gap: 0.3em;
margin-left: 0.5rem;
padding: 0.15rem 0.4rem;
justify-content: center;
padding: 0.15rem 0.45rem;
font-family: var(--font-label);
font-size: 0.625rem;
letter-spacing: 0.08em;
color: var(--color-muted);
background: var(--color-paper-3);
background: var(--color-paper-2);
border: var(--rule-hair) solid var(--color-rule);
opacity: 0;
transform: translateX(-4px);
pointer-events: none;
/* hover delay: 1.2s before reveal — Notion/Linear pattern */
min-height: 28px;
/* hover delay: 1.2s before reveal */
transition: opacity 180ms var(--ease-out) 1.2s,
transform 180ms var(--ease-out) 1.2s;
}
.themer__kbd::before {
content: "press ";
margin-right: 0.35em;
text-transform: uppercase;
}
.themer__trigger:hover ~ .themer__kbd {
opacity: 1;
transform: none;
@@ -107,7 +136,7 @@
.themer[aria-expanded="true"] .themer__kbd {
opacity: 1;
transform: none;
/* no delay on focus or open — keyboard users want it now */
/* no delay on focus or open */
transition: opacity 180ms var(--ease-out),
transform 180ms var(--ease-out);
}
@@ -117,30 +146,34 @@
color: var(--color-ink);
}
@media (max-width: 56rem) {
@media (max-width: 40rem) {
.themer__kbd { display: none; }
.themer__trigger { padding-block: 0.6rem; }
}
@media (prefers-reduced-motion: reduce) {
.themer__kbd { transition-duration: 0.01ms; transition-delay: 0.01ms; }
}
/* Popover panel */
/* Popover panel — opens upward from the chip */
.themer__panel {
position: absolute;
top: calc(100% + 0.5rem);
left: 50%;
transform: translateX(-50%) translateY(-4px);
width: min(48rem, calc(100vw - 2rem));
bottom: calc(100% + 0.5rem);
left: 0;
transform: translateY(4px);
width: min(40rem, calc(100vw - 2rem));
max-height: calc(100dvh - 6rem);
overflow-y: auto;
background: var(--color-paper);
border: var(--rule-hair) solid var(--color-rule-2);
padding: var(--space-lg);
z-index: var(--z-dropdown);
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-lg);
opacity: 0;
pointer-events: none;
box-shadow: 0 24px 60px -20px oklch(20% 0.02 60 / 0.18),
0 0 0 1px oklch(20% 0.02 60 / 0.04);
transition: opacity var(--dur-short) var(--ease-out),
transform var(--dur-short) var(--ease-out);
}
@@ -148,18 +181,27 @@
.themer[aria-expanded="true"] .themer__panel {
opacity: 1;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
transform: translateY(0);
}
@media (max-width: 60rem) {
.themer__panel {
grid-template-columns: repeat(2, 1fr);
width: min(28rem, calc(100vw - 2rem));
left: auto;
right: 0;
transform: translateY(-4px);
width: min(26rem, calc(100vw - 2rem));
}
}
@media (max-width: 36rem) {
.themer__panel {
grid-template-columns: 1fr 1fr;
/* anchor to viewport edges so it doesn't clip on small screens */
left: 0;
right: auto;
width: calc(100vw - 2rem);
max-width: 26rem;
padding: var(--space-md);
gap: var(--space-md);
}
.themer[aria-expanded="true"] .themer__panel { transform: translateY(0); }
}
.themer__group {
+2 -14
View File
@@ -11,35 +11,23 @@
/* — Nav ————————————————————————————————————————————— */
.nav {
display: grid;
grid-template-columns: auto 1fr auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-lg);
padding-block: var(--space-md);
}
.nav__themer { justify-self: center; }
.nav__meta {
font-family: var(--font-label);
font-size: var(--text-xs);
color: var(--color-muted);
letter-spacing: var(--tracking-label);
text-transform: uppercase;
justify-self: end;
}
.nav__meta a { color: var(--color-ink); }
@media (max-width: 56rem) {
.nav {
grid-template-columns: auto auto;
grid-template-rows: auto auto;
row-gap: var(--space-md);
}
.nav__themer { grid-column: 1 / -1; justify-self: stretch; }
}
/* — Section container ——————————————————————————————— */
.section {
display: grid;
+132 -131
View File
@@ -35,135 +35,6 @@
<!-- NAV -->
<nav class="nav" aria-label="Primary">
<a href="#top" class="wordmark">Hallmark</a>
<div class="themer nav__themer" aria-expanded="false">
<button class="themer__trigger" type="button" id="themer-trigger" aria-haspopup="dialog" aria-expanded="false">
<span class="themer__trigger-label">Theme&nbsp;·&nbsp;</span>
<span class="themer__trigger-current" data-theme-current>Specimen</span>
<span class="themer__trigger-caret" aria-hidden="true"></span>
</button>
<span class="themer__kbd" aria-hidden="true">
press <span class="themer__kbd-key">T</span>
</span>
<div class="themer__panel" id="themer-panel" role="dialog" aria-label="Choose a theme">
<div class="themer__group">
<p class="themer__group-label">Editorial</p>
<button class="themer__btn" type="button" data-theme-btn="specimen" aria-pressed="true">
<span>Specimen</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.012 80)"></span>
<span class="themer__swatch" style="background: oklch(18% 0.010 60)"></span>
<span class="themer__swatch" style="background: #FC4C02"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="newsprint" aria-pressed="false">
<span>Newsprint</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(95% 0.020 60)"></span>
<span class="themer__swatch" style="background: oklch(18% 0.012 250)"></span>
<span class="themer__swatch" style="background: oklch(48% 0.13 30)"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="atelier" aria-pressed="false">
<span>Atelier</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.012 70)"></span>
<span class="themer__swatch" style="background: oklch(12% 0.012 40)"></span>
<span class="themer__swatch" style="background: oklch(58% 0.115 65)"></span>
</span>
</button>
</div>
<div class="themer__group">
<p class="themer__group-label">Soft</p>
<button class="themer__btn" type="button" data-theme-btn="garden" aria-pressed="false">
<span>Garden</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.020 115)"></span>
<span class="themer__swatch" style="background: oklch(24% 0.050 160)"></span>
<span class="themer__swatch" style="background: #B6485E"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="salon" aria-pressed="false">
<span>Salon</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(95% 0.014 65)"></span>
<span class="themer__swatch" style="background: oklch(22% 0.024 35)"></span>
<span class="themer__swatch" style="background: oklch(58% 0.105 130)"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="linen" aria-pressed="false">
<span>Linen</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(94% 0.018 95)"></span>
<span class="themer__swatch" style="background: oklch(24% 0.022 140)"></span>
<span class="themer__swatch" style="background: oklch(54% 0.080 140)"></span>
</span>
</button>
</div>
<div class="themer__group">
<p class="themer__group-label">Technical</p>
<button class="themer__btn" type="button" data-theme-btn="midnight" aria-pressed="false">
<span>Midnight</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(14% 0.010 40)"></span>
<span class="themer__swatch" style="background: oklch(94% 0.006 80)"></span>
<span class="themer__swatch" style="background: #FC4C02"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="terminal" aria-pressed="false">
<span>Terminal</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(11% 0.010 145)"></span>
<span class="themer__swatch" style="background: oklch(86% 0.150 138)"></span>
<span class="themer__swatch" style="background: oklch(78% 0.180 138)"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="almanac" aria-pressed="false">
<span>Almanac</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(94% 0.012 80)"></span>
<span class="themer__swatch" style="background: oklch(16% 0.012 45)"></span>
<span class="themer__swatch" style="background: oklch(38% 0.135 250)"></span>
</span>
</button>
</div>
<div class="themer__group">
<p class="themer__group-label">Bold</p>
<button class="themer__btn" type="button" data-theme-btn="brutal" aria-pressed="false">
<span>Brutal</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(98% 0.003 90)"></span>
<span class="themer__swatch" style="background: oklch(10% 0.005 60)"></span>
<span class="themer__swatch" style="background: #FF2E2E"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="manifesto" aria-pressed="false">
<span>Manifesto</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(98% 0.004 90)"></span>
<span class="themer__swatch" style="background: oklch(8% 0.005 60)"></span>
<span class="themer__swatch" style="background: #E51A1A"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="sport" aria-pressed="false">
<span>Sport</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.006 250)"></span>
<span class="themer__swatch" style="background: oklch(16% 0.080 260)"></span>
<span class="themer__swatch" style="background: oklch(58% 0.190 35)"></span>
</span>
</button>
</div>
</div>
</div>
<div class="nav__meta">
v0.1 &nbsp;·&nbsp; <a href="#install" class="link">Install</a>
</div>
@@ -454,8 +325,8 @@
<article class="step">
<span class="step__num">III</span>
<div>
<h3 class="step__head">Ship, audit, refine</h3>
<p class="step__sub">The default verb builds. <code>hallmark audit</code> scores existing code against the tells. <code>hallmark refine</code> polishes in place.</p>
<h3 class="step__head">Ship, audit, refine, redesign</h3>
<p class="step__sub">The default verb builds. <code>hallmark audit</code> scores existing code against the tells. <code>hallmark refine</code> polishes in place. <code>hallmark redesign</code> rethinks the structure.</p>
</div>
<div class="step__body">
<pre class="code code--block">$ claude "hallmark audit ./components/hero.tsx"
@@ -526,6 +397,136 @@
</main>
<!-- Theme picker — fixed bottom-left, never in the navbar. -->
<div class="themer" aria-expanded="false">
<button class="themer__trigger" type="button" id="themer-trigger" aria-haspopup="dialog" aria-expanded="false">
<span class="themer__trigger-swatch" aria-hidden="true"></span>
<span class="themer__trigger-label">Theme&nbsp;·&nbsp;</span>
<span class="themer__trigger-current" data-theme-current>Specimen</span>
<span class="themer__trigger-caret" aria-hidden="true"></span>
</button>
<span class="themer__kbd" aria-hidden="true">
<span class="themer__kbd-key">T</span>
</span>
<div class="themer__panel" id="themer-panel" role="dialog" aria-label="Choose a theme">
<div class="themer__group">
<p class="themer__group-label">Editorial</p>
<button class="themer__btn" type="button" data-theme-btn="specimen" aria-pressed="true">
<span>Specimen</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.012 80)"></span>
<span class="themer__swatch" style="background: oklch(18% 0.010 60)"></span>
<span class="themer__swatch" style="background: #FC4C02"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="newsprint" aria-pressed="false">
<span>Newsprint</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(95% 0.020 60)"></span>
<span class="themer__swatch" style="background: oklch(18% 0.012 250)"></span>
<span class="themer__swatch" style="background: oklch(48% 0.13 30)"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="atelier" aria-pressed="false">
<span>Atelier</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.012 70)"></span>
<span class="themer__swatch" style="background: oklch(12% 0.012 40)"></span>
<span class="themer__swatch" style="background: oklch(58% 0.115 65)"></span>
</span>
</button>
</div>
<div class="themer__group">
<p class="themer__group-label">Soft</p>
<button class="themer__btn" type="button" data-theme-btn="garden" aria-pressed="false">
<span>Garden</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.020 115)"></span>
<span class="themer__swatch" style="background: oklch(24% 0.050 160)"></span>
<span class="themer__swatch" style="background: #B6485E"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="salon" aria-pressed="false">
<span>Salon</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(95% 0.014 65)"></span>
<span class="themer__swatch" style="background: oklch(22% 0.024 35)"></span>
<span class="themer__swatch" style="background: oklch(58% 0.105 130)"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="linen" aria-pressed="false">
<span>Linen</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(94% 0.018 95)"></span>
<span class="themer__swatch" style="background: oklch(24% 0.022 140)"></span>
<span class="themer__swatch" style="background: oklch(54% 0.080 140)"></span>
</span>
</button>
</div>
<div class="themer__group">
<p class="themer__group-label">Technical</p>
<button class="themer__btn" type="button" data-theme-btn="midnight" aria-pressed="false">
<span>Midnight</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(14% 0.010 40)"></span>
<span class="themer__swatch" style="background: oklch(94% 0.006 80)"></span>
<span class="themer__swatch" style="background: #FC4C02"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="terminal" aria-pressed="false">
<span>Terminal</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(11% 0.010 145)"></span>
<span class="themer__swatch" style="background: oklch(86% 0.150 138)"></span>
<span class="themer__swatch" style="background: oklch(78% 0.180 138)"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="almanac" aria-pressed="false">
<span>Almanac</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(94% 0.012 80)"></span>
<span class="themer__swatch" style="background: oklch(16% 0.012 45)"></span>
<span class="themer__swatch" style="background: oklch(38% 0.135 250)"></span>
</span>
</button>
</div>
<div class="themer__group">
<p class="themer__group-label">Bold</p>
<button class="themer__btn" type="button" data-theme-btn="brutal" aria-pressed="false">
<span>Brutal</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(98% 0.003 90)"></span>
<span class="themer__swatch" style="background: oklch(10% 0.005 60)"></span>
<span class="themer__swatch" style="background: #FF2E2E"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="manifesto" aria-pressed="false">
<span>Manifesto</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(98% 0.004 90)"></span>
<span class="themer__swatch" style="background: oklch(8% 0.005 60)"></span>
<span class="themer__swatch" style="background: #E51A1A"></span>
</span>
</button>
<button class="themer__btn" type="button" data-theme-btn="sport" aria-pressed="false">
<span>Sport</span>
<span class="themer__swatches" aria-hidden="true">
<span class="themer__swatch" style="background: oklch(96% 0.006 250)"></span>
<span class="themer__swatch" style="background: oklch(16% 0.080 260)"></span>
<span class="themer__swatch" style="background: oklch(58% 0.190 35)"></span>
</span>
</button>
</div>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>