/* ── 00-tokens.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/00-tokens.css — design tokens (CSS custom properties)

	The single source of truth for the data-room's color, spacing, motion, and
	typography tokens. Auto-injected into every site/templates/**\/*.html page
	by scripts/build-html.mjs.

	Companion to site/styles/app.css's @theme block — the @theme references
	these tokens via var() so a value change here propagates through both:
	hand-written component CSS in site/styles/[0-9]*.css AND every Tailwind
	utility class.

	Per docs/frontend-refactor-plan.md §3.4. Tokens deduplicated from the
	previously per-page :root blocks (14 files, identical 19-token core +
	per-page additions for status / admin / heatmap / motion). The full
	superset lives here so any page can reference any token without a
	per-page :root edit.

	Phase 3 swap (per #20, blocked on broker repo): this file gets replaced
	by `@import "@phlip/platform-ui/tokens";` once the cross-Phlip platform
	package ships.
	═══════════════════════════════════════════════════════════════════════════ */

:root {
	/* ── Brand palette — tonal ramp deep → highlight ───────────────── */
	--brand-deep: #04342c;
	--brand-back: #0b5c4a;
	--brand-mid: #12806a;
	--brand-front: #1d9e75;
	--brand-accent: #5dcaa5;
	--brand-highlight: #e6fff7;

	/* ── Surfaces — page background + card layers ──────────────────── */
	--bg: #090c0b;
	--surface-1: #111614;
	--surface-2: #161c1a;

	/* ── Borders ──────────────────────────────────────────────────── */
	--border: rgba(255, 255, 255, 0.06);
	--border-strong: rgba(255, 255, 255, 0.12);

	/* ── Text scale ──────────────────────────────────────────────── */
	--text-primary: #e6f1ed;
	--text-secondary: #a7b5b0;
	--text-muted: #6f7d78;

	/* ── Interaction (links, primary buttons) ─────────────────────── */
	--interaction: #2563eb;
	--interaction-hover: #1d4ed8;

	/* ── Status — success / warning / danger ──────────────────────── */
	--profit: #3ee089;
	--danger: #ef4444;
	--danger-bg: rgba(239, 68, 68, 0.08);
	--warning: #f59e0b;

	/* ── Callout chrome — used by terms.html's .callout (warning) and
	      .info-callout (info) blocks. Were referenced but never defined
	      until 2026-05-12; the design-token dedup pass that produced
	      this file missed these tokens because terms.html was the only
	      consumer at the time and its :root block was never lifted up.
	      Re-added here as part of the canonical-superset contract that
	      this file holds. ───────────────────────────────────────────── */
	--warn-bg: rgba(245, 158, 11, 0.08);
	--warn-ink: #fbbf24;
	--warn-border: #f59e0b;
	--info-bg: rgba(29, 158, 117, 0.1);
	--info-border: rgba(93, 202, 165, 0.4);

	/* Inline `<code>` background — translucent against the dark
	   surface so the monospace stands out without becoming a hard
	   visual break in prose. */
	--code-bg: rgba(255, 255, 255, 0.05);

	/* ── Admin chrome — only used on /admin/* pages, defined here for
	      consistency so a page-local :root never has to override. ── */
	--admin-accent: #f59e0b;
	--admin-accent-bg: rgba(245, 158, 11, 0.08);
	--admin-accent-border: rgba(245, 158, 11, 0.3);

	/* ── Heatmap accents — only used on /admin/analytics ──────────── */
	--accent-coral: #fb7185;
	--accent-lavender: #a78bfa;
	--accent-amber: #f4a259;

	/* ── Motion — spring easing for hover lifts + popovers ────────── */
	--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* ── 01-reset.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/01-reset.css — universal reset

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs (load order: after 00-tokens.css, before any
	component CSS).

	Deduplicated from the previously per-page reset block — was identical
	verbatim across all 14 pages. The pattern is the standard CSS reset
	used across the data-room since extraction.

	Per docs/frontend-refactor-plan.md §7 step 2.

	## Cascade-layer wrapping

	Wrapped in `@layer base` so Tailwind v4 utility classes (which live
	in `@layer utilities` per Tailwind's own architecture) can override
	the universal `margin: 0 / padding: 0` reset on elements that opt in
	via utility classes like `mt-6`, `mb-5`, `p-8`, etc.

	Pre-this-wrapping, the reset was un-layered. CSS cascade-layer
	rules state that un-layered author styles BEAT all `@layer` rules
	regardless of specificity. So `* { margin: 0 }` (specificity 0,0,0,
	un-layered) was winning over `.mb-5 { margin-bottom: ... }`
	(specificity 0,1,0, in @layer utilities) — all Tailwind margin /
	padding utilities had zero effect site-wide.

	With this wrapping + the corresponding `@layer base, ..., utilities`
	declaration in site/styles/app.css, the layer order becomes:
	  base (this file's reset) < utilities (Tailwind)
	Utilities now correctly override the reset on opted-in elements.

	Other shared modules in site/styles/ (10-nav, 11-forms, 12-stat-cards,
	13-footer, 14-misc, 15-a11y, 16-admin-layout) remain INTENTIONALLY
	un-layered — they're page-specific component CSS that needs to keep
	winning over generic Tailwind utility classes. Only the universal
	reset belongs in `@layer base`.
	═══════════════════════════════════════════════════════════════════════════ */

@layer base {
	*,
	*::before,
	*::after {
		box-sizing: border-box;
		margin: 0;
		padding: 0;
	}
}

/* ── 02-base-typography.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/02-base-typography.css — baseline typography defaults

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs (load order: after 01-reset.css, before
	component CSS like 10-nav.css, 11-forms.css, …).

	## What this module does

	Establishes sensible default styling for raw HTML tags that Tailwind
	v4 preflight resets. Without this module, any raw `<h1>` / `<h3>` /
	`<a>` / `<ul>` / `<button>` left un-classed in a template would
	render as inherited-color body-text under preflight — visibly broken.

	The data-room's component CSS already styles most semantic elements
	via scoped selectors (`.doc h1`, `.terms-content h1`, `.section-card
	h3`, `.btn`, etc.). This module fills the gap for raw tags that
	don't carry an explicit class or live inside an explicit-scoped
	parent.

	## Why this exists — the bare-import switch

	Per docs/tailwind.md and PR #103 (Tailwind PR 1 hotfix), preflight
	was originally disabled in site/styles/app.css because un-migrated
	pages depended on browser-default UA rendering for raw tags.
	Migration completed in PR #121 (Tailwind PR 13 — viewer.html).
	With this typography module in place, preflight can safely be
	re-enabled by switching app.css to the bare `@import "tailwindcss";`
	form — the standard Tailwind v4 idiom.

	## Cascade-layer wrapping

	Wrapped in `@layer base` for the same reason as 01-reset.css:
	Tailwind utility classes live in `@layer utilities`, and base
	beats utilities in source order — so adding inline utility classes
	like `text-2xl font-bold` to a `<h3>` overrides this module's
	default. Component CSS modules (10-nav.css, 11-forms.css, etc.)
	remain INTENTIONALLY un-layered so their page-chrome styles win
	over generic utility classes; this typography module belongs in
	the base layer because it's establishing per-element defaults that
	component CSS + utility classes both deliberately override.

	## Source-order tiebreaker within @layer base

	Tailwind's preflight + this module both live in `@layer base`.
	Within a layer, source order decides ties at equal specificity.
	Tailwind compiled output (/assets/tailwind.css) loads via `<link>`
	BEFORE the shared-modules `<style data-build-injected="shared">`
	block in `<head>`. So this module's rules come AFTER preflight in
	source order → preflight strips, this module re-establishes, then
	component CSS / utility classes override on opted-in elements.

	## Status site reuse

	This module is the canonical baseline-typography scaffolding for
	all Phlip frontends. status.phlip.app (the next surface) will
	auto-pick this up when it's stood up against the same shared-CSS
	pipeline.
	═══════════════════════════════════════════════════════════════════════════ */

@layer base {
	/*
	 * Headings — re-establish font-size hierarchy + bold weight.
	 *
	 * Values are scaled to the data-room's design density (slightly
	 * tighter than browser UA defaults: 1.75 / 1.5 / 1.25 / 1.125 /
	 * 1rem / 0.875rem instead of the UA's 2 / 1.5 / 1.17 / 1 / 0.83 /
	 * 0.67). Color uses --text-primary so headings stay legible on
	 * the dark-theme background; component CSS overrides where a
	 * different accent is wanted (e.g. terms.html's h3 uses
	 * --brand-accent).
	 *
	 * line-height is 1.2 for tight display sizes; component CSS
	 * (e.g. .doc h1's line-height: 1.15) overrides where finer
	 * control is needed.
	 *
	 * margin-bottom is 0 (the universal `* { margin: 0 }` from
	 * 01-reset.css). Components/utility classes opt in to spacing
	 * via mb-3, mb-4, etc.
	 */
	h1,
	h2,
	h3,
	h4,
	h5,
	h6 {
		font-weight: 700;
		line-height: 1.2;
		color: var(--text-primary);
		letter-spacing: -0.01em;
	}

	h1 {
		font-size: 1.75rem;
	}

	h2 {
		font-size: 1.5rem;
	}

	h3 {
		font-size: 1.25rem;
	}

	h4 {
		font-size: 1.125rem;
	}

	h5 {
		font-size: 1rem;
	}

	h6 {
		font-size: 0.875rem;
	}

	/*
	 * Links — brand-accent color + underline. Component CSS overrides
	 * for nav links (.nav__crumbs a, .sidebar-link, etc.) which want
	 * no underline + muted color in their resting state.
	 *
	 * :hover stays at the same color (brand-accent already pops);
	 * the underline stays so the click affordance is unambiguous.
	 * Component CSS overrides where a different hover treatment is
	 * desired.
	 */
	a {
		color: var(--brand-accent);
		text-decoration: underline;
	}

	a:hover {
		color: var(--brand-highlight);
	}

	/*
	 * Lists — re-establish bullets/numbers + indentation that
	 * preflight strips.
	 *
	 * `list-style: revert` would restore the UA default exactly
	 * (Chrome/Firefox: `disc` for ul, `decimal` for ol) — using
	 * explicit values here for cross-browser determinism.
	 *
	 * padding-left: 1.5rem matches the visual indent that markdown
	 * readers expect (viewer.html's .doc ul has padding-left: 1.5em
	 * — same value, em vs rem only matters at zoom; rem is more
	 * predictable).
	 */
	ul {
		list-style: disc;
		padding-left: 1.5rem;
	}

	ol {
		list-style: decimal;
		padding-left: 1.5rem;
	}

	li {
		margin: 0.25em 0;
	}

	li::marker {
		color: var(--text-muted);
	}

	/*
	 * Buttons — preflight strips the UA button chrome (background,
	 * border, padding). Re-establish a minimal "this is interactive"
	 * baseline so raw `<button>` elements without a `.btn`-family
	 * class still render coherently.
	 *
	 * Currently the only raw <button> in the data-room is
	 * admin/index.html's `<button id="bulk-clear">Clear selection</button>`;
	 * .bulk-actions wraps it in a flex container so the button
	 * inherits flex layout. This rule gives it visible chrome:
	 * subtle border + padding + cursor.
	 *
	 * Pages that want a fully-styled button (.btn, .btn.primary,
	 * .nav__signout, etc.) inherit zero from this rule because their
	 * scoped selectors are more specific.
	 */
	button {
		font: inherit;
		color: inherit;
		cursor: pointer;
		background-color: var(--surface-1);
		border: 1px solid var(--border-strong);
		border-radius: 6px;
		padding: 6px 12px;
		transition: border-color 150ms;
	}

	button:hover {
		border-color: var(--brand-accent);
	}

	/*
	 * Horizontal rules — preflight sets `border-top-width: 1px` +
	 * `height: 0` + `color: inherit`. Re-styled to use the design
	 * tokens for a subtle separator. viewer.html's markdown body
	 * may emit raw `<hr>` from renderMarkdown(); this rule provides
	 * the default appearance.
	 */
	hr {
		border: none;
		border-top: 1px solid var(--border);
		margin: 1.5rem 0;
	}

	/*
	 * Images / media — preflight sets `display: block` + `max-width:
	 * 100%` + `height: auto`. That's actually what we want (prevents
	 * overflow on responsive layouts), so no additional rules
	 * needed here. Inline SVG brand-marks aren't affected (they're
	 * styled via `.nav__brand svg`, `.a80-*`, etc. with explicit
	 * width/height attributes that override `height: auto`).
	 */

	/*
	 * Tables — preflight sets `border-collapse: collapse` +
	 * `text-indent: 0`, both desired. Component CSS (`.doc table`)
	 * adds the per-page table styling. No baseline rule needed.
	 */
}

/* ── 10-nav.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/10-nav.css — shared nav chrome (block + elements + modifiers)

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs. Load order: after 00-tokens.css and 01-reset.css,
	before any page-specific component CSS.

	Per docs/frontend-refactor-plan.md §7 step 3 sub-steps 2–3 + §4.2
	(BEM-lite naming convention).

	## What this module covers

	The entire `.nav` block + every element + every modifier that appears in
	the data room's top-of-page chrome:

	  • `.nav` — the sticky <header> wrapper
	  • `.nav__inner` — the centered horizontal flex container
	  • `.nav__brand` + `.nav__brand svg` — the phlip mark + animated SVG ramp
	  • `.nav__wordmark` — the "phlip" text next to the mark
	  • `.admin-badge` — the orange "Admin" pill (admin pages only; harmless
	     on investor pages — selector matches nothing there)
	  • `.nav__sublinks` + `.nav__sublink` + `.nav__sublink-icon` +
	     `.nav__sublink-label` — the admin sub-nav (Tokens / Analytics /
	     Webhooks). Active state via `[aria-current="page"]`.
	  • `.nav__actions` — the right-side button cluster
	  • `.nav__cta` + `.nav__cta-label` — the primary action (Invite, etc.)
	  • `.nav__signout` — the sign-out form button (style as link)

	## Mobile-first contract — see docs/frontend-refactor-plan.md §3.2

	Base styles target the smallest supported viewport (~320px / iPhone SE
	portrait). Enhancements for larger viewports are layered via
	`@media (min-width: <bp>)` queries. NO `max-width` queries in this
	module — the cascade resolves predictably and the per-page padding-jump
	bug from 2026-04-30 (different `.nav__sublink` padding across admin
	pages because of `max-width` cascade order drift) is structurally
	impossible in a mobile-first module.

	## Breakpoints — must match docs/frontend-refactor-plan.md §4.1

	  • 480px — wordmark + admin-badge become visible, sub-nav padding
	            relaxes one notch
	  • 720px — sub-nav switches from icon-only to icon+label, CTA gets
	            its label back, padding bumps to tablet density
	  • 960px — full-laptop tweaks (slightly larger sub-nav padding +
	            font size, wider sub-nav margin to balance the brand block)

	No 1280px rule today — the existing layout's max-width:1400px container
	handles ultrawide gracefully.

	## Shared chrome doesn't fight page-specific CSS

	scripts/build-html.mjs injects the build-shared <style> block ABOVE
	the page's existing <style> block, so any page-specific override in a
	template wins by cascade order. Pages that need to override (e.g.
	dashboard's investor-side `.nav__cta` recolors for the Cal.com schedule
	button) keep their inline rule and it takes precedence.
	═══════════════════════════════════════════════════════════════════════════ */

/* ── .nav block — sticky top-of-page chrome ──────────────────────────────── */

.nav {
	position: sticky;
	top: 0;
	z-index: 50;
	padding: 0 clamp(1rem, 4vw, 2.5rem);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	background: rgba(9, 12, 11, 0.85);
	border-bottom: 1px solid var(--border);
}

.nav__inner {
	max-width: 1400px;
	margin: 0 auto;
	display: flex;
	align-items: center;
	justify-content: space-between;
	height: 64px;
	gap: 16px;
}

/* ── .nav__brand — animated phlip mark + wordmark ────────────────────────── */

.nav__brand {
	display: flex;
	align-items: center;
	gap: 12px;
	text-decoration: none;
	color: inherit;
}

.nav__brand svg {
	width: 32px;
	height: 32px;
}

/* SVG ramp animation — the three tonal layers of the phlip mark fade-and-
 * settle from offset positions on page load. Final transforms match the
 * `<use transform="translate(...)">` values in the SVG markup, so the
 * post-animation visual state is byte-identical to a no-animation render. */
@keyframes a80b {
	to {
		opacity: 1;
		transform: translate(7px, -5px);
	}
}

@keyframes a80m {
	to {
		opacity: 1;
		transform: translate(3.5px, -2.5px);
	}
}

@keyframes a80f {
	to {
		opacity: 1;
		transform: translate(0, 0);
	}
}

/* Gated by prefers-reduced-motion — operators / investors who've opted
 * out of motion get the static post-animation state immediately. */
@media (prefers-reduced-motion: no-preference) {
	.nav__brand svg .a80-b {
		opacity: 0;
		transform: translate(14px, -10px);
		animation: a80b 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0s forwards;
	}

	.nav__brand svg .a80-m {
		opacity: 0;
		transform: translate(10px, -7px);
		animation: a80m 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.12s forwards;
	}

	.nav__brand svg .a80-f {
		opacity: 0;
		transform: translate(6px, -4px);
		animation: a80f 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.24s forwards;
	}
}

/* ── .nav__wordmark — "phlip" text. Hidden on small mobile to reclaim space. */

.nav__wordmark {
	display: none;
	font-size: 0.9375rem;
	font-weight: 700;
	letter-spacing: -0.03em;
}

/* ── .admin-badge — orange "Admin" pill, admin pages only ────────────────── */

.admin-badge {
	display: none;
	padding: 3px 9px;
	background: var(--admin-accent-bg);
	border: 1px solid var(--admin-accent-border);
	color: var(--admin-accent);
	border-radius: 999px;
	font-size: 0.6875rem;
	font-weight: 600;
	letter-spacing: 0.12em;
	text-transform: uppercase;
}

/* ── .nav__sublinks — admin-only sub-nav (Tokens/Analytics/Webhooks) ─────── */

.nav__sublinks {
	display: flex;
	align-items: center;
	gap: 4px;
	margin-left: 8px;
}

/* Icon-only by default (mobile), tightest padding. The label span is hidden
 * via .nav__sublink-label below; icons stay centered via gap: 0. */
.nav__sublink {
	display: inline-flex;
	align-items: center;
	gap: 0;
	padding: 5px 8px;
	color: var(--text-secondary);
	font-size: 0.75rem;
	font-weight: 500;
	text-decoration: none;
	border-radius: 6px;
	border: 1px solid transparent;
	transition:
		color 150ms,
		border-color 150ms,
		background 150ms;
}

.nav__sublink-icon {
	width: 14px;
	height: 14px;
	flex-shrink: 0;
}

/* Label text — hidden on mobile (icon-only sub-nav), shown ≥720px. */
.nav__sublink-label {
	display: none;
}

.nav__sublink:hover {
	color: var(--text-primary);
	background: var(--surface-1);
}

/* Active sublink — page the operator is currently on. Set declaratively
 * via `aria-current="page"`; the partial's inline JS resolves it from the
 * URL on load (so the partial itself is identical across all pages). */
.nav__sublink[aria-current='page'] {
	color: var(--text-primary);
	background: var(--surface-1);
	border-color: var(--border-strong);
}

/* ── .nav__actions — right-side button cluster ───────────────────────────── */

.nav__actions {
	display: flex;
	align-items: center;
	gap: 14px;
}

/* CTA — the primary admin action (Invite investor, etc.). Mobile starts
 * icon-only with tighter padding; ≥720px the label appears. */
.nav__cta {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 7px 10px;
	background: var(--interaction);
	color: white;
	border-radius: 8px;
	font-size: 0.8125rem;
	font-weight: 600;
	text-decoration: none;
	white-space: nowrap;
	transition: background 150ms;
}

.nav__cta:hover {
	background: var(--interaction-hover);
}

.nav__cta-label {
	display: none;
}

.nav__signout {
	color: var(--text-muted);
	font-size: 0.8125rem;
	text-decoration: none;
	background: none;
	border: none;
	cursor: pointer;
	font-family: inherit;
	padding: 0;
}

.nav__signout:hover {
	color: var(--text-primary);
}

/* ─────────────────────────────────────────────────────────────────────────── */
/* ── ≥480px — large mobile / small tablet ────────────────────────────────── */
/* ─────────────────────────────────────────────────────────────────────────── */

/* The wordmark + admin-badge come back; sub-nav padding relaxes one notch.
 * Sub-nav remains icon-only — labels reappear at 720px. */
@media (min-width: 480px) {
	.nav__wordmark {
		display: inline;
	}

	.admin-badge {
		display: inline-block;
	}

	.nav__sublinks {
		margin-left: 12px;
	}

	.nav__sublink {
		padding: 6px 9px;
	}
}

/* ─────────────────────────────────────────────────────────────────────────── */
/* ── ≥720px — tablet portrait / small laptop ─────────────────────────────── */
/* ─────────────────────────────────────────────────────────────────────────── */

/* Sub-nav switches to icon+label; CTA gets its label back. Padding bumps
 * to tablet density. */
@media (min-width: 720px) {
	.nav__sublink {
		gap: 6px;
		padding: 5px 10px;
	}

	.nav__sublink-label {
		display: inline;
	}

	.nav__cta {
		padding: 8px 14px;
	}

	.nav__cta-label {
		display: inline;
	}
}

/* ─────────────────────────────────────────────────────────────────────────── */
/* ── ≥960px — laptop+ ────────────────────────────────────────────────────── */
/* ─────────────────────────────────────────────────────────────────────────── */

/* Final padding + font tweaks for full-desktop density. Wider sub-nav
 * margin balances the brand block on roomy viewports. */
@media (min-width: 960px) {
	.nav__sublink {
		padding: 6px 12px;
		font-size: 0.8125rem;
	}

	.nav__sublinks {
		margin-left: 24px;
	}
}

/* ── 11-forms.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/11-forms.css — shared form chrome (.field + .btn)

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs. Load order: after 10-nav.css, before any
	page-specific component CSS.

	Per docs/frontend-refactor-plan.md §7 step 5 sub-step 1 + §4.2
	(BEM-lite naming convention).

	## What this module covers — the truly-shared baseline

	The audit across the 4 form-having templates (request-access,
	admin/login, admin/2fa, admin/invite) found that each page has
	INTENTIONAL design divergences:

	  • request-access uses uppercase letter-spaced labels for the
	    cold-inbound "branded" feel
	  • admin/login + admin/2fa use font-size: 16px on inputs to
	    sidestep iOS Safari's auto-zoom-on-focus
	  • admin/invite uses tighter padding (10px 12px) for admin-chrome density
	  • request-access uses padding 11px 14px, admin/login 12px 14px

	So this module carries the BASELINE shape — focus ring, placeholder
	color, select-chevron asset, disabled state, button transition curve
	— and each page keeps its page-specific overrides (padding,
	border-radius, font-size, label typography).

	Cascade order per scripts/build-html.mjs's design comment: 11-forms.css
	applies first (the shared baseline), each template's inline <style>
	applies second (the page-specific overrides). That's the intended
	design.

	## BEM-lite naming contract — see §4.2

	  .field                   — the block (form-field container)
	  .field--row              — modifier: two-column inline row
	  .field label             — descendant: form label
	  .field input             — descendant: text/number/email/password input
	  .field select            — descendant: dropdown
	  .field textarea          — descendant: multiline input
	  .field__hint             — element: helper text below input
	  .field__required         — element: required asterisk in label
	  .btn                     — the block (button base)
	  .btn--primary            — modifier: filled (interaction-color)
	  .btn--secondary          — modifier: outlined (border + transparent)
	  .btn:disabled            — disabled state (works on both modifiers)

	No descendant element classes (.field__label, .field__input) — pages
	use descendant selectors against the underlying HTML element, which
	keeps the markup clean and matches the convention admin/login +
	admin/invite already use. Pages CAN add explicit element classes for
	non-element children that don't have a unique tag (e.g. .field__hint
	is on a <div>, .field__required is on a <span>).

	═══════════════════════════════════════════════════════════════════════════ */

/* ── .field block — vertical stack of label + input + optional hint ──────── */

.field {
	display: flex;
	flex-direction: column;
	gap: 6px;
}

/* Modifier: two-column row for inline pairs like first-name + last-name. */
.field--row {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 16px;
}

/* ── Inputs — baseline shape, page-specific padding/font-size stays inline ── */

.field input,
.field select,
.field textarea {
	width: 100%;
	background: var(--bg);
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	color: var(--text-primary);
	font-family: inherit;
	transition:
		border-color 150ms,
		box-shadow 150ms;
}

.field input::placeholder,
.field textarea::placeholder {
	color: var(--text-muted);
}

/* Brand-accent focus ring — same treatment across every form on the site.
 * The 3px box-shadow gives the ring breathing room from the border without
 * shifting layout (vs. outline which respects the element box). */
.field input:focus,
.field select:focus,
.field textarea:focus {
	outline: none;
	border-color: var(--brand-accent);
	box-shadow: 0 0 0 3px rgba(93, 202, 165, 0.18);
}

/* <select> chevron — inline-SVG data URI matches the muted text color so the
 * caret reads as text-tone, not as a discrete UI element. Pages that want a
 * different caret color override the background-image. */
.field select {
	appearance: none;
	-webkit-appearance: none;
	background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236F7D78' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
	background-repeat: no-repeat;
	background-position: right 14px center;
	padding-right: 36px;
}

.field textarea {
	resize: vertical;
	min-height: 96px;
	font-family: inherit;
}

/* ── .field__hint — helper text below an input ───────────────────────────── */

.field__hint {
	font-size: 0.75rem;
	color: var(--text-muted);
	margin-top: 4px;
}

/* ── .field__required — required-asterisk span in a label ────────────────── */

.field__required {
	color: var(--admin-accent);
	margin-left: 2px;
}

/* ═══════════════════════════════════════════════════════════════════════════ */
/* ── .btn block + modifiers ──────────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════ */

/* Baseline button shape — pages set their own padding/font-size to match
 * the surface they're on (compact admin chrome vs. spacious cold-inbound CTA).
 * The transition curve is the universal spring-easing used across the site
 * for hover lifts. */
.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 6px;
	border-radius: 8px;
	border: 1px solid transparent;
	font-family: inherit;
	font-weight: 600;
	cursor: pointer;
	text-decoration: none;
	transition:
		background 150ms,
		transform 200ms var(--ease-spring),
		border-color 150ms,
		color 150ms;
}

/* Primary (filled) — the interaction-color CTA. Hover lifts 1px via the
 * shared spring transition. */
.btn--primary {
	background: var(--interaction);
	color: white;
	border-color: var(--interaction);
}

.btn--primary:hover {
	background: var(--interaction-hover);
	transform: translateY(-1px);
}

/* Block — full-width treatment. Used on auth forms where the submit is the
 * only button in its row and benefits from filling the form's content area.
 * Composes with --primary or --secondary; e.g. <button class="btn btn--primary
 * btn--block">. Standalone modifier (not coupled to --primary) so a future
 * full-width --secondary "Cancel" button works without extra rules. */
.btn--block {
	width: 100%;
}

/* Secondary (outline) — quieter alternate action. Hover brightens the
 * border + text without filling the background. */
.btn--secondary {
	background: transparent;
	color: var(--text-secondary);
	border-color: var(--border-strong);
}

.btn--secondary:hover {
	color: var(--text-primary);
	border-color: var(--text-secondary);
}

/* Disabled state — works on either modifier; the cursor + opacity + lift
 * cancellation are universal disabled-treatment props. */
.btn:disabled,
.btn[aria-disabled='true'] {
	opacity: 0.6;
	cursor: not-allowed;
	transform: none;
}

.btn:disabled:hover,
.btn[aria-disabled='true']:hover {
	transform: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
	.phlip-checkbox — branded custom checkbox

	Replaces the browser-default checkbox (which renders as a stark white
	square against the data-room's dark surface — jarring + visually
	inconsistent with the Phlip palette). The native <input> stays in the
	DOM as a positionally-absolute zero-opacity layer so click + keyboard
	(space-to-toggle, focus-visible) work natively; the visible appearance
	is a sibling `.box` driven by `:checked` / `:indeterminate` /
	`:focus-visible` on the underlying input.

	Pre-Step-7a this component was inlined in site/templates/admin/index.html
	only. The extraction here makes it reusable across surfaces — first
	consumer beyond admin/index is the request-access form's confidentiality
	consent checkbox.

	## Markup contract

		<label class="phlip-checkbox" aria-label="describe what's being toggled">
			<input type="checkbox" id="..." name="..." />
			<span class="box">
				<svg viewBox="0 0 24 24" aria-hidden="true">
					<polyline points="20 6 9 17 4 12" />
				</svg>
			</span>
		</label>

	Wrap the .phlip-checkbox in whatever parent makes sense for the surface
	(the consent row in request-access uses a parent <label> so the text
	label is also click-toggleable; admin/index uses standalone .phlip-
	checkbox elements inside table cells with `aria-label` for accessible-
	name binding).

	## States covered

	  • unchecked  → empty box, border-strong, transparent background
	  • hover      → border swaps to brand-accent
	  • checked    → background fills brand-accent, white checkmark scales in
	  • indeterminate → background fills brand-accent, horizontal bar
	  • focus-visible (keyboard) → 2px brand-accent outline + 2px offset

	═══════════════════════════════════════════════════════════════════════════ */

.phlip-checkbox {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 18px;
	height: 18px;
	cursor: pointer;
	vertical-align: middle;
	/* Don't let the wrapping <label>'s own `cursor: pointer` reset the box
	 * shape — flex-shrink: 0 keeps the 18px square intact when the parent
	 * is a tight flex container (e.g. inside .consent). */
	flex-shrink: 0;
}

.phlip-checkbox input[type='checkbox'] {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	margin: 0;
	opacity: 0;
	cursor: pointer;
	z-index: 1;
}

.phlip-checkbox .box {
	width: 16px;
	height: 16px;
	border: 1.5px solid var(--border-strong);
	border-radius: 4px;
	background: var(--bg);
	display: flex;
	align-items: center;
	justify-content: center;
	transition:
		border-color 150ms,
		background 150ms;
	pointer-events: none;
}

.phlip-checkbox .box svg {
	width: 11px;
	height: 11px;
	stroke: white;
	stroke-width: 3;
	fill: none;
	stroke-linecap: round;
	stroke-linejoin: round;
	opacity: 0;
	transform: scale(0.6);
	transition:
		opacity 100ms,
		transform 150ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.phlip-checkbox:hover .box {
	border-color: var(--brand-accent);
}

/* Checked — fill with brand-accent, show the white check */
.phlip-checkbox input[type='checkbox']:checked ~ .box {
	background: var(--brand-accent);
	border-color: var(--brand-accent);
}

.phlip-checkbox input[type='checkbox']:checked ~ .box svg {
	opacity: 1;
	transform: scale(1);
}

/* Indeterminate — half-filled with a horizontal bar */
.phlip-checkbox input[type='checkbox']:indeterminate ~ .box {
	background: var(--brand-accent);
	border-color: var(--brand-accent);
}

.phlip-checkbox input[type='checkbox']:indeterminate ~ .box::after {
	content: '';
	position: absolute;
	width: 8px;
	height: 2px;
	background: white;
	border-radius: 1px;
}

.phlip-checkbox input[type='checkbox']:indeterminate ~ .box svg {
	display: none;
}

/* Keyboard focus ring — only visible during keyboard navigation. */
.phlip-checkbox input[type='checkbox']:focus-visible ~ .box {
	outline: 2px solid var(--brand-accent);
	outline-offset: 2px;
}

/* ── 12-stat-cards.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/12-stat-cards.css — KPI / metric-tile surface

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs. Load order: after 11-forms.css, before any
	page-specific component CSS.

	Per docs/frontend-refactor-plan.md §7 step 5 sub-step 2.

	## What this module covers

	The .stat-card surface used by the admin KPI grids:

	  • admin/index.html        — top-level token KPIs (Total, Active,
	                                Expired, Pending requests)
	  • admin/investor/index.html — per-investor activity stats (Doc opens,
	                                Downloads, Page views, Last seen) —
	                                stat-cards generated dynamically by JS
	  • admin/webhooks/index.html — webhook receipt KPIs (Total, Cal.com,
	                                Telegram failures, Unmatched)

	The audit across those 3 templates found the .label rule was BYTE-
	IDENTICAL on every page (it had drifted only in obvious ways like
	naming, not values) and the .value rule shared its core (font-weight,
	letter-spacing, color, tnum). Page divergences (admin/index uses a
	bigger 1.75rem value font; webhooks uses JetBrains Mono on the value
	for numeric receipt counts; padding varies 14-18px) stay inline as
	page-specific overrides via the cascade.

	## BEM-lite naming contract — see §4.2

	  .stat-card                     — the block (the tile)
	  .stat-card__label              — element: the small uppercase
	                                     "TOTAL TOKENS" caption above the
	                                     metric value
	  .stat-card__value              — element: the big numeric value
	  .stat-card__value--accent      — modifier: brand-accent (green) for
	                                     positive metrics like "Active"
	  .stat-card__value--warning     — modifier: warning-orange for
	                                     negative metrics like "Expired"
	                                     or "Telegram failures"

	Pre-refactor the markup used descendant `.label` and `.value` classes
	inside `.stat-card`. That name collision with `.field label`,
	`.brand-tag-firm` etc. was confusing namespace pollution — the
	BEM-lite rename to .stat-card__label / .stat-card__value makes the
	scope explicit and removes the cross-component ambiguity.

	═══════════════════════════════════════════════════════════════════════════ */

/* ── .stat-card block — tile container ──────────────────────────────────── */

.stat-card {
	background: var(--surface-1);
	border: 1px solid var(--border);
	border-radius: 12px;
	padding: 16px 18px;
}

/* ── .stat-card__label — small uppercase caption above the value ────────── */

.stat-card__label {
	font-size: 0.6875rem;
	font-weight: 600;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--text-muted);
	margin-bottom: 6px;
}

/* ── .stat-card__value — the metric number ──────────────────────────────── */

/* Baseline value treatment. Pages override font-size for top-level KPIs
 * (admin/index uses 1.75rem for visual hierarchy) and font-family for
 * monospaced numbers (admin/webhooks uses JetBrains Mono so receipt
 * counts align column-wise across tiles).
 *
 * `font-feature-settings: 'tnum'` enables OpenType tabular-numerals on
 * fonts that support them — even when the font isn't monospaced overall,
 * the digit glyphs render at uniform width so values like "123" and
 * "999" line up vertically in adjacent tiles. */
.stat-card__value {
	font-size: 1.5rem;
	font-weight: 700;
	letter-spacing: -0.02em;
	color: var(--text-primary);
	font-feature-settings: 'tnum';
}

/* ── Modifiers — color variants for positive / negative metrics ────────── */

/* Each modifier maps to a token in 00-tokens.css so the color rolls forward
 * when the design system swaps palette. Modifier names mirror the token
 * names for grep-ability across the codebase. */

.stat-card__value--accent {
	color: var(--brand-accent);
}

.stat-card__value--profit {
	color: var(--profit);
}

.stat-card__value--warning {
	color: var(--warning);
}

.stat-card__value--danger {
	color: var(--danger);
}

/* ── 13-footer.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/13-footer.css — shared page footer chrome

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs. Load order: after 12-stat-cards.css, before
	any page-specific component CSS.

	Per docs/frontend-refactor-plan.md §7 step 6 (footer extraction).

	## What this module covers

	The .footer surface used by the authenticated investor surfaces —
	site/templates/index.html (magic-link landing) and site/templates/
	dashboard.html (docs hub). Pre-refactor the two pages carried
	BYTE-IDENTICAL footer CSS save for cosmetic margin/padding drift
	and the class-name divergence (.token-footer on index vs .footer on
	dashboard).

	The viewer.html doc-focused chrome intentionally omits the footer
	(the document is the content; no need for site chrome below it).
	gate.html and request-access.html have their own surface-specific
	footer treatments and are NOT consumers of this module.

	## BEM-lite naming contract — see §4.2

	  .footer                    — the block (semantic <footer> element)
	  .footer__confidential      — element: uppercase mono "Confidential ·
	                                Trade Secrets · Do Not Distribute"
	  .footer__token             — element: monospaced token + expiry line
	                                ("Token: <id> · Expires <date>")
	  .footer__terms-link        — element: "Terms of Access" link in the
	                                copyright line

	Pre-refactor the descendant selectors used bare `.conf` / `.token`
	class names that risked collision with anything else in the codebase
	using those token names. BEM-lite scopes them clearly to the footer
	component.

	## Mobile-first cascade

	Base styles target the smallest viewport: the .footer-terms-link is
	`display: block` so the "© INNOV8 Ventures LLC · Terms of Access"
	line wraps after the · separator instead of breaking mid-content
	inside the third flex column. At ≥1024px the third column has room
	for both halves on a single line, so the link goes inline.

	═══════════════════════════════════════════════════════════════════════════ */

/* ── .footer block — bottom-of-page chrome ──────────────────────────────── */

.footer {
	margin-top: 56px;
	padding-top: 24px;
	border-top: 1px solid var(--border);
	display: flex;
	justify-content: center;
	align-items: center;
	font-size: 0.6875rem;
	color: var(--text-muted);
	/*
	 * Mobile-first: wrap the three flex children so the footer doesn't
	 * push page-width past the viewport when the confidentiality banner
	 * + token line + copyright don't all fit on one row. Per-page smoke-
	 * test 2026-05-12 (PR #136 preflight switch) found that without wrap,
	 * the footer triggered horizontal scroll on iPhone 12 widths.
	 * justify-content: center keeps the stacked rows visually balanced
	 * while wrapped. The ≥1024px breakpoint restores justify-content:
	 * space-between so the row distributes its children edge-to-edge
	 * on desktop.
	 */
	flex-wrap: wrap;
	gap: 8px 16px;
}

/* ── Confidentiality banner — uppercase mono first column ───────────────── */

.footer__confidential {
	text-transform: uppercase;
	font-family: 'JetBrains Mono', ui-monospace, monospace;
	font-weight: 700;
	letter-spacing: 0.08em;
	/* The banner string "Confidential · Trade Secrets · Do Not Distribute"
	 * reads as one phrase; wrapping it mid-bullet (between the dots)
	 * fragments the message. nowrap keeps it on a single line; at narrow
	 * mobile widths it can still spill below the previous row via the
	 * parent's flex-wrap. */
	white-space: nowrap;
}

/* ── Token + expiry line — monospaced second column ─────────────────────── */

.footer__token {
	font-family: 'JetBrains Mono', ui-monospace, monospace;
	color: var(--text-muted);
	letter-spacing: 0.04em;
}

/* ── Terms of Access link in the copyright column ───────────────────────── */

/* `inline-block` so the link participates in the inline flow of the
 * surrounding "© 2026 INNOV8 Ventures LLC ·" copyright text but doesn't
 * itself wrap mid-text. Pre-this-PR the rule was `display: block` with
 * a desktop @media that flipped to `display: inline`; inline-block both
 * ways renders identically and is simpler. */
.footer__terms-link {
	color: var(--brand-accent);
	text-decoration: underline;
	display: inline-block;
}

/* Desktop: restore edge-to-edge layout. The three flex children fit on
 * a single row at ≥1024px so the wrap+center treatment isn't needed. */
@media (min-width: 1024px) {
	.footer {
		flex-wrap: nowrap;
		justify-content: space-between;
	}
}

/* ── 14-misc.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/14-misc.css — small shared components + utilities

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs. Load order: after 13-footer.css.

	Per docs/frontend-refactor-plan.md §7 step 6 sub-step 2 (the "misc"
	sweep). This module is intentionally a small catch-all: components +
	utilities that recur in 3+ templates but don't justify their own
	per-concern module file (those are 10-nav, 11-forms, 12-stat-cards,
	13-footer).

	Each section below is gated by an inline rationale so future
	contributors can decide whether new shared bits should land here or
	graduate to their own module. The rule of thumb: if a future bit
	pushes this module past ~150 lines OR introduces a 3rd unrelated
	concern, split it into its own file.

	═══════════════════════════════════════════════════════════════════════════ */

/* ── .crumb — "← Back to X" affordance ──────────────────────────────────

	Pre-refactor used by 4 templates (dashboard.html, viewer.html,
	admin/investor/index.html, admin/invite/index.html) with byte-
	identical rules save for cosmetic `margin-bottom` / negative-margin
	drift. The pattern: a small inline-flex link with an SVG chevron-
	left icon + label, sitting above page content as the first child
	of <main>. Reads as "navigate back one level."

	dashboard.html has a unique negative-top-margin to pull the crumb
	out of <main>'s 48px top padding — that page-specific positioning
	stays inline as an override. All other consumers use a flat
	margin-bottom: 14px to space the crumb from the content below,
	which becomes the canonical default here.

	The SVG icon's 14×14 fixed size is shared. Hover transitions to the
	brand-accent color across every consumer.

	─────────────────────────────────────────────────────────────────────── */

.crumb {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	color: var(--text-muted);
	font-size: 0.8125rem;
	text-decoration: none;
	padding: 4px 0;
	margin-bottom: 14px;
	transition: color 150ms;
}

.crumb:hover {
	color: var(--brand-accent);
}

.crumb svg {
	width: 14px;
	height: 14px;
}

/* ── .sr-only — screen-reader-only utility ──────────────────────────────

	Pre-refactor duplicated byte-identically across 4 admin templates
	(admin/2fa, admin/index, admin/login, admin/analytics). Visually
	hides an element while keeping it exposed to assistive tech.

	Used when sighted users have a visible cue (an <h1> + subtitle, an
	icon) but screen readers still need the underlying labelled element
	to navigate effectively. E.g.: admin/2fa's OTP input has a visible
	"Enter your verification code" heading for sighted users + an
	<label class="sr-only" for="code"> for screen readers.

	Standard recipe per WCAG 2.1 technique H58 (positioning content off-
	screen for assistive tech). Clip + 1px size keeps the element in the
	accessibility tree without consuming layout space.

	─────────────────────────────────────────────────────────────────────── */

.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* ── 15-a11y.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/15-a11y.css — universal a11y baseline

	Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs. Load order: after 14-misc.css.

	Two concerns:
	  1. Universal keyboard-focus indicator on every interactive element
	  2. Skip-to-content link that appears on focus

	Pre-this-module the same `:focus-visible` rule was duplicated (with
	drift) across 10 templates. admin/index.html was missing it entirely,
	leaving keyboard users with no focus indicator on its filter-bar
	inputs. Centralizing here fixes the gap AND eliminates the drift.

	The skip-link convention is a longstanding a11y pattern (WCAG 2.4.1
	"Bypass Blocks"): a hidden-until-focused anchor that lets keyboard
	users skip past navigation and jump straight to the page's main
	content. Without it, keyboard users have to tab through every nav
	link on every page — ~7 tabs on admin pages, 4 on dashboard, before
	reaching anything actionable.

	═══════════════════════════════════════════════════════════════════════════ */

/* ── Universal focus indicator ──────────────────────────────────────────── */

/*
 * Pattern: suppress the default focus ring (which renders inconsistently
 * across browsers and frequently clashes with our dark surfaces), then
 * restore a high-contrast brand-accent ring SPECIFICALLY for keyboard
 * navigation via :focus-visible. Mouse + touch focus (where the user
 * already knows where they clicked) stays unobtrusive; keyboard nav
 * gets a clear visual track.
 *
 * The matrix of selectors covers every interactive element we ship:
 *   • <a>          — navigation + sign-out links
 *   • <button>     — form submits, action buttons, custom triggers
 *   • <input>      — text/email/password/checkbox/etc.
 *   • <select>     — dropdowns
 *   • <textarea>   — multiline inputs
 *   • [role="button"]  — non-button elements promoted to button role
 *   • [tabindex]   — programmatically focusable elements (error banners,
 *                    the bulk-toolbar after selection, etc.)
 *
 * 2px ring + 2px offset + 2px border-radius. The offset keeps the ring
 * from touching the element's border (looks like a halo, not a stroke);
 * the small border-radius matches the typical button/input corner so the
 * ring traces a soft rectangle rather than the element's exact shape.
 */
:focus {
	outline: none;
}

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role='button']:focus-visible,
[tabindex]:focus-visible {
	outline: 2px solid var(--brand-accent);
	outline-offset: 2px;
	border-radius: 2px;
}

/* ── .skip-link — skip-to-main-content affordance ──────────────────────── */

/*
 * Hidden by default via a negative `top` position (visually off-screen
 * but still in the accessibility tree, so screen readers + keyboard
 * users can reach it). On focus, snaps to the top-left corner of the
 * viewport with high-contrast brand-accent styling.
 *
 * Sized for a comfortable tap-target if a touch-keyboard user (or a
 * power-user remapping their hardware) ever activates it. Z-index is
 * deliberately huge so it lands above any sticky nav / modal / overlay.
 *
 * Note on `:focus` vs `:focus-visible`: this is one of the rare places
 * we want :focus (not :focus-visible). Skip links exist explicitly
 * FOR keyboard nav, so the only time anyone reaches them is keyboard
 * focus. Using :focus-visible would still work in browsers that
 * support it, but :focus is the cross-browser-safe pattern that's
 * universally documented for skip links.
 */
.skip-link {
	position: absolute;
	top: -100px;
	left: 16px;
	z-index: 9999;
	padding: 12px 18px;
	background: var(--brand-accent);
	color: var(--brand-deep);
	border-radius: 6px;
	font-weight: 700;
	font-size: 0.875rem;
	text-decoration: none;
	transition: top 150ms;
}

.skip-link:focus {
	top: 16px;
}

.skip-link:focus-visible {
	outline: 2px solid var(--brand-deep);
	outline-offset: 2px;
}

/* ── 16-admin-layout.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/16-admin-layout.css — admin-scoped layout rules

	Layout rules that apply only to admin pages, scoped via the `admin` class
	on <body>. Auto-injected into every site/templates/**\/*.html page by
	scripts/build-html.mjs — investor-facing pages render the rules into
	their <style> chunk but the rules have no effect because their <body>
	doesn't carry the `admin` class.

	Pre-this-module the admin `main` mobile padding was duplicated 5 times
	(one per admin template's inline <style> block — different values, even,
	while combo PR 5's smoke-test fix-up was in flight). This module
	consolidates the rule and provides the right home for future admin-
	scoped layout dedup.

	## Future migration candidates

	Other admin-scoped layout rules that currently live as inline duplicates
	and could migrate here once the broader Tailwind v4 / mobile-first
	refactor settles direction:

	  • `.table-wrap--anti-jump` — currently inline on /admin/index.html
	    only; same pattern lives on /admin/analytics's bare `.table-wrap`.
	  • `.mobile-sort-picker` — duplicated inline on /admin/index.html and
	    /admin/analytics/index.html; was flagged in the inline CSS comments
	    of both pages as a future dedup target.

	## Companion to the broader frontend-refactor

	Pairs with the shared modules (00-tokens, 01-reset, 10-nav, 11-forms,
	12-stat-cards, 13-footer, 14-misc, 15-a11y). Same auto-inject pipeline;
	same `[0-9]*-*.css` naming convention.

	Phase 3 swap (per docs/frontend-refactor-plan.md §3.4): this file's
	rules may get re-expressed as Tailwind v4 utility classes on the
	templates themselves once the per-page Tailwind migration starts —
	at which point this module becomes empty and is deleted. Until then
	it's the canonical home for admin-scoped layout.
	═══════════════════════════════════════════════════════════════════════════ */

/*
 * Mobile-first base — comfortable 18px touch-margin inset (320-480px).
 * Replaces the per-template inline
 *   main { padding-left: 12px; padding-right: 12px }
 * blocks that lived in each admin template's <style> pre-this-module.
 *
 * Converted to mobile-first (min-width branch below) as part of #100
 * PR 1 (2026-05-13). Pre-conversion this rule was wrapped in
 * `@media (max-width: 480px)` with the per-template inline
 * `main { padding: 32px clamp(1rem, 4vw, 2.5rem) 64px; }` rule
 * cascading through at desktop. Post-conversion the shared module
 * owns the horizontal padding at both breakpoints, baking the clamp()
 * into the min-width branch so the cascade resolves byte-identically
 * to pre-PR at every viewport size.
 */
.admin main {
	padding-left: 18px;
	padding-right: 18px;
}

/*
 * Desktop (≥481px) — fluid horizontal padding tracking viewport width
 * between 1rem (16px floor) and 2.5rem (40px ceiling). The clamp()
 * value matches the per-template inline `main { padding: 32px clamp(...)
 * 64px; }` horizontal value verbatim; duplicating it here lets the
 * shared module's higher specificity (.admin main vs bare main) win
 * cleanly without the inline rule needing to know about breakpoints.
 *
 * Per-template inline rules keep their `padding: 32px clamp(...) 64px`
 * shorthand for vertical padding (32px top, 64px bottom) — only the
 * horizontal portion is now owned by this shared module.
 */
@media (min-width: 481px) {
	.admin main {
		padding-left: clamp(1rem, 4vw, 2.5rem);
		padding-right: clamp(1rem, 4vw, 2.5rem);
	}
}

/* ── 17-refresh-affordance.css ─────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
	site/styles/17-refresh-affordance.css — shared refresh-button + indicator

	Used by all 4 admin pages with refresh affordances: webhooks, analytics,
	index, investor. Consolidates rules that were previously duplicated inline
	in each template's <style> block (~95 lines × 4 = ~380 lines of dup).

	The numeric-prefix slot is 17- (after 16-admin-layout.css) — the file name
	`16-refresh-affordance.css` was originally specified in the task brief but
	16- is already taken by 16-admin-layout.css; bumping to 17- preserves the
	intended load order (after layout, before any future page-specific
	component CSS).

	## Two distinct visual signals, two distinct events

	  1. Auto-poll fires (htmx every-30s polling on #cron-rows-host /
	     #stats-host / #receipts-host / etc.) → htmx adds `.htmx-request` to
	     the polling container AND the hx-indicator target (= the indicator
	     <span>). The indicator pulses; nothing else animates.

	  2. Manual button click fires → the head-htmx event bridge (or the
	     JS-driven manual refresh on admin/index + admin/investor via
	     `phlip:refresh:*` CustomEvents) adds `.htmx-request` to the button
	     itself AND the indicator. The indicator pulses AND the SVG inside
	     the button rotates. The two signals layer: operator gets immediate
	     click-adjacent feedback (button spin) PLUS the page-level indicator
	     pulse.

	## Min-visible-time contract (no JS setTimeout)

	The `.htmx-request` class is NEVER removed mid-animation. The head-htmx
	event bridge queues removals into a WeakSet and waits for the next
	`animationiteration` event on the relevant animation:

	  • Button: `refresh-spin` 0.8s linear infinite — boundary every 800ms.
	  • Indicator: `refresh-pulse` 1.2s ease-in-out infinite (runs on the
	    inner .pulse-dot ALWAYS, not gated on the class) — boundary every
	    1.2s.

	A sub-frame request (e.g. cached /api/admin/webhooks) therefore keeps
	the class on long enough for at least one full visual cycle to play
	out before the indicator fades back to opacity 0.

	Both visual animations are gated by `prefers-reduced-motion: reduce` —
	operators who've opted out of motion get a static "is-refreshing"
	visual signal via opacity only.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── .refresh-btn ────────────────────────────────────────────────────────── */

/*
 * Mobile-first base — 44×44px tap-target floor + center-justified
 * label (320-480px). Touch-comfortable padding (14px 16px) and slightly
 * larger font (0.875rem) for legibility on phones. Desktop tightens
 * both back in the min-width branch below.
 *
 * Converted to mobile-first as part of #100 PR 1 (2026-05-13).
 * Pre-conversion the base was the desktop value with a
 * `@media (max-width: 480px)` override; post-conversion the base IS
 * the mobile value and `@media (min-width: 481px)` tightens to
 * desktop. Cascade resolves byte-identically at every viewport size.
 */
.refresh-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 6px;
	padding: 14px 16px;
	background: var(--surface-1);
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	color: var(--text-primary);
	font-family: inherit;
	font-size: 0.875rem;
	font-weight: 500;
	cursor: pointer;
	text-decoration: none;
	transition: border-color 150ms;
}

/* Desktop (≥481px) — tighter padding + smaller font, default flex
 * alignment (flex-start) replaces the mobile center-justification. */
@media (min-width: 481px) {
	.refresh-btn {
		padding: 8px 14px;
		font-size: 0.8125rem;
		justify-content: flex-start;
	}
}

.refresh-btn:hover {
	border-color: var(--brand-accent);
}

/*
 * Spin the SVG inside the button while a refresh-request is in flight.
 * The `.htmx-request` class is managed by the head-htmx event bridge for
 * htmx-driven buttons (htmx 2.x adds `.htmx-request` to the hx-indicator
 * target only, not to the initiator — the bridge fills that gap by
 * toggling the class on the button via `event.target.closest('.refresh-btn')`).
 * For JS-driven manual refresh paths (admin/index, admin/investor), the
 * `phlip:refresh:*` CustomEvents the bridge listens for drive the same
 * toggling logic.
 *
 * The animation is `infinite` so the bridge has continuous
 * `animationiteration` boundaries to terminate the class cleanly at —
 * one full rotation minimum, never mid-frame.
 */
.refresh-btn.htmx-request svg {
	animation: refresh-spin 0.8s linear infinite;
	transform-origin: center;
}

@keyframes refresh-spin {
	from {
		transform: rotate(0deg);
	}
	to {
		transform: rotate(360deg);
	}
}

/* (The mobile-only padding + font + center-justify rules previously
 * lived here in a `@media (max-width: 480px)` block. They moved up
 * into the base `.refresh-btn` declaration as part of #100 PR 1's
 * mobile-first conversion; the new `@media (min-width: 481px)` branch
 * up above carries the desktop overrides.) */

/* ── .refresh-indicator ──────────────────────────────────────────────────── */

/*
 * Fixed-position at top-right of viewport, just below the 64px sticky
 * admin-nav. Markup placement in each template is therefore irrelevant —
 * position: fixed removes the element from document flow and renders it
 * at the viewport edge regardless of source position. This sidesteps the
 * "indicator scrolls out of view" problem that page-head-anchored
 * indicators had on long pages.
 *
 * z-index 49 sits below the sticky nav's z-index: 50 so the nav always
 * paints over the indicator if their bounding boxes overlap (they
 * shouldn't, given the 76px top offset, but defense-in-depth).
 *
 * Opacity 0 baseline + the .htmx-request override below = the indicator
 * is invisible by default and becomes visible when the bridge toggles
 * the class. The 600ms ease-out transition on the baseline gives the
 * fade-OUT a perceivable window when the bridge eventually removes the
 * class at an animationiteration boundary.
 */
.refresh-indicator {
	position: fixed;
	top: 76px;
	right: 16px;
	z-index: 49;
	display: inline-flex;
	align-items: center;
	opacity: 0;
	transition: opacity 600ms ease-out;
}

/*
 * The override sets `transition: 0ms` on the fade-IN so the indicator
 * pops to opacity 1 instantly the moment the class is added — perceivable
 * even for sub-frame requests. The fade-OUT inherits the baseline's
 * 600ms transition when the class is removed, layered on top of the
 * min-visible-time the head-htmx bridge enforces via animationiteration
 * boundaries (at least one .pulse-dot cycle, ≥1.2s, before the bridge
 * actually removes the class).
 */
.refresh-indicator.htmx-request {
	opacity: 1;
	transition: opacity 0ms;
}

/*
 * Pulse-dot — runs its animation ALWAYS, not gated on `.htmx-request`.
 * Visibility is controlled by the parent indicator's opacity. The bridge
 * relies on this animation's `animationiteration` events (every 1.2s)
 * as natural boundaries for deferred class removal.
 */
.refresh-indicator .pulse-dot {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--brand-accent);
	animation: refresh-pulse 1.2s ease-in-out infinite;
	box-shadow: 0 0 8px rgba(93, 202, 165, 0.5);
}

@keyframes refresh-pulse {
	0%,
	100% {
		opacity: 0.4;
		transform: scale(1);
	}
	50% {
		opacity: 1;
		transform: scale(1.3);
	}
}

/* ── prefers-reduced-motion gate ─────────────────────────────────────────── */

/*
 * Operators with reduce-motion preferences get the same is-refreshing
 * VISUAL signal (opacity gating still works), but neither the SVG-spin
 * nor the pulse-dot animation runs. The head-htmx bridge detects this
 * via `window.matchMedia('(prefers-reduced-motion: reduce)')` and
 * removes `.htmx-request` immediately on clear (rather than waiting
 * for an animationiteration event that will never fire). The
 * indicator's 600ms opacity fade-out transition still provides a
 * perceivable visual window in reduce-motion mode.
 */
@media (prefers-reduced-motion: reduce) {
	.refresh-btn.htmx-request svg {
		animation: none;
	}
	.refresh-indicator .pulse-dot {
		animation: none;
	}
}
