@digithings-ai/design 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/site/site.css ADDED
@@ -0,0 +1,475 @@
1
+ /* ==========================================================================
2
+ digithings site foundation — shared component layer for the redesign.
3
+ Consumes the [data-theme] semantic tokens in ../tokens.css. Terminal-CLI,
4
+ utilitarian, spacious; light + dark; reduced-motion safe.
5
+ Page-specific overrides live in each site's local style.css.
6
+
7
+ Nav shell, hero layout, cards, pills/stage, connected graph, and scroll
8
+ reveal were reimplemented as React components in @digithings-ai/ui after
9
+ the Dec 2025 migration (commit 681273a) — those selectors were removed
10
+ here (#1240). What remains is what digithings-web / digiquant-web still
11
+ import directly: brand mark, theme toggle, buttons, kicker/prompt, the
12
+ standalone hero title, the terminal block, sections, ProductFrame,
13
+ principles, and footer.
14
+ ========================================================================== */
15
+
16
+ /* ── reset ──────────────────────────────────────────────────────────── */
17
+ *, *::before, *::after { box-sizing: border-box; }
18
+ * { margin: 0; }
19
+ html { scroll-behavior: auto; -webkit-text-size-adjust: 100%; background: var(--bg); }
20
+ @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
21
+
22
+ body {
23
+ font-family: var(--font-sans);
24
+ background: var(--bg);
25
+ color: var(--ink);
26
+ line-height: 1.65;
27
+ font-weight: 400;
28
+ letter-spacing: -0.011em;
29
+ -webkit-font-smoothing: antialiased;
30
+ text-rendering: optimizeLegibility;
31
+ overflow-x: hidden;
32
+ transition: background 0.4s var(--ease), color 0.4s var(--ease);
33
+ }
34
+ img, svg { max-width: 100%; display: block; }
35
+ a { color: inherit; text-decoration: none; }
36
+ ::selection { background: var(--accent); color: var(--on-accent); }
37
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 0; }
38
+
39
+ /* ── atmosphere ─────────────────────────────────────────────────────── */
40
+ .grain {
41
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
42
+ opacity: var(--grain-opacity, 0.03); mix-blend-mode: var(--grain-blend, screen);
43
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
44
+ }
45
+ .glow {
46
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
47
+ background: radial-gradient(48rem 30rem at 72% -8%, var(--accent-weak), transparent 60%);
48
+ animation: glow-drift 18s var(--ease) infinite alternate;
49
+ }
50
+ @keyframes glow-drift { to { transform: translate(-5%, 2%); } }
51
+
52
+ /* ── layout ─────────────────────────────────────────────────────────── */
53
+ .wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter); position: relative; z-index: 1; }
54
+
55
+ /* ── brand mark + theme toggle (nav shell lives in React DigiNav/DqNav) ─ */
56
+ /* Brand lockup. The wordmark is MONO at 400, matching the terminal mark it
57
+ alternates with — it used to inherit body's sans at .brand's weight 600, which
58
+ put a Geist Sans 600 word 0.6rem from a Geist Mono 400 mark: two typefaces and
59
+ a 200-weight gap side by side.
60
+
61
+ The two never render together, but that swap is NOT here: the brand anchor is
62
+ NavShell's own `.nav-shell-brand`, not `.brand`, so a `.brand .terminal-mark`
63
+ rule would never match. The responsive swap lives on the call sites in each
64
+ app's _nav.tsx as `max-[880px]:` utilities — 880px is NavShell's own
65
+ breakpoint, where its hamburger appears. */
66
+ .brand { --brand-mark: 1.625rem; --brand-gap: 0.6rem; --brand-word: 1.05rem; display: inline-flex; align-items: center; gap: var(--brand-gap); font-weight: 400; letter-spacing: -0.02em; }
67
+ .brand-mark { width: var(--brand-mark); height: var(--brand-mark); border-radius: 0; }
68
+ .brand-word { font-family: var(--font-mono); font-size: var(--brand-word); font-weight: 400; letter-spacing: 0; }
69
+ .brand-word b { color: var(--accent); font-weight: 400; }
70
+
71
+ .theme-toggle { --toggle-size: 2.375rem; --toggle-icon: 1.0625rem; display: inline-grid; place-items: center; width: var(--toggle-size); height: var(--toggle-size); border: 1px solid var(--hair); border-radius: 0; background: var(--surface); color: var(--ink-soft); cursor: pointer; transition: color 0.18s var(--ease), border-color 0.18s var(--ease), transform 0.18s var(--ease); }
72
+ .theme-toggle:hover { color: var(--ink); border-color: var(--hair-2); transform: translateY(-1px); }
73
+ .theme-toggle svg { width: var(--toggle-icon); height: var(--toggle-icon); }
74
+
75
+ /* ── buttons ───────────────────────────────────────────────────────────
76
+ Retired in the wave-3 close-out (Task 6). The kit `buttonVariants`
77
+ (@digithings-ai/ui/ui) is the canonical button dress; the last live consumer
78
+ (digithings-web DtNav) moved to it in T2, and the only remaining `.btn*`
79
+ carriers were the dead `chrome.tsx` `Nav` export and AuthCard's inert
80
+ `.btn-primary` (rendered by apps that never import this sheet) — both
81
+ cleaned in T6. Static `.closing-cta__secondary` links remain for the
82
+ markup-only site pages; use the kit `Button` anywhere React renders. */
83
+
84
+ /* ── kicker / prompt ────────────────────────────────────────────────── */
85
+ .prompt { color: var(--accent); font-weight: 600; font-family: var(--font-mono); }
86
+ .kicker { font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.02em; color: var(--accent); }
87
+
88
+ /* ── hero title (standalone, e.g. subsystem/module detail pages) ─────── */
89
+ .hero-title { font-family: var(--font-display); font-weight: 400; font-size: clamp(2.8rem, 6.4vw, 5rem); line-height: 1.02; letter-spacing: -0.02em; margin: 1.4rem 0 1.2rem; max-width: 16ch; }
90
+ .hero-title em { font-style: italic; color: var(--accent); }
91
+
92
+ /* ── terminal block (always dark) ───────────────────────────────────── */
93
+ .term { --term-dot: var(--text-xs); --term-bar-gap: var(--space-2); width: 100%; background: var(--term-bg); border: 1px solid var(--term-hair); border-radius: 0; overflow: hidden; box-shadow: 0 44px 100px -50px rgba(0,0,0,0.45); }
94
+ .term-bar { display: flex; align-items: center; gap: var(--term-bar-gap); padding: 0.7rem 0.9rem; background: var(--term-fill); border-bottom: 1px solid var(--term-hair); }
95
+ .term-bar i { width: var(--term-dot); height: var(--term-dot); border-radius: 50%; background: #3a342c; display: inline-block; }
96
+ .term-bar i:nth-child(1) { background: #ec6a5e; } .term-bar i:nth-child(2) { background: #f4bf4f; } .term-bar i:nth-child(3) { background: #61c554; }
97
+ .term-title { margin-left: auto; font-family: var(--font-mono); font-size: 0.72rem; color: var(--term-mute); }
98
+ .term-body { font-family: var(--font-mono); font-size: clamp(0.76rem, 1.2vw, 0.88rem); line-height: 1.95; color: var(--term-ink); padding: 1.2rem 1.3rem 1.4rem; margin: 0; min-height: 17em; white-space: pre-wrap; }
99
+ .tl-cmd { color: var(--term-ink); } .tl-cmd::before { content: "$ "; color: var(--accent); }
100
+ .tl-out { color: var(--term-mute); }
101
+ .tl-ok::before { content: "✓ "; color: var(--up); } .tl-ok { color: var(--term-mute); } .tl-ok b { color: var(--term-ink); font-weight: 500; }
102
+ .tl-mod { color: var(--term-ink); } .tl-mod b { color: var(--accent); font-weight: 500; }
103
+ .tl-install { color: var(--up); }
104
+ .tl-arrow { color: var(--accent); }
105
+ .term-cursor { display: inline-block; width: 0.6em; height: 1.05em; background: var(--accent); vertical-align: -0.16em; animation: blink 1.05s steps(1) infinite; }
106
+ @keyframes blink { 50% { opacity: 0; } }
107
+
108
+ /* inline install/clone command chip */
109
+ .cmdline { display: inline-flex; align-items: center; gap: 0.7rem; background: var(--term-bg); border: 1px solid var(--term-hair); border-radius: 0; padding: 0.55rem 0.65rem 0.55rem 1rem; font-family: var(--font-mono); font-size: 0.82rem; color: var(--term-ink); max-width: 100%; overflow-x: auto; }
110
+ .cmdline .prompt { color: var(--accent); }
111
+
112
+ /* ── sections ───────────────────────────────────────────────────────── */
113
+ .section { --site-section-y: clamp(4rem, 9vw, 7.5rem); padding: var(--site-section-y) 0; }
114
+ .section-alt { background: var(--surface-2); border-block: 1px solid var(--hair); }
115
+ .section-head { max-width: 56ch; margin-bottom: clamp(2.5rem, 5vw, 4rem); }
116
+ .section-head h2 { font-size: clamp(1.9rem, 4vw, 3rem); font-weight: 600; letter-spacing: -0.03em; line-height: 1.08; margin: 0.7rem 0 0.9rem; }
117
+ .section-head.center { margin-inline: auto; text-align: center; }
118
+ .section-head p { color: var(--ink-soft); font-size: clamp(1rem, 1.4vw, 1.12rem); }
119
+ .grid { display: grid; gap: var(--space-3); }
120
+
121
+ /* ── product frame (Graphite artboard / Cursor product screenshot) ────
122
+ Flat, CQ-scaled UI embed for marketing pages. "Atmospheric outside,
123
+ surgical inside" (EVOLUTION.md §7) — no mesh/glow/grain on the surface
124
+ itself; those live on the page background around the frame.
125
+ CQ scaling: `container-type: inline-size` on .product-frame turns its
126
+ own box into a query container, and .product-frame__surface sizes its
127
+ type in `cqi` (container-query inline-size units) clamped between a
128
+ floor and ceiling. That means UI text inside the frame shrinks with
129
+ the *frame's* width (e.g. a narrower column on mobile), not the
130
+ viewport — a screenshot-like embed stays legible without overflowing
131
+ at any breakpoint. ────────────────────────────────────────────────── */
132
+ .product-frame { width: 100%; max-width: var(--product-frame-w); margin-inline: auto; container-type: inline-size; }
133
+ .product-frame__surface { background: var(--surface); border: 1px solid var(--hair); border-radius: var(--r-lg); overflow: hidden; padding: clamp(0.85rem, 4cqi, 1.6rem); font-size: clamp(0.72rem, 2.4cqi, 1rem); line-height: 1.5; }
134
+ .product-frame__caption { margin-top: 0.75rem; font-family: var(--font-mono); font-size: 0.78rem; color: var(--ink-mute); text-align: center; }
135
+
136
+ /* ── bento grid (Cursor-style linked feature cells) ───────────────────
137
+ Mobile-first: single column, 2×2 from 768px. Flat --surface panels,
138
+ no glass morphism (EVOLUTION.md §10 anti-pattern #8). ─────────────── */
139
+ .bento { display: grid; grid-template-columns: 1fr; gap: 1.25rem; width: 100%; max-width: var(--wrap-wide); margin-inline: auto; }
140
+ .bento__cell { display: flex; flex-direction: column; gap: var(--space-2); background: var(--surface); border: 1px solid var(--hair); border-radius: var(--r-lg); padding: var(--space-4); transition: transform var(--duration-hover) var(--ease-glide), border-color var(--duration-hover) var(--ease-glide); }
141
+ .bento__cell:not(.bento__cell--static):hover { transform: translateY(-3px); border-color: var(--hair-2); }
142
+ .bento__cell--static { cursor: default; }
143
+ .bento__cell--span-2 { grid-column: span 2; }
144
+ .bento__thumb { border-radius: var(--r-md); overflow: hidden; }
145
+ .bento__kicker { font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.02em; color: var(--accent); }
146
+ .bento__title { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em; }
147
+ .bento__body { font-size: 0.9rem; color: var(--ink-soft); line-height: 1.5; }
148
+ .bento__cta { margin-top: auto; padding-top: var(--space-2); display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--accent); font-weight: 500; }
149
+ .bento__cta span[aria-hidden] { transition: transform var(--duration-hover) var(--ease-glide); }
150
+ .bento__cell:hover .bento__cta span[aria-hidden] { transform: translateX(3px); }
151
+ @media (min-width: 768px) {
152
+ .bento { grid-template-columns: repeat(2, 1fr); }
153
+ }
154
+
155
+ /* ── trust strip (proof / logo row) ───────────────────────────────────── */
156
+ .trust-strip { --trust-logo-h: 1.75rem; --trust-logo-max-w: 8.75rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: clamp(1.25rem, 4vw, 2.5rem); }
157
+ .trust-strip__item { display: inline-flex; align-items: center; font-family: var(--font-mono); font-size: 0.85rem; color: var(--ink-mute); }
158
+ .trust-strip__item img { height: var(--trust-logo-h); width: auto; max-width: var(--trust-logo-max-w); filter: grayscale(1); opacity: 0.7; }
159
+ /* --logos: integration-mark row (x.ai partner-mark pattern, #1229). Real
160
+ integrations only — no stock/generic logos. Marks are muted/grayscale by
161
+ default and lift to full color on hover; wrap each logo as
162
+ <span class="trust-strip__item"><img alt="Name"></span> (decorative-only
163
+ logos use alt=""). Works alongside the base text friction variant. */
164
+ .trust-strip--logos { gap: clamp(1.5rem, 4vw, 2.75rem); }
165
+ .trust-strip--logos .trust-strip__item { font-size: 0.95rem; color: var(--ink-soft); }
166
+ .trust-strip--logos .trust-strip__item img { transition: filter var(--duration-hover) var(--ease-glide), opacity var(--duration-hover) var(--ease-glide); }
167
+ .trust-strip--logos .trust-strip__item:hover { color: var(--ink); }
168
+ .trust-strip--logos .trust-strip__item:hover img { filter: grayscale(0); opacity: 1; }
169
+
170
+ /* ── reveal-up (scroll-triggered enter) ────────────────────────────────
171
+ CSS-only: owns the two visual states only. Trigger it either with
172
+ ../scroll-trigger.js's `activateSelector` (toggles `.active` when the
173
+ element crosses the scroll line) or from the React `Reveal` component's
174
+ className (packages/ui/src/motion/primitives.tsx) — both are valid
175
+ drivers, so both class names are wired below. ───────────────────────── */
176
+ .reveal-up { opacity: 0; transform: translateY(1rem); transition: opacity var(--duration-reveal) var(--ease-glide), transform var(--duration-reveal) var(--ease-glide); }
177
+ .reveal-up.is-visible, .reveal-up.active { opacity: 1; transform: none; }
178
+ /* No-JS fallback: the visible state is only ever added by a JS trigger, so
179
+ without scripting .reveal-up content would be permanently hidden. Show it. */
180
+ @media (scripting: none) {
181
+ .reveal-up { opacity: 1; transform: none; }
182
+ }
183
+
184
+ /* ── stat counter (xAI-style scroll-triggered metrics strip) ───────────
185
+ No-fake-data policy (EVOLUTION.md §10 anti-pattern #2): stat-counter.js
186
+ only counts up to whatever data-target says — placeholders must be
187
+ labeled as such, real wiring is just setting data-target. ────────────── */
188
+ .stat-counter-row { display: flex; flex-wrap: wrap; justify-content: center; gap: clamp(1.5rem, 5vw, 3.5rem); }
189
+ .stat-counter { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; text-align: center; }
190
+ .stat-counter__value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: clamp(1.7rem, 4vw, 2.5rem); font-weight: 600; letter-spacing: -0.02em; color: var(--ink); }
191
+ .stat-counter__label { font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-mute); }
192
+
193
+ /* ── changelog band (Cursor-style dated release rows) ───────────────────
194
+ Mobile: stacked (date above title). Desktop: fixed date column + title
195
+ row, from 640px. Data shape: { date, version?, title, href, tag? }[] —
196
+ see site/README.md for the documented contract and rendering is left to
197
+ the consumer (vanilla or React), same as TrustStrip. ─────────────────── */
198
+ .changelog-band { display: flex; flex-direction: column; max-width: var(--wrap-wide); margin-inline: auto; }
199
+ .changelog-row { display: grid; grid-template-columns: 1fr; gap: 0.3rem; padding: 1.1rem 0; border-bottom: 1px solid var(--hair); }
200
+ .changelog-row:last-child { border-bottom: none; }
201
+ .changelog-row__date { font-family: var(--font-mono); font-size: 0.78rem; color: var(--ink-mute); }
202
+ .changelog-row__title { font-size: 0.98rem; color: var(--ink); }
203
+ .changelog-row__title a { color: inherit; }
204
+ .changelog-row__title a:hover { color: var(--accent); }
205
+ .changelog-row__tag { display: inline-flex; align-items: center; margin-left: 0.6rem; padding: 0.15rem var(--space-2); border-radius: 0; border: 1px solid var(--hair-2); font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); }
206
+ .changelog-band__footer { margin-top: 1.2rem; text-align: center; }
207
+ .changelog-band__footer a { font-family: var(--font-mono); font-size: 0.85rem; color: var(--accent); }
208
+ @media (min-width: 640px) {
209
+ .changelog-row { grid-template-columns: 8rem 1fr; align-items: baseline; gap: var(--space-4); }
210
+ }
211
+
212
+ /* ── code sample band (tabbed SDK / curl snippets) ──────────────────────
213
+ WAI-ARIA tabs pattern, keyboard-navigable (../code-sample-band.js).
214
+ Always dark, reusing the terminal block's --term-* tokens regardless of
215
+ page theme (matches .term). ───────────────────────────────────────────── */
216
+ .code-sample-band { background: var(--term-bg); border: 1px solid var(--term-hair); border-radius: 0; overflow: hidden; }
217
+ /* __bar holds the tablist + copy button as siblings — the copy button must NOT
218
+ live inside role="tablist" (a tablist owns only role="tab" children). */
219
+ .code-sample-band__bar { display: flex; align-items: center; gap: var(--space-1); padding: 0.5rem 0.6rem; background: var(--term-fill); border-bottom: 1px solid var(--term-hair); }
220
+ .code-sample-band__tabs { display: flex; align-items: center; gap: var(--space-1); }
221
+ .code-sample-band__tab { font-family: var(--font-mono); font-size: 0.8rem; color: var(--term-mute); background: transparent; border: 1px solid transparent; border-radius: 0; padding: 0.35rem 0.7rem; cursor: pointer; }
222
+ .code-sample-band__tab[aria-selected="true"] { color: var(--term-ink); background: var(--term-bg); border-color: var(--term-hair); }
223
+ .code-sample-band__tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
224
+ .code-sample-band__copy { margin-left: auto; font-family: var(--font-mono); font-size: 0.7rem; color: var(--term-mute); background: var(--term-fill); border: 1px solid var(--term-hair); border-radius: 0; padding: 0.3rem 0.6rem; cursor: pointer; }
225
+ .code-sample-band__copy:hover { color: var(--term-ink); }
226
+ .code-sample-band__copy.is-ok { color: var(--up); }
227
+ .code-sample-band__panel { margin: 0; padding: 1.1rem 1.2rem; font-family: var(--font-mono); font-size: clamp(0.76rem, 1.2vw, 0.86rem); line-height: 1.7; color: var(--term-ink); white-space: pre-wrap; overflow-x: auto; }
228
+
229
+ /* ── capability card (xAI-style mini-UI preview + "Explore →") ──────────
230
+ Flat --surface panel, hairline border — no decorative eyebrow pills
231
+ (EVOLUTION.md §10 anti-pattern #3). Composes standalone (.capability-grid),
232
+ or drop a .capability-card inside a .bento__cell. The __preview slot takes
233
+ an <img>, a .product-frame child, or a .term snippet. ─────────────────── */
234
+ .capability-grid { display: grid; grid-template-columns: 1fr; gap: 1.25rem; width: 100%; max-width: var(--wrap-wide); margin-inline: auto; }
235
+ .capability-card { display: flex; flex-direction: column; gap: 0.6rem; background: var(--surface); border: 1px solid var(--hair); border-radius: var(--r-lg); padding: var(--space-4); transition: transform var(--duration-hover) var(--ease-glide), border-color var(--duration-hover) var(--ease-glide); }
236
+ .capability-card:hover { transform: translateY(-3px); border-color: var(--hair-2); }
237
+ .capability-card__preview { border-radius: var(--r-md); overflow: hidden; margin-bottom: 0.4rem; }
238
+ .capability-card__title { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em; }
239
+ .capability-card__body { font-size: 0.9rem; color: var(--ink-soft); line-height: 1.5; }
240
+ .capability-card__cta { margin-top: auto; padding-top: var(--space-2); display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--accent); font-weight: 500; }
241
+ .capability-card__cta span[aria-hidden] { transition: transform var(--duration-hover) var(--ease-glide); }
242
+ .capability-card:hover .capability-card__cta span[aria-hidden] { transform: translateX(3px); }
243
+ @media (min-width: 768px) {
244
+ .capability-grid { grid-template-columns: repeat(2, 1fr); }
245
+ }
246
+
247
+ /* ── horizontal scroll band (Cursor-style snap row) ─────────────────────
248
+ Overflow-x snap row for changelog cards, testimonial rows, and mobile
249
+ overflow bands. .h-scroll masks the edges; .h-scroll__track is the
250
+ focusable scroll container; .h-scroll__card are ~262px snap children
251
+ (Cursor changelog card width). CSS-only: keyboard access comes from the
252
+ track being a focusable (tabindex="0") native scroll container — arrow
253
+ keys scroll it — so no horizontal-scroll.js is needed (see site/README.md).
254
+ prefers-reduced-motion: reduce → cards stack vertically (no horizontal
255
+ scroll), so all content is reachable without horizontal gestures. ────── */
256
+ .h-scroll { --h-scroll-card-w: 16.375rem; position: relative; -webkit-mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent); mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent); }
257
+ .h-scroll__track { display: flex; gap: 1.25rem; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 1.25rem; padding: 0.25rem 1.25rem; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
258
+ .h-scroll__track::-webkit-scrollbar { display: none; }
259
+ .h-scroll__track:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-md); }
260
+ .h-scroll__card { flex: 0 0 auto; min-width: var(--h-scroll-card-w); max-width: var(--h-scroll-card-w); scroll-snap-align: start; display: flex; flex-direction: column; gap: var(--space-2); background: var(--surface); border: 1px solid var(--hair); border-radius: var(--r-lg); padding: var(--space-4); }
261
+ @media (prefers-reduced-motion: reduce) {
262
+ .h-scroll { -webkit-mask-image: none; mask-image: none; }
263
+ .h-scroll__track { flex-direction: column; overflow-x: visible; scroll-snap-type: none; padding-inline: 0; }
264
+ .h-scroll__card { min-width: 0; max-width: none; }
265
+ }
266
+
267
+ /* ── closing CTA band (Graphite/Cursor pre-footer conversion) ───────────
268
+ Full-width centered band placed directly above the footer: one headline,
269
+ one primary CTA (kit `Button` in React), and an optional mono secondary
270
+ link. Compose
271
+ with .reveal-up (above) for a scroll-in enter — reduced-motion is already
272
+ honored there. Copy slots (see site/README.md for digithings + digiquant
273
+ variants; tone lives in COPY_GUIDE.md):
274
+ .closing-cta__title headline — literal, ≤ ~20ch reads best
275
+ .closing-cta__sub optional one-line support — ≤ ~48ch
276
+ .closing-cta__actions primary: a kit `Button` (React) or a link
277
+ .closing-cta__secondary optional: <a href>label <span aria-hidden>&rarr;</span></a>
278
+ ───────────────────────────────────────────────────────────────────── */
279
+ .closing-cta { padding-block: var(--section-y); text-align: center; }
280
+ .closing-cta__inner { max-width: var(--wrap-wide); margin-inline: auto; display: flex; flex-direction: column; align-items: center; gap: 1.1rem; }
281
+ .closing-cta__title { font-size: clamp(1.8rem, 4vw, 2.75rem); font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; max-width: 20ch; }
282
+ .closing-cta__sub { font-size: var(--type-body); color: var(--ink-soft); max-width: 48ch; line-height: 1.6; }
283
+ .closing-cta__actions { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-3); margin-top: 0.4rem; }
284
+ .closing-cta__secondary { display: inline-flex; align-items: center; gap: 0.4rem; font-family: var(--font-mono); font-size: 0.88rem; color: var(--ink-soft); }
285
+ .closing-cta__secondary:hover { color: var(--ink); }
286
+ .closing-cta__secondary span[aria-hidden] { transition: transform var(--duration-hover) var(--ease-glide); }
287
+ .closing-cta__secondary:hover span[aria-hidden] { transform: translateX(3px); }
288
+
289
+ /* ── FAQ accordion (Graphite/Cursor pricing-page Q&A) ───────────────────
290
+ Native <details>/<summary> — no JS. For an exclusive "one open at a time"
291
+ accordion, give every .faq__item the same name attribute
292
+ (<details class="faq__item" name="pricing-faq">) — a modern-browser native
293
+ exclusive-accordion feature; omit name and each item toggles independently.
294
+ The disclosure chevron rotates on [open]; its transition is dropped under
295
+ reduced motion (shared block below). Data-driven content shape (per site):
296
+ { q, a }[] — see site/README.md.
297
+ ───────────────────────────────────────────────────────────────────── */
298
+ .faq { --faq-w: 47.5rem; max-width: var(--faq-w); width: 100%; margin-inline: auto; }
299
+ .faq__item { border-bottom: 1px solid var(--hair); }
300
+ .faq__q { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: 1.15rem 0; font-size: var(--type-body); font-weight: 500; color: var(--ink); cursor: pointer; list-style: none; }
301
+ .faq__q::-webkit-details-marker { display: none; }
302
+ .faq__q::after { content: ""; flex: 0 0 auto; width: 0.55rem; height: 0.55rem; border-right: 1.5px solid var(--ink-mute); border-bottom: 1.5px solid var(--ink-mute); transform: rotate(45deg); transition: transform var(--duration-hover) var(--ease-glide); }
303
+ .faq__item[open] .faq__q::after { transform: rotate(-135deg); }
304
+ .faq__q:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-sm); }
305
+ .faq__a { max-width: 60ch; margin: 0; padding: 0 0 1.25rem; color: var(--ink-soft); font-size: 0.95rem; line-height: 1.6; }
306
+
307
+ /* ── pricing matrix (open-core tiers + optional comparison table) ───────
308
+ 3 honest open-core tiers — no invented usage caps or fake "limited AI
309
+ requests" (EVOLUTION.md §10, anti-pattern #2). Tier content shape (per
310
+ site): { name, price, cadence?, desc, features: string[], cta: {label, href},
311
+ featured?: bool }[] — see site/README.md. .pricing__tier--featured lifts one
312
+ card with an --accent ring. The optional .pricing-table gives a feature ×
313
+ tier comparison grid with ✓ (--up) / — (--ink-mute) cells.
314
+ ───────────────────────────────────────────────────────────────────── */
315
+ .pricing { display: grid; grid-template-columns: 1fr; gap: 1.25rem; width: 100%; max-width: var(--wrap-wide); margin-inline: auto; }
316
+ .pricing__tier { display: flex; flex-direction: column; gap: 0.9rem; background: var(--surface); border: 1px solid var(--hair); border-radius: var(--r-lg); padding: 1.75rem; }
317
+ .pricing__tier--featured { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
318
+ .pricing__name { font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-mute); }
319
+ .pricing__price { font-size: clamp(1.6rem, 3vw, 2.1rem); font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; }
320
+ .pricing__price small { font-size: 0.9rem; font-weight: 400; color: var(--ink-mute); letter-spacing: 0; }
321
+ .pricing__desc { font-size: 0.9rem; color: var(--ink-soft); line-height: 1.55; }
322
+ .pricing__features { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; font-size: 0.9rem; color: var(--ink-soft); }
323
+ .pricing__features li { display: flex; align-items: flex-start; gap: 0.55rem; }
324
+ .pricing__features li::before { content: "✓"; flex: 0 0 auto; color: var(--up); font-weight: 600; line-height: 1.4; }
325
+ .pricing__cta { margin-top: auto; }
326
+ @media (min-width: 768px) {
327
+ .pricing { grid-template-columns: repeat(3, 1fr); }
328
+ }
329
+ .pricing-table { width: 100%; max-width: var(--wrap-wide); margin-inline: auto; border-collapse: collapse; font-size: 0.9rem; }
330
+ .pricing-table th, .pricing-table td { padding: 0.8rem var(--space-3); text-align: left; border-bottom: 1px solid var(--hair); }
331
+ .pricing-table thead th { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-mute); font-weight: 500; }
332
+ .pricing-table th + th, .pricing-table td + td { text-align: center; }
333
+ .pricing-table tbody th { font-weight: 500; color: var(--ink); }
334
+ .pricing-table .is-yes { color: var(--up); font-weight: 600; }
335
+ .pricing-table .is-no { color: var(--ink-mute); }
336
+
337
+ /* ── hero feature picker (Graphite icon-tab row swapping a ProductFrame) ─
338
+ Icon tablist below the hero that swaps which ProductFrame preview shows
339
+ (e.g. dashboard / Strategies / Pipeline). WAI-ARIA tabs — wire with
340
+ hero-picker.js (roving tabindex, ArrowLeft/Right/Home/End). Static UI crops
341
+ only (no video). Each panel wraps a .product-frame, so it inherits the
342
+ CQ-scaled sizing and never clips at browser zoom. Icon buttons are ~53×53px
343
+ (Graphite reference); use aria-label per button since they are icon-only.
344
+ ───────────────────────────────────────────────────────────────────── */
345
+ .hero-picker { --picker-tab: 3.3125rem; --picker-icon: 1.375rem; display: flex; flex-direction: column; align-items: center; gap: var(--space-4); width: 100%; }
346
+ .hero-picker__tabs { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.6rem; }
347
+ .hero-picker__tab { display: inline-flex; align-items: center; justify-content: center; width: var(--picker-tab); height: var(--picker-tab); padding: 0; background: var(--surface); border: 1px solid var(--hair); border-radius: var(--r-md); color: var(--ink-soft); cursor: pointer; transition: color var(--duration-hover) var(--ease-glide), border-color var(--duration-hover) var(--ease-glide); }
348
+ .hero-picker__tab:hover { color: var(--ink); border-color: var(--hair-2); }
349
+ .hero-picker__tab[aria-selected="true"] { color: var(--accent); border-color: var(--accent); }
350
+ .hero-picker__tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
351
+ .hero-picker__tab svg { width: var(--picker-icon); height: var(--picker-icon); }
352
+ .hero-picker__panels { width: 100%; display: flex; justify-content: center; }
353
+ .hero-picker__panel[hidden] { display: none; }
354
+
355
+ /* ── announcement bar (Graphite full-width clickable notice above nav) ──
356
+ 48px full-width bar, content-gated by announcement.js: it renders only when
357
+ handed enabled content, so the bar is OFF by default (empty / {enabled:false}
358
+ → nothing). When href is set the whole bar is the link (stretched-link
359
+ overlay); the optional dismiss button sits above the overlay and can persist
360
+ a per-id dismissal in localStorage. No slide-in animation (reduced-motion
361
+ friendly by design). Content shape: { enabled, id?, text, href?, linkLabel? }
362
+ — see site/README.md and ../announcement-example.json.
363
+ ───────────────────────────────────────────────────────────────────── */
364
+ .announcement { --announce-min-h: 3rem; --announce-pad-inline: 2.75rem; --announce-dismiss: 1.75rem; position: relative; display: flex; align-items: center; justify-content: center; gap: 0.6rem; min-height: var(--announce-min-h); padding: var(--space-2) var(--announce-pad-inline); background: var(--surface); border-bottom: 1px solid var(--hair); color: var(--ink-soft); font-size: 0.88rem; text-align: center; }
365
+ .announcement[hidden] { display: none; }
366
+ .announcement__text { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--space-2); }
367
+ .announcement__link { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--accent); font-weight: 500; }
368
+ .announcement__link span[aria-hidden] { transition: transform var(--duration-hover) var(--ease-glide); }
369
+ .announcement--link .announcement__link::after { content: ""; position: absolute; inset: 0; }
370
+ .announcement--link:hover .announcement__link span[aria-hidden] { transform: translateX(3px); }
371
+ .announcement__dismiss { position: absolute; right: 0.6rem; top: 50%; transform: translateY(-50%); z-index: 1; display: inline-flex; align-items: center; justify-content: center; width: var(--announce-dismiss); height: var(--announce-dismiss); padding: 0; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--ink-mute); cursor: pointer; }
372
+ .announcement__dismiss:hover { color: var(--ink); }
373
+ .announcement__dismiss:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
374
+
375
+ /* ── case-study card (Graphite {org} × product social proof) ────────────
376
+ Flat --surface card (same sizing family as .bento__cell / .capability-card):
377
+ a mono "{org} × digithings" label, a quote block, and an attribution row with
378
+ an optional logo slot. Drop into a .bento, .capability-grid, or an .h-scroll
379
+ track for a horizontal row. CONTENT-GATED (anti-pattern #2): ship dormant —
380
+ no fabricated testimonials. Real attribution required in production (name,
381
+ title, company, or a public GitHub/OSS reference). Add .case-study--example
382
+ for a watermarked placeholder in demos/docs only. Content shape (per site):
383
+ { org, quote, name, title?, href?, logo? } — see site/README.md.
384
+ ───────────────────────────────────────────────────────────────────── */
385
+ .case-study { --case-logo-h: 1.5rem; --case-logo-max-w: 7.5rem; position: relative; display: flex; flex-direction: column; gap: var(--space-3); background: var(--surface); border: 1px solid var(--hair); border-radius: var(--r-lg); padding: 1.75rem; }
386
+ .case-study__label { font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-mute); }
387
+ .case-study__quote { margin: 0; font-size: 1.05rem; line-height: 1.55; color: var(--ink); }
388
+ .case-study__attr { display: flex; align-items: center; gap: 0.6rem; margin-top: auto; font-size: 0.85rem; color: var(--ink-soft); }
389
+ .case-study__attr strong { color: var(--ink); font-weight: 600; }
390
+ .case-study__logo { height: var(--case-logo-h); width: auto; max-width: var(--case-logo-max-w); filter: grayscale(1); opacity: 0.8; }
391
+ .case-study--example::after { content: "EXAMPLE"; position: absolute; top: 0.85rem; right: 0.85rem; font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.12em; color: var(--ink-mute); border: 1px solid var(--hair-2); border-radius: var(--r-sm); padding: 0.15rem 0.4rem; }
392
+
393
+ /* ── principles ─────────────────────────────────────────────────────── */
394
+ .principles { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); }
395
+ .principle { padding-top: 1.4rem; border-top: 1px solid var(--hair-2); }
396
+ .principle-num { font-family: var(--font-mono); font-size: 0.78rem; color: var(--accent); }
397
+ .principle h3 { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em; margin: 0.7rem 0 0.5rem; }
398
+ .principle p { font-size: 0.9rem; color: var(--ink-soft); }
399
+
400
+ /* ── footer ─────────────────────────────────────────────────────────── */
401
+ .footer { border-top: 1px solid var(--hair); padding: 2.5rem 0; }
402
+ .footer-inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }
403
+ .footer-links { display: flex; gap: var(--space-4); flex-wrap: wrap; }
404
+ .footer-links a { font-size: 0.9rem; color: var(--ink-soft); transition: color 0.18s var(--ease); }
405
+ .footer-links a:hover { color: var(--ink); }
406
+ .footer-meta { font-family: var(--font-mono); font-size: 0.78rem; color: var(--ink-mute); }
407
+
408
+ /* ── colophon — the page's last word (canon §08) ────────────────────────
409
+ The module's name at giant scale, 1px hairline outline by default.
410
+ Scroll-scrubbed with zero JS: CSS scroll-driven animation behind
411
+ @supports; reduced-motion / no-support renders the name standing. The
412
+ <b> suffix reads var(--accent) — livery on module sites, ink on the
413
+ umbrella (which collapses --accent to ink). An opt-in glow sweep
414
+ (Colophon `sweep` prop, .colo-sweep below) is the one sanctioned
415
+ exception to outline-only. */
416
+ /* overflow-x: clip, NOT overflow: hidden — the clip has to come from a box
417
+ that is not a scroll container. `hidden` makes .colophon the nearest
418
+ scrollport for its own child .colo-word, so the child's `view()` timeline
419
+ below resolves against a box that never scrolls: it saturates (rise stuck
420
+ at 1, fill frozen) and the scroll-scrubbed rise silently never runs — the
421
+ wordmark just sits there, fully risen. `clip` clips the nowrap lockup
422
+ identically (and, unlike `hidden`, leaves overflow-y: visible without
423
+ computing to `auto`), so the view() timeline binds to the viewport again. */
424
+ .colophon { overflow-x: clip; overflow-y: visible; padding: 5rem 0 0; user-select: none; pointer-events: none; }
425
+ /* fallback stroke first: without color-mix() support the refined declaration
426
+ drops, and transparent fill + no stroke would render nothing.
427
+ position: relative anchors the optional .colo-sweep overlay. */
428
+ .colo-word { position: relative; display: block; font-family: var(--font-mono); font-weight: 500; font-size: clamp(3rem, 15.5vw, 15rem); line-height: 0.82; letter-spacing: -0.05em; white-space: nowrap; text-align: center; color: transparent; -webkit-text-stroke: 1px var(--ink); -webkit-text-stroke: 1px color-mix(in srgb, var(--ink) 30%, transparent); }
429
+ .colo-word b { font-weight: 500; color: transparent; -webkit-text-stroke: 1px var(--accent); -webkit-text-stroke: 1px color-mix(in srgb, var(--accent) 70%, transparent); }
430
+ /* Opt-in glow sweep (Colophon `sweep` prop) — a light passing left-to-right
431
+ across the sign, ported from the reference footer specimen. A duplicate
432
+ overlay span carries an accent gradient clipped to its own glyphs; the
433
+ component (Motion) scrubs background-position from off-left to off-right
434
+ against the colophon's scroll progress. Parked off-screen by default
435
+ (132% ≈ band clear of the left edge), so no-JS and reduced-motion both
436
+ show just the static outlined wordmark, no glow. */
437
+ .colo-sweep { display: block; position: absolute; inset: 0; -webkit-text-stroke-width: 0; color: transparent; background: linear-gradient(100deg, transparent 38%, color-mix(in srgb, var(--accent) 72%, transparent) 50%, transparent 62%); background-size: 250% 100%; background-repeat: no-repeat; background-position: 132% 0; -webkit-background-clip: text; background-clip: text; }
438
+ @supports (animation-timeline: view()) {
439
+ @media (prefers-reduced-motion: no-preference) {
440
+ /* entry-based ranges: the colophon sits at the document's end, so
441
+ cover-based ranges can never complete — entry 100% (fully visible)
442
+ is reachable at max scroll. */
443
+ .colo-word { opacity: 0; transform: translateY(34%); animation: colo-rise linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; }
444
+ .colo-word b { animation: colo-fill linear both; animation-timeline: view(); animation-range: entry 55% entry 100%; }
445
+ }
446
+ }
447
+ @keyframes colo-rise { to { opacity: 1; transform: none; } }
448
+ @keyframes colo-fill { to { color: color-mix(in srgb, var(--accent) 16%, transparent); } }
449
+
450
+ /* ── responsive ─────────────────────────────────────────────────────── */
451
+ @media (max-width: 940px) {
452
+ .principles { grid-template-columns: repeat(2, 1fr); }
453
+ }
454
+ @media (max-width: 560px) {
455
+ .principles { grid-template-columns: 1fr; }
456
+ }
457
+
458
+ /* ── no-JS content-first (law 06) ───────────────────────────────────────
459
+ SSR ships html.no-js (removed pre-paint by an inline script in each app
460
+ layout). Motion primitives SSR their hidden initial styles inline, and
461
+ the [data-motion] children of a Stagger inherit variants — neutralize
462
+ all of it when scripts never run. */
463
+ html.no-js [data-motion],
464
+ html.no-js [data-motion] * {
465
+ opacity: 1 !important;
466
+ transform: none !important;
467
+ }
468
+
469
+ @media (prefers-reduced-motion: reduce) {
470
+ .glow { animation: none; }
471
+ .term-cursor { animation: none; }
472
+ .reveal-up { opacity: 1 !important; transform: none !important; transition: none !important; }
473
+ .faq__q::after { transition: none; }
474
+ * { scroll-behavior: auto !important; }
475
+ }