diff --git a/civiccore-ui/README.md b/civiccore-ui/README.md new file mode 100644 index 0000000..8ace8bd --- /dev/null +++ b/civiccore-ui/README.md @@ -0,0 +1,39 @@ +# civiccore-ui — the CivicSuite design system + +The shared, versioned UI layer every CivicSuite module builds on. Three layers, +each the single source of truth for its concern: + +| Layer | File | Owns | +|---|---|---| +| **Tokens** | [`tokens/tokens.css`](tokens/tokens.css) | Values — color, ink ramp, status palette, type families, density, radii. WCAG-AA-validated ([`tokens/tokens-reference.html`](tokens/tokens-reference.html)). | +| **Components** | [`components/components.css`](components/components.css) | Structure — buttons, badges, status ribbon, tables, forms, alerts, first-run steps, empty states, audit drawer ([`components/components-reference.html`](components/components-reference.html)). | +| **Shell** | [`shell/shell.css`](shell/shell.css) | The app frame — brand block, topbar, surface switcher, nav, workspace, page header, letterhead, search overlay. | + +## Dependency direction + +``` +tokens.css ─┐ + ├─► components.css ─► shell.css + ┘ +``` + +Components consume tokens; the shell consumes both. Load in that order. No layer +redefines a `:root` custom property that `tokens.css` already owns. + +## Consumption (offline binaries cannot `@import` at runtime) + +Consumers — the Tauri/WebView2 desktop app, the umbrella prototype — **vendor a +generated copy** of each file, pinned to a `civiccore` version, with a CI +`--check` drift gate that fails the build if a vendored copy drifts from the +source here. Any JS/JSON mirror is generated, never hand-maintained. This is the +same idiom as the `source_commit` pins and the generated topology block. See the +umbrella repo's `docs/design/windows-desktop-design-control.md` (Token Authority). + +## Provenance + +`components.css` and `shell.css` were extracted faithfully from the canonical +prototype (`docs/design/ui-ux-prototype/styles.css` in the umbrella repo) — same +class names, same behavior — with one accessibility correction: `.badge.gold` +uses `--gold-strong` (5.43:1 on `--gold-soft`) instead of `--gold-2` (4.31:1, an +AA fail). Migrating the prototype and desktop app to consume these files (rather +than their own copies) is the incremental follow-on, one surface at a time. diff --git a/civiccore-ui/components/.gitkeep b/civiccore-ui/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/civiccore-ui/components/components-reference.html b/civiccore-ui/components/components-reference.html new file mode 100644 index 0000000..13679d4 --- /dev/null +++ b/civiccore-ui/components/components-reference.html @@ -0,0 +1,145 @@ + + + + +CivicSuite — Component Reference + + + + + + + +
+
+
CivicSuite · Design System
+

Component Reference

+

The canonical component primitives, rendered from components.css on the token authority. Every color, type, and density value comes from tokens.css. This is the structure source the way tokens.css is the value source.

+
+ +
+

Buttons

+

.btn · .primary .ghost .danger · sizes .sm .lg. No gold button: white-on-gold fails AA; use .primary for a solid CTA.

+
+ + + + + + +
+
+ +
+

Badges & status

+

.badge · .ok .warn .err .info .gold · .dot. Gold uses --gold-strong (5.43:1, AA).

+
+ Neutral + Released + Needs attention + Failed + Platform + Queued +
+
+ +
+

Status ribbon (lifecycle)

+

.ribbon > .ribbon-step (.done .current). Status conveyed by shape + label, never color alone.

+
+
+
1Drafted
+
2Reviewed
+
3On agenda
+
4Posted
+
5Archived
+
+
+
+ +
+

Table

+

.tbl with .id .ttl .meta cells; honors [data-density].

+
+ + + + + + + +
IDNoticeStatusUpdated
N-1042Public hearing — rezoningPosted2026-07-01
N-1043Budget adoption noticeQueued2026-07-02
N-1044Special meetingDraft2026-07-03
+
+
+ +
+

Form fields

+

.field > label (.req) / .hint + .input .select .textarea. Focus ring is the AA navy ring.

+
+
+
Used on public-records responses.
+
+
+
+
+ +
+

Alerts

+

.alert · .ok .warn .err .info. Used for the "AI engine not ready" fallback state and inline validation.

+
+
AI engine not readyEvery tool still works in labeled sample mode.
+
i
This is a draft. A human reviews and decides before anything is published.
+
Review saved. Records-ready export is available.
+
+
+ +
+

First-run setup step

+

.setup-step (.current .done) — the canonical three-state wizard step.

+
+

City profile

Done
+
3

First administrator

Now
+
4

Backup folder

+
+
+ +
+

Empty state

+
No saved reviews yet
Run an accessibility review and save it to see it listed here with a records-ready export.
+
+ +
+

Audit drawer

+

.audit-drawer — the hash-chained, cross-module audit trail (shown inline here; it is a right-docked drawer in the app).

+
+
+
Audit trail
Hash-chained · this record
+
+
+
Review created
clerk@city.gov · 2026-07-02 14:22
Accessibility review opened for notice N-1043.
a1b2…9f4c
+
AI analysis attached
gemma-4-12b-it-qat-q4_0 · 2026-07-02 14:23
Advisory remediation analysis stored (draft; status unchanged).
c3d4…1a77
+
Review saved
clerk@city.gov · 2026-07-02 14:25
e5f6…8b22
+
+
+
+
+
+ + diff --git a/civiccore-ui/components/components.css b/civiccore-ui/components/components.css new file mode 100644 index 0000000..2e83c62 --- /dev/null +++ b/civiccore-ui/components/components.css @@ -0,0 +1,352 @@ +/* ============================================================================= + CivicSuite component primitives — CANONICAL COMPONENT LAYER + ----------------------------------------------------------------------------- + Buttons, badges, status ribbon, tables, forms, alerts, first-run setup steps, + empty states, and the audit drawer. Extracted faithfully from the canonical + prototype (docs/design/ui-ux-prototype/styles.css in the CivicSuite umbrella + repo) — same class names, same behavior — so this file becomes the single + source for component STRUCTURE the way tokens.css is the source for VALUES. + + CONSUMPTION: consumes civiccore-ui/tokens/tokens.css (all colors/type/density + come from those custom properties). Load tokens.css first, then this file. + Offline consumers vendor a generated copy pinned to a civiccore version, with + a CI --check drift gate (same idiom as tokens.css). + + AA: one correction vs the prototype at extraction time — .badge.gold uses + --gold-strong (5.43:1 on --gold-soft) instead of --gold-2 (4.31:1, a fail). + ========================================================================== */ + +/* Card */ +.card { + background: var(--card); + border: 1px solid var(--rule); + border-radius: var(--radius-lg); + overflow: hidden; +} +.card-h { + padding: 14px 16px 12px; + border-bottom: 1px solid var(--rule); + display: flex; align-items: center; gap: 10px; +} +.card-h .ttl { + font-size: 13px; + font-weight: 600; + letter-spacing: 0.01em; + color: var(--ink); +} +.card-h .ttl small { + font-weight: 400; + color: var(--ink-3); + margin-left: 6px; + letter-spacing: 0; +} +.card-h .right { margin-left: auto; display: flex; gap: 6px; } +.card-b { padding: 12px 16px; } +.card-b.flush { padding: 0; } +[data-density="compact"] .card-h { padding: 10px 12px 10px; } +[data-density="compact"] .card-b { padding: 10px 12px; } + + +/* Buttons */ +.btn { + appearance: none; + display: inline-flex; align-items: center; gap: 6px; + height: 32px; + padding: 0 12px; + border-radius: 7px; + border: 1px solid var(--rule); + background: var(--card); + color: var(--ink); + font-size: 12.5px; + font-weight: 500; + letter-spacing: 0.005em; +} +.btn:hover { background: var(--paper-2); } +.btn.primary { background: var(--navy); color: #fff; border-color: var(--navy-2); } +.btn.primary:hover { background: var(--navy-2); } +/* .btn.gold removed: white-on-gold is 3.22:1, fails WCAG AA. Gold is accent/border + only; use .btn.primary for a solid CTA, or --gold-2 for text-bearing gold. */ +.btn.ghost { background: transparent; border-color: transparent; } +.btn.ghost:hover { background: var(--paper-2); border-color: var(--rule); } +.btn.danger { color: var(--err); border-color: var(--err-soft); } +.btn.sm { height: 26px; padding: 0 8px; font-size: 11.5px; } +.btn.lg { height: 38px; padding: 0 16px; font-size: 13.5px; } + +/* Badges / Status */ +.badge { + display: inline-flex; align-items: center; gap: 4px; + font-size: 10.5px; + font-weight: 500; + letter-spacing: 0.02em; + padding: 2px 7px; + border-radius: 100px; + background: var(--neutral-soft); + color: var(--ink-2); + white-space: nowrap; + border: 1px solid transparent; +} +.badge.ok { background: var(--ok-soft); color: var(--ok); } +.badge.warn { background: var(--warn-soft); color: var(--warn); } +.badge.err { background: var(--err-soft); color: var(--err); } +.badge.info { background: var(--info-soft); color: var(--info); } +.badge.gold { background: var(--gold-soft); color: var(--gold-strong); } /* AA: gold-strong 5.43:1 (gold-2 was 4.31:1) */ +.badge.dot::before { + content: ""; + width: 6px; height: 6px; + border-radius: 50%; + background: currentColor; + display: inline-block; +} + +/* Status ribbon (lifecycle) */ +.ribbon { + display: flex; + align-items: center; + gap: 0; + padding: 0; + background: var(--paper-2); + border: 1px solid var(--rule); + border-radius: 8px; + overflow: hidden; + font-size: 11.5px; +} +.ribbon-step { + flex: 1; + padding: 10px 14px 10px 22px; + position: relative; + color: var(--ink-3); + background: transparent; + letter-spacing: 0.005em; + white-space: nowrap; +} +.ribbon-step:first-child { padding-left: 14px; } +.ribbon-step::after { + content: ""; + position: absolute; + right: -10px; top: 0; bottom: 0; + width: 20px; + background: inherit; + clip-path: polygon(0 0, 60% 50%, 0 100%); + z-index: 2; + border-right: 1px solid var(--rule); +} +.ribbon-step:last-child::after { display: none; } +.ribbon-step.done { + background: var(--ok-soft); + color: var(--ok); +} +.ribbon-step.current { + background: var(--navy); + color: #fff; + font-weight: 500; +} +.ribbon-step.current .num { color: rgba(255,255,255,0.6); } +.ribbon-step .num { + font-family: var(--font-mono); + font-size: 10px; + color: var(--ink-4); + margin-right: 6px; +} +.ribbon-step.done .num { color: var(--ok); opacity: 0.6; } + +/* Tables */ +.tbl { width: 100%; border-collapse: collapse; font-size: 13px; } +.tbl thead th { + text-align: left; + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ink-3); + padding: 8px 12px; + border-bottom: 1px solid var(--rule); + background: var(--paper-2); + position: sticky; top: 0; +} +.tbl tbody td { + padding: 10px 12px; + border-bottom: 1px solid var(--rule); + vertical-align: middle; +} +[data-density="compact"] .tbl tbody td { padding: 6px 12px; } +.tbl tbody tr:hover td { background: var(--paper-2); } +.tbl tbody tr.selected td { background: var(--navy-soft); } +.tbl .id { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-3); } +.tbl .ttl { color: var(--ink); font-weight: 500; } +.tbl .meta { color: var(--ink-3); font-size: 12px; } +.tbl tr { cursor: default; } + +/* Forms */ +.field { display: flex; flex-direction: column; gap: 4px; } +.field label { + font-size: 11px; font-weight: 500; letter-spacing: 0.02em; + color: var(--ink-2); +} +.field label .req { color: var(--seal); } +.field .hint { font-size: 11px; color: var(--ink-3); } +.input, .select, .textarea { + appearance: none; + border: 1px solid var(--rule); + background: var(--card); + border-radius: 7px; + padding: 8px 10px; + font: inherit; + font-size: 13px; + color: var(--ink); + outline: none; + transition: border-color 120ms, box-shadow 120ms; +} +.input:focus, .select:focus, .textarea:focus { + border-color: var(--navy); + box-shadow: 0 0 0 3px rgba(26,58,82,0.12); +} +.textarea { resize: vertical; min-height: 80px; } + +/* Alerts */ +.alert { + display: flex; align-items: flex-start; gap: 10px; + padding: 10px 14px; + border-radius: 8px; + border: 1px solid var(--rule); + background: var(--paper-2); + font-size: 12.5px; + color: var(--ink-2); +} +.alert.warn { background: var(--warn-soft); border-color: rgba(138,90,20,0.2); color: var(--warn); } +.alert.err { background: var(--err-soft); border-color: rgba(138,42,42,0.2); color: var(--err); } +.alert.ok { background: var(--ok-soft); border-color: rgba(47,107,58,0.2); color: var(--ok); } +.alert.info { background: var(--info-soft); border-color: rgba(31,74,107,0.2); color: var(--info); } +.alert .ic { flex-shrink: 0; margin-top: 1px; } +.alert .body { flex: 1; } +.alert .body b { display: block; font-weight: 600; margin-bottom: 2px; } +.alert .actions { display: flex; gap: 6px; margin-top: 8px; } + +/* First-run setup checklist (canonical three-state step list). */ +.setup-step { + display: grid; + grid-template-columns: 30px minmax(0,1fr); + gap: var(--gap); + padding: 14px; + border: 1px solid var(--rule); + border-radius: var(--radius-lg); + background: var(--card); +} +.setup-step.current { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); } +.setup-step > .num { + width: 30px; height: 30px; border-radius: 50%; + display: grid; place-items: center; font-weight: 600; + background: var(--paper-2); color: var(--ink-3); +} +.setup-step.current > .num { background: var(--navy); color: #fff; } +.setup-step.done > .num { background: var(--ok-soft); color: var(--ok); } +.setup-step-h { display: flex; align-items: center; justify-content: space-between; gap: 8px; } +.setup-step-h h3 { margin: 0; font-family: var(--font-serif); font-size: 15px; } +.setup-action-needed { margin: 10px 0; } +.setup-fields { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 10px; margin-top: 6px; } +/* .input:focus (navy ring, L612) already provides the AA focus appearance. */ + +/* Empty */ +.empty { + text-align: center; + padding: 40px 24px; + color: var(--ink-3); +} +.empty .ic { font-size: 24px; opacity: 0.4; margin-bottom: 8px; } +.empty .ttl { font-family: var(--font-serif); font-size: 16px; color: var(--ink); margin-bottom: 4px; } +.empty .body { font-size: 13px; max-width: 320px; margin: 0 auto 12px; } + +/* Audit drawer */ +.audit-drawer { + position: fixed; + top: 52px; right: 0; bottom: 0; + width: 380px; + background: var(--card); + border-left: 1px solid var(--rule); + box-shadow: -8px 0 24px rgba(0,0,0,0.04); + z-index: 50; + display: flex; flex-direction: column; + transform: translateX(100%); + transition: transform 240ms cubic-bezier(0.16,1,0.3,1); +} +.audit-drawer.open { transform: translateX(0); } +.audit-h { + padding: 14px 18px; + border-bottom: 1px solid var(--rule); + background: var(--paper-2); + display: flex; align-items: center; gap: 10px; +} +.audit-h .ttl { + font-family: var(--font-serif); + font-weight: 600; + font-size: 14px; +} +.audit-h .sub { font-size: 11px; color: var(--ink-3); margin-top: 2px; } +.audit-tabs { + display: flex; gap: 0; + border-bottom: 1px solid var(--rule); + padding: 0 12px; + background: var(--paper-2); +} +.audit-tabs button { + appearance: none; border: 0; background: transparent; + padding: 8px 10px; + font-size: 12px; + color: var(--ink-3); + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} +.audit-tabs button.on { color: var(--navy); border-bottom-color: var(--navy); font-weight: 500; } +.audit-b { + flex: 1; overflow-y: auto; + padding: 16px 18px; +} +.audit-event { + display: grid; + grid-template-columns: 14px 1fr; + gap: 10px; + padding-bottom: 16px; + position: relative; +} +.audit-event:not(:last-child)::after { + content: ""; + position: absolute; + left: 6px; top: 14px; bottom: 0; + width: 1px; background: var(--rule); +} +.audit-event .marker { + width: 13px; height: 13px; + border-radius: 50%; + background: var(--card); + border: 2px solid var(--navy); + margin-top: 2px; + z-index: 2; +} +.audit-event.publish .marker { border-color: var(--gold); } +.audit-event.error .marker { border-color: var(--err); } +.audit-event .ev-h { + font-size: 12.5px; + font-weight: 500; +} +.audit-event .ev-meta { + font-size: 11px; + color: var(--ink-3); + font-family: var(--font-mono); + margin-top: 2px; +} +.audit-event .ev-body { + font-size: 12px; + color: var(--ink-2); + margin-top: 4px; + line-height: 1.5; +} +.audit-event .ev-hash { + display: inline-block; + font: 500 10.5px/1 var(--font-mono); + background: var(--paper-2); + border: 1px solid var(--rule); + padding: 3px 6px; + border-radius: 4px; + margin-top: 6px; + color: var(--ink-3); +} + diff --git a/civiccore-ui/shell/.gitkeep b/civiccore-ui/shell/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/civiccore-ui/shell/shell.css b/civiccore-ui/shell/shell.css new file mode 100644 index 0000000..0699bc1 --- /dev/null +++ b/civiccore-ui/shell/shell.css @@ -0,0 +1,446 @@ +/* ============================================================================= + CivicSuite desktop shell chrome — CANONICAL SHELL LAYER + ----------------------------------------------------------------------------- + The app frame the six workflow modules render inside: brand block, topbar, + surface switcher (staff / resident / admin), primary nav, main workspace, + page header, letterhead, and the global search overlay. Extracted faithfully + from the canonical prototype (docs/design/ui-ux-prototype/styles.css). + + CONSUMPTION: consumes civiccore-ui/tokens/tokens.css and pairs with + civiccore-ui/components/components.css. Load tokens.css, then components.css, + then this file. Offline consumers vendor a generated copy, drift-gated in CI. + ========================================================================== */ + +/* ─────────── App shell ─────────── */ +.app { + display: grid; + grid-template-columns: 232px 1fr; + grid-template-rows: 52px 1fr; + grid-template-areas: + "brand topbar" + "nav main"; + height: 100vh; + background: var(--paper); +} + +.app[data-surface="resident"] { + grid-template-columns: 1fr; + grid-template-rows: 64px 1fr; + grid-template-areas: + "topbar" + "main"; +} + +/* Brand block */ +.brand { + grid-area: brand; + display: flex; + align-items: center; + gap: 10px; + padding: 0 16px; + border-right: 1px solid var(--rule); + border-bottom: 1px solid var(--rule); + background: var(--paper-2); +} +.seal { + width: 28px; height: 28px; + border-radius: 50%; + background: var(--navy); + display: grid; place-items: center; + color: var(--gold-soft); + font-family: var(--font-serif); + font-weight: 600; + font-size: 13px; + letter-spacing: 0.02em; + position: relative; + flex-shrink: 0; +} +.seal::before { + content: ""; + position: absolute; inset: 2px; + border-radius: 50%; + border: 1px solid var(--gold); + opacity: 0.55; +} +.brand-name { + font-family: var(--font-serif); + font-weight: 600; + font-size: 15px; + letter-spacing: -0.005em; + color: var(--ink); +} +.brand-sub { + font-size: 11px; + color: var(--ink-3); + letter-spacing: 0.02em; +} + +/* Topbar */ +.topbar { + grid-area: topbar; + display: flex; + align-items: center; + gap: 12px; + padding: 0 16px; + border-bottom: 1px solid var(--rule); + background: var(--card); +} +.topbar .search { + flex: 1; + max-width: 640px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 12px; + height: 34px; + border: 1px solid var(--rule); + border-radius: 8px; + background: var(--paper); + color: var(--ink-3); + cursor: text; + transition: border-color 120ms, background 120ms; +} +.topbar .search:hover { background: var(--paper-2); } +.topbar .search.active { + background: var(--card); + border-color: var(--navy); + box-shadow: 0 0 0 3px rgba(26,58,82,0.12); + color: var(--ink); +} +.topbar .search kbd { + margin-left: auto; + font: 500 10px/1 var(--font-mono); + padding: 3px 6px; + border: 1px solid var(--rule); + border-radius: 4px; + background: var(--paper-2); + color: var(--ink-3); +} +.topbar-spacer { flex: 1; } +.topbar-right { display: flex; align-items: center; gap: 6px; } + +.iconbtn { + width: 32px; height: 32px; + border: 1px solid transparent; + background: transparent; + border-radius: 7px; + display: grid; place-items: center; + color: var(--ink-2); + position: relative; +} +.iconbtn:hover { background: var(--paper-2); border-color: var(--rule); } +.iconbtn .dot { + position: absolute; + top: 6px; right: 7px; + width: 6px; height: 6px; + border-radius: 50%; + background: var(--seal); + border: 1.5px solid var(--card); +} + +.user-chip { + display: flex; align-items: center; gap: 8px; + padding: 4px 10px 4px 4px; + border: 1px solid var(--rule); + border-radius: 100px; + background: var(--paper); +} +.avatar { + width: 24px; height: 24px; + border-radius: 50%; + background: var(--navy); + color: var(--gold-soft); + display: grid; place-items: center; + font-size: 10px; font-weight: 600; + letter-spacing: 0.02em; +} +.user-chip .nm { font-size: 12px; font-weight: 500; } +.user-chip .role { font-size: 10px; color: var(--ink-3); letter-spacing: 0.02em; } + +/* Surface switcher */ +.surface-switch { + display: flex; + border: 1px solid var(--rule); + border-radius: 100px; + padding: 2px; + background: var(--paper); + gap: 0; +} +.surface-switch button { + appearance: none; + border: 0; + background: transparent; + padding: 4px 10px; + border-radius: 100px; + font-size: 11px; + font-weight: 500; + color: var(--ink-3); + letter-spacing: 0.01em; +} +.surface-switch button.on { + background: var(--navy); + color: #fff; +} + +/* Nav */ +.nav { + grid-area: nav; + border-right: 1px solid var(--rule); + background: var(--paper-2); + padding: 12px 0; + overflow-y: auto; + display: flex; flex-direction: column; + gap: 2px; +} +.nav-section { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-4); + padding: 14px 18px 6px; +} +.nav-section:first-child { padding-top: 4px; } +.nav-item { + display: flex; align-items: center; gap: 10px; + padding: 7px 14px 7px 18px; + margin: 0 8px; + border-radius: 6px; + font-size: 13px; + color: var(--ink-2); + position: relative; +} +.nav-item:hover { background: var(--paper-3); color: var(--ink); } +.nav-item.active { + background: var(--card); + color: var(--ink); + font-weight: 500; + box-shadow: inset 0 0 0 1px var(--rule); +} +.nav-item.active::before { + content: ""; + position: absolute; left: -8px; top: 6px; bottom: 6px; + width: 3px; + background: var(--navy); + border-radius: 0 2px 2px 0; +} +.nav-item .ic { + width: 16px; height: 16px; + display: grid; place-items: center; + color: var(--ink-3); + flex-shrink: 0; +} +.nav-item.active .ic { color: var(--navy); } +.nav-item .count { + margin-left: auto; + font-size: 10px; + font-variant-numeric: tabular-nums; + color: var(--ink-3); + background: var(--paper); + padding: 2px 6px; + border-radius: 10px; + border: 1px solid var(--rule); +} +.nav-item.active .count { background: var(--gold-soft); border-color: var(--gold); color: var(--gold-2); } + +.nav-recent { + margin: 8px; + padding: 8px 10px; + border-top: 1px solid var(--rule); +} +.nav-recent .lbl { + font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; + color: var(--ink-4); margin-bottom: 6px; +} +.nav-recent a { + display: block; + font-size: 12px; + color: var(--ink-2); + padding: 3px 4px; + border-radius: 4px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + text-decoration: none; +} +.nav-recent a:hover { background: var(--paper-3); } + +/* Main */ +.main { + grid-area: main; + overflow-y: auto; + position: relative; +} + +/* ─────────── Page header ─────────── */ +.page-head { + padding: 24px 32px 16px; + border-bottom: 1px solid var(--rule); + background: var(--card); +} +[data-density="compact"] .page-head { padding: 16px 24px 12px; } +.crumbs { + font-size: 11px; + color: var(--ink-3); + letter-spacing: 0.02em; + margin-bottom: 6px; + display: flex; gap: 6px; align-items: center; +} +.crumbs .sep { color: var(--ink-4); } +.crumbs a { color: var(--ink-3); text-decoration: none; } +.crumbs a:hover { color: var(--navy); } +.page-title { + font-family: var(--font-serif); + font-size: 26px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--ink); + margin: 0 0 4px; +} +[data-density="compact"] .page-title { font-size: 22px; } +.page-sub { + font-size: 13px; + color: var(--ink-2); + margin: 0; +} +.page-head-row { + display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; +} + +.page-tabs { + display: flex; gap: 4px; + margin-top: 16px; + margin-bottom: -16px; + border-bottom: 1px solid transparent; +} +.page-tabs button { + appearance: none; + border: 0; + background: transparent; + padding: 8px 12px; + font-size: 13px; + color: var(--ink-3); + border-bottom: 2px solid transparent; + margin-bottom: -1px; + font-weight: 500; +} +.page-tabs button.on { color: var(--navy); border-bottom-color: var(--navy); } +.page-tabs button:hover:not(.on) { color: var(--ink); } +.page-tabs button .pill { + display: inline-block; + font-size: 10px; + font-weight: 500; + background: var(--paper-2); + color: var(--ink-3); + padding: 1px 6px; + margin-left: 4px; + border-radius: 8px; +} + +/* ─────────── Body ─────────── */ +.body { padding: 24px 32px; } +[data-density="compact"] .body { padding: 16px 24px; } + +.row { display: flex; gap: var(--gap); } +.col { display: flex; flex-direction: column; gap: var(--gap); } + +.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.grid-3 { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; } +.grid-4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 16px; } +.grid-cards { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; } + + +/* Letterhead — used at top of detail pages */ +.letterhead { + background: linear-gradient(180deg, var(--paper-2), var(--card)); + border-bottom: 1px solid var(--rule); + padding: 20px 32px 24px; + position: relative; +} +.letterhead::after { + content: ""; position: absolute; left: 32px; right: 32px; bottom: 0; + height: 1px; background: var(--gold); opacity: 0.4; +} +.letterhead .seal-row { + display: flex; align-items: center; gap: 10px; + font-family: var(--font-serif); + font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; + color: var(--gold-2); + margin-bottom: 8px; +} +.letterhead .seal-row::before, .letterhead .seal-row::after { + content: ""; height: 1px; background: var(--gold); opacity: 0.4; flex: 0 0 32px; +} +.letterhead .seal-row::after { flex: 1; } + + +/* Search overlay */ +.search-overlay { + position: fixed; inset: 0; + background: rgba(20,30,42,0.32); + backdrop-filter: blur(2px); + display: grid; + place-items: start center; + padding-top: 12vh; + z-index: 100; +} +.search-modal { + width: min(680px, 92vw); + background: var(--card); + border: 1px solid var(--rule); + border-radius: 12px; + box-shadow: 0 24px 60px rgba(20,30,42,0.24); + overflow: hidden; + display: flex; flex-direction: column; + max-height: 70vh; +} +.search-modal .input-wrap { + display: flex; align-items: center; gap: 10px; + padding: 14px 18px; + border-bottom: 1px solid var(--rule); +} +.search-modal input { + flex: 1; + border: 0; outline: 0; + background: transparent; + font-size: 16px; +} +.search-modal .scope { + font-size: 11px; color: var(--ink-3); + font-family: var(--font-mono); +} +.search-modal .results { + overflow-y: auto; + flex: 1; +} +.search-group { + padding: 8px 0; +} +.search-group .lbl { + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-4); + font-weight: 600; + padding: 6px 18px 4px; +} +.search-result { + display: grid; + grid-template-columns: 28px 1fr auto; + gap: 10px; + padding: 8px 18px; + align-items: center; +} +.search-result:hover, .search-result.active { + background: var(--paper-2); +} +.search-result .ic { + width: 28px; height: 28px; + background: var(--paper-2); + border-radius: 6px; + display: grid; place-items: center; + color: var(--ink-3); + border: 1px solid var(--rule); +} +.search-result .ttl { font-size: 13px; font-weight: 500; color: var(--ink); } +.search-result .meta { font-size: 11px; color: var(--ink-3); margin-top: 2px; } +.search-result .right { font-size: 10.5px; color: var(--ink-3); font-family: var(--font-mono); } +