/* Inter Tight, self-hosted, served from this site so a visitor's browser never calls Google.
   Same two subsets castomapi.com ships (SIL Open Font License). */

@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/inter-tight-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/inter-tight-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ikonekt.com — the one stylesheet. The shared chrome (header, link underline, footer shelves, help
   row, back-to-top ring) is castomui-web-2026's, itself fabls.io's and castomo.com's, so every
   ecosystem site shares one header, footer and component set (ECOSYSTEM-GUIDELINES §0); only the ramp
   changes: lime (#3F6212 / #84CC16 / #BEF264) on the shared plate #0B0C10. Lime is a LIGHT fill, so
   the words on it are ink (#0B0C10 on #84CC16 is 10.7:1; white on it is 1.9:1): --on-fill is ink in
   every theme but high-contrast light, whose accent is the dark stop.
   Order: tokens → themes → reader preferences → reset → type → chrome → components → sections → print. */

/* ------------------------------------------------------------------ tokens (light) */
:root {
  --paper: #F4F5F8;
  --paper-2: #FFFFFF;
  --field: #E9EBF0;
  --ink: #0B0C10;
  --ink-2: #454A5C;
  --ink-3: #5F6478;
  --rule: #D7DAE3;
  --rule-w: 1px;
  /* --accent is the lime mid stop; --accent-ink is the dark stop, because lime on paper is
     unreadable as text. Every word and rule uses --accent-ink; only fills use --accent. */
  --accent: #84CC16;
  --accent-ink: #3F6212;
  --accent-soft: rgba(132, 204, 22, .16);
  --ok: #0E7C3A;
  --warn: #8A5200;
  --bad: #C2261A;
  --brand-1: #3F6212;
  --brand-2: #84CC16;
  --brand-3: #BEF264;
  --fill-hover: #BEF264;
  --on-fill: #0B0C10;
  --shadow: 0 1px 2px rgba(11, 12, 16, .06), 0 12px 32px -16px rgba(11, 12, 16, .25);
  --btn-bg: var(--ink);
  --btn-fg: var(--paper);
  --radius: 14px;
  --radius-sm: 8px;
  --radius-pill: 999px;
  --t-1: .875rem;
  --t0: 1.0625rem;
  --t1: 1.25rem;
  --t2: 1.5625rem;
  --t3: 1.95rem;
  --t4: 2.44rem;
  --t5: 3.05rem;
  --t6: 3.8rem;
  --wrap: 1320px;
  --bar-h: 64px;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --dur: .18s;
  /* DESIGN §4 and §5 tokens. They carry no colour, so they live here once and every theme inherits
     them. The spacing steps are for new components only; older rules keep their own numbers rather
     than being refactored for the sake of it. --wrap-hero is the hero's wider stage (§4). */
  --wrap-hero: var(--wrap);
  --s-1: 4px; --s0: 8px; --s1: 12px; --s2: 16px; --s3: 24px; --s4: 32px; --s5: 48px; --s6: 64px; --s7: 96px;
  --ease-ink: var(--ease);
  --ease-paper: cubic-bezier(.65, 0, .35, 1);
  --ease-stamp: cubic-bezier(.34, 1.56, .64, 1);
  --t-write: 320ms; --t-paint: 480ms; --t-stamp: 420ms; --t-lift: 640ms;
  --t-settle: 600ms; --t-tear: 520ms; --t-slide: 220ms; --t-stagger: 70ms;
  color-scheme: light;
}

/* ------------------------------------------------------------------ dark */
[data-theme="dark"] {
  --paper: #0B0C10;
  --paper-2: #14161D;
  --field: #1B1E27;
  --ink: #E8EAEE;
  --ink-2: #B4B8C6;
  --ink-3: #9296A6;
  --rule: #262A36;
  --rule-w: 1px;
  --accent: #84CC16;
  --accent-ink: #BEF264;
  --accent-soft: rgba(132, 204, 22, .20);
  --ok: #5FD898;
  --warn: #FFC46B;
  --bad: #FF8D81;
  --brand-1: #3F6212;
  --brand-2: #84CC16;
  --brand-3: #BEF264;
  --fill-hover: #BEF264;
  --on-fill: #0B0C10;
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 32px -16px rgba(0, 0, 0, .8);
  --btn-bg: var(--ink);
  --btn-fg: var(--paper);
  color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --paper: #0B0C10;
    --paper-2: #14161D;
    --field: #1B1E27;
    --ink: #E8EAEE;
    --ink-2: #B4B8C6;
    --ink-3: #9296A6;
    --rule: #262A36;
    --rule-w: 1px;
    --accent: #84CC16;
    --accent-ink: #BEF264;
    --accent-soft: rgba(132, 204, 22, .20);
    --ok: #5FD898;
    --warn: #FFC46B;
    --bad: #FF8D81;
    --brand-1: #3F6212;
    --brand-2: #84CC16;
    --brand-3: #BEF264;
    --fill-hover: #BEF264;
    --on-fill: #0B0C10;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 32px -16px rgba(0, 0, 0, .8);
    --btn-bg: var(--ink);
    --btn-fg: var(--paper);
    color-scheme: dark;
  }
}

/* ------------------------------------------------------------------ high contrast (light) */
[data-theme="contrast"] {
  --paper: #FFFFFF;
  --paper-2: #FFFFFF;
  --field: #F0F0F0;
  --ink: #000000;
  --ink-2: #000000;
  --ink-3: #000000;
  --rule: #000000;
  --rule-w: 2px;
  --accent: #3F6212;
  --accent-ink: #2C440C;
  --accent-soft: rgba(0, 0, 0, 0);
  --ok: #005A24;
  --warn: #6B3F00;
  --bad: #A81208;
  --brand-1: #2C440C;
  --brand-2: #2C440C;
  --brand-3: #2C440C;
  --fill-hover: #2C440C;
  --on-fill: #FFFFFF;
  --shadow: none;
  --btn-bg: #000000;
  --btn-fg: #FFFFFF;
  color-scheme: light;
}
@media (prefers-contrast: more) {
  :root:not([data-theme]) {
    --paper: #FFFFFF;
    --paper-2: #FFFFFF;
    --field: #F0F0F0;
    --ink: #000000;
    --ink-2: #000000;
    --ink-3: #000000;
    --rule: #000000;
    --rule-w: 2px;
    --accent: #3F6212;
    --accent-ink: #2C440C;
    --accent-soft: rgba(0, 0, 0, 0);
    --ok: #005A24;
    --warn: #6B3F00;
    --bad: #A81208;
    --brand-1: #2C440C;
    --brand-2: #2C440C;
    --brand-3: #2C440C;
    --fill-hover: #2C440C;
    --on-fill: #FFFFFF;
    --shadow: none;
    --btn-bg: #000000;
    --btn-fg: #FFFFFF;
    color-scheme: light;
  }
}

/* ------------------------------------------------------------------ high contrast (dark) */
[data-theme="contrast-dark"] {
  --paper: #000000;
  --paper-2: #000000;
  --field: #111111;
  --ink: #FFFFFF;
  --ink-2: #FFFFFF;
  --ink-3: #FFFFFF;
  --rule: #FFFFFF;
  --rule-w: 2px;
  --accent: #BEF264;
  --accent-ink: #BEF264;
  --accent-soft: rgba(0, 0, 0, 0);
  --ok: #7FFF9F;
  --warn: #FFE066;
  --bad: #FF8D81;
  --brand-1: #BEF264;
  --brand-2: #BEF264;
  --brand-3: #BEF264;
  --fill-hover: #FFFFFF;
  --on-fill: #000000;
  --shadow: none;
  --btn-bg: #FFFFFF;
  --btn-fg: #000000;
  color-scheme: dark;
}

/* ------------------------------------------------------------------ reader preferences */
[data-size="large"] { font-size: 112.5%; }
[data-size="xlarge"] { font-size: 125%; }
[data-spacing="wide"] body { letter-spacing: .12em; word-spacing: .16em; line-height: 1.8; }
[data-spacing="wide"] p, [data-spacing="wide"] li { margin-block-end: 2em; }
[data-spacing="wide"] h1, [data-spacing="wide"] h2, [data-spacing="wide"] h3 { letter-spacing: .04em; }
[data-motion="reduce"] *, [data-motion="reduce"] *::before, [data-motion="reduce"] *::after {
  transition: none !important; animation: none !important; scroll-behavior: auto !important;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}

/* ------------------------------------------------------------------ reset */
*, *::before, *::after { box-sizing: border-box; }
/* Reserved permanently, not just while a popover is locking scroll — otherwise the scrollbar's own
   width appearing and disappearing shifts every fixed-width thing on the page sideways. */
html { font-size: 106.25%; scroll-behavior: smooth; -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }
/* The bar is sticky, so an anchor jump or a focused field would otherwise land underneath it
   (WCAG 2.4.11). The same padding keeps a :target heading on a legal page in view. */
html { scroll-padding-block-start: calc(var(--bar-h) + 8px); }
/* The scrollbar wears the brand accent instead of the browser default — `scrollbar-color` for
   Firefox and current Chrome, the `::-webkit-scrollbar-*` pseudo-elements for older Safari. */
html { scrollbar-color: var(--accent) var(--paper); }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--paper); }
::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent-ink); }
body {
  margin: 0; background: var(--paper); color: var(--ink);
  font-family: 'Inter Tight', system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--t0); line-height: 1.6; font-weight: 400;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
  /* §0.14: no text ever runs out of its box. `anywhere` breaks a word only when it cannot fit whole
     (a long Polish or German compound, a URL, an e-mail address) and, unlike `break-word`, also lowers
     the word's minimum width, so a grid or flex column holding it shrinks instead of overflowing.
     It is inherited, so it reaches the components' shadow roots too. */
  overflow-wrap: anywhere;
  word-break: normal;
}
/* A grid or flex child may shrink below its content's width only if it is allowed to: without this, one
   long word sets the column's minimum and pushes the row off a phone's screen. */
:where(.grid, [class*="-grid"], [class*="tiles"], .split, .cards) > * { min-width: 0; }
/* Long words in narrow, running text are hyphenated where the language allows it (the page's lang
   attribute picks the dictionary); display headlines never are, they break where the author put <br>. */
@media (max-width: 600px) { :where(p, li, dd, td, figcaption, .card h3, .sc-tile h3) { hyphens: auto; -webkit-hyphens: auto; } }
img, svg { max-width: 100%; display: block; }
svg { fill: none; }
button, input, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; }
/* Text links (ECOSYSTEM-GUIDELINES §0.11): no full underline at rest, only a short line under the
   first third, which stretches to full width with a spring on hover and focus. The line is a
   background, not text-decoration, so it follows a link that wraps onto two lines. Only class-less
   links get it: a link with a class is a component (a card, a tile, a menu row, a button) that draws
   its own affordance, and the list below switches it off for the class-less links inside those. */
:root {
  --u-rest: max(33%, 1.5em);
  --u-thick: 2px;
  --u-spring: cubic-bezier(.34, 1.56, .64, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
  :root { --u-spring: linear(0, .21 7%, .6 15%, .92 23%, 1.08 30%, 1.13 35%, 1.1 41%, 1.02 50%, .98 57%, .985 66%, 1.003 80%, 1); }
}
[data-theme^="contrast"] { --u-thick: 3px; }
@media (prefers-contrast: more) { :root:not([data-theme]) { --u-thick: 3px; } }
a { color: var(--accent-ink); text-decoration: none; }
p a, li a, td a { overflow-wrap: anywhere; }
a:not([class]) {
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0 100%;
  background-repeat: no-repeat;
  background-size: var(--u-rest) var(--u-thick);
  padding-block-end: 2px;
  transition: background-size .6s var(--u-spring), color var(--dur, .2s);
}
a:not([class]):is(:hover, :focus-visible, :active) { background-size: 100% var(--u-thick); }
:is(.nav-menu, .lang-menu, .subnav, .pager, .read-on, .board-job, .ad-tile, .lg-links, .lg-post h3, .skip-list) a:not([class]) { background-image: none; padding-block-end: 0; }
/* The line is for links in the content only. Header, local bar and footer links are navigation, marked
   by their place and their colour change, so they never get it (Maciej, 2026-09-24). */
:is(body > header, body > footer, .bar, .foot) a:not([class]) { background-image: none; padding-block-end: 0; }
/* The shelf tiles draw their own affordance, but keep their padding: the reset above would zero it at the foot. */
.foot-family a:not([class]) { background-image: none; }
/* An arrow typed at the end of a text link ("How we count →") is wrapped by the build; it nudges
   along with the line. */
.link-arrow { display: inline-block; transition: translate .35s var(--u-spring); }
a:is(:hover, :focus-visible) .link-arrow { translate: 3px 0; }
@media (prefers-reduced-motion: reduce) { a:not([class]), .link-arrow { transition-duration: .01s; } }
[data-motion="reduce"] :is(a:not([class]), .link-arrow) { transition-duration: .01s; }
@media (forced-colors: active) { a:not([class]) { background-image: none; text-decoration: underline; } }
:focus-visible { outline: 3px solid var(--accent-ink); outline-offset: 3px; border-radius: 4px; }
:focus:not(:focus-visible) { outline: none; }
::selection { background: var(--brand-3); color: #0B0C10; }
hr { border: 0; border-top: var(--rule-w) solid var(--rule); margin: 32px 0; }
code, pre, kbd, samp { font-family: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: .92em; }
[hidden] { display: none !important; }
.sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ------------------------------------------------------------------ type */
h1, h2, h3, h4 { font-weight: 600; letter-spacing: -.025em; line-height: 1.12; margin: 0 0 .5em; text-wrap: balance; color: var(--ink); }
h1 { font-size: clamp(var(--t4), 5.5vw, var(--t6)); }
h2 { font-size: clamp(var(--t2), 3.4vw, var(--t4)); }
h3 { font-size: var(--t2); }
h4 { font-size: var(--t1); }
p { margin: 0 0 1.1em; }
.lead { font-size: var(--t1); color: var(--ink-2); line-height: 1.5; max-width: 60ch; text-wrap: pretty; }
.eyebrow { font-size: var(--t-1); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--accent-ink); margin: 0 0 12px; }
/* On a phone the hero has to get to the headline fast, and this line is scene-setting the rest
   of the page repeats. Only the hero's own eyebrow goes: the ones that label sections further
   down are headings people navigate by, and they stay. */
@media (max-width: 960px) { .hero .eyebrow { display: none; } }
/* The kicker above a section's heading. It had no rule of its own and read as a stray sentence;
   this is Maciej's, a small bold uppercase line in the accent. `.panel .objection` further down is
   more specific and keeps the panels' own larger treatment. */
.objection { font-weight: bold; text-transform: uppercase; font-size: max(80%, var(--t-1)); color: var(--accent-ink); letter-spacing: 2px; line-height: 1.4; margin-block-end: .9em; }
.num { font-variant-numeric: tabular-nums; }
.muted { color: var(--ink-3); }

/* ------------------------------------------------------------------ layout */
.wrap { width: min(var(--wrap), 100% - clamp(32px, 8vw, 64px)); margin-inline: auto; position: relative; z-index: 1; }
.section { padding-block: clamp(56px, 8vw, 96px); }
.section + .section { padding-top: 0; }
.section.band + .section, .section.band-ink + .section,
.section + .section.band, .section + .section.band-ink { padding-top: clamp(56px, 8vw, 96px); }
.section-head { margin-bottom: clamp(24px, 4vw, 40px); max-width: 68ch; }
.band { background: var(--paper-2); border-block: var(--rule-w) solid var(--rule); }
/* The inverted band: ink ground, paper text — in the dark theme it is the light band. Written in
   the two tokens only, never a literal, or one theme fails contrast. */
.band-ink { background: var(--ink); color: var(--paper); }
.band-ink h2, .band-ink h3 { color: var(--paper); }
.band-ink a:not(.btn) { color: var(--paper); }
.band-ink .lead, .band-ink .muted { color: var(--paper); opacity: .85; }
.band-ink .btn-primary { background: var(--paper); color: var(--ink); }
.band-ink .btn-primary:hover { background: var(--brand-3); color: #0B0C10; }
/* In the contrast themes --brand-3 is the theme's own deep accent, which #0B0C10 cannot sit on; the
   hover there inverts to the band's own pair and says so with an outline instead of a colour. */
[data-theme^="contrast"] .band-ink .btn-primary:hover { background: var(--ink); color: var(--paper); outline: 3px solid var(--paper); outline-offset: 2px; }
@media (prefers-contrast: more) { :root:not([data-theme]) .band-ink .btn-primary:hover { background: var(--ink); color: var(--paper); outline: 3px solid var(--paper); outline-offset: 2px; } }
.band-ink .btn-ghost { color: var(--paper); border-color: var(--paper); }
.band-ink .btn-ghost:hover { background: var(--paper); color: var(--ink); }
.grid { display: grid; gap: 20px; }
/* No gaps in a structured grid (ECOSYSTEM-GUIDELINES §0.13). The numbered grids are flex rows, not CSS
   grid: every cell starts at 1/--cols of the row and may grow, so a short last row widens to fill the
   line (7 in threes is 3 + 3 + 1 with the one full width; 8 is 3 + 3 + 2 with the two at half) instead
   of leaving holes. The column count still drops at each breakpoint, and the same rule holds there. */
.grid:is(.grid-2, .grid-3, .grid-4) { display: flex; flex-wrap: wrap; --cols: 3; }
.grid-2 { --cols: 2; }
.grid-4 { --cols: 4; }
/* The basis allows for the widest gap any band sets (24 px), so a full row never wraps early. */
.grid:is(.grid-2, .grid-3, .grid-4) > * { flex: 1 1 calc((100% - (var(--cols) - 1) * 32px) / var(--cols)); }
@media (max-width: 960px) { .grid:is(.grid-3, .grid-4) { --cols: 2; } }
@media (max-width: 600px) { .grid:is(.grid-2, .grid-3, .grid-4) { --cols: 1; } }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(24px, 5vw, 64px); align-items: center; }
@media (max-width: 860px) { .split { grid-template-columns: 1fr; } }
/* Every layout child may shrink below its content; the scroll containers inside then scroll. */
.grid > *, .split > *, .hero-grid > * { min-width: 0; }

/* ------------------------------------------------------------------ skip + bar */
.skip { position: absolute; inset-inline-start: 16px; inset-block-start: 8px; transform: translateY(-160%); background: var(--ink); color: var(--paper); padding: 10px 16px; border-radius: var(--radius-pill); z-index: 100; text-decoration: none; font-weight: 600; }
.skip:focus { transform: none; }

/* The header rests see-through, with no rule under it, and turns white (99% --paper-2) once the page
   scrolls; it never shows a border (the owner, 2026-09-24). Phones: .is-scrolled from header.js;
   desktop: the pill layer below does the same with --p. The border stays, transparent, so the bar
   keeps its height. */
.bar { position: sticky; top: 0; z-index: 50; background-color: color-mix(in srgb, var(--paper-2) 0%, transparent); border-bottom: var(--rule-w) solid transparent; }
@media (max-width: 1200px) { .bar.is-scrolled { background-color: color-mix(in srgb, var(--paper-2) 99%, transparent); } }
.bar-in { display: flex; align-items: center; gap: 10px; min-height: var(--bar-h); }
.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; font-weight: 700; font-size: var(--t1); letter-spacing: -.02em; flex: none; }
.brand svg { width: 32px; height: 32px; }
.brand b { color: var(--accent-ink); font-weight: 700; }
/* The mark's plate stays dark under a dark theme instead of following the light one it was drawn
   for — a light square on a dark bar reads as a mistake, not a mark. */
:is([data-theme="dark"], [data-theme="contrast-dark"]) .brand svg > rect { fill: #111111; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .brand svg > rect { fill: #111111; }
}
/* Marks pop on hover — ported from castomapi.com, after the brand kit's "Draw — Pop" cut
   (kupisiewicz/brand-kit, tools/build_animations.py draw_pop): each tile of a mark pops in from 45%
   with a small overshoot, one after another, and settles. The kit's own keyframes and easing, cut to
   the 0.64 s draw-in so it plays once per hover instead of looping. The animation exists only while
   the link is hovered or focused, so each new hover replays it; `both` holds each tile at its first
   frame until its stagger has passed.

   The header logo sits on its plate, with its tiles inside a <g> that carries the plate's scale, so
   the logo's tiles are `svg > g > *` and that <g> is never touched — a CSS transform-origin or
   transform reaching an SVG transform attribute moves it. A family tile's mark is plate-less, its
   tiles the direct children of its <svg>, and the faint background copy (.shelf-bg, site.js) stays
   out. The hover rules set the animation longhands, because the shorthand would reset the per-tile
   delays below. Under reduced motion the global rule removes the animation and the mark stays whole. */
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > * { transform-box: fill-box; transform-origin: center; }
:is(.brand:hover, .brand:focus-visible) svg > g > *, :is(.foot-family a:hover, .foot-family a:focus-visible) > svg:not(.shelf-bg) > * {
  animation-name: mk-pop; animation-duration: .64s; animation-timing-function: cubic-bezier(.2, .7, .3, 1); animation-fill-mode: both;
}
@keyframes mk-pop {
  0% { opacity: 0; transform: scale(.45); }
  70% { opacity: 1; transform: scale(1.08); }
  100% { opacity: 1; transform: scale(1); }
}
/* The kit's stagger, by position: tile k of n starts (k-1) × 0.42 s / (n-1) after the hover begins.
   `:nth-child(k):nth-last-child(n-k+1)` picks tile k of a mark with exactly n tiles, so no delay is
   written into the markup. Marks of up to five tiles are covered (castomapi-www's tools/sync_marks.py
   refuses a mark with more). The first tile of every mark starts at once. */
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(2):nth-last-child(1) { animation-delay: .42s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(2):nth-last-child(2) { animation-delay: .21s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(3):nth-last-child(1) { animation-delay: .42s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(2):nth-last-child(3) { animation-delay: .14s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(3):nth-last-child(2) { animation-delay: .28s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(4):nth-last-child(1) { animation-delay: .42s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(2):nth-last-child(4) { animation-delay: .105s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(3):nth-last-child(3) { animation-delay: .21s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(4):nth-last-child(2) { animation-delay: .315s; }
:is(.brand svg > g, .foot-family a > svg:not(.shelf-bg)) > :nth-child(5):nth-last-child(1) { animation-delay: .42s; }
*[data-current] { color: var(--accent); }
.nav-links { display: flex; align-items: center; gap: 2px; margin-inline-start: 8px; margin-inline-end: auto; }
.nav-top, .nav-group > summary { font-size: var(--t-1); font-weight: 500; color: var(--ink-2); padding: 8px 12px; border-radius: var(--radius-sm); text-decoration: none; white-space: nowrap; line-height: 1.2; }
.nav-top:hover, .nav-group > summary:hover, .nav-group[open] > summary { color: var(--ink); background: var(--field); }
/* The section you are in marks itself in the top bar (build_site.py puts data-current on its menu),
   which is what tells a reader arriving from a search result where they have landed. The mark is on
   the <details>, but its <summary> sets a colour of its own and would keep it — including while the
   menu is open — so the summary is told to take the group's instead. */
*[data-current] { color: var(--accent-ink); }
.nav-group[data-current] > summary, .nav-group[data-current][open] > summary { color: inherit; }
.nav-group { position: relative; }
.nav-group > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary::after { content: ""; width: 7px; height: 7px; border-right: 1.8px solid currentColor; border-bottom: 1.8px solid currentColor; transform: translateY(-2px) rotate(45deg); transition: transform var(--dur) var(--ease); }
.nav-group[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.nav-menu { position: absolute; top: calc(100% + 10px); inset-inline-start: 0; background: var(--paper-2); border: var(--rule-w) solid var(--rule); border-radius: var(--radius); box-shadow: var(--shadow); padding: 12px; display: grid; gap: 4px; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); width: min(var(--menu-w, 40rem), calc(100vw - 2.5rem)); z-index: 60; animation: menu-in var(--dur) var(--ease); }
/* The gap between the trigger and the panel would drop the hover; this bridges it. */
.nav-menu::before { content: ""; position: absolute; inset-inline: 0; top: -12px; height: 12px; }
.nav-menu[data-cols="3"] { --menu-w: 56rem; }
.nav-menu[data-cols="1"] { --menu-w: 22rem; }
@keyframes menu-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.nav-menu a { display: grid; grid-template-columns: 2.4rem 1fr; grid-template-rows: auto auto; column-gap: 12px; align-items: center; padding: 8px 10px; border-radius: var(--radius-sm); text-decoration: none; color: var(--ink); }
.nav-menu a:hover, .nav-menu a:focus-visible { background: var(--field); }
.nav-menu a b { font-weight: 600; font-size: var(--t-1); line-height: 1.3; }
.nav-menu a span:not(.nav-ico) { grid-column: 2; font-size: var(--t-1); color: var(--ink-3); line-height: 1.35; }
.nav-ico { grid-row: 1 / span 2; width: 2.4rem; height: 2.4rem; border-radius: 10px; background: var(--accent-soft); color: var(--accent-ink); display: grid; place-items: center; border: var(--rule-w) solid transparent; }
[data-theme="contrast"] .nav-ico, [data-theme="contrast-dark"] .nav-ico { border-color: var(--rule); }
.nav-ico svg { width: 1.3rem; height: 1.3rem; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* The hover fill is the pair that always holds words (DESIGN §2.2): --accent under --on-fill, which is
   white on the dark stop in three themes and black on the light stop in contrast-dark. */
.nav-menu a:hover .nav-ico { background: var(--accent); color: var(--on-fill); }
[data-theme="contrast"] .nav-menu a:hover .nav-ico, [data-theme="contrast-dark"] .nav-menu a:hover .nav-ico { background: var(--ink); color: var(--paper); }
.nav-menu a.compact { grid-template-rows: auto; padding: 5px 8px; }
.nav-menu a.compact .nav-ico { grid-row: 1; width: 1.9rem; height: 1.9rem; border-radius: 8px; }
.nav-menu a.compact .nav-ico svg { width: 1.05rem; height: 1.05rem; }
.nav-col { display: grid; gap: 2px; align-content: start; }
.nav-col h3 { font-size: var(--t-1); font-weight: 600; color: var(--ink-3); letter-spacing: .06em; text-transform: uppercase; margin: 8px 10px 6px; }
.nav-col + .nav-col { border-inline-start: var(--rule-w) solid var(--rule); padding-inline-start: 12px; }
.nav-menu .nav-more { grid-column: 1 / -1; margin-top: 6px; padding: 10px; border-top: var(--rule-w) solid var(--rule); font-size: var(--t-1); font-weight: 600; text-align: center; display: block; }

.bar-end { display: flex; align-items: center; gap: 6px; flex: none; }
.icon-btn { display: inline-grid; place-items: center; width: 40px; height: 40px; border-radius: var(--radius-pill); border: var(--rule-w) solid transparent; background: transparent; color: var(--ink-2); }
.icon-btn:hover, .icon-btn[aria-expanded="true"] { background: var(--field); color: var(--ink); }
.icon-btn svg { width: 20px; height: 20px; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 11px 20px; border-radius: var(--radius-pill); font-weight: 600; font-size: var(--t-1); text-decoration: none; border: var(--rule-w) solid transparent; line-height: 1.2; min-height: 44px; transition: transform var(--dur) var(--ease), background var(--dur), color var(--dur); }
.btn:hover { transform: translateY(-1px); }
.btn-primary { background: var(--btn-bg); color: var(--btn-fg); }
/* Ink on lime, not white on lime: --on-fill is ink wherever the fill is lime. */
.btn-primary:hover { background: var(--fill-hover); color: var(--on-fill); }
[data-theme="contrast"] .btn-primary:hover, [data-theme="contrast-dark"] .btn-primary:hover { background: var(--btn-bg); color: var(--btn-fg); outline: 3px solid var(--accent-ink); outline-offset: 2px; }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--rule); }
.btn-ghost:hover { background: var(--field); }
.btn-lg { font-size: var(--t0); padding: 14px 26px; }
.btn svg { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* The icon every button carries. The build picks it from where the button goes and gives it one of
   the icon set's motions, which icons.css plays on hover or keyboard focus of the button and freezes
   under prefers-reduced-motion — so nothing here has to animate it. It is decoration in the strict
   sense: the label already says what the button does, and the icon is aria-hidden.
   `overflow: visible` lets a motion that leaves the icon's own box, like the rocket's, actually
   leave it, and the small inset stops the larger ones touching the button's edge. */
.btn .btn-ico { overflow: visible; margin-inline-end: 1px; }
.btn-lg .btn-ico { width: 20px; height: 20px; }
/* Every button icon here is drawn once, pointing right — an arrow, an external-link
   corner, a send glyph — because that is the reading direction the paths were drawn
   for. Under RTL the button's own text and layout already mirror through logical
   properties (margin-inline-end above, the flex order that follows dir); the icon
   is a fixed SVG path and does not, so it has to be told to. */
[dir="rtl"] .btn .btn-ico { transform: scaleX(-1); }
/* The working ring is not an arrow: it has no direction to mirror. */
[dir="rtl"] .btn .btn-ring { transform: none; }

/* the language picker and the accessibility panel */
.lang, .a11y { position: relative; }
.lang-btn { display: inline-flex; align-items: center; gap: 4px; height: 40px; padding: 0 10px; border-radius: var(--radius-pill); border: var(--rule-w) solid transparent; background: transparent; color: var(--ink-2); font-weight: 600; font-size: var(--t-1); text-transform: uppercase; letter-spacing: .04em; }
.lang-btn:hover, .lang-btn[aria-expanded="true"] { background: var(--field); color: var(--ink); }
.lang-btn svg { width: 10px; height: 6px; stroke: currentColor; }
.pop { position: absolute; inset-inline-end: 0; top: calc(100% + 8px); background: var(--paper-2); border: var(--rule-w) solid var(--rule); border-radius: var(--radius); box-shadow: var(--shadow); padding: 8px; min-width: 12rem; z-index: 70; animation: menu-in var(--dur) var(--ease); }
.pop-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 12px; }
.pop-close { width: 32px; height: 32px; flex: none; }
.pop-close svg { width: 16px; height: 16px; }
.lang-menu { list-style: none; margin: 0; padding: 0; }
.lang-menu a { display: block; padding: 8px 12px; border-radius: var(--radius-sm); text-decoration: none; color: var(--ink); font-size: var(--t-1); }
.lang-menu a:hover { background: var(--field); }
.lang-menu a[aria-current="true"] { font-weight: 600; box-shadow: inset 3px 0 0 var(--accent); }
.lang-menu li[hidden] { display: none; }
.a11y-panel { width: min(22rem, calc(100vw - 2rem)); padding: 16px; }
.a11y-h { font-size: var(--t0); margin: 0; }
/* the language popover: a search box over a scrollable, multi-column list — 99 languages as a single
   vertical column was a screen and a half of scrolling before a reader's own language turned up. */
.lang-pop { width: min(26rem, calc(100vw - 2rem)); max-height: min(70vh, 34rem); padding: 12px; display: flex; flex-direction: column; }
.lang-pop-body { overflow-y: auto; -webkit-overflow-scrolling: touch; margin: 0 -4px; padding: 0 4px; }
.lang-search { display: flex; align-items: center; gap: 8px; height: 40px; margin: 0 0 10px; padding: 0 10px; border-radius: var(--radius-pill); border: var(--rule-w) solid var(--rule); background: var(--field); flex: none; }
.lang-search svg { width: 16px; height: 16px; stroke: var(--ink-3); stroke-width: 1.8; flex: none; }
.lang-search input { flex: 1; min-width: 0; border: 0; background: none; font: inherit; color: var(--ink); }
.lang-search input:focus { outline: none; }
.lang-group-h { font-size: var(--t-1); font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: .06em; margin: 14px 0 6px; }
.lang-group-h:first-child { margin-top: 0; }
/* The languages suggested from the browser's own list, ruled off above everything else. */
.lang-suggested { margin: 0 0 12px; padding: 0 0 10px; border-bottom: var(--rule-w) solid var(--rule); }
.lang-menu--suggested { display: grid; gap: 2px; }
.lang-menu--all { columns: 2; column-gap: 10px; }
.lang-menu--all li { break-inside: avoid; }
.lang-empty { font-size: var(--t-1); color: var(--ink-3); margin: 0; padding: 12px 4px; }
@media (min-width: 760px) { .lang-pop { width: min(34rem, calc(100vw - 2rem)); } .lang-menu--all { columns: 3; } }
@media (max-width: 420px) { .lang-menu--all { columns: 1; } }
/* a scrim behind a full-screen mobile popover, so it reads as a takeover rather than a stray box */
.pop-scrim { display: none; }
.a11y-set { border: 0; padding: 0; margin: 0 0 14px; }
.a11y-set legend { font-size: var(--t-1); font-weight: 600; color: var(--ink-2); padding: 0; margin-bottom: 6px; display: block; }
.a11y-opts { display: flex; flex-wrap: wrap; gap: 6px; }
.a11y-opts button { padding: 7px 12px; border-radius: var(--radius-pill); border: var(--rule-w) solid var(--rule); background: transparent; font-size: var(--t-1); font-weight: 500; min-height: 36px; }
.a11y-opts button[aria-checked="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.a11y-switch { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 8px 0; background: none; border: 0; font-weight: 600; font-size: var(--t-1); text-align: left; min-height: 40px; }
.a11y-dot { width: 40px; height: 22px; border-radius: 11px; background: var(--field); border: var(--rule-w) solid var(--rule); position: relative; flex: none; }
.a11y-dot::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--ink-3); transition: transform var(--dur) var(--ease), background var(--dur); }
.a11y-switch[aria-checked="true"] .a11y-dot { background: var(--accent); border-color: var(--accent); }
.a11y-switch[aria-checked="true"] .a11y-dot::after { transform: translateX(18px); background: var(--on-fill); }
.a11y-hint { font-size: var(--t-1); color: var(--ink-3); margin: -2px 0 6px; }
.a11y-foot { margin: 8px 0 0; font-size: var(--t-1); }
/* Below 640px the trigger buttons sit close to the bar's own edge, so a popover anchored to their
   right edge (inset-inline-end: 0 above) runs off the LEFT of the viewport instead — the panel is
   there, it is just mostly off-screen. Below that width both popovers become a full-screen sheet
   instead of a dropdown: no anchor point to overflow, room for the language list, a close button
   because outside-click is not a reliable gesture once the sheet covers everything. */
@media (max-width: 640px) {
  .pop { position: fixed; inset: 0; width: auto; max-width: none; min-width: 0; max-height: none; height: 100dvh; border-radius: 0; border: 0; box-shadow: none; padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom)); z-index: 80; animation: pop-sheet-in var(--dur) var(--ease); }
  .a11y-panel, .lang-pop { width: auto; max-height: none; }
  /* The sheet is exactly one viewport tall and the page under it is locked, so a panel taller than a
     short or landscape phone (or one grown by its own "Larger" text setting) must scroll ITSELF, or
     its last options sit below the fold with nothing able to reach them. Side insets clear the notch. */
  .a11y-panel { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding-inline: max(16px, env(safe-area-inset-left)) max(16px, env(safe-area-inset-right)); }
  /* Below .bar's own z-index: 50 — the popover is a descendant of .bar and cannot render above a
     sibling stacking context whose root sits higher than .bar's, however high its OWN z-index is. */
  .pop-scrim:not([hidden]) { display: block; position: fixed; inset: 0; background: rgba(10, 12, 16, .45); z-index: 45; }
}
@keyframes pop-sheet-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
/* The page is locked only while a popover is the full-screen phone sheet above. On desktop the
   popover hangs from the bar and the page must stay as it is: overflow: hidden on <body> turns it
   into a clipping scroll container at scroll 0, and the sticky .bar/.subnav then stick to its top,
   i.e. jump off screen when the page is scrolled down. */
@media (max-width: 640px) { html.pop-lock, html.pop-lock body { overflow: hidden; } }

/* read-aloud: one button on the bar, a stop button beside it while reading */
.player { display: inline-flex; align-items: center; gap: 4px; }
.player[data-state="unsupported"] { display: none; }
.player-btn { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 14px 0 10px; border-radius: var(--radius-pill); border: var(--rule-w) solid var(--rule); background: transparent; color: var(--ink-2); font-weight: 600; font-size: var(--t-1); }
.player-btn:hover { background: var(--field); color: var(--ink); }
.player-btn svg { width: 16px; height: 16px; fill: currentColor; }
.player[data-state="idle"] .i-pause, .player[data-state="playing"] .i-play, .player[data-state="paused"] .i-pause { display: none; }
.player.is-active .player-btn { background: var(--accent); color: var(--on-fill); border-color: var(--accent); }
.is-reading { outline: 3px solid var(--accent-ink); outline-offset: 6px; border-radius: 4px; }
@media (max-width: 720px) { .player-label { display: none; } .player-btn { width: 40px; padding: 0; justify-content: center; } }

/* the drawer */
.menu-toggle { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; pointer-events: none; }
.burger { display: none; }
.menu-body { display: contents; }
/* The drawer takes over at 1200, not at the 1080 the rest of the layout turns on: the bar carries five
   menus and a link since 2026-09-21, and below 1200 they no longer fit beside the call to action. */
@media (max-width: 1200px) {
  .bar-in { gap: 6px; }
  .burger { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--radius-pill); color: var(--ink-2); order: 10; cursor: pointer; }
  .burger:hover { background: var(--field); color: var(--ink); }
  .burger svg { width: 22px; height: 22px; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
  .burger .burger-x { display: none; }
  .menu-toggle:checked ~ .burger .burger-x { display: block; }
  .menu-toggle:checked ~ .burger svg:not(.burger-x) { display: none; }
  .menu-toggle:focus-visible ~ .burger { outline: 3px solid var(--accent-ink); outline-offset: 2px; }
  .bar-end { margin-inline-start: auto; }
  .bar-end .bar-cta { display: none; }
  .menu-body { display: none; position: absolute; inset-inline: 0; top: 100%; background: var(--paper-2); border-bottom: var(--rule-w) solid var(--rule); box-shadow: var(--shadow); padding: 12px clamp(16px, 4vw, 32px) 20px; max-height: calc(100vh - var(--bar-h)); overflow: auto; }
  .menu-toggle:checked ~ .menu-body { display: block; }
  .nav-links { display: grid; gap: 4px; margin: 0; }
  .nav-top, .nav-group > summary { display: flex; align-items: center; justify-content: space-between; font-size: var(--t0); padding: 12px 10px; }
  .nav-group { position: static; }
  .nav-menu, .nav-menu[data-cols="3"] { position: static; width: auto; box-shadow: none; border: 0; padding: 4px 0 12px 8px; grid-template-columns: 1fr; animation: none; }
  .nav-menu::before { display: none; }
  .nav-col + .nav-col { border: 0; padding: 0; }
  .nav-menu a b { font-size: var(--t0); }
  .drawer-cta { display: flex; margin-top: 12px; }
}
/* On a wide screen the burger is hidden, so its checkbox must not exist as a form control either —
   an off-screen checkbox with a hidden label is an unlabelled field to a checker. */
@media (min-width: 1201px) { .drawer-cta { display: none; } .menu-toggle { display: none; } }

/* The section tab row under the header (dash-b's landing shape, and castomapi.com's): the current
   page's siblings on one line, scrolling sideways when there are more than fit. An underline, not a
   pill, so it reads as "where you are" rather than as a row of buttons. */
.subnav { border-bottom: var(--rule-w) solid var(--rule); background: var(--paper-2); position: sticky; top: var(--bar-h); z-index: 30; }
.subnav ul { display: flex; gap: 4px; list-style: none; margin: 0; padding: 0; overflow-x: auto; scrollbar-width: none; }
.subnav ul::-webkit-scrollbar { display: none; }
.subnav li { flex: none; }
.subnav a { display: block; padding: 14px 10px; font-size: var(--t-1); font-weight: 500; color: var(--ink-2); text-decoration: none; white-space: nowrap; border-bottom: 2px solid transparent; }
.subnav a:hover { color: var(--ink); }
.subnav a[aria-current="page"] { color: var(--ink); font-weight: 600; border-bottom-color: var(--accent); }
.subnav a:focus-visible { outline: 3px solid var(--accent-ink); outline-offset: -3px; border-radius: var(--radius-sm); }
@media print { .subnav { display: none; } }

/* ------------------------------------------------------------------ prose (the written pages) */
.prose { max-width: 68ch; }
/* When .prose sits ON the .wrap rather than inside it, capping the wrap itself would centre the
   column and leave the headings above it starting somewhere else. Cap the children instead: the box
   keeps the page's left edge, the measure stays readable. */
.wrap.prose { max-width: none; }
.wrap.prose > * { max-width: 68ch; }
.prose > * + h2 { margin-top: 1.8em; }
.prose > * + h3 { margin-top: 1.5em; }
.prose h2 { font-size: var(--t2); }
.prose h3 { font-size: var(--t1); }
.prose ul, .prose ol { padding-inline-start: 1.3em; margin: 0 0 1.2em; color: var(--ink-2); }
.prose li + li { margin-top: .4em; }
.prose blockquote { margin: 1.5em 0; padding: 4px 0 4px 20px; border-inline-start: 3px solid var(--accent); color: var(--ink-2); font-size: var(--t1); }
.prose dl { margin: 0 0 1.2em; }
.prose dt { font-weight: 600; margin-top: 1em; }
.prose dd { margin: 0 0 .2em; color: var(--ink-2); }
.prose table { font-size: var(--t-1); margin: 1.2em 0; }
.prose th, .prose td { text-align: left; padding: 10px 12px; border-bottom: var(--rule-w) solid var(--rule); vertical-align: top; }
.prose th { font-weight: 600; color: var(--ink-2); }
.table-wrap { overflow-x: auto; }

/* The page head on an inner page: where you are, what this is, and nothing else competing. */
.page-head { padding: clamp(36px, 6vw, 64px) 0 0; }
.page-head h1 { font-size: clamp(var(--t3), 5vw, var(--t5)); letter-spacing: -.03em; margin: 8px 0 16px; max-width: 20ch; }
.page-head .lead { font-size: clamp(var(--t0), 2vw, var(--t1)); }
.crumbs { font-size: var(--t-1); color: var(--ink-3); margin-block: 0 4px; }
.crumbs ol { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 8px; }
.crumbs li + li::before { content: "/"; content: "/" / ""; margin-inline-end: 8px; color: var(--rule); }
.crumbs a { color: var(--ink-3); text-decoration: none; }
.crumbs a:hover { color: var(--ink); }

/* The next/previous pair at the foot of a section page: the tab row's job for someone reading
   straight through rather than picking. */
.pager { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: clamp(32px, 5vw, 56px); }
@media (max-width: 600px) { .pager { grid-template-columns: 1fr; } }
.pager a { display: block; padding: 18px 20px; border: var(--rule-w) solid var(--rule); border-radius: var(--radius); text-decoration: none; color: var(--ink); background: var(--paper-2); }
.pager a:hover { border-color: var(--accent); }
.pager small { display: block; color: var(--ink-3); font-size: var(--t-1); margin-bottom: 4px; }
.pager b { font-weight: 600; }
.pager .next { text-align: end; }

/* A quiet aside for the joke that would break the paragraph it belongs to. */
/* The italic is the aside's identity (DESIGN §3): the one place the site speaks in a dry human voice. */
.aside { border-inline-start: 3px solid var(--accent); padding: 4px 0; padding-inline-start: 18px; margin: 24px 0; color: var(--ink-2); font-size: var(--t-1); font-style: italic; max-width: 60ch; }
.aside b { color: var(--ink); font-style: normal; }

/* ------------------------------------------------------------------ footer + back to top */
/* `overflow-x: clip` because the family shelf below runs out to the edges of the window, and 100vw
   counts the scrollbar — without it the page would gain a few pixels of sideways scroll. */
.foot { margin-top: clamp(64px, 10vw, 120px); border-top: var(--rule-w) solid var(--rule); background: var(--paper-2); padding: clamp(40px, 6vw, 64px) 0 32px; font-size: var(--t-1); color: var(--ink-2); overflow-x: clip; }
.foot-grid { display: grid; grid-template-columns: 1.4fr repeat(4, 1fr); gap: 24px; }
@media (max-width: 1080px) { .foot-grid { grid-template-columns: repeat(3, 1fr); } .foot-brand { grid-column: 1 / -1; } }
@media (max-width: 600px) { .foot-grid { grid-template-columns: repeat(2, 1fr); } }
.foot h2 { font-size: var(--t-1); font-weight: 600; color: var(--ink); letter-spacing: .06em; text-transform: uppercase; margin: 0 0 12px; }
.foot ul { list-style: none; margin: 0; padding: 0; }
.foot li + li { margin-top: 8px; }
.foot a { color: var(--ink-2); text-decoration: none; }
.foot a:hover { color: var(--ink); }
.foot-brand .brand { margin-bottom: 12px; }
.foot-brand p { max-width: 30ch; }
/* "Open iKonekt" is the footer's one call to action, so it takes the accent fill rather than the ink
   of the help buttons beside it. Lime holds ink, not white (--on-fill); hover lightens to the light stop.
   green. Contrast themes drop the tint for the plain high-contrast button. */
.foot .btn-accent { background: var(--accent); border-color: var(--accent); color: var(--on-fill); }
.foot .btn-accent:hover { background: var(--fill-hover); border-color: var(--fill-hover); color: var(--on-fill); }
[data-theme^="contrast"] .foot .btn-accent, [data-theme^="contrast"] .foot .btn-accent:hover { background: var(--btn-bg); border-color: var(--btn-bg); color: var(--btn-fg); }
/* "Report a bug", "Open iKonekt", "Contact us" (§0.12): one centred row right under the link columns,
   above the ecosystem shelf, with the call to action in the middle (Maciej, 2026-09-24). `.foot a`
   would repaint the buttons' text in the footer's link grey, so their own colours are restated here. */
.foot-help { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-block-start: clamp(32px, 5vw, 48px); }
.foot-help .btn { min-height: 44px; }
/* A phone cannot fit the three in one row, and wrapping would push the call to action off-centre, so
   they stack in the same order at one width. */
@media (max-width: 520px) { .foot-help { flex-direction: column; align-items: center; } .foot-help .btn { width: min(100%, 280px); justify-content: center; } }
.foot .foot-help .btn-primary { color: var(--btn-fg); }
.foot .foot-help .btn-primary:hover { color: var(--on-fill); }
.foot .foot-help .btn-ghost { color: var(--ink); }

/* The family shelf, ported from castomapi.com (after apple.com's product shelves): large rounded
   tiles in ONE row that scrolls sideways and snaps tile by tile, starting at the content column and
   running out to the right edge of the window. The heading is two-tone — the name in ink, the line
   after it in the muted ink. Every tile links straight to that product's own website. Without
   site.js it is a native scrolling row; site.js makes it circular, adds the arrow buttons
   (`.shelf-nav`) and the faint background mark (`.shelf-bg`). */
.foot-family { position: relative; margin-top: clamp(48px, 7vw, 80px); padding-top: clamp(32px, 5vw, 48px); border-top: var(--rule-w) solid var(--rule); }
/* Two shelves since 2026-09-21 (ecosystem, then tools). The second is a continuation, not a new
   chapter of the footer, so it drops the rule line and most of the gap. */
.foot-family + .foot-family { margin-top: clamp(8px, 2vw, 24px); padding-top: 0; border-top: 0; }
.foot-family h2 { font-size: var(--t3); font-weight: 600; line-height: 1.15; letter-spacing: -.025em; text-transform: none; color: var(--ink); margin: 0 0 clamp(16px, 2.5vw, 28px); text-wrap: balance; }
.foot-family h2 span { color: var(--ink-3); }
/* The icon sits above the heading, not inline in it, so it centers as its own line with a tight
   gap underneath — the wide inline gutter used elsewhere in text would read as a mistake here. */
.foot-family-centered { text-align: center; }
.foot-family-centered .foot-fam-ico { display: block; margin: 0 auto 6px; --ci-size: 40px; }
.foot-family-centered h2 { text-align: center; }
.foot-family-centered h2 span { display: block; margin-top: 6px; }
[data-theme^="contrast"] .foot-fam-ico { color: var(--ink); }
.foot-fam-ico { color: var(--accent-ink); }
/* The row bleeds to both window edges: the negative margin pulls it out of the content column, and
   the same amount as padding puts the first tile back on the column. `scroll-padding` makes every
   snap land a tile on that same line. The vertical padding leaves room for the hover lift and shadow. */
.foot-family ul { display: flex; gap: 20px; list-style: none; margin: 0 calc(50% - 50vw); padding: 12px calc(50vw - 50%) 28px; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scroll-padding-inline: calc(50vw - 50%); scrollbar-width: none; }
.foot-family ul::-webkit-scrollbar { display: none; }
.foot-family li { flex: 0 0 min(298px, 65.6vw); margin: 0; scroll-snap-align: start; }
.foot-family li + li { margin-top: 0; }
/* Each tile is one link. Grid rows place the markup (mark, name, line, domain) the way an Apple tile
   reads: the name as a small uppercase eyebrow, the line as the headline, the mark large below, the
   domain as a pill at the foot. The mark is aria-hidden, so the reading order is untouched. */
.foot-family a { position: relative; display: grid; grid-template-rows: auto auto 1fr auto; justify-items: start; height: 100%; min-height: clamp(304px, 40vh, 368px); padding: clamp(24px, 3vw, 34px); overflow: hidden; color: var(--ink-2); text-decoration: none; background: rgba(1, 1, 1, .001); border: var(--rule-w) solid rgba(1, 1, 1, .001); box-shadow: var(--shadow); border-radius: calc(var(--radius) * 1.5); transition: transform var(--dur) var(--ease), background-color var(--dur) var(--ease), border-color var(--dur) var(--ease); }
/* The product's own hue names it, but a raw brand hue (livee's yellow, iKonekt's cyan) drops to 2:1 on
   the pale shelf, so it is deepened toward black in light and lifted toward white in dark, as the ad
   tiles do; the contrast palettes use plain ink (§0.3). */
.foot-family b { grid-row: 1; justify-self: center; font-size: var(--t-1); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: color-mix(in srgb, var(--hue, var(--accent-ink)) 55%, #000); }
[data-theme="dark"] .foot-family b { color: color-mix(in srgb, var(--hue, var(--accent-ink)) 50%, #fff); }
@media (prefers-color-scheme: dark) { :root:not([data-theme]) .foot-family b { color: color-mix(in srgb, var(--hue, var(--accent-ink)) 50%, #fff); } }
[data-theme^="contrast"] .foot-family b { color: var(--ink); }
.foot-family span { grid-row: 2; margin-top: 10px; font-size: var(--t2); font-weight: 600; line-height: 1.2; letter-spacing: -.02em; color: var(--ink); text-wrap: balance; }
.foot-family svg { grid-row: 3; align-self: center; justify-self: center; width: clamp(96px, 11vw, 100px); height: auto; aspect-ratio: 1; margin: 24px 0; }
.foot-family small { grid-row: 4; justify-self: center; display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: var(--rule-w) solid var(--rule); border-radius: var(--radius-pill); font-size: var(--t-1); font-weight: 500; line-height: 1.2; color: var(--ink-2); transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease); }
/* Hover and keyboard focus, never an underline: the tile lifts and its near-transparent wash deepens, an accent line
   draws across its top edge, and the domain pill fills while its arrow nudges forward. The accent is
   the BRAND'S colour — each tile carries `--hue` inline (data/marks.py HUES, from the kit) — with the
   site accent as the fallback. The focus ring is an outline, outside the box, so it is never clipped. */
.foot-family a:hover, .foot-family a:focus-visible { color: var(--ink-2); text-decoration: none; transform: translateY(-4px); background: rgba(1, 1, 1, .031); border-color: rgba(1, 1, 1, .031); }
.foot-family a::before { content: ""; position: absolute; inset-block-start: 0; inset-inline: 0; height: 3px; background: var(--hue, var(--accent)); transform: scaleX(0); transform-origin: 0 50%; transition: transform var(--dur) var(--ease); pointer-events: none; }
.foot-family small::after { content: ""; display: inline-block; flex: none; width: .9em; height: .9em; background: currentColor; -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat; transition: transform var(--dur) var(--ease); }
.foot-family a:hover::before, .foot-family a:focus-visible::before { transform: scaleX(1); }
/* The pill fills with the brand colour and its text turns white and bold on every tile — one
   consistent hover, by decision 2026-09-23 (it used to flip to black on light hues, which read as
   some tiles not reacting). Plain #fff, not --paper, so the dark theme stays white too. */
.foot-family a:hover small, .foot-family a:focus-visible small { background: var(--hue, var(--accent-ink)); border-color: var(--hue, var(--accent-ink)); color: #fff; font-weight: 700; }
.foot-family a:hover small::after, .foot-family a:focus-visible small::after { transform: translateX(3px); }
/* The contrast palettes keep one high-contrast accent, so a brand colour gives way to it there and the
   background mark (a tint) is dropped. `!important` because --hue arrives as an inline style. */
[data-theme="contrast"] .foot-family a, [data-theme="contrast-dark"] .foot-family a { --hue: var(--accent-ink) !important; }
[data-theme="contrast"] .foot-family .shelf-bg, [data-theme="contrast-dark"] .foot-family .shelf-bg { display: none; }
@media (prefers-contrast: more) {
  :root:not([data-theme]) .foot-family a { --hue: var(--accent-ink) !important; }
  :root:not([data-theme]) .foot-family .shelf-bg { display: none; }
}
/* The tile's own mark again, large and faint, as its background, turned and pushed off the bottom
   corner. site.js adds it; every other child of the link is positioned, so they paint above it. */
.foot-family .shelf-bg { position: absolute; inset-block-end: -18%; inset-inline-end: -18%; width: 74%; height: auto; aspect-ratio: 1; margin: 0; opacity: .08; transform: rotate(-14deg); transition: transform .6s var(--ease), opacity var(--dur) var(--ease); pointer-events: none; }
.foot-family a > :not(.shelf-bg) { position: relative; }
.foot-family a:hover .shelf-bg, .foot-family a:focus-visible .shelf-bg { opacity: .14; transform: rotate(-6deg) scale(1.06); }
/* The two arrow buttons site.js adds, at the end of the shelf. The row is circular, so neither is
   ever disabled. The chevron is a mask, flipped for "previous". */
.shelf-nav { display: flex; justify-content: flex-end; gap: 12px; }
.shelf-nav button { display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: var(--rule-w) solid var(--rule); border-radius: var(--radius-pill); background: var(--field); color: var(--ink); cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
.shelf-nav button::before { content: ""; width: 18px; height: 18px; background: currentColor; -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 5l7 7-7 7'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 5l7 7-7 7'/%3E%3C/svg%3E") center / contain no-repeat; }
.shelf-nav .shelf-prev::before { transform: scaleX(-1); }
.shelf-nav button:hover { background: var(--ink); color: var(--paper); }
@media (max-width: 600px) {
  .foot-family ul { gap: 12px; }
  .foot-family a { min-height: 272px; }
}
.foot-legal { margin: 32px 0 0; padding-top: 20px; border-top: var(--rule-w) solid var(--rule); color: var(--ink-3); line-height: 1.7; }
.foot-line { display: flex; flex-wrap: wrap; gap: 8px 24px; justify-content: space-between; align-items: center; margin-top: 12px; color: var(--ink-3); }
.foot-line a { display: inline-block; padding: 8px 0; }
.totop { position: fixed; inset-inline-end: 20px; inset-block-end: 20px; width: 48px; height: 48px; border-radius: var(--radius-pill); background: var(--ink); color: var(--paper); display: grid; place-items: center; box-shadow: var(--shadow); opacity: 0; transform: translateY(12px); pointer-events: none; transition: opacity var(--dur), transform var(--dur) var(--ease); z-index: 40; }
.totop[data-show] { opacity: 1; transform: none; pointer-events: auto; }
.totop svg { width: 20px; height: 20px; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.totop:hover { background: var(--fill-hover); color: var(--on-fill); }

/* ------------------------------------------------------------------ components */
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: var(--radius-pill); font-size: var(--t-1); font-weight: 600; background: color-mix(in srgb, var(--accent) 9%, var(--paper)); color: var(--accent-ink); border: var(--rule-w) solid transparent; line-height: 1.3; white-space: nowrap; }
[data-theme="contrast"] .chip, [data-theme="contrast-dark"] .chip { border-color: var(--rule); }
.chips { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
ul.chips, ol.chips { list-style: none; margin: 0; padding: 0; }
/* A chip that is a button — the hero's example briefs. It FILLS the prompt and never submits (WCAG
   3.2.2), so it looks like a light choice rather than like a call to action: paper, a rule and the
   secondary ink, answering the pointer with the accent edge. The words may wrap, because a Polish
   "Gabinet stomatologiczny, Kraków" is wider than a 320 px phone. 44 px under a finger (2.5.8). */
button.chip { min-block-size: 36px; padding: 6px 14px; background: var(--paper-2); color: var(--ink-2); border-color: var(--rule); font-weight: 500; white-space: normal; text-align: start; cursor: pointer; transition: color var(--dur), border-color var(--dur), background var(--dur); }
button.chip:hover { color: var(--ink); border-color: var(--accent-ink); }
button.chip:active { background: var(--field); }
@media (pointer: coarse) { button.chip { min-block-size: 44px; } }
/* The quiet chip: "Another example" — a different kind of choice from the named briefs, so it drops
   the fill and takes a dashed edge. */
.chip-plain { background: transparent; color: var(--ink-2); border-color: var(--rule); border-style: dashed; }
button.chip-plain:hover { border-style: solid; }

.card { background: var(--paper-2); border: var(--rule-w) solid var(--rule); border-radius: var(--radius); padding: clamp(18px, 2.5vw, 28px); position: relative; }
a.card { color: inherit; text-decoration: none; display: block; transition: transform var(--dur) var(--ease), border-color var(--dur); }
a.card:hover { transform: translateY(-2px); border-color: var(--accent); }
.card h3 { font-size: var(--t1); margin: 0 0 6px; }
.card p { color: var(--ink-2); margin: 0; font-size: var(--t-1); line-height: 1.5; }
.card p + p { margin-block-start: .7em; }
.card .nav-ico { margin-bottom: 14px; }
.card-arrow::after { content: "→"; position: absolute; inset-inline-end: 20px; inset-block-start: 18px; color: var(--ink-3); font-size: var(--t1); transition: transform var(--dur) var(--ease); }
a.card-arrow:hover::after { transform: translateX(4px); color: var(--accent-ink); }

.note { padding: 14px 18px; border: var(--rule-w) solid var(--rule); border-inline-start: 4px solid var(--accent); border-radius: var(--radius-sm); background: var(--paper-2); font-size: var(--t-1); color: var(--ink-2); margin: 20px 0; }
.note b { color: var(--ink); }

/* A row of buttons written as a plain paragraph of links — which is how every page body writes its
   calls to action. Inline, two buttons that wrap touch each other; as a flex row they keep a gap in
   both directions. Only a paragraph holding nothing but buttons is changed. */
p:has(> .btn):not(:has(> :not(.btn, br))) { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }

/* ---- FAQ ---------------------------------------------------------------------------------------
   Native <details>, so it works with scripting off and the builder can lift the same markup into
   FAQPage JSON-LD. The marker is a masked plus that turns into a cross when open: no glyph from a
   font subset, nothing for a screen reader to read, and it takes the colour of its state. */
/* One column left-aligned left half the section empty on a wide screen, so from 900 px the answers
   sit in two columns across the full width. Each question carries both rules, so the two columns
   stay ruled evenly whichever one opens; `align-items: start` keeps an opened answer from
   stretching its neighbour. */
.faq { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: clamp(32px, 5vw, 64px); align-items: start; }
.faq details { border-block: var(--rule-w) solid var(--rule); margin-block-end: calc(-1 * var(--rule-w)); }
@media (min-width: 900px) { .faq { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* An odd count in two columns leaves a hole under the last question (§0.13): it takes the whole row. */
@media (min-width: 900px) { .faq > details:last-child:nth-child(odd) { grid-column: 1 / -1; } }
.faq summary { list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 20px;
  min-block-size: 56px; padding: 14px 0; font-weight: 600; font-size: var(--t0); line-height: 1.35; color: var(--ink); transition: color var(--dur); }
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--accent-ink); }
.faq summary::after { content: ""; flex: none; inline-size: 18px; block-size: 18px; background: var(--ink-3);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M12 4v16M4 12h16'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M12 4v16M4 12h16'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform var(--dur) var(--ease), background var(--dur); }
.faq details[open] > summary::after { transform: rotate(45deg); background: var(--accent-ink); }
.faq summary:focus-visible { outline-offset: 4px; }
.faq details > :not(summary) { margin: 0; padding: 0 0 18px; color: var(--ink-2); max-width: 68ch; }
.faq details > :not(summary) + :not(summary) { margin-block-start: -6px; }
@media (forced-colors: active) { .faq summary::after { forced-color-adjust: none; background: CanvasText; } }

/* ------------------------------------------------------------------ page sections */
.hero { padding: clamp(48px, 9vw, 120px) 0 clamp(40px, 6vw, 80px); position: relative; overflow: hidden; }
/* Set to the same scale as the rest of the family (castomAPI and the other landings): the hero
   headline is a headline, not a full-bleed display line, and the lead sits at body scale. The 14ch
   measure stays — this headline is three words longer than castomAPI's and 12ch would break it
   somewhere it does not want to break. */
.hero h1 { font-size: clamp(2.1rem, 5vw, 3.6rem); font-weight: 700; letter-spacing: -.03em; line-height: 1.02; margin: 0 0 20px; max-width: 14ch; }
.hero h1 em { font-style: normal; color: var(--accent-ink); }
.hero .lead { font-size: clamp(var(--t-1), 1.5vw, var(--t1)); margin-bottom: 28px; max-width: 46ch; }
.hero-ctas { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 20px; }
/* The one-line microcopy under the prompt (cost, account, data, PLAN §2.1) and the lane note. In the
   hero a .note is a plain line, not a boxed notice: it is read after the button, not before it. */
.hero .note { margin: 12px 0 0; padding: 0; border: 0; border-radius: 0; background: none; color: var(--ink-3); max-width: 60ch; }
.hero .note a { color: var(--ink-2); }
/* The lane note arrives late, from GET /v1/lanes on the first focus, so its line is reserved from the
   first paint: text dropping in under the button must not push anything down (CLS 0). */
.lane-note { min-block-size: 1.5em; min-block-size: 1lh; margin: 6px 0 0; font-size: var(--t-1); color: var(--ink-2); }
.hero .chips { margin: 4px 0 6px; }
/* The ask above the prompt. The h1 carries the promise, so this line carries the mechanism and has to
   read as an instruction to the reader, not as a second headline: same weight as the lead, a rule over
   it to separate promise from demo. */
.hero-ask-h {
  margin: 0 0 12px; max-width: 46ch; font-size: var(--t0); font-weight: 560; color: var(--ink);
  border-block-start: 2px solid var(--ink); padding-block-start: 14px;
}
.hero-ask-h #write-ask { color: var(--accent-ink); }

/* The prompt as the scaffold's one-line pill: an input and a button on one row. */
.hero-ask {
  position: relative; display: flex; gap: 8px; align-items: center; margin: 4px 0 18px; max-width: 480px;
  padding: 6px 6px 6px 18px; background: var(--paper-2); border: var(--rule-w) solid var(--rule);
  border-radius: var(--radius-pill); box-shadow: var(--shadow);
  transition: border-color var(--dur), box-shadow var(--dur);
}
.hero-ask:focus-within { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft), var(--shadow); }
.hero-ask input { flex: 1; min-width: 0; border: 0; background: transparent; color: var(--ink); font: inherit; font-size: var(--t-1); padding: 10px 0; outline: none; }
.hero-ask input::placeholder, .hero-ask textarea::placeholder { color: var(--ink-3); opacity: 1; }
.hero-ask .btn { flex: none; }
/* Hover, in the hero: white text on the brand fill, always — the base button's own :hover pairs a
   light fill with near-black text (#0B0C10), which is what reads as "black text" here. --brand-1
   stays a fixed dark green across light and dark (only the contrast themes redefine it), so this
   reads correctly without a dark-mode branch; contrast themes get their own max-contrast pairing. */
.hero-ctas .btn:hover, .hero-ask .btn:hover { background: var(--brand-1); color: #fff; border-color: var(--brand-1); }
[data-theme^="contrast"] .hero-ctas .btn:hover, [data-theme^="contrast"] .hero-ask .btn:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); outline: 3px solid var(--accent-ink); outline-offset: 2px; }
.btn .btn-ico { transition: transform var(--dur) var(--ease); }
.btn:hover .btn-ico { transform: translateX(3px); }
@media (max-width: 520px) {
  .hero-ask { flex-wrap: wrap; max-width: none; border-radius: var(--radius); padding: 10px; }
  .hero-ask .btn { width: 100%; }
}
.hero-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: clamp(24px, 5vw, 64px); align-items: center; }
@media (max-width: 960px) { .hero-grid { grid-template-columns: 1fr; } .hero h1 { max-width: none; } }
/* The home hero's photograph when it sits in an ordinary .hero-grid. .shot is built as a panel
   backdrop — position absolute, inset 0, with its own darkening gradient — so dropped into the grid
   it would cover the column beside it. Inside the grid it becomes an ordinary framed block. */
.hero-grid > .shot { position: static; inset: auto; margin: 0; border-radius: var(--radius); overflow: hidden; aspect-ratio: 4 / 5; }
.hero-grid > .shot::after { display: none; }
.shot > picture { display: contents; }
@media (max-width: 960px) { .hero-grid > .shot { aspect-ratio: 16 / 9; order: 2; } }
/* On a phone the thing worth landing on is the prompt (Maciej's rule; PLAN §2.2): the lead and the
   second row of buttons step aside, and the photograph — which the builder already swaps for a
   1-pixel image below 720 px, so it is never downloaded — is not given an empty box either. */
@media (max-width: 719.98px) { .hero .shot[data-hero], .hero .hero-peek { display: none; } }
@media (max-width: 640px) {
  .hero { padding-block: 20px 28px; }
  .hero .lead { display: none; }
  .hero-grid .hero-ctas { display: none; }
  .hero-ask .hero-ctas { display: flex; }
}

/* The hero board: a departure board for towns, over the same halo glow the mark used to sit in.
   It is a glimpse of what the lookup below already answers — every count in it was read off the
   live demo at 3 km (data/pages.py BOARD says when), so the hero cannot promise a number the
   lookup would not repeat. Rows roll upward the way a station board pages through departures,
   which is also why the count is digits alone: the column heading says what they are once, and a
   board of numbers needs no sentence per line to be read.

   The track holds the list twice and travels exactly half its height, so the loop has no seam —
   the same mechanism castomapi.com's hero request stream uses. It pauses while the pointer or the
   keyboard is anywhere in the hero, so a reader can hold a row still and read it. */
.hero-peek { position: relative; max-width: 520px; margin-inline: auto; width: 100%; }
.hero-peek-halo { position: absolute; inset: -8% -6%; width: auto; height: auto; }
.hero-peek-halo .halo { animation: orb-halo 9s ease-in-out infinite; transform-origin: 50% 50%; }
.board {
  position: relative; padding: 14px 6px 10px; border-radius: calc(var(--radius) + 6px);
  background: color-mix(in srgb, var(--paper-2) 88%, transparent);
  border: var(--rule-w) solid var(--rule); box-shadow: var(--shadow);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
/* The heading is the board's only words, and the only part of it a translation has to carry. */
.board-head {
  display: flex; justify-content: space-between; gap: 12px; margin: 0 0 8px; padding: 0 14px 10px;
  border-bottom: var(--rule-w) solid var(--rule);
  font-size: .68rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3);
}
/* The mask fades the first and last row rather than cutting them: a row half-arrived is what makes
   the list read as still rolling instead of as a short list that happens to move. */
.board-roll {
  height: 336px; overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 14%, #000 86%, transparent);
  mask-image: linear-gradient(180deg, transparent, #000 14%, #000 86%, transparent);
}
.board-track { list-style: none; margin: 0; padding: 0; animation: board-roll 34s linear infinite; }
.hero:hover .board-track, .hero:focus-within .board-track { animation-play-state: paused; }
.board-row {
  display: flex; align-items: center; gap: 12px; min-width: 0;
  padding: 9px 14px; border-radius: var(--radius-sm);
}
/* Every other row tinted, as a real board rules its lines: it keeps a long column of numbers
   trackable across the gap between the town and its count. */
.board-row:nth-child(even) { background: color-mix(in srgb, var(--ink) 4%, transparent); }
/* A country-code badge, not a flag: iKonekt's try-ideas rail marks each town with its country this
   way, so the hero uses a convention the visitor meets again a few scrolls down. */
.board-cc {
  flex: none; min-width: 30px; padding: 3px 7px; border-radius: var(--radius-pill); text-align: center;
  font-weight: 700; font-size: .66rem; letter-spacing: .04em; color: #fff; background: hsl(84 62% 32%);
}
.board-town { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--t-1); }
/* Tabular figures: without them the digits shift width row to row and the column stops being one. */
.board-n {
  margin-inline-start: auto; font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
  font-weight: 700; font-size: var(--t-1); color: var(--accent-ink); white-space: nowrap;
}
@keyframes board-roll { from { transform: translateY(0); } to { transform: translateY(-50%); } }
/* Stopped, the board is a plain readable list of the first rows — which is the whole content. */
@media (prefers-reduced-motion: reduce) { .board-track { animation: none; } }
@media (max-width: 960px) { .hero-peek { max-width: 420px; } .board-roll { height: 214px; } }
@keyframes orb-halo { 0%, 100% { opacity: .55; transform: scale(1); } 50% { opacity: .85; transform: scale(1.04); } }

.reasons { counter-reset: reason; }
.reason { display: grid; grid-template-columns: 5rem 1fr; gap: 16px clamp(16px, 3vw, 40px); padding: clamp(32px, 5vw, 56px) 0; border-top: var(--rule-w) solid var(--rule); }
.reason:last-child { border-bottom: var(--rule-w) solid var(--rule); }
.reason-n { font-size: var(--t1); font-weight: 700; color: var(--accent-ink); font-variant-numeric: tabular-nums; letter-spacing: -.02em; line-height: 1.2; padding-top: .35em; }
.reason-n::before { counter-increment: reason; content: counter(reason, decimal-leading-zero); }
.reason-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(20px, 4vw, 48px); align-items: start; }
.reason h2 { font-size: clamp(var(--t2), 3vw, var(--t4)); margin-bottom: 12px; }
.reason p { color: var(--ink-2); max-width: 52ch; }
.reason .more { font-weight: 600; }
@media (max-width: 960px) { .reason-body { grid-template-columns: 1fr; } }
@media (max-width: 600px) { .reason { grid-template-columns: 1fr; gap: 6px; } }

/* The thread card: what one contact looks like once it has landed. Not a screenshot — the same
   markup the console draws, so it cannot go stale against a design that has moved on. */
.thread { border: var(--rule-w) solid var(--rule); border-radius: var(--radius); background: var(--paper-2); overflow: hidden; font-size: var(--t-1); min-width: 0; }
.thread-h { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; padding: 12px 14px; background: var(--field); border-bottom: var(--rule-w) solid var(--rule); }
.thread-h b { font-weight: 600; font-size: var(--t0); }
.thread-h .chip { margin-inline-start: auto; }
.thread ul { list-style: none; margin: 0; padding: 0; }
/* Every cell is placed explicitly. Left to auto-placement the avatar's row span pushes the message
   into the 2rem column, which wraps it one word per line — and it looks like a font bug, not a
   layout one. The third column is the timestamp's; a row without one simply leaves it empty. */
.thread li { display: grid; grid-template-columns: 2rem auto minmax(0, 1fr); column-gap: 12px; row-gap: 2px; padding: 12px 14px; border-top: var(--rule-w) solid var(--rule); }
.thread li:first-child { border-top: 0; }
.thread li i { grid-column: 1; grid-row: 1 / span 2; width: 2rem; height: 2rem; border-radius: 50%; background: var(--accent-soft); color: var(--accent-ink); display: grid; place-items: center; font-style: normal; font-weight: 700; font-size: .75rem; letter-spacing: .02em; }
[data-theme="contrast"] .thread li i, [data-theme="contrast-dark"] .thread li i { border: var(--rule-w) solid var(--rule); }
.thread li b { grid-column: 2; grid-row: 1; font-weight: 600; color: var(--ink); }
.thread li time { grid-column: 3; grid-row: 1; justify-self: start; align-self: baseline; color: var(--ink-3); font-size: .8rem; }
.thread li span { grid-column: 2 / -1; grid-row: 2; color: var(--ink-2); line-height: 1.45; }
.thread li.is-note { background: var(--accent-soft); }
[data-theme="contrast"] .thread li.is-note, [data-theme="contrast-dark"] .thread li.is-note { background: transparent; box-shadow: inset 3px 0 0 var(--ink); }
.thread-f { padding: 10px 14px; border-top: var(--rule-w) solid var(--rule); color: var(--ink-3); display: flex; flex-wrap: wrap; gap: 6px 14px; }

/* The pipeline: the stages a contact walks through, as one row that scrolls on a phone. */
.pipe { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(11rem, 1fr); gap: 0; overflow-x: auto; border: var(--rule-w) solid var(--rule); border-radius: var(--radius); background: var(--paper-2); }
.pipe > div { padding: 18px 20px; border-inline-start: var(--rule-w) solid var(--rule); min-width: 0; }
.pipe > div:first-child { border-inline-start: 0; }
.pipe b { display: block; font-weight: 600; color: var(--ink); }
.pipe span { display: block; color: var(--ink-3); font-size: var(--t-1); margin-top: 4px; line-height: 1.45; }
.pipe em { font-style: normal; display: block; width: 26px; height: 26px; border-radius: 50%; background: var(--brand-1); color: #fff; display: grid; place-items: center; font-size: .8rem; font-weight: 700; margin-bottom: 10px; }
[data-theme="contrast"] .pipe em, [data-theme="contrast-dark"] .pipe em { background: var(--ink); color: var(--paper); }

/* ---- steps: `.steps` > `.step` (+ `.step-ico`, a 132 px watermark) ------------------------------
   `.steps` lays its own steps out, so a page need not add grid classes; a .grid holding .step cards
   works as well and keeps its own columns. */
.steps { counter-reset: step; list-style: none; margin: 0; padding: 0; display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }
.grid { counter-reset: step; }
.step { position: relative; display: grid; grid-template-columns: 2.6rem 1fr; gap: 14px; align-items: start; overflow: hidden; }
.step::before { counter-increment: step; content: counter(step); width: 2.6rem; height: 2.6rem; border-radius: 50%; background: var(--ink); color: var(--paper); display: grid; place-items: center; font-weight: 700; position: relative; z-index: 1; }
.step h3 { font-size: var(--t1); margin: 4px 0 4px; }
.step p { margin: 0; color: var(--ink-2); }
/* The step already carries its own numbered marker; a second icon inline with the heading fought it
   for the same job. Moved to a large, faint watermark instead — a brand touch, not a second label. */
.step-bg { position: absolute; inset-block-end: -22%; inset-inline-end: -12%; width: 52%; height: auto; aspect-ratio: 1; color: var(--ink); opacity: .07; pointer-events: none; --ci-stroke: 1.4; }
[data-theme^="contrast"] .step-bg { display: none; }

/* ---- the closing band: `.cta-band` ---------------------------------------------------------------
   One sentence and one action, centred. Behind it, bleeding off the corner, the castomo ring in thin
   line art — the open C's two arcs, never closed (DESIGN §0.3) — at 8 %, the one mark of the brand
   the page ends on. Hidden on phones and in the contrast themes, and never printed. */
.cta-band { position: relative; overflow: clip; isolation: isolate; text-align: center; padding: clamp(56px, 9vw, 112px) 0; border-block-start: var(--rule-w) solid var(--rule); }
.cta-band h2 { font-size: clamp(var(--t3), 4.5vw, var(--t5)); max-width: 20ch; margin-inline: auto; }
.cta-band p { max-width: 56ch; margin-inline: auto; color: var(--ink-2); text-wrap: pretty; }
.cta-band p:has(> .btn) { justify-content: center; margin-block-start: 28px; }
/* The section-head itself keeps a 68ch max-width (see above); without centring that box too, a
   short centred heading reads as left-shifted because its own box never moved off the left edge. */
.cta-band .section-head { margin-inline: auto; }
.cta-band .lead { margin-inline: auto; }
.cta-band .hero-ctas { justify-content: center; }
.cta-band::before { content: ""; position: absolute; z-index: -1; inline-size: 560px; block-size: 560px; inset-block-end: -230px; inset-inline-end: -150px;
  background: var(--accent-ink); opacity: .08; rotate: -18deg; pointer-events: none;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='.09'%3E%3Cpath d='M4.628 11.355A7.4 7.4 0 0 1 16.76 6.33M16.76 17.67A7.4 7.4 0 0 1 4.628 12.645'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='.09'%3E%3Cpath d='M4.628 11.355A7.4 7.4 0 0 1 16.76 6.33M16.76 17.67A7.4 7.4 0 0 1 4.628 12.645'/%3E%3C/svg%3E") center / contain no-repeat; }
[data-theme^="contrast"] .cta-band::before { display: none; }
@media (max-width: 719.98px), (prefers-contrast: more), (forced-colors: active) { .cta-band::before { display: none; } }

/* ---- page heads -----------------------------------------------------------------------------------
   Every inner page opens on a .section whose .section-head holds the kicker, the h1 and the lead. It
   gets the page head's measure: an h1 capped at --t5 (DESIGN §3, long Polish titles run past forty
   characters), a little more air above than a section between two others, and a lead that ends the
   head rather than floating into what follows. */
main > .section:first-child { padding-block-start: clamp(40px, 6vw, 80px); }
.section-head h1 { font-size: clamp(var(--t4), 5.5vw, var(--t5)); letter-spacing: -.03em; line-height: 1.06; max-width: 22ch; margin-block: 0 .42em; }
.section-head .lead:last-child { margin-block-end: 0; }
.section-head .objection:first-child { margin-block-start: 0; }
/* A page head with a drawing beside it (tools/compose_art.py): the words keep their measure on the
   left, the drawing takes the rest, and the two share the head's margin below. Under 860px the
   drawing drops beneath the lead, the same break the cloned slides use. */
.head-art { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 440px); gap: clamp(28px, 6vw, 96px); align-items: center; margin-block-end: clamp(24px, 4vw, 40px); }
.head-art .section-head { margin-block-end: 0; }
@media (max-width: 860px) { .head-art { grid-template-columns: 1fr; } }
/* The internal shelf of every drawing (/drawings/): a name under each, in the code font, because
   the name is what a page's art tag uses. */
.art-grid { margin-block-end: clamp(32px, 5vw, 56px); }
.art-grid .lg-art { padding: clamp(12px, 2vw, 24px); border: var(--rule-w) solid var(--rule); border-radius: var(--radius); }
.art-grid .lg-ink { max-block-size: 300px; }
.art-grid figcaption { margin-block-start: 12px; font-size: var(--t-1); color: var(--ink-2); }
.art-grid figcaption b { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; color: var(--ink); }

/* ------------------------------------------------------------------ the showpiece panel
   A full-viewport photograph with the words on top — dash-b.com's shape, which is where this was
   borrowed from. The panel is always dark whatever theme the reader has chosen: the ground is a
   photograph, so the text colour cannot follow the palette without failing on half the pictures. */
.panel { position: relative; display: grid; align-content: end; min-block-size: 100svh; padding-block: clamp(96px, 14vh, 168px) clamp(52px, 9vh, 104px); overflow: hidden; isolation: isolate; color: #fff; }
.panel.p-mid { align-content: center; }
/* Only the stacking context here: .wrap already carries the page's gutter and max width, and
   setting a width on it again would run the headline into the left edge of the screen. */
.panel > .wrap { position: relative; z-index: 2; }

/* A real <img>, not a background-image: only an element gets to pick a width out of srcset, to be
   deferred with loading="lazy" and to be decoded off the main thread. */
.shot { position: absolute; inset: 0; z-index: 0; margin: 0; }
.shot img { inline-size: 100%; block-size: 100%; object-fit: cover; object-position: var(--focus, 50% 45%); background: var(--lqip) center / cover no-repeat; }
/* Two gradients, because the words need contrast and the photograph does not. The darkening is
   heavy where the text sits and gone by the far edge, so the picture is still a picture. */
.shot::after { content: ""; position: absolute; inset: 0; z-index: 1; --edge: linear-gradient(to right, rgba(6, 7, 10, .70), rgba(6, 7, 10, 0) 66%); background: linear-gradient(to top, rgba(6, 7, 10, .94) 0%, rgba(6, 7, 10, .78) 24%, rgba(6, 7, 10, .32) 58%, rgba(6, 7, 10, .46) 100%), var(--edge); }
/* High contrast asks for certainty, not atmosphere: the photograph stays, nearly blacked out, so
   the words measure against something close to solid ink instead of against a sunlit wall. */
[data-theme="contrast"] .shot::after, [data-theme="contrast-dark"] .shot::after { background: rgba(0, 0, 0, .88); }
[data-theme="contrast"] .panel .objection, [data-theme="contrast-dark"] .panel .objection { color: #fff; }

/* The sentence the visitor already thought, set as the thing being answered. It reads as a
   quotation rather than as a label — a label announces a section, this one starts an argument. */
.panel .objection { font-size: clamp(.95rem, 1.7vw, 1.2rem); font-weight: 500; color: var(--brand-3); letter-spacing: .005em; margin: 0; }
.panel-say { max-inline-size: 46rem; }
.panel :is(h1, h2) { margin-block-start: .42em; font-size: clamp(2.2rem, 6.4vw, 4.9rem); line-height: 1.02; letter-spacing: -.035em; font-weight: 700; color: #fff; text-wrap: balance; }
.panel-say p:not(.objection) { margin-block-start: 1.05em; max-inline-size: 36ch; font-size: clamp(1.04rem, 1.35vw, 1.22rem); color: rgba(255, 255, 255, .86); text-wrap: pretty; }
/* The same short line that stretches as every text link (§0.11), drawn on a dark panel. */
.panel-say .more { display: inline-flex; align-items: center; gap: .5em; margin-block-start: 1.5em; font-weight: 600; color: #fff; text-decoration: none; padding-block-end: 3px;
  background: linear-gradient(currentColor, currentColor) 0 100% / var(--u-rest) var(--u-thick) no-repeat; transition: background-size .6s var(--u-spring), gap var(--dur); }
.panel-say .more:is(:hover, :focus-visible) { background-size: 100% var(--u-thick); gap: .8em; }
.panel .hero-ctas { margin-block-start: 1.6em; }
.panel .btn-primary { background: var(--brand-2); color: #0B0C10; }
.panel .btn-primary:hover { background: var(--brand-3); color: #0B0C10; }
.panel .btn-ghost { border-color: rgba(255, 255, 255, .34); color: #fff; background: rgba(255, 255, 255, .07); }
.panel .btn-ghost:hover { border-color: #fff; background: rgba(255, 255, 255, .14); }
.panel :focus-visible { outline-color: #fff; }
@media (max-height: 720px) { .panel { min-block-size: auto; padding-block: clamp(88px, 12vh, 120px) clamp(44px, 8vh, 72px); } }

/* A .p-mid panel needs its own scrim. The showpiece gradient is weighted to the bottom, because that
   is where a .panel's words normally sit — by mid-height it has faded to about a third. Measured over
   the photographs these pages carry, white type in the middle of a bright one fell to 2.6:1. So a
   centred panel gets a band across its middle instead of a foot at its bottom, and a slightly longer
   reach on the edge layer, because the copy is left-aligned. */
.panel.p-mid .shot::after {
  --edge: linear-gradient(to right, rgba(6, 7, 10, .62), rgba(6, 7, 10, 0) 72%);
  background: linear-gradient(to top,
    rgba(6, 7, 10, .50) 0%,
    rgba(6, 7, 10, .74) 30%,
    rgba(6, 7, 10, .78) 58%,
    rgba(6, 7, 10, .44) 100%), var(--edge);
}

/* One motion idea, done once: as a panel arrives its photograph relaxes out of a slight overscale.
   No parallax — a scroll-linked transform on a full-bleed image is the cheapest way to make a fast
   page feel slow. Both switches are honoured, the reader's and the system's, and the resting state
   is the un-scaled one so a browser that never adds .in still shows the picture correctly. */
@media (prefers-reduced-motion: no-preference) {
  .js .panel .shot img { transform: scale(1.06); transition: transform 1.4s cubic-bezier(.16, 1, .3, 1); }
  .js .panel.in .shot img { transform: none; }
}
[data-motion="reduce"] .panel .shot img { transform: none !important; }

/* The photographs come from media.castomapi.com under licences that all require attribution, so a
   page with a photograph ends with a generated credit line. */
.credits { font-size: var(--t-1); color: var(--ink-3); max-inline-size: 80ch; }
.credits a { color: inherit; }
/* The credit line is the last thing on every page, so nothing can scroll it out from under the
   floating back-to-top and chat buttons (--castomo-size, 20px off the edge): it sat under back-to-top
   at every width (2026-09-24, found by tools/site-checks `float`). Its bottom padding clears them. */
.section-credits { padding-block: 18px calc(var(--castomo-size, 56px) + 36px); }
.foot-credits { margin: 24px 0 0; color: var(--ink-3); font-size: var(--t-1); }
.foot-credits a { color: var(--ink-3); }
.foot-credits a:hover { color: var(--ink); }

/* These sit INSIDE their heading and render as blocks (the reset makes every expanded <svg> one),
   so the icon takes its own line above the words. That is the intended look — but at 30px with no
   bottom margin the heading sat right under it. Bigger, and with real space beneath.
   vertical-align and margin-inline-end below are inert while the icon is a block; they are kept for
   any placement that is still inline. */
.step-ico, .reason-ico, .mark-ico { width: 40px; height: 40px; margin-bottom: 12px; color: var(--accent-ink); vertical-align: -8px; margin-inline-end: 8px; }
[data-theme^="contrast"] .step-ico, [data-theme^="contrast"] .reason-ico, [data-theme^="contrast"] .mark-ico { color: var(--ink); }

/* ------------------------------------------------------------------ castomo-icons placements */
/* Big doodles are purely decorative background accents, positioned per instance with an inline
   style (the CSP's style-src-attr already allows it, and it beats dozens of one-off classes for
   one corner flourish each). `.has-deco` promotes the host so the accent clips instead of ever
   forcing horizontal scroll; `.wrap` carries z-index:1 above, so a `.deco` — z-index:0, no pointer
   events — can never sit on top of real text even when it overlaps the box. Colour comes from the
   same ramp token as every other icon on the site, with the high-contrast themes' own override. */
.has-deco { position: relative; overflow: hidden; }
.deco { position: absolute; pointer-events: none; color: var(--accent-ink); z-index: 0; opacity: .9; }
[data-theme^="contrast"] .deco { color: var(--ink); opacity: .5; }
/* Placeholder attributes are limited to class + a few named ones (see tools/castomo_icons.py) — a
   raw `style` on the tag is not carried through to the built SVG — so position, size and tilt are
   each a small combinable class instead of one-off inline coordinates. */
.deco-tr { inset-inline-end: 3%; inset-block-start: 6%; }
.deco-tl { inset-inline-start: 3%; inset-block-start: 6%; }
.deco-br { inset-inline-end: 3%; inset-block-end: 6%; }
.deco-bl { inset-inline-start: 3%; inset-block-end: 6%; }
.deco-sm { --ci-big: 100px; }
.deco-md { --ci-big: 125px; }
.deco-lg { --ci-big: 150px; }
.deco-xl { --ci-big: 175px; }
.deco-rot-a { transform: rotate(-6deg); }
.deco-rot-b { transform: rotate(-3deg); }
.deco-rot-c { transform: rotate(3deg); }
.deco-rot-d { transform: rotate(6deg); }
@media (max-width: 720px) { .deco { display: none; } }

/* ------------------------------------------------------------------ print */
@media print {
  .bar, .foot, .totop, .player, .skip, .deco { display: none !important; }
  body { background: #fff; color: #000; }
  a { color: #000; }
  .card, .thread { break-inside: avoid; }
}

/* ------------------------------------------------------------------ the written pages, continued
   Legal and explanatory pages are long prose with numbered h2s and a contents list. The contents list
   is a <nav class="prose"> — boxed, in two columns where there is room. A heading reached from it is
   kept clear of the sticky bar and the section row, and marked while it is the :target, with an
   underline rather than a bar, so nothing moves when it is. */
nav.prose { padding: clamp(18px, 2.4vw, 26px) clamp(20px, 2.8vw, 30px); border: var(--rule-w) solid var(--rule); border-radius: var(--radius); background: var(--paper-2); }
.wrap > nav.prose { max-width: 56rem; }
nav.prose h2 { font-size: var(--t-1); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); margin: 0 0 12px; }
nav.prose ol, nav.prose ul { columns: 2 17rem; column-gap: 36px; margin: 0; padding-inline-start: 1.5em; color: var(--ink-3); }
nav.prose li { break-inside: avoid; }
nav.prose li + li { margin-block-start: .4em; }
.prose :is(h2, h3)[id] { scroll-margin-block-start: calc(var(--bar-h) + 64px); }
.prose :is(h2, h3):target { text-decoration: underline 3px var(--accent); text-underline-offset: .22em; }
.prose p, .prose li { text-wrap: pretty; }
.prose code { padding: 1px 5px; border-radius: 4px; background: var(--field); color: var(--ink); font-size: .88em; }
:lang(pl) .prose { hyphens: auto; }

/* §0.2 icons on the script-built and video buttons: the section chip's chevron, the story steps'
   play mark and the frames' sound mark. Each moves on hover or focus and holds still under reduced
   motion, like every other button icon. */
.secnav-chip { gap: 6px; }
.secnav-ico { width: 1em; height: 1em; flex: none; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; transition: translate .3s var(--u-spring), rotate .35s var(--ease); }
.secnav-chip:is(:hover, :focus-visible) .secnav-ico { translate: 0 2px; }
.secnav-chip[aria-expanded="true"] .secnav-ico { rotate: 180deg; }
.vmute { display: inline-flex; align-items: center; gap: 8px; }
.story-go { display: inline-flex; align-items: center; gap: 8px; }
@media (prefers-reduced-motion: reduce) { .secnav-ico { transition: none; } }
[data-motion="reduce"] .secnav-ico { transition: none; }

/* Static sub menu = the castomo.com/demos filter pills, copied from its computed styles (Maciej,
   2026-09-24): 12px/900 caps-free label, 1px tracking, 10px 30px padding, 22px pill, soft grey rest,
   brand gradient for the current page (iKonekt: the flat lime fill with ink on it). The grey is mixed from the page's own ink and paper, so it
   follows every theme; high contrast swaps the gradient for solid ink, because white on the light
   end of the gradient is under 4.5:1 (§0.3). */
:root {
  --pill-bg: color-mix(in srgb, var(--ink) 4.5%, var(--paper));
  --pill-bg-hover: color-mix(in srgb, var(--ink) 9%, var(--paper));
  --pill-on: var(--accent);
  --pill-on-ink: var(--on-fill);
}
.subnav ul { gap: 8px; padding-block: 10px; }
.subnav li > a:not([class]) {
  padding: 10px 30px; border: 1px solid var(--paper); border-radius: 22px;
  background: var(--pill-bg); color: var(--ink);
  font-size: 12px; font-weight: 900; line-height: 1.3; letter-spacing: 1px;
  transition: background-color .25s ease-in-out, color .25s ease-in-out;
}
.subnav li > a:not([class]):hover { background-color: var(--pill-bg-hover); color: var(--ink); }
.subnav li > a:not([class])[aria-current="page"] { background: var(--pill-on); border-color: transparent; color: var(--pill-on-ink); font-weight: 900; transition-duration: .7s; }
.subnav li > a:not([class]):focus-visible { border-radius: 22px; outline-offset: 2px; }
[data-theme^="contrast"] .subnav li > a:not([class])[aria-current="page"] { background: var(--ink); color: var(--paper); }
@media (prefers-contrast: more) { :root:not([data-theme]) .subnav li > a:not([class])[aria-current="page"] { background: var(--ink); color: var(--paper); } }
@media (forced-colors: active) { .subnav li > a:not([class])[aria-current="page"] { forced-color-adjust: none; background: Highlight; color: HighlightText; } }

/* Breadcrumbs: smaller, with air above and below, and no resting underline; the line only grows on
   hover and focus (Maciej, 2026-09-24). */
.crumbs { font-size: 13px; }
.crumbs-bar { padding-block: 18px 6px; }
.crumbs a:not([class]) { background-size: 0% var(--u-thick); }
.crumbs a:not([class]):is(:hover, :focus-visible, :active) { background-size: 100% var(--u-thick); }

/* Header polish (Maciej, 2026-09-24, guidelines §0.21).
   The logo is the icon alone: no plate, no hairline frame. The rect stays in the markup (it sets the
   viewBox's ground for the kit's cut) but paints nothing. */
.brand svg > rect, :is([data-theme="dark"], [data-theme="contrast-dark"]) .brand svg > rect { fill: none; stroke: none; }
@media (prefers-color-scheme: dark) { :root:not([data-theme]) .brand svg > rect { fill: none; stroke: none; } }

/* Idle logo: site.js sets data-idle on the header mark every 5–7 s, picking one of these cuts at
   random (never the same twice running); each plays once and the mark rests until the next. The
   global reduced-motion rules remove them, and site.js stops scheduling under reduced motion. */
.bar .brand svg[data-idle] > g > * { animation-duration: .9s; animation-timing-function: cubic-bezier(.2, .7, .3, 1); animation-fill-mode: both; }
.bar .brand svg[data-idle="pop"] > g > * { animation-name: mk-pop; }
.bar .brand svg[data-idle="spin"] > g > * { animation-name: mk-spin; }
.bar .brand svg[data-idle="bounce"] > g > * { animation-name: mk-bounce; }
.bar .brand svg[data-idle="wiggle"] > g > * { animation-name: mk-wiggle; }
.bar .brand svg[data-idle="flip"] > g > * { animation-name: mk-flip; }
@keyframes mk-spin { 0% { transform: rotate(0); } 80% { transform: rotate(372deg); } 100% { transform: rotate(360deg); } }
@keyframes mk-bounce { 0%, 100% { transform: translateY(0); } 30% { transform: translateY(-18%); } 55% { transform: translateY(0) scale(1.06, .94); } 75% { transform: translateY(-6%); } }
@keyframes mk-wiggle { 0%, 100% { transform: rotate(0); } 20% { transform: rotate(-12deg); } 40% { transform: rotate(10deg); } 60% { transform: rotate(-6deg); } 80% { transform: rotate(3deg); } }
@keyframes mk-flip { 0% { transform: perspective(80px) rotateY(0); } 100% { transform: perspective(80px) rotateY(360deg); } }

/* Main menu: the hovered or open item is the brand accent with white text. In high-contrast dark the
   accent is a light orange, so its text turns dark to keep 7:1. The section-you-are-in mark keeps
   its own colour until hovered. The menu row is nudged 4px down so its baseline meets the wordmark's
   (menu 36px vs wordmark 40px, measured at 1280px); padding, not transform, so dropdowns follow. */
/* Lime holds ink, so the accent's text is --on-fill (ink; white only in high-contrast light). */
:root, [data-theme] { --on-accent: var(--on-fill); }
.nav-top:hover, .nav-group > summary:hover, .nav-group[open] > summary,
.nav-group[data-current] > summary:hover, .nav-group[data-current][open] > summary { color: var(--on-accent); background: var(--accent); }
.nav-group > summary:hover svg, .nav-group[open] > summary svg { color: inherit; }
.nav-links { padding-block-start: 8px; }
@media (max-width: 1200px) { .nav-links { padding-block-start: 0; } }

/* Static sub menu: the current page is the flat accent (no gradient); every pill is 5px 15px with
   0.3px tracking. */
.subnav li > a:not([class]) { padding: 5px 15px; letter-spacing: .3px; }
.subnav li > a:not([class])[aria-current="page"] { background: var(--accent); color: var(--on-accent); }

.crumbs { font-size: 11px; }

/* ALTERNATIVE shelf style, opt-in: add class "shelf-alt" to a .foot-family section. Everything down to
   the end of this block is scoped to it; the default shelf above is the original. Shelf tiles, "mark is the hero" (Maciej, 2026-09-24; tile-lab #3 without the drawings, so the same
   tile works on every ecosystem site whatever its illustration kit). Visuals only: the scrolling row,
   snap, arrows and markup above are untouched. A white card holds a tinted window (::after, the
   brand hue at 10%); the mark sits in it, large and frameless, with its faint twin (.shelf-bg) filling
   the window behind it; name, line and domain pill follow below. The window is 188px tall; the
   background mark is the window's own size and clipped to its corners, its paths scaled up inside
   the SVG's viewport (transform-box: view-box keeps a multi-part mark in one piece). */
.foot-family.shelf-alt a {
  isolation: isolate; grid-template-rows: 188px auto auto 1fr; justify-items: start; text-align: start;
  padding: 12px 12px 20px; background: var(--paper); border-color: var(--rule);
  transition: transform .35s cubic-bezier(.3, 1.45, .5, 1), box-shadow .35s var(--ease);
}
.foot-family.shelf-alt a:hover, .foot-family.shelf-alt a:focus-visible { background: var(--paper); border-color: var(--rule); box-shadow: 0 26px 50px -24px rgba(11, 12, 16, .45); }
.foot-family.shelf-alt a::after { content: ""; position: absolute; inset: 12px 12px auto; height: 188px; border-radius: calc(var(--radius) * 1.1); background: color-mix(in srgb, var(--hue, var(--accent)) 10%, var(--paper-2)); z-index: -1; pointer-events: none; }
.foot-family.shelf-alt a::before { z-index: 2; }
.foot-family.shelf-alt a > svg:not(.shelf-bg) {
  grid-row: 1; align-self: center; justify-self: center; width: 96px; margin: 0; z-index: 1;
  transition: transform .5s cubic-bezier(.3, 1.7, .5, 1);
}
.foot-family.shelf-alt a:hover > svg:not(.shelf-bg), .foot-family.shelf-alt a:focus-visible > svg:not(.shelf-bg) { transform: translateY(-10px) rotate(-6deg) scale(1.08); }
.foot-family.shelf-alt .shelf-bg {
  grid-area: auto; inset: 12px auto auto 12px; width: calc(100% - 24px); height: 188px; aspect-ratio: auto; overflow: hidden;
  border-radius: calc(var(--radius) * 1.1); opacity: .1; transform: none; z-index: 0;
}
.foot-family.shelf-alt .shelf-bg > * { transform-box: view-box; transform-origin: 50% 50%; transform: translate(26%, 20%) scale(1.9) rotate(-14deg); transition: transform .6s var(--ease); }
.foot-family.shelf-alt a:hover .shelf-bg, .foot-family.shelf-alt a:focus-visible .shelf-bg { opacity: .16; transform: none; }
.foot-family.shelf-alt a:hover .shelf-bg > *, .foot-family.shelf-alt a:focus-visible .shelf-bg > * { transform: translate(22%, 16%) scale(2) rotate(-6deg); }
.foot-family.shelf-alt b { grid-row: 2; justify-self: start; margin: 16px 6px 0; font-size: 20px; font-weight: 700; letter-spacing: -.02em; text-transform: none; }
.foot-family.shelf-alt span { grid-row: 3; margin: 4px 6px 0; font-size: var(--t0); font-weight: 400; line-height: 1.5; letter-spacing: 0; color: var(--ink-2); }
.foot-family.shelf-alt small { grid-row: 4; align-self: end; justify-self: start; margin: 16px 6px 0; }
@media (max-width: 600px) { .foot-family.shelf-alt a { min-height: 0; grid-template-rows: 160px auto auto 1fr; } .foot-family.shelf-alt a::after, .foot-family.shelf-alt .shelf-bg { height: 160px !important; } }
@media (prefers-reduced-motion: reduce) { .foot-family.shelf-alt a > svg:not(.shelf-bg), .foot-family.shelf-alt .shelf-bg > * { transition: none; } }
/* Round 2 (Maciej, 2026-09-24): white card, centred text and pill, 28px foot padding, and the mark
   pops OUT of the tile on hover. The scrolling row clips vertically too (overflow-x forces it), so
   the row gets 64px of headroom above the cards, handed back with a negative margin so the layout
   does not move. The card itself stops clipping; the faint mark clips itself (overflow: hidden). */
:root { --tile-bg: #fff; }
:is([data-theme="dark"], [data-theme="contrast-dark"]) { --tile-bg: var(--paper-2); }
@media (prefers-color-scheme: dark) { :root:not([data-theme]) { --tile-bg: var(--paper-2); } }
.foot-family.shelf-alt ul { padding-top: 76px; margin-top: -64px; }
.foot-family.shelf-alt a, .foot-family.shelf-alt a:hover, .foot-family.shelf-alt a:focus-visible { background: var(--tile-bg); }
.foot-family.shelf-alt a { overflow: visible; justify-items: center; text-align: center; }
/* The class-less-link reset (§0.11) zeroes padding-block-end at 0,2,1; this beats it. */
.foot-family.shelf-alt li > a:not([class]) { padding-block-end: 28px; }
.foot-family.shelf-alt a::before { border-radius: calc(var(--radius) * 1.5) calc(var(--radius) * 1.5) 0 0; }
.foot-family.shelf-alt b, .foot-family.shelf-alt small { justify-self: center; }
.foot-family.shelf-alt span { justify-self: center; text-wrap: balance; }
.foot-family.shelf-alt a:hover > svg:not(.shelf-bg), .foot-family.shelf-alt a:focus-visible > svg:not(.shelf-bg) {
  transform: translateY(-96px) scale(1.18) rotate(-6deg); filter: drop-shadow(0 14px 16px rgba(11, 12, 16, .22));
}
/* Round 3 (Maciej, 2026-09-24): the window rests grey (and the faint mark greyscale) and takes the
   brand colour on hover; the popped mark grows to three times its size and paints above the accent
   line (z-index 3 over the ::before's 2). Three times the 96px mark is 288px, so the row's headroom
   grows to 112px to keep the popped mark whole. */
.foot-family.shelf-alt ul { padding-top: 124px; margin-top: -112px; }
.foot-family.shelf-alt a::after { background: color-mix(in srgb, var(--ink) 5%, var(--tile-bg)); transition: background-color .35s var(--ease); }
.foot-family.shelf-alt a:hover::after, .foot-family.shelf-alt a:focus-visible::after { background: color-mix(in srgb, var(--hue, var(--accent)) 12%, var(--tile-bg)); }
.foot-family.shelf-alt .shelf-bg { filter: grayscale(1); transition: filter .35s var(--ease), opacity var(--dur) var(--ease); }
.foot-family.shelf-alt a:hover .shelf-bg, .foot-family.shelf-alt a:focus-visible .shelf-bg { filter: none; }
.foot-family.shelf-alt a:hover > svg:not(.shelf-bg), .foot-family.shelf-alt a:focus-visible > svg:not(.shelf-bg) {
  z-index: 3; transform: translateY(-44px) scale(3) rotate(-6deg); filter: drop-shadow(0 6px 6px rgba(11, 12, 16, .18));
}
/* The accent line stops 10px short of each side (Maciej, 2026-09-24). */
.foot-family.shelf-alt a::before { left: 10px; right: 10px; }
/* Hover shadow: smaller, and the row gets 28px more room below (handed back by a negative margin) so
   the scroller, which clips vertically, never cuts it off. */
.foot-family.shelf-alt ul { padding-bottom: 56px; margin-bottom: -28px; }
.foot-family.shelf-alt a:hover, .foot-family.shelf-alt a:focus-visible { box-shadow: 0 16px 28px -18px rgba(11, 12, 16, .4); }

/* Original shelf: the class-less-link reset (§0.11, 0,2,1) zeroed the tile's padding-block-end, gluing
   the domain pill to the bottom edge. This restores the tile's own padding (Maciej, 2026-09-24). */
.foot-family li > a:not([class]) { padding-block-end: clamp(24px, 3vw, 34px); }

/* ==== Site header: pill + bubble (header lab #13, adopted 2026-09-24; ECOSYSTEM-GUIDELINES §0.26) ====
   One header on every page. Resting: a full-width bar, logo left, bold menu on the true centre, tools
   and the "Start now!" CTA right; the sub menu and the breadcrumbs centred, with no strip of their
   own. Scrolled (desktop): the bar gathers into a floating pill, the sub menu into a second pill.
   Submenus open in one shared speech bubble that glides between sections, an Andy scene leaping in.
   Behaviour lives in assets/header.js; the no-JS page keeps the plain bar and <details> menus. */

/* §0.22 CTA: accent pill, one moving icon, attention pulse on load + every ~10 s. */
.bar-cta.btn{background:var(--accent);color:var(--on-accent,#fff);border-color:transparent;box-shadow:0 6px 18px -6px color-mix(in srgb,var(--accent) 70%,transparent);transition:transform .25s cubic-bezier(.34,1.56,.64,1),box-shadow .25s}
.bar-cta.btn:hover,.bar-cta.btn:focus-visible{transform:translateY(-2px);box-shadow:0 10px 24px -8px color-mix(in srgb,var(--accent) 80%,transparent)}
.bar-cta.btn.cta-ping{animation:cta-ping 1.1s cubic-bezier(.2,.7,.3,1)}
.bar-cta.btn.cta-ping svg{animation:hdr-nudge 1.1s ease-in-out}
@keyframes cta-ping{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 55%,transparent)}70%{box-shadow:0 0 0 12px transparent}100%{box-shadow:0 6px 18px -6px color-mix(in srgb,var(--accent) 70%,transparent)}}
@keyframes hdr-nudge{0%,100%{transform:none}30%{transform:translateX(4px)}55%{transform:translateX(0)}75%{transform:translateX(3px)}}

/* #11: flags, rocket CTA, prompter, floating controls (back-to-top left, chat right, tools bottom-centre when no room) */
.flag{width:20px;height:14px;border-radius:2px;flex:none;box-shadow:0 0 0 1px var(--rule)}
.lang-btn{display:inline-flex;align-items:center;gap:6px}
.lang-menu a{display:flex!important;align-items:center;gap:10px}
.bar-end{gap:8px}
.bar-end .lang{order:9}.bar-end .bar-cta{order:10}
.bar-tools{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--rule);border-radius:99px;padding:2px}
.bar-tools>*{border:0!important}
body.tools-dock .bar-tools{position:fixed;left:50%;transform:translateX(-50%);bottom:max(16px,env(safe-area-inset-bottom));z-index:70;background:var(--paper);box-shadow:0 10px 30px -12px rgba(0,0,0,.35)}
body.tools-dock .bar-tools .pop{position:fixed!important;left:50%!important;right:auto!important;top:auto!important;bottom:calc(max(16px,env(safe-area-inset-bottom)) + 56px)!important;transform:translateX(-50%);max-height:70vh;overflow:auto;width:min(360px,calc(100vw - 32px))}
.totop{inset-inline-end:auto!important;inset-inline-start:20px!important}
.bar-cta .rkt{width:18px;height:18px;margin-inline-start:8px;transition:transform .5s cubic-bezier(.34,1.56,.64,1)}
.bar-cta:hover .rkt,.bar-cta:focus-visible .rkt,.bar-cta.cta-ping .rkt{animation:rkt-fly 1.1s cubic-bezier(.3,.7,.3,1)}
.bar-cta.cta-ping svg:not(.rkt){animation:none}
@keyframes rkt-fly{0%{transform:none;opacity:1}40%{transform:translate(14px,-14px) rotate(8deg);opacity:0}41%{transform:translate(-12px,12px);opacity:0}100%{transform:none;opacity:1}}
.prompter[hidden]{display:none}
.prompter{position:fixed;inset:0;z-index:100;display:grid;place-items:start center;padding:10vh 16px 16px;background:color-mix(in srgb,var(--ink) 35%,transparent)}
.prompter-box{position:relative;width:min(680px,100%);max-height:80vh;overflow:auto;background:var(--paper);color:var(--ink);border-radius:22px;padding:24px;box-shadow:0 30px 80px -30px rgba(0,0,0,.5)}
.prompter-box h2{margin:0 0 6px;font-size:24px}.prompter-box p{margin:0 0 14px;color:var(--ink-2)}
.prompter textarea{width:100%;min-height:96px;border:1px solid var(--rule);border-radius:14px;padding:12px;font:inherit;background:var(--paper-2);color:var(--ink);resize:vertical}
.prompter-row{display:flex;gap:8px;justify-content:space-between;align-items:center;margin-top:10px;flex-wrap:wrap}
.prompter ol{list-style:none;margin:16px 0 0;padding:0;display:grid;gap:8px}
.prompter li a{display:grid;gap:2px;padding:12px 14px;border-radius:14px;background:var(--pill-bg);color:var(--ink)}
.prompter li b{font-weight:700}.prompter li span{font-size:14px;color:var(--ink-2)}
.prompter .prompter-x{position:absolute;right:14px;top:14px}
/* The name stays beside the mark on a phone (it used to hide under 420 px, leaving a lone icon): the
   pair just tightens, and a very narrow screen (under 350 px) sets the name one step smaller still. */
@media(max-width:420px){.bar .brand{gap:7px;font-size:18px}.bar .brand svg{width:28px;height:28px}}
@media(max-width:350px){.bar .brand{gap:5px;font-size:16px}.bar .brand svg{width:24px;height:24px}}
.prompter-note{font-size:12px!important;color:var(--ink-3)!important;margin-top:12px!important}

/* ── #12 morphing pill ─────────────────────────────────────────────────────────────────────────
   The bar keeps its size forever; a background layer is clipped to insets that grow with scroll,
   so it gathers from a full-width bar into a floating pill hugging logo, menu and tools. The logo
   and menu slide right and the tools slide left by the same amount (transform only), so nothing
   below ever re-lays out. */
@media (min-width: 1201px) {
  .bar { background: transparent; border-bottom-color: transparent; }
  .bar-in { position: relative; z-index: 2; }
  .pill-glow { position: absolute; inset: 0; z-index: 0; pointer-events: none;
    filter: drop-shadow(0 12px 24px rgb(11 12 16 / calc(var(--p, 0) * .18))); }
  /* See-through at rest, 99% --paper-2 as it gathers into the pill; no rule under it at any point. */
  .pill { position: absolute; inset: 0; background: color-mix(in srgb, var(--paper-2) calc(var(--p, 0) * 99%), transparent);
    clip-path: inset(var(--iy, 0px) var(--ixr, 0px) var(--iy, 0px) var(--ixl, 0px) round calc(var(--p, 0) * 999px)); }
  .bar-in > .brand, .bar-in .nav-links { transform: translateX(var(--shift-l, 0px)); }
  .bar-in .bar-end { transform: translateX(calc(-1 * var(--shift-r, 0px))); }
  .bar[data-layout="centre"] .bar-in .nav-links { transform: none; }

  /* ── #12 bubble submenu ──────────────────────────────────────────────────────────────────────
     Each section's own <details> panel stays where it is (links, focus order, no-JS all intact)
     but loses its box; one shared bubble behind them glides and resizes to whichever is open, and
     its tail slides to point at the open item. Switching sections morphs instead of blinking. */
  .nav-menu { background: transparent; border-color: transparent; box-shadow: none; animation: none;
    top: calc(100% + 40px); --menu-w: 50rem; padding: 16px 16px 16px 196px; min-height: 236px; }
  .nav-menu[data-cols="3"] { --menu-w: 66rem; }
  .nav-menu::before { top: -42px; height: 42px; }
  .bubble { position: absolute; left: 0; top: 0; z-index: 1; width: var(--w, 0px); height: var(--h, 0px);
    transform: translate(var(--x, 0px), var(--y, 0px)) scale(.94); transform-origin: var(--tail, 50%) 0;
    background: var(--paper-2); border: var(--rule-w) solid var(--rule); border-radius: 26px;
    box-shadow: 0 30px 60px -30px rgb(11 12 16 / .45), 0 2px 6px rgb(11 12 16 / .06);
    opacity: 0; pointer-events: none;
    transition: transform .42s cubic-bezier(.3, 1.3, .5, 1), width .42s cubic-bezier(.3, 1.3, .5, 1),
                height .42s cubic-bezier(.3, 1.3, .5, 1), opacity .18s ease; }
  .bubble.on { opacity: 1; transform: translate(var(--x, 0px), var(--y, 0px)); }
  .bubble.instant, .bubble.instant::before { transition: none; }
  /* The tail: a rotated square sharing the bubble's fill, so the pair reads as one speech bubble. */
  .bubble::before { content: ""; position: absolute; top: -12px; left: calc(var(--tail, 50%) - 12px); width: 24px; height: 24px;
    background: inherit; border-left: var(--rule-w) solid var(--rule); border-top: var(--rule-w) solid var(--rule);
    border-top-left-radius: 5px; transform: rotate(45deg); transition: left .42s cubic-bezier(.3, 1.3, .5, 1); }
  /* The character's stage: a tinted well on the bubble's left, the Andy scene leaping in. */
  .bubble::after { content: ""; position: absolute; left: 12px; top: 12px; bottom: 12px; width: 172px; border-radius: 18px;
    background: color-mix(in srgb, var(--accent) 9%, var(--paper-2)); }
  .nav-art { position: absolute; left: 18px; bottom: 18px; width: 160px; pointer-events: none; }
  /* Air (2026-09-24): every row gets real padding and the groups real separation. The .bar prefix
     outranks the §0.11 link reset (0,2,1), which would zero padding-block-end. */
  /* Selectors carry .bar .nav-links (0,3,x) on purpose: the §0.11 reset
     `:is(…, .lg-post h3, …) a:not([class])` weighs 0,2,2 and zeroes padding-block-end on plain links,
     and the site's bar-anchored menus (`.nav-group[data-anchor="bar"] > .nav-menu`, 0,3,0) set their
     own top. */
  .bar .nav-links .nav-group > .nav-menu { top: calc(100% + 30px); padding: 22px 22px 22px 216px; gap: 6px 14px; }
  /* The hover bridge covers only the gap between the bar row and the bubble: never the other menu
     items, so moving sideways to the next section always reaches it at once. */
  .bar .nav-links .nav-group > .nav-menu::before { top: -30px; height: 30px; }
  .bar .nav-links .nav-menu > a { padding: 12px 14px; column-gap: 14px; row-gap: 3px; border-radius: 14px; }
  .bar .nav-links .nav-menu > .nav-lead { margin-block-end: 10px; padding: 12px 14px; border-radius: 14px; position: relative; border-bottom: 0; }
  .bar .nav-links .nav-menu > .nav-lead::after { content: ""; position: absolute; left: 14px; right: 14px; bottom: -6px; border-bottom: var(--rule-w) solid var(--rule); }
  .bar .nav-links .nav-menu > .nav-more { margin-top: 10px; padding: 14px; border-radius: 14px; border-top: 0; position: relative; }
  .bar .nav-links .nav-menu > .nav-more::after { content: ""; position: absolute; left: 14px; right: 14px; top: -6px; border-top: var(--rule-w) solid var(--rule); }
  /* Every submenu icon moves on hover/focus (§0.2): the tile pops and wiggles, the glyph redraws
     itself (pathLength=1 is set by v.js, so one dash length fits every shape). */
  .bar .nav-links .nav-menu > a:is(:hover, :focus-visible) .nav-ico { animation: hdr-ico .6s cubic-bezier(.3, 1.6, .5, 1); }
  .bar .nav-links .nav-menu > a:is(:hover, :focus-visible) .nav-ico svg :is(path, circle, rect, line, polyline) { stroke-dasharray: 1; animation: hdr-draw .7s cubic-bezier(.4, 0, .2, 1); }
  .nav-menu[data-cols="3"] { --menu-w: 70rem; }
  .bubble::after { left: 14px; top: 14px; bottom: 14px; width: 184px; }
  .nav-art { left: 22px; bottom: 22px; width: 168px; }
  .nav-group[open] .nav-art { animation: hdr-leap .7s cubic-bezier(.3, 1.2, .5, 1) .05s both; }
  .nav-group[open] .nav-art::part(prop) { animation: hdr-pop .55s cubic-bezier(.3, 1.6, .5, 1) calc(.4s + var(--k, 0) * 90ms) both; }
  .nav-group[open] .nav-menu > a { animation: hdr-in .34s cubic-bezier(.2, .7, .3, 1) both; }
  .nav-menu[data-from="left"] > a { --dx: -26px; }
  .nav-menu[data-from="right"] > a { --dx: 26px; }
  .nav-group[open] .nav-menu > a:nth-of-type(2) { animation-delay: .03s; }
  .nav-group[open] .nav-menu > a:nth-of-type(3) { animation-delay: .06s; }
  .nav-group[open] .nav-menu > a:nth-of-type(4) { animation-delay: .09s; }
  .nav-group[open] .nav-menu > a:nth-of-type(n+5) { animation-delay: .12s; }
}
@keyframes hdr-in { from { opacity: 0; transform: translate(var(--dx, 0), 8px); } }
@keyframes hdr-leap {
  0% { opacity: 0; transform: translate(18%, 70%) rotate(14deg) scale(.7); }
  55% { opacity: 1; transform: translate(0, -10%) rotate(-5deg) scale(1.04); }
  78% { transform: translate(0, 2%) scale(1.03, .96); }
  100% { transform: none; }
}
@keyframes hdr-ico { 30% { transform: scale(1.18) rotate(-10deg); } 60% { transform: scale(.96) rotate(6deg); } }
@keyframes hdr-draw { from { stroke-dashoffset: 1; } }
@keyframes hdr-pop { from { opacity: 0; transform: scale(.2) rotate(-25deg); } }
@media (min-width: 1201px) and (prefers-reduced-motion: reduce) {
  .bubble, .bubble::before { transition: opacity .18s ease; }
  .nav-group[open] .nav-art, .nav-group[open] .nav-art::part(prop), .nav-group[open] .nav-menu > a { animation: none; }
  .bar .nav-links .nav-menu > a .nav-ico, .bar .nav-links .nav-menu > a .nav-ico svg * { animation: none !important; }
}
[data-motion="reduce"] .bar .nav-links .nav-menu > a :is(.nav-ico, .nav-ico svg *) { animation: none !important; }
[data-motion="reduce"] .bubble, [data-motion="reduce"] .bubble::before { transition: opacity .18s ease; }
[data-motion="reduce"] .nav-group[open] :is(.nav-art, .nav-menu > a), [data-motion="reduce"] .nav-group[open] .nav-art::part(prop) { animation: none; }
@media (max-width: 1200px) { .nav-art, .bubble, .pill-glow { display: none; } }

@media (min-width: 1201px) {
  /* Raw tool icons, as tall as the CTA (44px), no frame: only a soft round wash on hover. */
  body:not(.tools-dock) .bar .bar-tools { border: 0; padding: 0; gap: 2px; background: none; }
  body:not(.tools-dock) .bar .bar-tools .player-label { display: none; }
  body:not(.tools-dock) .bar .bar-tools :is(.player-btn, .icon-btn) {
    width: 37px; height: 37px; padding: 0; justify-content: center; border: 0 !important; border-radius: 50%;
    background: transparent; color: var(--ink); transition: background-color .2s, transform .35s cubic-bezier(.34, 1.56, .64, 1); }
  body:not(.tools-dock) .bar .bar-tools :is(.player-btn, .icon-btn):is(:hover, :focus-visible, [aria-expanded="true"]) { background: var(--field); transform: scale(1.08); }
  body:not(.tools-dock) .bar .bar-tools .player-btn svg { width: 21px; height: 21px; }
  body:not(.tools-dock) .bar .bar-tools .icon-btn svg { width: 24px; height: 24px; stroke-width: 1.9; }
  body:not(.tools-dock) .bar .bar-tools .player.is-active .player-btn { background: var(--accent); color: var(--on-fill); }

  /* Sub menu pill: same technique as the header pill. The strip keeps its size; a background layer
     clipped to insets gathers around the pills, and the pill row slides to the centre (transform). */
  .subnav { background: transparent; border-bottom-color: transparent; }
  .subnav > .wrap { position: relative; z-index: 1; }
  .subnav ul { transform: translateX(var(--sshift, 0px)); }
  .sub-glow { position: absolute; inset: 0; pointer-events: none; filter: drop-shadow(0 10px 20px rgb(11 12 16 / calc(var(--p, 0) * .16))); }
  .sub-pill { position: absolute; inset: 0; background: var(--paper-2);
    border-bottom: var(--rule-w) solid color-mix(in srgb, var(--rule) calc((1 - var(--p, 0)) * 100%), transparent);
    clip-path: inset(var(--siy, 0px) var(--sixr, 0px) var(--siy, 0px) var(--sixl, 0px) round calc(var(--p, 0) * 999px)); }
}
@media (min-width: 1201px) {
  /* In the pill every item shares one centre line (32px): the site's 8px top pad (§0.21 baseline
     alignment with the wordmark) left the menu 4px below the logo, tools and CTA. */
  .bar .nav-links { padding-top: 0; }
  .bar .bar-tools > * { display: inline-flex; align-items: center; }
}
@media (min-width: 1201px) {
  /* Three columns: logo left, menu centred on the page, tools + CTA right. Equal 1fr sides keep
     the menu on the true centre whatever the two sides weigh. The resting bar is full width. The
     40px gap (GAP_X in header.js) leaves room for a tool's hover lift and focus ring beside the menu. */
  .bar .bar-in.wrap { width: calc(100% - clamp(32px, 4vw, 72px)); max-width: none;
    display: grid; grid-template-columns: minmax(max-content, 1fr) auto minmax(max-content, 1fr); column-gap: 40px; }
  .bar .bar-in > .brand { grid-column: 1; justify-self: start; }
  .bar .bar-in .nav-links { grid-column: 2; grid-row: 1; margin: 0; }
  .bar .bar-in > .bar-end { grid-column: 3; grid-row: 1; justify-self: end; }
  .bar .nav-links :is(.nav-top, .nav-group > summary) { font-weight: 700; }

  /* One family for play, accessibility and language: same 37px ghost shape, same ink, same wash
     and lift on hover, circle-based 22px icons at one stroke weight, the flag a circle too. */
  body:not(.tools-dock) .bar :is(.bar-tools .player-btn, .bar-tools .icon-btn, .lang-btn) {
    height: 37px; border: 0 !important; border-radius: 999px; background: transparent; color: var(--ink);
    transition: background-color .2s, transform .35s cubic-bezier(.34, 1.56, .64, 1); }
  body:not(.tools-dock) .bar :is(.bar-tools .player-btn, .bar-tools .icon-btn, .lang-btn):is(:hover, :focus-visible, [aria-expanded="true"]) {
    background: var(--field); transform: scale(1.08); }
  body:not(.tools-dock) .bar .bar-tools :is(.player-btn, .icon-btn) svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
  body:not(.tools-dock) .bar .bar-tools .icon-btn svg circle[fill] { fill: currentColor; }
  body:not(.tools-dock) .bar .lang-btn { padding: 0 10px 0 8px; gap: 6px; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: .02em; }
  body:not(.tools-dock) .bar .lang-btn .flag { width: 17px; height: 17px; margin: 0 2px; border-radius: 50%; box-shadow: 0 0 0 1.9px currentColor; }
  body:not(.tools-dock) .bar .lang-btn > svg:last-child { width: 10px; height: 6px; }
  body:not(.tools-dock) .bar .bar-end { gap: 4px; }
}
@media (min-width: 1201px) {
  /* The sub menu rests with no strip of its own (no white/dark band, no rule): just the centred
     pills on the page. Its background exists only as the scrolled pill, fading in as it gathers. */
  .sub-pill { opacity: var(--p, 0); border-bottom: 0; }
}
/* Centred sub menu at every width. Auto margins on the end pills, not justify-content: centre,
   so an overflowing row still scrolls to its first pill instead of cutting it off on the left. */
.subnav ul > li:first-child { margin-inline-start: auto; }
.subnav ul > li:last-child { margin-inline-end: auto; }
.crumbs-bar ol { justify-content: center; }
@media (max-width: 1200px) { .sub-glow { display: none; } }

/* Breadcrumbs: a faint home icon leads the trail (a soft left inset more than a signal), and the
   trail gets the same air below as above. */
.crumbs-bar { padding-block: 18px 22px; }
.crumbs-bar li:first-child a { display: inline-flex; align-items: center; gap: 6px; }
.crumb-home { width: 13px; height: 13px; flex: none; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; opacity: .3; }

/* Sub menu arrows: shown only while the row overflows and only on the side that has more to show.
   The row fades out under each visible arrow so a cut-off pill reads as "more this way". */
.subnav > .wrap { position: relative; }
.sub-arrow { position: absolute; top: 50%; z-index: 2; display: inline-grid; place-items: center; width: 32px; height: 32px;
  margin-top: -16px; padding: 0; border: 0; border-radius: 50%; background: var(--paper); color: var(--ink);
  box-shadow: 0 4px 12px -4px rgb(11 12 16 / .35), 0 0 0 1px var(--rule); cursor: pointer; transition: transform .3s cubic-bezier(.34, 1.56, .64, 1); }
.sub-arrow[hidden] { display: none; }
.sub-arrow.prev { left: calc(var(--ul-l, 0px) + 4px); }
.sub-arrow.next { right: calc(var(--ul-r, 0px) + 4px); }
.sub-arrow svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; transition: transform .3s cubic-bezier(.34, 1.56, .64, 1); }
.sub-arrow:hover, .sub-arrow:focus-visible { transform: scale(1.1); }
.sub-arrow.prev:hover svg, .sub-arrow.prev:focus-visible svg { transform: translateX(-2px); }
.sub-arrow.next:hover svg, .sub-arrow.next:focus-visible svg { transform: translateX(2px); }
.subnav[data-more~="prev"] ul { --fade-l: 48px; }
.subnav[data-more~="next"] ul { --fade-r: 48px; }
.subnav[data-more] ul { scroll-behavior: smooth;
  mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-l, 0px), #000 calc(100% - var(--fade-r, 0px)), transparent 100%); }
@media (prefers-reduced-motion: reduce) { .subnav[data-more] ul { scroll-behavior: auto; } }
[data-motion="reduce"] .subnav[data-more] ul { scroll-behavior: auto; }
/* The bottom dock (phones, or no room in the bar): tools 20% bigger than the site's 40px. */
body.tools-dock .bar-tools :is(.player-btn, .icon-btn) { height: 48px; min-width: 48px; }
body.tools-dock .bar-tools .player-btn svg { width: 19px; height: 19px; }
body.tools-dock .bar-tools .icon-btn svg { width: 24px; height: 24px; }

.player-btn svg { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
body.tools-dock .bar-tools .player-btn svg { width: 24px; height: 24px; }
.bar-cta .rkt { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.prompter-row .rkt { width: 18px; height: 18px; margin-inline-start: 8px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform .3s cubic-bezier(.34, 1.56, .64, 1); }
.prompter-row .btn:is(:hover, :focus-visible) .rkt { transform: translateX(4px); }
.crumb-home { width: 13px; height: 13px; flex: none; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; opacity: .3; }

/* Phones and tablets (the drawer layout, <= 1200px): no CTA in the bar — the drawer carries its own
   call to action — and the bar slides away while the reader scrolls down, leaving only the sub menu
   (when the page has one) pinned at the top; scrolling up brings the bar back (header.js). */
@media (max-width: 1200px) {
  .bar-end .bar-cta { display: none; }
  .bar { transition: transform .32s cubic-bezier(.2, .7, .2, 1), background-color .25s ease; }
  .subnav { transition: top .32s cubic-bezier(.2, .7, .2, 1); }
  html.bar-away .bar { transform: translateY(-100%); }
  html.bar-away .subnav { top: 0; }
}
@media (max-width: 1200px) and (prefers-reduced-motion: reduce) { .bar, .subnav { transition: none; } }
[data-motion="reduce"] :is(.bar, .subnav) { transition: none; }
.prompter-box h2 { padding-inline-end: 48px; }

/* Phones and tablets: the tools stay in the bar (no bottom dock, so nothing jumps after load) and
   look exactly as on desktop: raw round icons, one family with the language button (§0.26). The
   sides of the desktop grid never shrink below their content, so the tools never collide with the
   centred menu either: at the tightest width the menu merely sits a little off centre. */
@media (max-width: 1200px) {
  .bar .bar-tools { border: 0; padding: 0; gap: 0; background: none; }
  .bar .bar-tools .player-label { display: none; }
  .bar .bar-tools :is(.player-btn, .icon-btn), .bar .lang-btn {
    height: 40px; border: 0 !important; border-radius: 999px; background: transparent; color: var(--ink); }
  .bar .bar-tools :is(.player-btn, .icon-btn) { width: 40px; padding: 0; justify-content: center; }
  .bar .bar-tools :is(.player-btn, .icon-btn):is(:hover, :focus-visible, [aria-expanded="true"]), .bar .lang-btn:is(:hover, :focus-visible, [aria-expanded="true"]) { background: var(--field); }
  .bar .bar-tools :is(.player-btn, .icon-btn) svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.9; }
  .bar .bar-tools .icon-btn svg circle[fill] { fill: currentColor; }
  .bar .bar-tools .player.is-active .player-btn { background: var(--accent); color: var(--on-fill); }
  .bar .lang-btn { padding: 0 8px; gap: 6px; font-size: 14px; font-weight: 700; text-transform: uppercase; }
  .bar .lang-btn .flag { width: 18px; height: 18px; border-radius: 50%; box-shadow: 0 0 0 1.9px currentColor; }
  .bar .bar-end { gap: 0; }
}

/* Back to top matches the chat button (2026-09-24): both are --castomo-size, both 20px off the
   bottom. The chat defines the variable on its own element; the root copy is the same 56px, so the
   pair stays equal even before the chat script has loaded. A ring draws around it with the scroll
   and is closed at the bottom of the page (site.js sets --totop-p). */
:root { --castomo-size: 56px; }
.totop { width: var(--castomo-size); height: var(--castomo-size); inset-block-end: 20px; }
.totop svg:not(.totop-ring) { width: 22px; height: 22px; }
.totop-ring { position: absolute; inset: 0; width: 100% !important; height: 100% !important; transform: rotate(-90deg); pointer-events: none; }
.totop-ring circle { fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round;
  stroke-dasharray: 1; stroke-dashoffset: calc(1 - var(--totop-p, 0)); transition: stroke-dashoffset .12s linear; }
[data-theme^="contrast"] .totop-ring circle { stroke: var(--paper); }

/* The ecosystem and tools shelves: every tile snaps to the centre of the row, and the row opens on
   the first tile centred (site.js), on phones and desktops alike. */
.foot-family ul { scroll-padding-inline: 0; }
.foot-family li { scroll-snap-align: center; }

/* The narrowest phones: the footer's link lists stack in one column. */
@media (max-width: 370px) { .foot-grid { grid-template-columns: 1fr; } }

