/* ==========================================================================
   tv-test.com - site stylesheet

   Compatibility baseline is deliberately conservative, because TV browsers
   are years behind desktop. See docs/TECH.md. In short, this file avoids:

     :where() / :is()   - Chrome 88+. An unsupported selector invalidates the
                          whole rule, which would silently kill focus styles
                          on exactly the old TVs we care about.
     CSS nesting        - far too new
     :has()             - far too new
     container queries  - far too new
     color-mix()        - far too new
     flexbox gap        - Chrome 84+. Grid gap is much older and is fine, so
                          layouts that need gap use grid.

   clamp() is used for fluid type, but every clamp() is preceded by a plain
   fallback declaration, so an engine that drops it still gets a sane size.

   Two skins, both driven by attributes on <html> set by js/detect.js:
     [data-skin="dark"]  color swap
     [data-tv="true"]    10-foot sizing (implies dark)
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
	color-scheme: light;

	/* Brand */
	--brand: #6D3BF5;
	--brand-strong: #5527D9;
	--brand-soft: #EFE9FF;
	--accent: #FF2E93;          /* decoration and gradients only, 3.46:1 on white */

	/* Result semantics - fills and indicators */
	--pass: #00C46A;
	--warn: #FFA800;
	--fail: #FF3B5C;
	--unknown: #8A93A0;

	/* Result semantics - text-safe on light backgrounds */
	--pass-text: #06462B;
	--warn-text: #6B4000;
	--fail-text: #A3001C;
	--unknown-text: #4A5260;

	/* Surfaces */
	--bg: #FAF8FF;
	--panel: #FFFFFF;
	--panel-2: #F4F1FE;
	--ink: #14122B;
	--muted: #5D5A78;
	--line: rgba(20, 18, 43, 0.13);

	/* Focus - identical in both skins, on purpose */
	--focus: #FFD400;
	--focus-width: 4px;

	/* Type */
	--font: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
	--font-mono: "SF Mono", "Cascadia Code", Consolas, monospace;

	/* Rhythm */
	--pad: 28px;
	--shell: 1120px;

	/* Elevation, varied by PURPOSE rather than applied uniformly.
	   One radius plus the same soft shadow on every surface is the default look
	   of generated UI, and it flattens the hierarchy: if everything floats,
	   nothing does. Here a shadow means "this genuinely sits above the page",
	   which is true of exactly two things - the cookie notice and a hovered
	   interactive card. Everything else is flat with a hairline border, and
	   radius scales with the size of the surface. */
	--radius: 14px;        /* large surfaces: panels, tiles, verdict */
	--radius-sm: 10px;     /* small surfaces: disclosures, related links */

	--shadow-float: 0 16px 40px rgba(20, 18, 43, 0.18);   /* true overlays */
	--shadow-hover: 0 4px 14px rgba(60, 40, 140, 0.12);   /* hover affordance */
}

/* Dark skin. Semantic result colors deliberately do not change - they are
   the product's result vocabulary and must mean the same thing everywhere. */
html[data-skin="dark"] {
	color-scheme: dark;

	--bg: #0B0A14;
	--panel: #17162A;
	--panel-2: #201E38;
	--ink: #F5F4FF;
	--muted: #A7A3C4;
	--line: rgba(255, 255, 255, 0.16);

	--brand-soft: #241B4A;
	--pass-text: #7BF0BC;
	--warn-text: #FFD489;
	--fail-text: #FFA8B8;
	--unknown-text: #B9C0CC;
}

/* TV skin. Bigger everything. Applied on top of the dark skin. */
html[data-tv="true"] {
	--pad: 40px;
	--radius: 18px;
	--shell: 1600px;
}


/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
	margin: 0;
	background: var(--bg);
	color: var(--ink);
	font-family: var(--font);
	font-size: 1.05rem;
	line-height: 1.6;
}

html[data-tv="true"] body {
	font-size: 24px;         /* the floor for readable text at 3 metres */
	line-height: 1.5;
}

h1, h2, h3 {
	line-height: 1.15;
	margin: 0 0 0.5em;
	font-weight: 700;
}

h1 { font-size: 2.4rem; font-size: clamp(2.2rem, 5vw, 4rem); }
h2 { font-size: 1.9rem; font-size: clamp(1.6rem, 3vw, 2.4rem); }
h3 { font-size: 1.25rem; }

/* Fixed sizes, not clamp(): TV browsers report unhelpful viewport dimensions
   and vw-based sizing gives unpredictable results across platforms.
   These must not end up smaller than the desktop maximums, or the hierarchy
   reads as weaker on the screen that is furthest from the viewer. */
html[data-tv="true"] h1 { font-size: 68px; }
html[data-tv="true"] h2 { font-size: 44px; }
html[data-tv="true"] h3 { font-size: 32px; }

p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }

a { color: var(--brand); font-weight: 600; }
html[data-skin="dark"] a { color: #B9A2FF; }

code, kbd, samp {
	font-family: var(--font-mono);
	font-size: 0.92em;
}

.muted { color: var(--muted); }
.tnum { font-variant-numeric: tabular-nums; }

.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0 0 0 0);
	white-space: nowrap;
	border: 0;
}


/* --------------------------------------------------------------------------
   3. Focus

   Yellow, 4px. Unambiguous against every other color here, survives color
   vision deficiency, and is not one of the result colors, so a focused element
   is never mistaken for a failing one.

   The ring uses :focus-visible, NOT plain :focus. A plain :focus fires on an
   ordinary MOUSE CLICK too, which left a large yellow rectangle around any
   button or <summary> the user clicked on desktop. :focus-visible fires for
   keyboard and programmatic focus but not for a mouse click, which is exactly
   what we want.

   :focus-visible is Chrome 86+, so the oldest TV browsers do not understand it
   and drop that rule. Two fallbacks cover them, each a separate rule so a parse
   failure on one never takes out the others:
     - js/tvnav.js applies .is-focused as it moves focus (the D-pad path, and
       the primary mechanism on a TV)
     - on the TV skin there is no mouse, so a plain :focus ring is safe there
       and acts as belt and braces.
   -------------------------------------------------------------------------- */

a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
	outline: var(--focus-width) solid var(--focus);
	outline-offset: 3px;
}

.is-focused {
	outline: var(--focus-width) solid var(--focus);
	outline-offset: 3px;
}

/* TV skin only: no mouse here, so a plain :focus ring cannot be triggered by a
   click and is a safe fallback for engines without :focus-visible. */
html[data-tv="true"] a:focus,
html[data-tv="true"] button:focus,
html[data-tv="true"] summary:focus,
html[data-tv="true"] [tabindex]:focus {
	outline: var(--focus-width) solid var(--focus);
	outline-offset: 3px;
}


/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */

.shell {
	width: 100%;
	max-width: var(--shell);
	margin-left: auto;
	margin-right: auto;
	padding-left: 20px;
	padding-right: 20px;
}

/* TVs overscan. Keep anything that matters inside a 5% margin. The overscan
   test tool deliberately breaks this rule to show the user what is cropped. */
html[data-tv="true"] .shell {
	padding-left: 5%;
	padding-right: 5%;
}

/* Vertical padding uses LONGHAND, deliberately.
   These classes are combined with .shell (e.g. <section class="shell section">),
   and the shorthand `padding: 56px 0` would reset .shell's horizontal padding
   to 0, putting content flush against the viewport edge. Desktop hides it
   because max-width centering leaves margin anyway; on mobile the text runs
   into both edges. Always padding-top / padding-bottom here. */
.section { padding-top: 56px; padding-bottom: 56px; }
html[data-tv="true"] .section { padding-top: 40px; padding-bottom: 40px; }

.stack > * + * { margin-top: 20px; }

.panel {
	background: var(--panel);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	padding: var(--pad);
}

.grid {
	display: grid;
	gap: 20px;
	grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

/* auto-fit produces columns too narrow to read at 3 metres. Pin it. */
html[data-tv="true"] .grid { grid-template-columns: repeat(2, 1fr); gap: 28px; }


/* --------------------------------------------------------------------------
   5. Header and footer
   -------------------------------------------------------------------------- */

.site-header {
	border-bottom: 1px solid var(--line);
	background: var(--panel);
}

.site-header .shell {
	display: grid;
	grid-template-columns: auto 1fr;
	align-items: center;
	gap: 16px;
	min-height: 68px;
}

/* justify-self is required, not decorative: as a grid item this element is
   blockified (inline-block computes to block) and would otherwise stretch to
   fill the whole column, turning the pill into a full-width bar on mobile. */
.brand {
	display: inline-block;
	justify-self: start;
	padding: 7px 16px;
	border-radius: 999px;
	background: linear-gradient(135deg, var(--brand) 0%, var(--accent) 100%);
	color: #fff;
	font-size: 15px;
	font-weight: 700;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	text-decoration: none;
}

html[data-tv="true"] .brand { font-size: 22px; padding: 10px 22px; }

.site-nav { justify-self: end; }

.site-nav ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-auto-flow: column;
	gap: 22px;
}

.site-nav a { text-decoration: none; }
.site-nav a:hover { text-decoration: underline; }

.site-footer {
	border-top: 1px solid var(--line);
	margin-top: 40px;
	padding-top: 36px;
	padding-bottom: 36px;
	color: var(--muted);
	font-size: 0.92rem;
}

html[data-tv="true"] .site-footer { font-size: 20px; }

.site-footer p { margin: 0 0 8px; }


/* --------------------------------------------------------------------------
   6. Hero
   -------------------------------------------------------------------------- */

.hero { padding-top: 64px; padding-bottom: 32px; }
html[data-tv="true"] .hero { padding-top: 32px; padding-bottom: 24px; }

.hero .lede {
	font-size: 1.2rem;
	color: var(--muted);
	max-width: 60ch;
}

html[data-tv="true"] .hero .lede { font-size: 28px; max-width: none; }


/* --------------------------------------------------------------------------
   7. Buttons
   -------------------------------------------------------------------------- */

.btn {
	display: inline-block;
	border: 1px solid transparent;
	border-radius: 999px;
	padding: 14px 28px;
	min-height: 48px;
	font: inherit;
	font-weight: 700;
	text-decoration: none;
	text-align: center;
	cursor: pointer;
	background: var(--brand);
	color: #fff;
}

.btn:hover { background: var(--brand-strong); color: #fff; }

.btn-secondary {
	background: transparent;
	border-color: var(--line);
	color: var(--ink);
}

.btn-secondary:hover { background: var(--panel-2); color: var(--ink); }

/* A remote is imprecise and the user is 3 metres away. */
html[data-tv="true"] .btn { min-height: 64px; padding: 18px 36px; font-size: 24px; }


/* --------------------------------------------------------------------------
   8. Result presentation

   Three layer disclosure: verdict, meaning, detail. See docs/DESIGN.md.
   -------------------------------------------------------------------------- */

.verdict {
	border-radius: var(--radius);
	border: 1px solid var(--line);
	background: var(--panel);
	padding: var(--pad);
	text-align: center;
}

.verdict-line {
	font-size: 2rem;
	font-size: clamp(1.7rem, 4vw, 3rem);
	font-weight: 700;
	line-height: 1.1;
	margin: 0 0 10px;
}

html[data-tv="true"] .verdict-line { font-size: 64px; }

.verdict-sub {
	color: var(--muted);
	max-width: 62ch;
	margin-left: auto;
	margin-right: auto;
}

html[data-tv="true"] .verdict-sub { max-width: none; font-size: 26px; }

/* Status chip. Always carries an icon AND a word - never color alone.
   Half our users may be looking at a TV whose color is the thing under
   test, so a red/green-only signal would be meaningless. */
.badge {
	display: inline-block;
	border-radius: 999px;
	padding: 4px 14px;
	font-size: 0.85rem;
	font-weight: 700;
	letter-spacing: 0.02em;
	white-space: nowrap;
	border: 1px solid transparent;
}

html[data-tv="true"] .badge { font-size: 20px; padding: 6px 18px; }

.badge-pass { background: rgba(0, 196, 106, 0.14); color: var(--pass-text); border-color: var(--pass); }
.badge-warn { background: rgba(255, 168, 0, 0.16); color: var(--warn-text); border-color: var(--warn); }
.badge-fail { background: rgba(255, 59, 92, 0.14); color: var(--fail-text); border-color: var(--fail); }
.badge-unknown { background: rgba(138, 147, 160, 0.16); color: var(--unknown-text); border-color: var(--unknown); }

/* Capability rows */
.rows { border-top: 1px solid var(--line); }

.row {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: 12px 20px;
	align-items: start;
	padding: 16px 0;
	border-bottom: 1px solid var(--line);
}

.row-k { font-weight: 600; }
.row-note { color: var(--muted); font-size: 0.94rem; margin: 4px 0 0; }
html[data-tv="true"] .row-note { font-size: 21px; }
.row-v { text-align: right; }

.row-val {
	font-weight: 700;
	font-variant-numeric: tabular-nums;
	display: block;
	margin-bottom: 4px;
}

/* Confidence marker. "reported" means the platform told us and may be lying;
   this is the honesty policy expressed in the UI. */
.conf {
	display: inline-block;
	font-size: 0.76rem;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

html[data-tv="true"] .conf { font-size: 18px; }

/* Gap between a status pill and the confidence label when they sit on the same
   line ("Yes  MEASURED"). Scoped to the adjacency so it does NOT apply when the
   label drops below a block value, which is already spaced. */
.badge + .conf { margin-left: 10px; }
html[data-tv="true"] .badge + .conf { margin-left: 16px; }


/* --------------------------------------------------------------------------
   9. Disclosure: details / FAQ / honesty block
   -------------------------------------------------------------------------- */

details.disclose,
details.faq {
	background: var(--panel);
	border: 1px solid var(--line);
	border-radius: var(--radius-sm);
	padding: 2px 20px;
	margin-bottom: 10px;
}

details.disclose > summary,
details.faq > summary {
	cursor: pointer;
	font-weight: 600;
	padding: 15px 0;
	list-style: none;
	display: block;
}

details.disclose > summary::-webkit-details-marker,
details.faq > summary::-webkit-details-marker { display: none; }

details.disclose > summary::after,
details.faq > summary::after {
	content: "+";
	float: right;
	color: var(--brand);
	font-size: 1.35rem;
	line-height: 1;
	font-weight: 700;
}

details.disclose[open] > summary::after,
details.faq[open] > summary::after { content: "\2212"; }

html[data-skin="dark"] details.disclose > summary::after,
html[data-skin="dark"] details.faq > summary::after { color: #B9A2FF; }

details.disclose > *:not(summary),
details.faq > *:not(summary) { margin-bottom: 16px; }

/* --------------------------------------------------------------------------
   9b. Honesty treatments

   Deliberately NOT the tinted box with a colored left stripe and rounded
   corners. That is the default admonition style in every docs framework
   (Docusaurus, VitePress, MkDocs, Notion), so it reads as generated rather
   than designed. Two purpose-built treatments instead - see D-015.

   A. .band   - full-bleed inverted band, for the homepage statement of position
   B. .ledger - expectation versus reality table, for per-tool caveats
   -------------------------------------------------------------------------- */

/* --- A: full-bleed inverted band ---
   Sits OUTSIDE .shell in the markup and carries its own inner .shell. That
   avoids the `margin-left: calc(50% - 50vw)` full-bleed trick, which counts
   the scrollbar in 100vw and causes horizontal overflow on desktop. */
.band {
	background: #1B1040;
	color: #F3F0FF;
	padding-top: 64px;
	padding-bottom: 64px;
}

html[data-skin="dark"] .band { background: #16112E; }
html[data-tv="true"] .band { padding-top: 48px; padding-bottom: 48px; }

.band-rule {
	width: 56px;
	height: 3px;
	background: var(--accent);
	margin-bottom: 26px;
}

.band h2 {
	color: #F3F0FF;
	max-width: 24ch;
	margin-bottom: 18px;
}

.band p {
	color: #C9C1EE;
	max-width: 66ch;
}

.band p.band-lead {
	color: #F3F0FF;
	font-size: 1.2rem;
}

html[data-tv="true"] .band p { max-width: 56ch; }
html[data-tv="true"] .band p.band-lead { font-size: 28px; }

/* --- B: expectation versus reality ledger --- */
.ledger { border-top: 2px solid var(--ink); }

.ledger > h2 { margin-top: 22px; margin-bottom: 6px; }
.ledger > p { color: var(--muted); max-width: 62ch; margin-bottom: 26px; }

.ledger-row {
	display: grid;
	grid-template-columns: 1fr 1.6fr;
	gap: 8px 40px;
	align-items: baseline;
	padding: 20px 0;
	border-bottom: 1px solid var(--line);
}

.ledger-claim { font-weight: 700; color: var(--ink); }
.ledger-truth { color: var(--muted); margin: 0; }
.ledger-truth b { color: var(--ink); font-weight: 700; }

html[data-tv="true"] .ledger-row { gap: 8px 56px; padding: 26px 0; }

@media (max-width: 640px) {
	.ledger-row { grid-template-columns: 1fr; gap: 4px; }
}

/* Raw report textarea, for pasting results back */
.report-out {
	width: 100%;
	min-height: 220px;
	font-family: var(--font-mono);
	font-size: 0.86rem;
	line-height: 1.5;
	padding: 14px;
	border-radius: var(--radius-sm);
	border: 1px solid var(--line);
	background: var(--panel-2);
	color: var(--ink);
	resize: vertical;
}


/* --------------------------------------------------------------------------
   10. No-JS fallback

   Every page must present its content and explanation without JavaScript.
   The interactive test is an enhancement on top of a page that already works.
   .requires-js is hidden by default and revealed by script.
   -------------------------------------------------------------------------- */

.requires-js { display: none; }
html[data-js="true"] .requires-js { display: block; }
html[data-js="true"] .no-js-note { display: none; }

.no-js-note {
	border: 1px solid var(--line);
	border-left: 5px solid var(--warn);
	border-radius: var(--radius-sm);
	background: var(--panel-2);
	padding: 18px 22px;
}


/* --------------------------------------------------------------------------
   11. Motion

   Restrained on purpose. A stuttering animation on a site that claims to test
   motion handling would be embarrassing. Test patterns that need precise
   timing drive their own rAF loop and are exempt.
   -------------------------------------------------------------------------- */

.btn, .panel, a { transition: background-color 140ms ease, color 140ms ease; }

/* The TV skin drops decorative transition entirely: focus movement must feel
   instant under a remote. */
html[data-tv="true"] .btn,
html[data-tv="true"] .panel,
html[data-tv="true"] a { transition: none; }

@media (prefers-reduced-motion: reduce) {
	* {
		animation-duration: 0.001ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.001ms !important;
	}
}


/* --------------------------------------------------------------------------
   12. Home page
   -------------------------------------------------------------------------- */

.hero-home {
	padding-top: 72px;
	padding-bottom: 48px;
	text-align: center;
}

html[data-tv="true"] .hero-home { padding-top: 36px; padding-bottom: 28px; text-align: left; }

/* Constrain the headline so it breaks into balanced lines instead of leaving
   a one-word orphan ("do.") on the last line at desktop widths.
   text-wrap: balance is a progressive enhancement - it is Chrome 114+, but an
   unknown *property* is simply ignored rather than invalidating the rule, so
   it is safe under our old-engine baseline. max-width does the real work. */
.hero-home h1 {
	margin-bottom: 16px;
	max-width: 20ch;
	margin-left: auto;
	margin-right: auto;
	text-wrap: balance;
}

html[data-tv="true"] .hero-home h1 { max-width: 24ch; margin-left: 0; }

/* The gradient is decoration, so it is safe to use --accent here. Body text
   never uses it: at 3.46:1 on white it fails AA. */
.hero-mark {
	display: inline-block;
	padding: 8px 20px;
	margin-bottom: 22px;
	border-radius: 999px;
	background: linear-gradient(135deg, var(--brand) 0%, var(--accent) 100%);
	color: #fff;
	font-size: 14px;
	font-weight: 700;
	letter-spacing: 0.1em;
	text-transform: uppercase;
}

/* Cap the measure. Without a max-width this ran the full 1600px of the TV
   shell, which is a punishing line length to read from three metres away. */
.hero-home .lede {
	margin-left: auto;
	margin-right: auto;
	max-width: 62ch;
	font-size: 1.25rem;
}

html[data-tv="true"] .hero-home .lede {
	margin-left: 0;
	max-width: 52ch;
	font-size: 28px;
}

.hero-actions { margin-top: 28px; }
.hero-actions .btn { margin: 6px 8px; }

/* Trust strip: the privacy posture is a genuine selling point in this
   category, so it goes above the fold rather than in a footer. */
.trust {
	display: grid;
	gap: 10px 28px;
	grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
	margin-top: 32px;
	padding: 20px 0 0;
	border-top: 1px solid var(--line);
	color: var(--muted);
	font-size: 0.95rem;
	text-align: left;
}

html[data-tv="true"] .trust { font-size: 22px; grid-template-columns: repeat(2, 1fr); }

.trust b { color: var(--ink); display: block; }

/* Tool tiles */
.tool-tile {
	display: block;
	height: 100%;
	padding: var(--pad);
	background: var(--panel);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	text-decoration: none;
	color: inherit;
	transition: border-color 140ms ease, box-shadow 140ms ease;
}

/* Only an actionable tile lifts, and only on hover. */
a.tool-tile:hover {
	border-color: var(--brand);
	box-shadow: var(--shadow-hover);
	color: inherit;
}

.tool-tile h3 { margin-bottom: 8px; }
.tool-tile p { color: var(--muted); margin: 0; }

.tool-tag {
	display: inline-block;
	margin-bottom: 12px;
	padding: 3px 12px;
	border-radius: 999px;
	font-size: 0.74rem;
	font-weight: 700;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	background: var(--brand-soft);
	color: var(--brand);
	border: 1px solid var(--brand);
}

html[data-skin="dark"] .tool-tag { color: #C3AEFF; border-color: #C3AEFF; }
html[data-tv="true"] .tool-tag { font-size: 17px; }

/* A tile for something not built yet. Muted and explicitly labelled, because
   a grid of dead links that pretend to work is worse than an honest gap. */
.tool-tile-soon {
	opacity: 0.72;
	border-style: dashed;
}

.tool-tile-soon .tool-tag {
	background: transparent;
	color: var(--muted);
	border-color: var(--line);
}


/* --------------------------------------------------------------------------
   12b. Cross-promo carousel

   House pattern from screenrecording.app: one product at a time, with a
   per-product gradient set inline by js/promo.js.

   The white-on-gradient text here is a deliberate exception to the palette:
   these gradients belong to the sibling products, not to us.
   -------------------------------------------------------------------------- */

.promo {
	position: relative;
	overflow: hidden;
	border-radius: var(--radius);
	padding: 26px 28px;
	color: #fff;
	/* Fallback until promo.js sets the first product's gradient. */
	background: linear-gradient(135deg, #0f2137 0%, #1a3a5c 55%, #0e7490 100%);
	transition: background 600ms ease;
}

@media (prefers-reduced-motion: reduce) {
	.promo { transition: none; }
}

.promo-eyebrow {
	margin: 0 0 16px;
	font-size: 0.78rem;
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: rgba(255, 255, 255, 0.75);
}

.promo-track {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 18px;
}

.promo-icon {
	width: 64px;
	height: 64px;
	border-radius: 16px;
	flex-shrink: 0;
	object-fit: cover;
}

.promo-body {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.promo-title {
	color: #fff;
	font-size: 1.15rem;
	font-weight: 700;
}

.promo-subtitle {
	color: rgba(255, 255, 255, 0.85);
	font-size: 0.92rem;
	line-height: 1.4;
}

.promo-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex-shrink: 0;
	min-height: 44px;
	padding: 0 24px;
	border-radius: 999px;
	border: 1.5px solid rgba(255, 255, 255, 0.45);
	background: rgba(255, 255, 255, 0.18);
	color: #fff;
	font-weight: 700;
	font-size: 0.92rem;
	text-decoration: none;
	white-space: nowrap;
}

.promo-btn:hover {
	background: rgba(255, 255, 255, 0.32);
	border-color: rgba(255, 255, 255, 0.7);
	color: #fff;
}

.promo-dots {
	display: flex;
	gap: 6px;
	justify-content: center;
	margin-top: 18px;
}

/* Real buttons, not decorative spans, so tvnav.js can reach them with a
   remote and screen readers announce them. */
.promo-dot {
	width: 32px;
	height: 32px;
	padding: 0;
	border: none;
	background: transparent;
	cursor: pointer;
	display: inline-flex;
	align-items: center;
	justify-content: center;
}

.promo-dot::before {
	content: "";
	display: block;
	width: 7px;
	height: 7px;
	border-radius: 999px;
	background: rgba(255, 255, 255, 0.4);
	transition: background 300ms ease, width 300ms ease;
}

.promo-dot.active::before {
	background: #fff;
	width: 22px;
}

html[data-tv="true"] .promo { padding: 40px; }
html[data-tv="true"] .promo-icon { width: 96px; height: 96px; border-radius: 22px; }
html[data-tv="true"] .promo-title { font-size: 30px; }
html[data-tv="true"] .promo-subtitle { font-size: 24px; }
html[data-tv="true"] .promo-btn { min-height: 64px; font-size: 24px; padding: 0 34px; }
html[data-tv="true"] .promo-dot { width: 48px; height: 48px; }

/* --------------------------------------------------------------------------
   12g. Fullscreen pattern engine (js/fullscreen.js)

   Used by the dead pixel, backlight bleed, color banding and calibration
   tools. No safe-area margin on purpose: these tests need the true panel edge,
   same reasoning as the overscan test.
   -------------------------------------------------------------------------- */

.fs-stage {
	display: none;
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	z-index: 9999;
	background: #000;
	cursor: pointer;
}

.fs-stage.fs-open { display: block; }

html.fs-lock, html.fs-lock body { overflow: hidden; }

.fs-surface {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}

/* The label fades out so the field becomes genuinely pure. On a dead pixel or
   backlight test a persistent overlay is the exact bright artefact the viewer
   is trying to spot. */
.fs-label {
	position: absolute;
	left: 50%;
	bottom: 6%;
	transform: translateX(-50%);
	max-width: 90%;
	padding: 12px 22px;
	border-radius: 999px;
	background: rgba(0, 0, 0, 0.72);
	border: 1px solid rgba(255, 255, 255, 0.28);
	color: #fff;
	text-align: center;
	opacity: 0;
	transition: opacity 500ms ease;
	pointer-events: none;
}

.fs-label-on { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
	.fs-label { transition: none; }
}

.fs-label b { display: block; font-size: 1.1rem; }
.fs-label span { font-size: 0.86rem; color: rgba(255, 255, 255, 0.75); }

html[data-tv="true"] .fs-label b { font-size: 30px; }
html[data-tv="true"] .fs-label span { font-size: 22px; }

/* Building blocks the pattern definitions compose. */
.fs-bars { display: flex; width: 100%; height: 100%; }
.fs-bars > i { flex: 1 1 0; height: 100%; display: block; }

.fs-rows { display: flex; flex-direction: column; width: 100%; height: 100%; }
.fs-rows > i { flex: 1 1 0; width: 100%; display: block; }

.fs-grid {
	width: 100%;
	height: 100%;
	background-image:
		repeating-linear-gradient(0deg, #fff 0 1px, transparent 1px 5%),
		repeating-linear-gradient(90deg, #fff 0 1px, transparent 1px 5%);
}

/* Stuck pixel fixer: rapid sub-pixel cycling, done in CSS so there is no timer
   to leak and it stops dead when the surface is torn down. The page warns about
   flashing before the user opts in. */
.fs-fixer {
	width: 100%;
	height: 100%;
	background: #FF0000;
	animation: fs-fixer-cycle 0.12s steps(1) infinite;
}

@keyframes fs-fixer-cycle {
	0%   { background: #FF0000; }
	25%  { background: #00FF00; }
	50%  { background: #0000FF; }
	75%  { background: #FFFFFF; }
}

.fs-center-box {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	border: 2px solid #fff;
}


/* --------------------------------------------------------------------------
   12h. Audio sync test
   -------------------------------------------------------------------------- */

.sync-stage {
	display: flex;
	align-items: center;
	justify-content: center;
	height: 200px;
	border-radius: var(--radius);
	background: #0B0A14;
	border: 1px solid var(--line);
}

html[data-tv="true"] .sync-stage { height: 300px; }

.sync-dot {
	width: 92px;
	height: 92px;
	border-radius: 50%;
	background: #2A2740;
	transition: none;
}

html[data-tv="true"] .sync-dot { width: 150px; height: 150px; }

/* Flat white at full brightness: the flash has to be unambiguous, and a
   gradient or glow would blur the moment it actually happens. */
.sync-dot-on {
	background: #FFFFFF;
	box-shadow: 0 0 60px rgba(255, 255, 255, 0.85);
}


/* --------------------------------------------------------------------------
   12f. Related tests

   In-content cross-links between sibling tools. These are body content, not
   nav chrome, on purpose: search engines weigh in-content links far more than
   repeated site-wide navigation, and this is what ties the tools together into
   one topical cluster instead of five near-islands.
   -------------------------------------------------------------------------- */

.related-grid {
	display: grid;
	gap: 12px;
	grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

html[data-tv="true"] .related-grid { grid-template-columns: repeat(3, 1fr); gap: 20px; }

.related-link {
	display: block;
	padding: 16px 18px;
	border: 1px solid var(--line);
	border-radius: var(--radius-sm);
	background: var(--panel);
	text-decoration: none;
	color: inherit;
}

.related-link:hover { border-color: var(--brand); box-shadow: var(--shadow-hover); color: inherit; }

.related-link b {
	display: block;
	color: var(--brand);
	margin-bottom: 2px;
}

html[data-skin="dark"] .related-link b { color: #B9A2FF; }

.related-link span { color: var(--muted); font-size: 0.92rem; }

html[data-tv="true"] .related-link { padding: 26px; }
html[data-tv="true"] .related-link span { font-size: 21px; }

.promo-note {
	margin-top: 20px;
	color: var(--muted);
	font-size: 0.95rem;
}

html[data-tv="true"] .promo-note { font-size: 22px; }

@media (max-width: 560px) {
	.promo-btn { width: 100%; }
}


/* --------------------------------------------------------------------------
   12c. Speed test
   -------------------------------------------------------------------------- */

.speed-number {
	font-size: 4rem;
	font-size: clamp(3rem, 9vw, 6rem);
	font-weight: 700;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	color: var(--brand);
}

html[data-skin="dark"] .speed-number { color: #B9A2FF; }
html[data-tv="true"] .speed-number { font-size: 110px; }

.speed-unit {
	font-size: 1.2rem;
	font-weight: 600;
	color: var(--muted);
	margin-left: 8px;
}

html[data-tv="true"] .speed-unit { font-size: 28px; }

.speed-bar {
	height: 10px;
	border-radius: 999px;
	background: var(--panel-2);
	overflow: hidden;
	margin: 22px 0 12px;
}

.speed-bar-fill {
	height: 100%;
	width: 0;
	border-radius: 999px;
	background: linear-gradient(90deg, var(--brand) 0%, var(--accent) 100%);
	transition: width 300ms ease;
}

@media (prefers-reduced-motion: reduce) {
	.speed-bar-fill { transition: none; }
}

html[data-tv="true"] .speed-bar { height: 16px; }


/* --------------------------------------------------------------------------
   12d. Overscan test

   The one place the site deliberately breaks its own 5% TV safe area:
   drawing INTO the region a TV crops is the entire point of the test.
   -------------------------------------------------------------------------- */

.os-stage {
	display: none;
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: #000;
	z-index: 9999;
}

html.os-open, html.os-open body { overflow: hidden; }

.os-pattern {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}

.os-frame {
	position: absolute;
	border: 2px solid #fff;
}

.os-tag {
	position: absolute;
	font-family: var(--font-mono);
	font-size: 14px;
	font-weight: 700;
	background: rgba(0, 0, 0, 0.6);
	padding: 1px 5px;
}

.os-tag-tl { top: 2px; left: 4px; }
.os-tag-br { bottom: 2px; right: 4px; }

html[data-tv="true"] .os-tag { font-size: 22px; padding: 2px 8px; }

/* Corner brackets at the true panel edge. A missing corner means that corner
   is cropped, which also exposes a picture that is not centered. */
.os-corner {
	position: absolute;
	width: 44px;
	height: 44px;
	border: 4px solid #FF2E93;
}

.os-tl { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.os-tr { top: 0; right: 0; border-left: 0; border-bottom: 0; }
.os-bl { bottom: 0; left: 0; border-right: 0; border-top: 0; }
.os-br { bottom: 0; right: 0; border-left: 0; border-top: 0; }

html[data-tv="true"] .os-corner { width: 72px; height: 72px; border-width: 6px; }

.os-center {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	text-align: center;
	color: #fff;
	max-width: 70%;
}

.os-center-title { font-size: 1.6rem; font-weight: 700; margin: 0 0 8px; }
.os-center-sub { color: rgba(255, 255, 255, 0.75); margin: 0; }

html[data-tv="true"] .os-center-title { font-size: 44px; }
html[data-tv="true"] .os-center-sub { font-size: 26px; }

.os-exit {
	position: absolute;
	left: 50%;
	bottom: 8%;
	transform: translateX(-50%);
	z-index: 2;
}


/* --------------------------------------------------------------------------
   12e. Speaker test
   -------------------------------------------------------------------------- */

/* 180px minimum, not 150: at 150 the longer labels ("Right channel",
   "Both channels") wrapped to two lines while the short ones did not, so the
   row of buttons came out at uneven heights. */
.speaker-grid {
	display: grid;
	gap: 12px;
	grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
	margin-bottom: 20px;
	align-items: stretch;
}

.speaker-grid .btn { display: flex; align-items: center; justify-content: center; }

html[data-tv="true"] .speaker-grid { grid-template-columns: repeat(3, 1fr); gap: 20px; }

.speaker-now {
	text-align: center;
	font-size: 1.15rem;
	min-height: 2em;
	color: var(--muted);
}

html[data-tv="true"] .speaker-now { font-size: 26px; }

.speaker-freq {
	text-align: center;
	font-family: var(--font-mono);
	font-size: 2rem;
	font-weight: 700;
	color: var(--brand);
	min-height: 1.2em;
	font-variant-numeric: tabular-nums;
}

html[data-skin="dark"] .speaker-freq { color: #B9A2FF; }
html[data-tv="true"] .speaker-freq { font-size: 48px; }


/* --------------------------------------------------------------------------
   13. Prose pages (privacy, changelog)
   -------------------------------------------------------------------------- */

.prose { max-width: 74ch; }
html[data-tv="true"] .prose { max-width: none; }

.prose h2 { margin-top: 1.6em; }
.prose h3 { margin-top: 1.3em; }
.prose ul { padding-left: 22px; }
.prose li { margin-bottom: 10px; }

.prose-back { margin-bottom: 24px; }
.prose-updated { color: var(--muted); }

.changelog-entry {
	padding-top: 24px;
	margin-top: 24px;
	border-top: 1px solid var(--line);
}

.changelog-entry:first-of-type { border-top: 0; margin-top: 0; padding-top: 0; }
.changelog-entry h2 { margin-top: 0; font-size: 1.4rem; }
html[data-tv="true"] .changelog-entry h2 { font-size: 30px; }


/* --------------------------------------------------------------------------
   14. Cookie notice

   Ported from screenrecording.app so the portfolio behaves consistently:
   informational, dismissed state in localStorage, shown once.

   Two TV-specific changes. It sits inside the 5% safe area rather than hard
   against the edge, where a TV would crop it. And it is a normal focusable
   region, so tvnav.js can reach the Accept button with a remote - a banner
   that cannot be dismissed with a D-pad is a trap, not a notice.
   -------------------------------------------------------------------------- */

.cookie-alert {
	position: fixed;
	bottom: 16px;
	bottom: calc(16px + env(safe-area-inset-bottom, 0px));
	left: 16px;
	max-width: 420px;
	z-index: 60;
	opacity: 0;
	transform: translateY(120%);
	transition: opacity 400ms ease-out, transform 400ms ease-out;
	pointer-events: none;
}

.cookie-alert.show {
	opacity: 1;
	transform: translateY(0%);
	pointer-events: auto;
}

.cookie-alert > div {
	background: var(--panel);
	border: 1px solid var(--line);
	border-radius: var(--radius-sm);
	box-shadow: var(--shadow-float);
	padding: 20px 22px;
}

.cookie-alert p {
	margin: 0 0 14px;
	font-size: 0.92rem;
	color: var(--muted);
}

.btnCookieAlert {
	display: inline-block;
	margin-right: 10px;
	padding: 9px 18px;
	border-radius: 999px;
	font-size: 0.92rem;
	font-weight: 700;
	text-decoration: none;
	border: 1px solid var(--line);
	color: var(--ink);
}

.btnCookieAlert-primary {
	background: var(--brand);
	border-color: var(--brand);
	color: #fff;
}

.btnCookieAlert-primary:hover { background: var(--brand-strong); color: #fff; }

html[data-tv="true"] .cookie-alert {
	left: 5%;
	right: 5%;
	bottom: 5%;
	max-width: none;
}

html[data-tv="true"] .cookie-alert p { font-size: 22px; }
html[data-tv="true"] .btnCookieAlert { font-size: 22px; padding: 14px 28px; min-height: 64px; }

@media (max-width: 480px) {
	.cookie-alert { left: 12px; right: 12px; max-width: none; }
}

@media (prefers-reduced-motion: reduce) {
	.cookie-alert { transition: none; }
}


/* --------------------------------------------------------------------------
   15. Small screens
   -------------------------------------------------------------------------- */

@media (max-width: 640px) {
	.section { padding-top: 36px; padding-bottom: 36px; }
	.panel, .tool-tile { padding: 22px; }
	.site-header .shell { grid-template-columns: 1fr; gap: 10px; padding-top: 12px; padding-bottom: 12px; }
	.site-nav { justify-self: start; }
	.site-nav ul { grid-auto-flow: row; gap: 10px; }
	.row { grid-template-columns: 1fr; }
	.row-v { text-align: left; }
	.hero-home { padding-top: 44px; padding-bottom: 32px; }
}
