/* ============================================================
   TRICSY — WEB KIT
   Part 1 of 2: tokens, base type and core components. This half is
   the brand manual verbatim — it is generated from the same values
   as tricsy.tokens.json and must not be edited by hand.

   Part 2 (layout, page furniture, forms, tables, dialogs) lives at
   the bottom of this file under SITE LAYER.

   TRICSY — DESIGN TOKENS
   Warm monochrome. Two modes, one ramp.

   White is #FFF9F2, never #FFFFFF.
   Black is #070605, never #000000.
   Every neutral sits between 28 and 34 degrees hue.

   Light mode ships as the default. Dark mode is applied with
   <html data-theme="dark"> and also follows the OS setting
   when no attribute is present.
   ============================================================ */

:root {
  /* --- surfaces ------------------------------------------------
     Sections are the extreme, the ground is one step toward the
     middle. A card is cut into the plane, it does not float. */
  --surface:        #FFF9F2;
  --surface-sunk:   #F9F2E8;
  --paper:          #F6EFE4;

  /* --- ink --------------------------------------------------- */
  --ink:            #14120F;
  --ink-secondary:  #4A4239;
  --ink-muted:      #766D63;   /* 4.89:1 on --surface — clears WCAG AA */
  --ink-inverse:    #FFF9F2;

  /* Which way an image logo has to be flipped to land on this plane. Every
     logo file we ship is white-on-transparent, so the light plane inverts it
     and the dark plane leaves it alone — one rule for the whole set instead
     of a per-file dark-mode exception. */
  --logo-ink-invert: 1;

  /* --- lines ------------------------------------------------- */
  --line:           #E4DCD0;
  --line-strong:    #D8CFC1;
  --hatch:          #EDE5D9;

  /* --- action ------------------------------------------------ */
  --accent:         #14120F;
  --accent-hover:   #332E27;
  --on-accent:      #FFF9F2;
  --accent-tint:    #EFE8DC;

  /* --- data -------------------------------------------------- */
  --chart-1:        #201C18;
  --chart-2:        #4A423A;
  --chart-3:        #857A6D;
  --chart-4:        #B0A697;
  --chart-5:        #D6CCBD;
  --chart-grid:     #E4DCD0;
  --chart-track:    #F0E9DD;

  /* --- gradients ---------------------------------------------
     Only ever under a data curve. Never a card, button, section
     or hero. Both fade to fully transparent so the ground shows
     through and they work in either mode unchanged. */
  --grad-warm-top:    #201C1845;
  --grad-warm-bottom: #201C1800;
  --grad-cool-top:    #007FCF33;
  --grad-cool-bottom: #00BFFF00;
  --grad-wash-top:    #14120F1A;
  --grad-wash-bottom: #14120F00;

  /* --- state -------------------------------------------------
     The only non-monochrome colours in the system. A badge, a
     failed field, a threshold. Never a background. */
  --positive:       #00A96E;
  --positive-tint:  #DEF0E4;
  --negative:       #D9455C;
  --negative-tint:  #F8E3DF;
  --warning:        #B87400;

  /* --- type -------------------------------------------------- */
  --font-display: "Geist", system-ui, sans-serif;
  --font-body:    "Geist", system-ui, sans-serif;
  --font-mono:    "Geist Mono", ui-monospace, monospace;

  /* Tracking is expressed in em, never px. Headings use fluid clamp()
     sizes, so a fixed px value drifts: -2px is -3.8% at 52px but
     -6.7% at 30px and the letters collide on small screens. */
  /* Nine steps, in rem so the reader's own font-size setting still
     works. Two parallel tracks share the bottom: a sans track for
     reading (body, body-sm) and a mono track for machine metadata
     (spec, mono-label). Nothing else exists — if a size is not on
     this list it does not belong in the system. */
  /* Fluid between a 375px phone and a 1400px canvas. The rem floor is
     the mobile size, the rem ceiling is the desktop size printed in the
     manual — nothing outside that range exists. This is why tracking is
     em: a fixed -2px is -3.8% at 64px but -6.7% at 30px, and the letters
     collide the moment the heading shrinks. */
  --size-display:    clamp(2.75rem, 1.81rem + 4.79vw, 6rem);     /* 44 -> 96, reaching 96 at ~1400 */
  --tracking-display: -0.036em;
  --size-h1:         clamp(2.25rem, 1.32rem + 3.98vw, 4rem);     /* 36 -> 64 */
  --tracking-h1:      -0.0375em;
  --size-h2:         clamp(1.75rem, 1.35rem + 1.71vw, 2.5rem);   /* 28 -> 40 */
  --tracking-h2:      -0.035em;
  --size-h3:         clamp(1.375rem, 1.17rem + 0.85vw, 1.75rem); /* 22 -> 28 */
  --tracking-h3:      -0.029em;
  --size-h4:         clamp(1.125rem, 1.06rem + 0.28vw, 1.25rem); /* 18 -> 20 */
  --tracking-h4:      -0.02em;
  --size-body:       1rem;      /* 16 */
  --size-body-sm:    0.875rem;  /* 14 */
  --size-spec:       0.75rem;   /* 12 */  --tracking-spec:     0.02em;
  --size-mono-label: 0.6875rem; /* 11 */  --tracking-mono-label: 0.145em;

  --leading-display: 0.95;
  --leading-heading: 1.1;
  --leading-body:    1.55;
  --leading-caption: 1.4;

  /* --- space ------------------------------------------------- */
  --space-1:  4px;
  --space-2:  8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --space-9: 96px;

  /* --- shape -------------------------------------------------
     Zero. The single exception in the whole system is the
     percentage pill inside a funnel chart. */
  --radius: 0px;
  --border-hairline: 1px;
  --border-rail:     2px;

  /* --- grid -------------------------------------------------- */
  --grid-columns:     12;
  --grid-cell:        200px;
  --grid-cell-mobile: 100px;
  --grid-gutter:      24px;
  --grid-content-max: 1400px;
  --grid-margin:      100px;

  /* --- blueprint nodes --------------------------------------- */
  --node-xs:       5px;
  --node-sm:       7px;
  --node-md:       9px;
  --node-lg:      13px;
  --node-hover:   16px;
  --node-hit:     88px;   /* reveal radius, not the visible node */

  /* --- motion ------------------------------------------------ */
  --reveal-in:  120ms;
  --reveal-out: 200ms;
  --ease:       cubic-bezier(0.16, 1, 0.3, 1);
}

/* Dark mode. Same tokens, mirrored values.
   Note the inversion: sections become the darkest layer and the
   ground sits one step lighter. Do not "fix" this — it is the
   defining trait of the system. */
:root[data-theme="dark"] {
  --surface:        #070605;
  --surface-sunk:   #0E0C0B;
  --paper:          #100E0D;

  --ink:            #FFF9F2;
  --ink-secondary:  #C3B9AE;
  --ink-muted:      #8A8078;
  --ink-inverse:    #070605;
  --logo-ink-invert: 0;

  --line:           #2A2622;
  --line-strong:    #322D28;
  --hatch:          #181513;

  --accent:         #FFF9F2;
  --accent-hover:   #E6DED4;
  --on-accent:      #070605;
  --accent-tint:    #16130F;

  --chart-1:        #F5EDE3;
  --chart-2:        #C9BCAC;
  --chart-3:        #978A7C;
  --chart-4:        #6B6155;
  --chart-5:        #3A342D;
  --chart-grid:     #2A2622;
  --chart-track:    #100E0D;

  --grad-warm-top:    #F5EDE345;
  --grad-warm-bottom: #F5EDE300;
  --grad-cool-top:    #007FCF45;
  --grad-cool-bottom: #00BFFF00;
  --grad-wash-top:    #FFF9F22E;
  --grad-wash-bottom: #FFF9F200;

  --positive:       #3FBF93;
  --positive-tint:  #17231C;
  --negative:       #FF7A7A;
  --negative-tint:  #2B1A1A;
  --warning:        #E0A54A;
}

/* Follow the operating system when the user has not chosen. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --surface:        #070605;
    --surface-sunk:   #0E0C0B;
    --paper:          #100E0D;
    --ink:            #FFF9F2;
    --ink-secondary:  #C3B9AE;
    --ink-muted:      #8A8078;
    --ink-inverse:    #070605;
    --logo-ink-invert: 0;
    --line:           #2A2622;
    --line-strong:    #322D28;
    --hatch:          #181513;
    --accent:         #FFF9F2;
    --accent-hover:   #E6DED4;
    --on-accent:      #070605;
    --accent-tint:    #16130F;
    --chart-1:        #F5EDE3;
    --chart-2:        #C9BCAC;
    --chart-3:        #978A7C;
    --chart-4:        #6B6155;
    --chart-5:        #3A342D;
    --chart-grid:     #2A2622;
    --chart-track:    #100E0D;
    --grad-warm-top:    #F5EDE345;
    --grad-warm-bottom: #F5EDE300;
    --grad-cool-top:    #007FCF45;
    --grad-wash-top:    #FFF9F22E;
    --grad-wash-bottom: #FFF9F200;
    --positive:       #3FBF93;
    --positive-tint:  #17231C;
    --negative:       #FF7A7A;
    --negative-tint:  #2B1A1A;
    --warning:        #E0A54A;
  }
}

/* ============================================================
   BASE
   ============================================================ */

* { box-sizing: border-box; }

html {
  color-scheme: light dark;
  /* The header is sticky and 72px tall, so an anchor that scrolls to the
     very top lands underneath it. Declared once here rather than as a
     scroll-margin on each target, which would have to be repeated on
     every new section and every pillar card. */
  scroll-padding-top: calc(72px + var(--space-4));
}
html[data-theme="dark"] { color-scheme: dark; }
html[data-theme="light"] { color-scheme: light; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  font-kerning: normal;
  text-wrap: pretty;          /* no single-word last lines */
}

/* Ivory on warm black reads a shade lighter than warm black on
   ivory, and light type on a dark ground needs more air. One extra
   notch of leading in dark mode, nowhere else. */
:root[data-theme="dark"] body { line-height: calc(var(--leading-body) + 0.06); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body { line-height: calc(var(--leading-body) + 0.06); }
}

/* Figures that sit in a column — table cells, metrics, axis labels —
   must align on the decimal. Geist Mono is already fixed-width; this
   is for the sans face. */
.tabular, table td, .metric-value { font-variant-numeric: tabular-nums; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: var(--leading-heading);
  margin: 0;
  text-wrap: balance;         /* even line lengths on multi-line headings */
}
h1 { font-size: var(--size-h1); letter-spacing: var(--tracking-h1); }
h2 { font-size: var(--size-h2); letter-spacing: var(--tracking-h2); font-weight: 600; }
h3 { font-size: var(--size-h3); letter-spacing: var(--tracking-h3); font-weight: 600; }
h4 { font-size: var(--size-h4); letter-spacing: var(--tracking-h4); font-weight: 600; }

/* The mono label is a signature of the system. Uppercase, wide
   tracking, muted. Use it for section eyebrows, table headers, tags
   and metadata.

   HARD LIMIT: three words, or about forty characters. Uppercase and
   wide tracking work by removing word shapes, which is fine for a
   label the eye recognises whole and fatal for anything it has to
   read. Past that limit use .label-spec below. */
.label-mono {
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  /* letter-spacing also adds its space AFTER the last character, which is
     typographically meaningless and counts toward the element's width. At
     this tracking that is ~1.6px, and it is what pushed German's
     "06 — ZIELGRUPPENMESSUNG" 2px past its 153px cell at 768px: the word is
     already on its own line and still does not fit. Reclaiming the trailing
     space fixes it with no visible change and helps every long label. */
  margin-right: calc(var(--tracking-mono-label) * -1);
}

/* Except where the label is a bordered chip: there the trailing space sits
   inside the padding, and removing it would make the right padding narrower
   than the left. */
.video__label { margin-right: 0; }

/* Between 768 and 899px the pillar grid is three across and a card is only
   ~219px, which leaves the label 155px — and German's ZIELGRUPPENMESSUNG is
   155px on the nose. Reclaiming the trailing space above clears it, but by
   less than a pixel, and text measurement differs slightly between engines.
   Easing the tracking in this band only buys 4px of real headroom; at 11px
   the change is 0.27px per character, which is not visible, and no other
   label on the site is touched. */
@media (min-width: 768px) and (max-width: 899px) {
  .pillars .label-mono {
    letter-spacing: 0.12em;
    margin-right: -0.12em;
  }
}

/* The same voice for anything longer than a label: a spec line, a
   chart footnote, a caption carrying a full sentence. Still mono, so
   it still reads as machine metadata — but sentence case, normal
   tracking and one step larger, so it can actually be read. */
.label-spec {
  font-family: var(--font-mono);
  font-size: var(--size-spec);
  letter-spacing: var(--tracking-spec);
  line-height: 1.5;
  color: var(--ink-secondary);
}

/* ============================================================
   COMPONENTS
   ============================================================ */

.card {
  background: var(--surface);
  border: var(--border-hairline) solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-6);
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 12px 20px;
  border: 0;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  font-weight: 500;
  cursor: pointer;
}
.btn:hover { background: var(--accent-hover); }

.btn--secondary {
  background: var(--surface);
  color: var(--ink);
  border: var(--border-hairline) solid var(--line-strong);
}
/* A secondary button must not inherit .btn:hover. That rule paints the accent
   fill, which on a button whose ink stays --ink meant dark text on a dark
   ground: measured at 1.39:1, against 17.88:1 at rest. It sank instead of
   lighting up. It moves within its own palette now, the way the framed
   controls in the header do. */
.btn--secondary:hover {
  background: var(--surface-sunk);
  border-color: var(--ink);
}
.btn--quiet     { background: var(--surface-sunk); color: var(--ink); }
.btn--ghost     { background: transparent;         color: var(--ink); }
/* Text on a destructive fill reads against the ground, not a fixed
   value — in dark mode --negative is a light salmon. */
.btn--danger    { background: var(--negative);     color: var(--paper); }
.btn:disabled   { background: var(--surface-sunk); color: var(--ink-muted); cursor: not-allowed; }

.btn--sm { padding:  8px 12px; font-size: var(--size-body-sm); }
.btn--lg { padding: 15px 26px; font-size: var(--size-body); }

.field {
  width: 100%;
  padding: 11px 12px;
  background: var(--surface);
  border: var(--border-hairline) solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
}
.field::placeholder { color: var(--ink-muted); }
.field:focus        { outline: none; border-color: var(--accent); }
.field[aria-invalid="true"] { border-color: var(--negative-ink); }

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 4px 8px;
  background: var(--positive-tint);
  color: var(--positive-ink);
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  font-weight: 500;
}
.badge--negative { background: var(--negative-tint); color: var(--negative-ink); }
.badge--warning  { background: var(--surface); color: var(--warning-ink); border: 1px solid var(--warning); }
.badge--neutral  { background: var(--accent-tint); color: var(--ink); }

.tag {
  display: inline-flex;
  padding: 5px 9px;
  border: var(--border-hairline) solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  color: var(--ink-secondary);
}

/* ============================================================
   BLUEPRINT GRID
   The construction grid behind hero sections. Nodes sit on the
   intersections: most are absent, the rest are a mix of solid
   dots and open rings at four sizes.
   ============================================================ */

.blueprint {
  position: relative;
  background-image:
    linear-gradient(to right,  var(--line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line) 1px, transparent 1px);
  background-size: var(--grid-cell) var(--grid-cell);
}

/* Invisible hit target, centred on the intersection. The node is then
   centred inside the hit, so it lands exactly on the crossing. Do not
   position the node with negative margins alone — it would be offset
   by half the hit as well as half itself.

   z-index 0 keeps the whole field underneath the content. The grid is
   construction, not decoration laid over the work — a node must never
   sit on top of the logo or a headline. */
.blueprint__hit {
  position: absolute;
  z-index: 0;
  width: var(--node-hit);
  height: var(--node-hit);
  transform: translate(-50%, -50%);
}

/* Anything placed inside a .blueprint sits above the field. Give it
   pointer-events:none so the nodes underneath still react. */
.blueprint > .blueprint__over { position: relative; z-index: 1; pointer-events: none; }
/* ...but a button on top of the field still has to be clickable. The
   field keeps everything else that falls through it. */
.blueprint__over :is(a, button, input, select, textarea, summary) { pointer-events: auto; }

.blueprint__node {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: var(--node-md);
  height: var(--node-md);
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--line-strong);
}
.blueprint__node--solid { background: var(--line-strong); border: 0; }
.blueprint__node--xs { width: var(--node-xs); height: var(--node-xs); }
.blueprint__node--sm { width: var(--node-sm); height: var(--node-sm); }
.blueprint__node--lg { width: var(--node-lg); height: var(--node-lg); }

/* A ghost node is absent at rest and appears under the cursor. */
.blueprint__node--ghost { opacity: 0; transition: opacity var(--reveal-out) var(--ease); }
.blueprint__hit:hover .blueprint__node--ghost { opacity: 1; transition-duration: var(--reveal-in); }

.blueprint__hit:hover .blueprint__node {
  width: var(--node-hover);
  height: var(--node-hover);
  background: var(--ink);
  border-color: var(--ink);
  box-shadow: 0 0 0 3px var(--paper);
  transition: width var(--reveal-in) var(--ease), height var(--reveal-in) var(--ease),
              background-color var(--reveal-in) var(--ease), border-color var(--reveal-in) var(--ease);
}
/* Never animate the grid lines themselves. */

/* 45 degree hatch. Marks an empty cell. */
.hatch {
  background-image: repeating-linear-gradient(
    45deg,
    var(--hatch) 0, var(--hatch) 1px,
    transparent 1px, transparent 10px
  );
}

/* ============================================================
   MOTION

   The timing tokens above are the whole vocabulary. Two rules use
   them outside the blueprint field.

   FEEDBACK. Every control that changes colour on hover, focus or
   press eases into it instead of snapping. 120ms is long enough to
   read as an answer and short enough that a fast hand never waits.
   A press is a 1px sink, never a scale: things in this system are
   cut into the plane, so they push in rather than pop out.

   THE MODE SWITCH. Light and dark are one ramp mirrored, and the
   switch is what makes that legible — you watch paper and surface
   trade places, and the inverted depth stops being a claim in a
   document. The transition is armed only while the switch happens:
   set data-switching on the root, clear it after ~320ms. Nothing
   carries a permanent transition on every element, so this costs
   nothing the rest of the time.

       document.documentElement.dataset.switching = '';
       setTimeout(() => delete document.documentElement.dataset.switching, 320);
   ============================================================ */

.btn, .field, .tab, .badge {
  transition: background-color var(--reveal-in) var(--ease),
              border-color     var(--reveal-in) var(--ease),
              color            var(--reveal-in) var(--ease);
}
.btn { transition: background-color var(--reveal-in) var(--ease), transform 80ms var(--ease); }
.btn:active { transform: translateY(1px); }

tbody tr { transition: background-color var(--reveal-in) var(--ease); }
tbody tr:hover { background: var(--surface-sunk); }

:root[data-switching] *,
:root[data-switching] *::before,
:root[data-switching] *::after {
  transition: background-color 260ms var(--ease),
              border-color     260ms var(--ease),
              color            260ms var(--ease),
              fill             260ms var(--ease),
              stroke           260ms var(--ease) !important;
}

@media (max-width: 768px) {
  :root { --grid-cell: var(--grid-cell-mobile, 100px); --grid-margin: 24px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================
   ============================================================
   SITE LAYER
   Part 2 of 2. Everything a page needs that a brand manual does
   not: the container, the grid, the header and footer, forms,
   tables, dialogs, prose.

   Nothing here invents a value. Every size, colour and duration
   comes from the tokens above. If a number appears in this half
   that is not built from a token, it is a bug.
   ============================================================
   ============================================================ */


/* ============================================================
   STATE COLOUR, AS TEXT
   The three state colours in the manual are picked to read as marks
   — a dot, a fill, a band on a chart. As *text* on the ivory they
   fall short: the green measures 2.66:1 against the page, where 4.5
   is the floor for type this small, and a badge sets 11px mono in it.

   So the ramp keeps its values and gains three darker siblings for
   type only. Same hue, same saturation, lower lightness. Anything
   that draws — a chart series, a fill, an icon beside readable text
   — still uses --positive, --negative and --warning.

   In dark mode the originals already clear 8:1, so the two are the
   same colour there and the distinction costs nothing.

   MEASURED (worst ground, including each badge's own tint):
     --positive-ink  4.54:1     --negative-ink  4.51:1
     --warning-ink   4.50:1
   ============================================================ */

:root {
  --positive-ink: #007A50;
  --negative-ink: #C62841;
  --warning-ink:  #9A6100;
}
:root[data-theme="dark"] {
  --positive-ink: var(--positive);
  --negative-ink: var(--negative);
  --warning-ink:  var(--warning);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --positive-ink: var(--positive);
    --negative-ink: var(--negative);
    --warning-ink:  var(--warning);
  }
}

/* ============================================================
   BASE, CONTINUED
   The manual styles type. A page also has links, images, lists,
   rules and a focus ring.
   ============================================================ */

/* Links carry a 1px underline offset far enough to clear the
   descenders. Underline is not decoration here — it is the only
   thing distinguishing a link from body copy, because colour is
   reserved for state. Never remove it in running text. */
a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--line-strong);
  transition: text-decoration-color var(--reveal-in) var(--ease);
}
a:hover { text-decoration-color: var(--ink); }

/* A link that is a block — a card, a logo, a nav item — carries no
   underline, because its whole box is the target. */
a.u-plain, a.u-plain:hover { text-decoration: none; }

/* THE FOCUS RING.
   One ring, everywhere, for keyboard users only. The offset is what
   makes it readable on a zero-radius system: the ring sits outside
   the element rather than tracing its edge, so it never reads as a
   border that grew. Do not set outline:none anywhere without putting
   an equivalent back. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--border-rail) solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius);
}

/* Selection follows the accent, muted so a selected paragraph stays
   readable rather than becoming a block. */
::selection { background: var(--accent-tint); color: var(--ink); }

img, svg, video { max-width: 100%; height: auto; }
img { display: block; }

hr, .divider {
  height: 0;
  margin: var(--space-8) 0;
  border: 0;
  border-top: var(--border-hairline) solid var(--line);
}

p { margin: 0 0 var(--space-4); }
p:last-child { margin-bottom: 0; }

small { font-size: var(--size-body-sm); color: var(--ink-secondary); }

code, kbd, pre, samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
}
code {
  padding: 2px 5px;
  background: var(--surface-sunk);
  border: var(--border-hairline) solid var(--line);
}
pre {
  overflow-x: auto;
  margin: 0 0 var(--space-5);
  padding: var(--space-5);
  background: var(--surface-sunk);
  border: var(--border-hairline) solid var(--line);
  font-size: var(--size-spec);
  line-height: 1.6;
}
pre code { padding: 0; background: none; border: 0; }

/* Skip link. Off-screen until focused, then the first thing on the
   page. A keyboard user should never have to tab through a whole
   navigation to reach the content. */
.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: 100;
  padding: 10px 16px;
  background: var(--accent);
  color: var(--on-accent);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  transform: translateY(-200%);
}
.skip-link:focus { transform: translateY(0); }


/* ============================================================
   LAYOUT
   12 columns, 24px gutter, 1400px of content, 100px of margin.
   The margin collapses to 24px below 768px — a phone has no room
   to spend on air at the edges.
   ============================================================ */

.container {
  width: 100%;
  max-width: calc(var(--grid-content-max) + var(--grid-margin) * 2);
  margin-inline: auto;
  padding-inline: var(--grid-margin);
}

/* For running text. Roughly 70 characters a line, which is where
   the eye stops having to hunt for the start of the next one. */
.container--narrow { max-width: 760px; }
.container--wide   { max-width: none; }

/* Vertical rhythm between the bands of a page. One value, so two
   sections stacked never produce an accidental double gap. */
.section { padding-block: var(--space-8); }
.section--tight { padding-block: var(--space-7); }
.section--flush { padding-block: 0; }

/* A section cut into the plane. Remember rule 3: the surface is the
   extreme and the page is one step in, so this reads as cut, not
   floating — in dark mode it is darker than the page, not lighter. */
.section--surface {
  background: var(--surface);
  border-block: var(--border-hairline) solid var(--line);
}
.section--sunk { background: var(--surface-sunk); }

/* The 12-column grid itself. Children take .col-N. */
.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  gap: var(--grid-gutter);
}
.col-1  { grid-column: span 1; }
.col-2  { grid-column: span 2; }
.col-3  { grid-column: span 3; }
.col-4  { grid-column: span 4; }
.col-5  { grid-column: span 5; }
.col-6  { grid-column: span 6; }
.col-7  { grid-column: span 7; }
.col-8  { grid-column: span 8; }
.col-9  { grid-column: span 9; }
.col-10 { grid-column: span 10; }
.col-11 { grid-column: span 11; }
.col-12 { grid-column: span 12; }

/* Equal-width cards, the common case, without counting columns.
   auto-fit means they reflow on their own — no breakpoint needed. */
.cards {
  display: grid;
  gap: var(--grid-gutter);
  grid-template-columns: repeat(auto-fit, minmax(var(--card-min, 280px), 1fr));
}
.cards--2 { --card-min: 420px; }
.cards--4 { --card-min: 220px; }

/* A stack is vertical rhythm between siblings. The gap is the only
   thing that sets it, so no child ever needs a margin. */
.stack       { display: flex; flex-direction: column; gap: var(--space-5); }
.stack--tight{ gap: var(--space-3); }
.stack--loose{ gap: var(--space-7); }

/* A cluster is a row of things that wraps: buttons, tags, meta. */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}
.cluster--between { justify-content: space-between; }

/* Two columns that become one. The ratio is the asymmetric default
   from the manual — 7 and 5 of 12, not a half and a half. */
/* minmax(0, …) rather than a bare fr, and this is not a detail. A grid
   item defaults to min-width:auto, so one wide child — a code block, a
   long table, an unbreakable URL — pushes its whole column past the
   container instead of scrolling inside it. Every fr in this file is
   wrapped for that reason. */
.split {
  display: grid;
  gap: var(--space-8) var(--grid-gutter);
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  align-items: start;
}
.split--even    { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.split--reverse { direction: rtl; }
.split--reverse > * { direction: ltr; }


/* ============================================================
   PAGE FURNITURE
   ============================================================ */

/* ============================================================
   ICONS
   Lucide, at the version pinned in icons/tricsy-icons.svg. One local
   sprite, no CDN, no JavaScript, one cached request for the whole set.

       <svg class="icon" aria-hidden="true">
         <use href="/icons/tricsy-icons.svg#check"></use>
       </svg>

   The symbol carries geometry only; colour, size and stroke width all
   come from here, so an icon inherits the ink of whatever it sits in
   and flips with the mode like everything else.

   Stroke is 1.5, not Lucide's default 2 — the manual sets the lighter
   weight and it is what makes the icons sit with the type rather than
   shout over it.

   An icon that carries meaning on its own needs a label. An icon
   beside text that already says the same thing takes aria-hidden.
   ============================================================ */

.icon {
  width: 16px;
  height: 16px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: middle;
}
.icon--sm { width: 13px; height: 13px; }
.icon--lg { width: 20px; height: 20px; }
.icon--xl { width: 24px; height: 24px; }

/* Inside a button the icon takes the button's own colour, and the gap
   is already set by .btn — nothing else to do. */
.btn .icon { stroke-width: 1.75; }

/* THE LOGO.
   A mask, not an <img>. An <img> paints its own colours and cannot
   inherit anything, so it would need a second file for dark mode. A
   mask uses the SVG as a stencil and fills it with currentColor, so
   one file serves both modes and any ink you set on the parent.

   Set the height; the aspect ratio supplies the width. Never set both
   — the lockup and the symbol have different proportions.

   The minimums from the manual: 160px lockup, 96px small lockup,
   24px symbol. Below those the dot closes up and the mark stops
   being legible. */
.logo {
  display: block;
  /* Without this the box stretches to the width of a flex column and the
     mask centres itself inside it — the mark drifts to the middle of the
     footer. The aspect ratio sets the width; nothing else may. */
  align-self: start;
  height: 24px;
  aspect-ratio: 259.5 / 46.3;
  background-color: currentColor;
  -webkit-mask: url("tricsy-lockup.svg") no-repeat center / contain;
          mask: url("tricsy-lockup.svg") no-repeat center / contain;
}
.logo--ring {
  aspect-ratio: 361.41 / 88.72;
  -webkit-mask-image: url("tricsy-lockup-ring.svg");
          mask-image: url("tricsy-lockup-ring.svg");
}
.logo--symbol {
  aspect-ratio: 1;
  -webkit-mask-image: url("tricsy-symbol.svg");
          mask-image: url("tricsy-symbol.svg");
}
.logo--bare {
  aspect-ratio: 96 / 108;
  -webkit-mask-image: url("tricsy-symbol-bare.svg");
          mask-image: url("tricsy-symbol-bare.svg");
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--paper);
  border-bottom: var(--border-hairline) solid var(--line);
}

/* The bar is 72px so the lockup can sit at its 24px symbol minimum
   with real air around it. */
.site-header__bar {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-height: 72px;
}

/* The lockup inherits --ink through currentColor, so it flips with
   the mode and never needs a second file. Height, never width — the
   lockup and the symbol have different aspect ratios. */
.site-header__logo { display: flex; align-items: center; color: var(--ink); }
.site-header__logo .logo { height: 22px; }

.nav {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  margin-left: auto;
}
.nav__link {
  color: var(--ink-secondary);
  text-decoration: none;
  font-size: var(--size-body-sm);
  padding-block: var(--space-2);
  transition: color var(--reveal-in) var(--ease);
}
.nav__link:hover { color: var(--ink); }

/* The current page is marked with the rail, not with colour —
   colour is a signal and a location is not a state. */
.nav__link[aria-current="page"] {
  color: var(--ink);
  box-shadow: inset 0 -2px 0 var(--ink);
}

/* Burger. Hidden on desktop, and it is a real button with a real
   label, not a div with three spans. */
.nav-toggle {
  display: none;
  margin-left: auto;
  padding: var(--space-2);
  background: none;
  border: 0;
  color: var(--ink);
  cursor: pointer;
}
.nav-toggle svg { display: block; }

/* ============================================================
   SITE LAYER — NAV MENU
   The Product entry is a <details>, so the open state lives in the
   markup and the element answers the keyboard without help. The panel
   hangs off the summary and stays narrow enough to clear the right
   edge of the bar; on a phone there is no panel at all, it unfolds in
   place inside the navigation sheet.
   ============================================================ */
.nav-menu { position: relative; }

/* Both markers have to go: list-style covers Firefox and the modern
   engines, ::-webkit-details-marker is still what Safari reads. */
.nav-menu__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  cursor: pointer;
}
.nav-menu__summary::-webkit-details-marker { display: none; }
.nav-menu__summary::marker { content: ""; }
.nav-menu[open] > .nav-menu__summary { color: var(--ink); }

.nav-menu__chevron {
  width: 14px;
  height: 14px;
  transition: transform var(--reveal-in) var(--ease);
}
.nav-menu[open] .nav-menu__chevron { transform: rotate(180deg); }

.nav-menu__panel {
  position: absolute;
  top: calc(100% + var(--space-3));
  left: calc(var(--space-4) * -1);
  z-index: 10;
  width: 300px;
  padding: var(--space-4);
  background: var(--paper);
  border: var(--border-hairline) solid var(--line);
}

.nav-menu__label {
  margin: var(--space-4) 0 var(--space-2);
  padding-inline: var(--space-3);
}
.nav-menu__label:first-child { margin-top: 0; }

.nav-menu__link {
  display: block;
  padding: var(--space-2) var(--space-3);
  color: var(--ink-secondary);
  text-decoration: none;
  font-size: var(--size-body-sm);
  transition: color var(--reveal-in) var(--ease),
              background var(--reveal-in) var(--ease);
}
.nav-menu__link:hover,
.nav-menu__link:focus-visible {
  color: var(--ink);
  background: var(--surface-sunk);
}

/* The number is the same count the pillar cards carry, so the menu and
   the section agree on what 03 is. */
.nav-menu__index {
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  color: var(--ink-muted);
  margin-right: var(--space-2);
}
.nav-menu__note {
  display: block;
  margin-top: 2px;
  font-size: var(--size-mono-label);
  color: var(--ink-muted);
}

/* ============================================================
   SITE LAYER — LANGUAGE MENU
   The same <details> as the Product menu, so it needs almost nothing
   of its own: a flag, a narrower panel, and a right edge rather than a
   left one, because it sits at the end of the bar and a panel opening
   rightwards would leave the viewport.
   ============================================================ */
.lang-menu__panel {
  left: auto;
  right: 0;
  width: 208px;
}
.lang-menu__link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* The one place colour is allowed on the page. A flag carries its meaning
   in its colour, so it cannot be desaturated the way a partner logo is —
   see the note at the top of flags.svg. The hairline is inside the sprite,
   which is why there is no border here. */
.flag {
  flex: none;
  width: 21px;
  height: 14px;
  display: block;
}

/* On the bar the trigger is a flag and a chevron, nothing else — the row is
   already carrying four other items and the language name is the one label a
   flag can actually replace. In the mobile sheet the name comes back: there
   every row is a written label, and a lone flag among them reads as a stray
   icon rather than a control. The accessible name carries the language in
   both cases, so nothing is lost when the text is not drawn. */
.lang-menu__current { font-size: var(--size-body-sm); }
/* No frame here, deliberately. A border in this bar marks a BUTTON — the
   mode switch acts the moment it is clicked, so it is drawn as something to
   press. Product and this language menu are not buttons: they are both a
   <details> with a chevron that opens a list, and two identical widgets have
   to look identical. The flag needs no box to be findable either — it is the
   only colour on the page. */
@media (min-width: 769px) {
  .lang-menu__current { display: none; }
  .lang-menu > .nav-menu__summary { gap: var(--space-2); }
}

/* The language a reader is already on stays in the list — removing it
   would make the menu shift under the pointer — but it stops inviting
   a click that goes nowhere. */
.lang-menu__link[aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
}

@media (max-width: 768px) {
  /* In the sheet the panel is in flow like the Product one, and the
     summary fills the row so the chevron reaches the far edge. */
  .lang-menu__panel { width: auto; }
  .lang-menu__link { padding-inline: var(--space-4); font-size: var(--size-body); }

  /* The sheet's summary rule is space-between, which is right for a row of two
     but spreads a row of three: the flag went left, the chevron right and the
     language name floated into the middle. The name claims the slack instead,
     so it stays beside its flag and the chevron keeps the far edge. */
  .lang-menu > .nav-menu__summary { gap: var(--space-3); }
  .lang-menu__current { margin-inline-end: auto; }
}

.site-footer {
  padding-block: var(--space-8) var(--space-7);
  background: var(--surface);
  border-top: var(--border-hairline) solid var(--line);
}
.site-footer__grid {
  display: grid;
  gap: var(--space-7) var(--grid-gutter);
  grid-template-columns: minmax(0, 2fr) repeat(3, minmax(0, 1fr));
}
.site-footer__col { display: flex; flex-direction: column; gap: var(--space-3); }
.site-footer__col a {
  color: var(--ink-secondary);
  text-decoration: none;
  font-size: var(--size-body-sm);
}
.site-footer__col a:hover { color: var(--ink); text-decoration: underline; }
.site-footer__base {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: space-between;
  margin-top: var(--space-8);
  padding-top: var(--space-5);
  border-top: var(--border-hairline) solid var(--line);
}


/* ============================================================
   HERO
   The blueprint field goes here and nowhere else. Content inside a
   .blueprint must carry .blueprint__over or the nodes will sit on
   top of the headline.
   ============================================================ */

.hero { position: relative; overflow: hidden; padding-block: var(--space-9); }
.hero--sub { padding-block: var(--space-7); }
/* 900px was set against the English headline, which fills two lines of the
   display face almost exactly. It left no room for a language that says the
   same thing in more words: Polish went to three lines, French to three.
   Measured across all eight, 1100px is the point where every one of them
   lands on two. The lede below carries its own 60ch cap, so widening this
   moves the headline and nothing else. Re-measure when a headline changes. */
.hero__inner { max-width: 1100px; }
.hero__title { margin-bottom: var(--space-5); }
.hero__title--display {
  font-size: var(--size-display);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-display);
}

/* The opening paragraph. One step up from body, secondary ink, and
   capped at 60 characters so it stays a lede and not a column. */
.lede {
  max-width: 60ch;
  font-size: clamp(1.0625rem, 1rem + 0.28vw, 1.25rem);
  line-height: 1.5;
  color: var(--ink-secondary);
}

/* The section eyebrow: mono, uppercase, three words maximum. Past
   that limit use .label-spec — see rule 7. */
.eyebrow {
  display: block;
  margin-bottom: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* Section head: eyebrow, heading, one paragraph. The pattern that
   opens most bands on the page. */
.section-head { max-width: 720px; margin-bottom: var(--space-7); }
.section-head > * + * { margin-top: var(--space-4); }


/* ============================================================
   VIDEO FACADE

   What loads is a poster we host. The YouTube iframe is built in
   JavaScript on click and never before — see tricsy.js. The markup
   is an <a> with a real href, not a <button>, so with the script
   off the click still reaches the video.

   .video holds the 16:9 box and .video > * fills it, so the same
   rules dress the poster now and the iframe after the swap. Neither
   needs to know the other exists.

   overflow:hidden belongs on the poster, NOT on .video — the focus
   ring is drawn outside the link and a clipping parent would eat it.
   ============================================================ */

.video {
  position: relative;
  aspect-ratio: 16 / 9;
  max-width: 880px;
  margin-inline: auto;
  /* --surface, not --surface-sunk: the section around it is already sunk,
     so the two would be the same colour and the frame would be an invisible
     hole for as long as the lazy poster takes to arrive. */
  background: var(--surface);
  border: var(--border-hairline) solid var(--line);
}

/* Whatever is inside fills the frame — the link now, the iframe after
   the click. Neither needs to know the other exists, and the frame
   keeps its size through the swap. */
.video > * {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.video__facade {
  color: inherit;
  text-decoration: none;
}

/* The eyebrow names the building and links to it. Left to the global
   <a> rule the name would jump to full ink inside a muted label and
   shout louder than the heading under it, so it keeps the label's
   colour and earns its affordance from the underline instead. */
.eyebrow a {
  color: inherit;
  text-decoration-color: var(--line-strong);
}

.eyebrow a:hover { color: var(--ink); text-decoration-color: var(--ink); }

/* object-fit stays although the poster is already 16:9 — it is what keeps a
   future replacement from stretching. overflow on an <img> did nothing. */
.video__poster {
  object-fit: cover;
  transition: opacity var(--reveal-in) var(--ease);
}

.video__facade:hover .video__poster,
.video__facade:focus-visible .video__poster { opacity: 0.88; }

/* The chip is centred on the poster and is decoration: the link
   around it already carries the name. */
.video__play {
  position: absolute;
  /* transform, not the independent translate/scale properties: those need
     Safari 14.1+ / Chrome 104+, and where they are missing the chip does not
     degrade gracefully — it sits with its corner on the centre point. */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 72px;
  height: 72px;
  display: grid;
  place-items: center;
  background: var(--surface);
  border: var(--border-hairline) solid var(--line-strong);
  color: var(--ink);
  transition: transform var(--reveal-in) var(--ease);
}

.video__facade:hover .video__play { transform: translate(-50%, -50%) scale(1.06); }

/* The accessible name of the link. Visible under the frame rather
   than hidden, because "90 seconds" is worth knowing before the
   click, not after. */
.video__label {
  position: absolute;
  inset: auto auto var(--space-4) var(--space-4);
  width: auto;
  height: auto;
  padding: 6px 12px;
  background: var(--surface);
  border: var(--border-hairline) solid var(--line-strong);
  color: var(--ink);
}


/* The claim under the player. Reads after the film, not before it — the
   lede above says what the video is, this says what it means. */
.video__claim {
  max-width: 880px;
  margin: var(--space-5) auto 0;
  text-align: center;
  text-wrap: balance;
}

/* Under ~400px the 16:9 frame is only about 150px tall, and a 72px chip
   centred in it reaches the label sitting at the bottom — measured: they
   overlap at 320px in every language. Shrink both rather than drop the
   label, which carries the link's accessible name. */
@media (max-width: 400px) {
  .video__play { width: 52px; height: 52px; }
  .video__play svg { width: 22px; height: 22px; }
  .video__label { inset: auto auto 8px 8px; padding: 4px 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .video__poster, .video__play { transition: none; }
  .video__facade:hover .video__play { transform: translate(-50%, -50%); }
}


/* ============================================================
   CARDS, METRICS, MEDIA
   ============================================================ */

.card--link {
  display: block;
  color: inherit;
  text-decoration: none;
  transition: border-color var(--reveal-in) var(--ease);
}
.card--link:hover { border-color: var(--line-strong); }
.card--flush { padding: 0; }
.card__body { padding: var(--space-6); }

/* A card head that reads as machine metadata above human copy. */
.card__meta {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-6);
  background: var(--surface-sunk);
  border-bottom: var(--border-hairline) solid var(--line);
}

/* The KPI tile. Figures take tabular numerals so a column of them
   lines up on the decimal — this is the whole reason the token
   exists. The delta is the one place a number carries colour. */
.metric { display: flex; flex-direction: column; gap: var(--space-2); }
.metric__label {
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  color: var(--ink-muted);
}
.metric__value {
  font-size: clamp(2rem, 1.5rem + 2vw, 2.75rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.metric__delta {
  font-family: var(--font-mono);
  font-size: var(--size-spec);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}
.metric__delta--up   { color: var(--positive-ink); }
.metric__delta--down { color: var(--negative-ink); }

figure { margin: 0 0 var(--space-6); }
figcaption {
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--size-spec);
  letter-spacing: var(--tracking-spec);
  color: var(--ink-secondary);
}


/* ============================================================
   TABLE
   ============================================================ */

/* The wrapper is not optional. A table cannot shrink below its
   content, so without this the whole page scrolls sideways on a
   phone instead of the table doing it. */
.table-wrap {
  overflow-x: auto;
  border: var(--border-hairline) solid var(--line);
  background: var(--surface);
}
.table-wrap > table { border: 0; }

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  font-size: var(--size-body-sm);
}
thead th {
  padding: var(--space-3) var(--space-4);
  background: var(--surface-sunk);
  border-bottom: var(--border-hairline) solid var(--line-strong);
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  font-weight: 400;
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  text-align: left;
  white-space: nowrap;
}
tbody td {
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-hairline) solid var(--line);
  color: var(--ink-secondary);
  vertical-align: top;
}
tbody tr:last-child td { border-bottom: 0; }

/* Figures right-align so the decimal points stack. */
.td-num, th.td-num { text-align: right; font-variant-numeric: tabular-nums; }
caption {
  padding-bottom: var(--space-3);
  color: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: var(--size-spec);
  text-align: left;
}


/* ============================================================
   FORMS
   .field already covers input. These are the rest of the controls
   and the structure around them.
   ============================================================ */

.form-row { display: flex; flex-direction: column; gap: var(--space-2); }
.form-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.form-row--full { grid-column: 1 / -1; }

label, .label {
  font-size: var(--size-body-sm);
  font-weight: 500;
  color: var(--ink);
}

/* The required mark is the one ornament allowed on a label. */
.label__required { color: var(--negative-ink); }

textarea.field { min-height: 140px; resize: vertical; line-height: var(--leading-body); }

/* The chevron is drawn inline rather than loaded, so it inherits the
   ink and costs no request. */
select.field {
  appearance: none;
  padding-right: 36px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23766D63' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
}

/* Checkbox and radio keep the native control — it is accessible,
   it is keyboard-operable, and accent-color repaints it for free.
   Do not rebuild these out of divs. */
/* Two shapes, both valid: a <label class="choice"> wrapping the input, or a
   <div class="choice"> holding an input and a <label for>. Use the second
   whenever the text contains a link — inside a label, a click on the link
   would toggle the box as well as follow the link. */
.choice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--size-body-sm);
  color: var(--ink-secondary);
  cursor: pointer;
}
.choice label { font-size: inherit; font-weight: 400; color: inherit; cursor: pointer; }
.choice input {
  flex: none;
  width: 16px;
  height: 16px;
  margin: 3px 0 0;
  accent-color: var(--accent);
}

.help  { font-size: var(--size-body-sm); color: var(--ink-muted); }
.error {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--size-body-sm);
  color: var(--negative-ink);
}

/* Focus on a field: the ring plus an ink border. The manual's
   .field:focus removed the outline; the ring above puts it back. */
.field:focus-visible { border-color: var(--accent); }

fieldset { margin: 0; padding: 0; border: 0; }
legend {
  padding: 0 0 var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  color: var(--ink-muted);
}


/* ============================================================
   NOTICES, TABS, ACCORDION, DIALOG, PAGINATION
   ============================================================ */

/* An alert is a signal, so it carries colour — on a hairline and an
   icon, never as a filled background. Rule 5. */
.alert {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: var(--border-hairline) solid var(--line-strong);
  border-left-width: var(--border-rail);
  font-size: var(--size-body-sm);
  color: var(--ink-secondary);
}
.alert__icon { flex: none; margin-top: 2px; color: var(--ink-muted); }
.alert--positive { border-left-color: var(--positive); }
.alert--positive .alert__icon { color: var(--positive); }
.alert--negative { border-left-color: var(--negative); }
.alert--negative .alert__icon { color: var(--negative); }
.alert--warning  { border-left-color: var(--warning); }
.alert--warning .alert__icon { color: var(--warning); }

.tabs__list {
  display: flex;
  gap: var(--space-6);
  border-bottom: var(--border-hairline) solid var(--line);
}
.tab {
  padding: var(--space-3) 0;
  margin-bottom: -1px;
  background: none;
  border: 0;
  border-bottom: var(--border-rail) solid transparent;
  color: var(--ink-muted);
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  font-weight: 500;
  cursor: pointer;
}
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); }
.tabs__panel { padding-top: var(--space-6); }
.tabs__panel[hidden] { display: none; }

/* The accordion is <details>. It opens without a line of JavaScript,
   it is keyboard-operable and screen readers announce it correctly.
   Do not replace it with a button and a class. */
.accordion { border-top: var(--border-hairline) solid var(--line); }
.accordion details { border-bottom: var(--border-hairline) solid var(--line); }
.accordion summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5) 0;
  font-size: var(--size-h4);
  font-weight: 600;
  letter-spacing: var(--tracking-h4);
  cursor: pointer;
  list-style: none;
}
.accordion summary::-webkit-details-marker { display: none; }
/* A plus that becomes a minus. Two rules, no icon file. */
.accordion summary::after {
  content: "";
  flex: none;
  width: 14px;
  height: 14px;
  background:
    linear-gradient(var(--ink-muted), var(--ink-muted)) center / 14px 1px no-repeat,
    linear-gradient(var(--ink-muted), var(--ink-muted)) center / 1px 14px no-repeat;
  transition: transform var(--reveal-out) var(--ease);
}
.accordion details[open] summary::after {
  background: linear-gradient(var(--ink), var(--ink)) center / 14px 1px no-repeat;
  transform: rotate(180deg);
}
.accordion__body { padding-bottom: var(--space-5); color: var(--ink-secondary); }
.accordion__body > *:last-child { margin-bottom: 0; }

/* Native <dialog>. showModal() gives focus trapping, Escape and the
   backdrop for free — every one of which is a bug waiting to happen
   in a hand-rolled modal. */
dialog {
  width: min(560px, calc(100vw - var(--space-8)));
  padding: 0;
  background: var(--surface);
  border: var(--border-hairline) solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--ink);
}
dialog::backdrop { background: rgb(0 0 0 / 0.45); }
.dialog__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-6);
  border-bottom: var(--border-hairline) solid var(--line);
}
.dialog__body { padding: var(--space-6); }
.dialog__foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-6);
  background: var(--surface-sunk);
  border-top: var(--border-hairline) solid var(--line);
}
.dialog__close {
  padding: var(--space-2);
  background: none;
  border: 0;
  color: var(--ink-muted);
  cursor: pointer;
  line-height: 0;
}
.dialog__close:hover { color: var(--ink); }

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--size-spec);
  color: var(--ink-muted);
}
.breadcrumb a { color: var(--ink-muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--ink); }
.breadcrumb li + li::before { content: "/"; margin-right: var(--space-2); color: var(--line-strong); }
.breadcrumb ol { display: contents; }
.breadcrumb li { display: flex; align-items: center; }

.pagination { display: flex; gap: var(--space-2); align-items: center; }
.pagination a, .pagination span {
  min-width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--space-2);
  border: var(--border-hairline) solid var(--line);
  color: var(--ink-secondary);
  text-decoration: none;
  font-size: var(--size-body-sm);
  font-variant-numeric: tabular-nums;
}
.pagination a:hover { border-color: var(--line-strong); color: var(--ink); }
.pagination [aria-current="page"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

/* A list with no bullets, for navigation and stacks of links. */
.list-plain { margin: 0; padding: 0; list-style: none; }


/* ============================================================
   PROSE
   Long-form copy: an article, a legal page, a changelog. Apply to
   the wrapper and stop styling the elements inside it.
   ============================================================ */

.prose { max-width: 68ch; color: var(--ink-secondary); }
.prose > * + * { margin-top: var(--space-5); }
.prose h2 { margin-top: var(--space-8); color: var(--ink); }
.prose h3 { margin-top: var(--space-7); color: var(--ink); }
.prose h2 + *, .prose h3 + * { margin-top: var(--space-4); }
.prose p { margin-bottom: 0; }
.prose ul, .prose ol { padding-left: var(--space-5); }
.prose li + li { margin-top: var(--space-2); }
.prose li::marker { color: var(--ink-muted); }
.prose strong { color: var(--ink); font-weight: 600; }
.prose blockquote {
  margin: 0;
  padding-left: var(--space-5);
  border-left: var(--border-rail) solid var(--line-strong);
  color: var(--ink);
}
.prose img { margin-block: var(--space-6); }
.prose a { color: var(--ink); }


/* ============================================================
   THE MODE SWITCH
   The control itself. The behaviour is in tricsy.js.
   ============================================================ */

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  background: none;
  border: var(--border-hairline) solid var(--line);
  color: var(--ink-secondary);
  cursor: pointer;
  transition: border-color var(--reveal-in) var(--ease), color var(--reveal-in) var(--ease);
}
.theme-toggle:hover { border-color: var(--line-strong); color: var(--ink); }
.theme-toggle svg { display: block; }

/* One button, two icons: whichever mode is active hides its own.
   Both selectors are scoped to .theme-toggle so they outrank the
   `.theme-toggle svg { display: block }` above — a class on its own
   loses to a class plus an element, and the icon would never hide. */
.theme-toggle .theme-toggle__sun  { display: none; }
:root[data-theme="dark"] .theme-toggle .theme-toggle__sun  { display: block; }
:root[data-theme="dark"] .theme-toggle .theme-toggle__moon { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .theme-toggle__sun  { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .theme-toggle__moon { display: none; }
}


/* ============================================================
   UTILITIES
   A short list on purpose. If a page needs a utility that is not
   here, it usually needs a component instead.
   ============================================================ */

.u-center      { text-align: center; }
.u-center .lede, .u-center .section-head { margin-inline: auto; }
.u-muted       { color: var(--ink-muted); }
.u-secondary   { color: var(--ink-secondary); }
.u-mono        { font-family: var(--font-mono); }
.u-tabular     { font-variant-numeric: tabular-nums; }
.u-full        { width: 100%; }
.u-nowrap      { white-space: nowrap; }

/* Visible to a screen reader, invisible on screen. For the label a
   sighted user reads from an icon and a blind user cannot. */
.u-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* ============================================================
   BREAKPOINTS
   Three, and they all move in the same direction: the grid folds,
   the margin shrinks, the navigation collapses. The type needs no
   breakpoint at all — it is fluid.
   ============================================================ */

@media (max-width: 1024px) {
  :root { --grid-margin: 48px; }
  .split, .split--even { grid-template-columns: minmax(0, 1fr); }
  .site-footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .col-4, .col-5, .col-6, .col-7, .col-8 { grid-column: span 6; }
}

@media (max-width: 768px) {
  /* The core layer already sets this at the same breakpoint, but that
     block sits earlier in the file than the 1024px rule above — and
     between two equally specific rules the later one wins. Repeating
     it here is what makes 24px actually apply on a phone. */
  :root { --grid-margin: 24px; }

  .section { padding-block: var(--space-8); }
  .hero    { padding-block: var(--space-8); }
  .grid > [class*="col-"] { grid-column: span 12; }
  .site-footer__grid { grid-template-columns: minmax(0, 1fr); }

  /* The navigation becomes a panel under the bar. It is hidden with
     the hidden attribute, not with a class, so the markup itself
     says whether it is open. */
  .nav-toggle { display: block; }
  .nav {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin: 0;
    padding: var(--space-3) var(--grid-margin) var(--space-5);
    background: var(--paper);
    border-bottom: var(--border-hairline) solid var(--line);
  }
  .nav[hidden] { display: none; }
  .nav__link {
    padding-block: var(--space-4);
    border-bottom: var(--border-hairline) solid var(--line);
    font-size: var(--size-body);
  }
  .nav__link[aria-current="page"] { box-shadow: none; font-weight: 600; }
  .nav .btn, .nav .theme-toggle { margin-top: var(--space-4); align-self: flex-start; }

  /* In the sheet there is nothing to hang a panel off, so the menu stops
     floating and unfolds where it stands. The summary keeps the same rule
     under it as a plain link, so the row reads as one list. */
  /* inline-flex shrinks to its content, so space-between has nothing to
     push apart — the row has to fill the sheet before the chevron can sit
     at the far edge. */
  .nav-menu__summary { display: flex; justify-content: space-between; }
  .nav-menu__panel {
    position: static;
    width: auto;
    padding: var(--space-3) 0 var(--space-4);
    background: none;
    border: 0;
    border-bottom: var(--border-hairline) solid var(--line);
  }
  .nav-menu__link { padding-inline: var(--space-4); font-size: var(--size-body); }
  .nav-menu__label { padding-inline: var(--space-4); }
}

@media (max-width: 480px) {
  .cards { grid-template-columns: minmax(0, 1fr); }
  .cluster > .btn { width: 100%; justify-content: center; }
}

/* Above the mobile breakpoint the panel is always a row, whatever the
   hidden attribute says — the toggle is gone, so nothing would ever
   put it back. */
@media (min-width: 769px) {
  .nav[hidden] { display: flex; }
}


/* ============================================================
   PRINT
   The blueprint field, the navigation and the mode switch are
   screen furniture. Paper gets the content.
   ============================================================ */

@media print {
  .site-header, .site-footer, .nav, .theme-toggle, .skip-link,
  .blueprint__hit, .nav-toggle { display: none !important; }
  :root { --paper: #fff; --surface: #fff; --ink: #000; --ink-secondary: #333; }
  body { background: #fff; }
  .section { padding-block: var(--space-5); }
  a { text-decoration: underline; }
  /* A printed link is a dead end unless the address comes with it. */
  .prose a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }
}

/* ============================================================
   SITE LAYER — CAPABILITY PILLARS
   Five cards. Explicit column counts, not auto-fit, so five items
   collapse evenly instead of stranding a lone card: 5 across on a
   wide screen, then 3 + 2, then 2, then 1 — each step a little airier.
   ============================================================ */
.pillars {
  display: grid;
  gap: var(--grid-gutter);
  grid-template-columns: minmax(0, 1fr);
}
/* At five columns a pillar heading gets about 170px, which is narrower than a
   single German compound: "Besucheranmeldung" alone wanted 262px and ran out
   through the side of its card. Hyphenation is the typographic answer rather
   than a smaller heading or fewer columns — every page carries a correct lang
   attribute, so the browser breaks the word where the language actually allows
   it. break-word is the floor for a viewer with no dictionary for that
   language: an ugly break beats text outside the box.

   English was already 3px over on "Anonymous audience measurement" before any
   of this. German only made it obvious. */
/* The floor, everywhere: a word wider than its column breaks rather than
   leaving the card. Ugly, but text outside the box is worse. */
.card h3,
.card p,
.card__more { overflow-wrap: break-word; }

/* Hyphenation costs a hyphen, so it is asked for only where a column is
   genuinely too narrow for the language rather than everywhere.

   A four-up grid is narrow at every width — about 220px — which is less than
   a single German compound: "Ausweisdokument" in a step heading wanted 236.
   So that grid hyphenates always.

   Three-up columns are wide enough until the viewport tightens. Above 1024
   they were buying nothing, and English was breaking "ex-perience" in a 400px
   card to pay for it. Every page carries a correct lang attribute, so the
   browser breaks where the language allows rather than mid-syllable. */
.cards--4 h3 { hyphens: auto; }

@media (max-width: 1023px) {
  .card h3,
  .card p,
  .card__more { hyphens: auto; }
}

@media (min-width: 481px)  { .pillars { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 768px)  { .pillars { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* No fifth column. Three is the widest the pillars go, which lands them as
   3 + 2 on a desktop.

   The five-across row was set out in English and only just held it: a
   heading got about 170px, and "Anonymous audience measurement" was already
   3px over the edge before the site had a second language. German made that
   undeniable — "Besucheranmeldung" alone wants 262px — and at three columns
   every language gets a heading it can set without breaking a word. The
   hyphenation rule above stays as the floor for narrow viewports. */

/* ============================================================
   SITE LAYER — LOGO STRIP
   A hairline matrix of partner marks. One component, three uses:
   the integrations we speak to, the cameras we drive, the buildings
   that run us. Monochrome by rule 4 — colour never enters the plane,
   so an image logo is desaturated to a single ink and only lifts to
   full weight on hover.

   The hairlines are drawn by each cell, not by the container: a 1px
   spread shadow on the item plus a 1px gap, so two neighbours meet in
   a single line and the outer edge closes itself. An earlier version
   painted a --line ground under the grid instead, which was fine only
   while the item count happened to fill the last row — nine marks in
   an eight-column grid left seven empty cells glowing in line colour.
   Drawing per cell means any count works at any width, with no
   nth-child arithmetic and nothing to retune when a brand is added.
   ============================================================ */
.logo-strip {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  gap: var(--border-hairline);

  /* The column count is declared, not discovered. A fluid auto-fit picks the
     most columns that fit and leaves whatever does not divide stranded on a
     row of its own — eight marks and then a nineth, alone. Since a strip only
     looks right when the count and the columns agree, each strip states its
     own. These three are for nine marks: three-by-three fills exactly, and the
     widest plane takes five and carries one empty cell, which still reads as a
     strip where a square block would not. */
  --cols:    2;
  --cols-md: 3;
  --cols-lg: 5;
}
@media (min-width: 600px)  { .logo-strip { grid-template-columns: repeat(var(--cols-md), minmax(0, 1fr)); } }
@media (min-width: 1100px) { .logo-strip { grid-template-columns: repeat(var(--cols-lg), minmax(0, 1fr)); } }

/* Three marks: one row from tablet up, stacked on a phone rather than two
   and a straggler. */
.logo-strip--trio { --cols: 1; --cols-md: 3; --cols-lg: 3; }

/* Four marks: two and two, then one row. Four divides at every step, so
   nothing is ever left on a line of its own. */
.logo-strip--quad { --cols: 2; --cols-md: 2; --cols-lg: 4; }
.logo-strip__item {
  /* --cell-bg is the cell's own colour, named because a knocked-out logo has
     to paint with it: KONE's glyphs are cut out of its plates, so they follow
     the cell through hover and through both themes instead of freezing on one
     value. Every state below sets it. */
  --cell-bg: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: 104px;
  padding: var(--space-5) var(--space-4);
  background: var(--cell-bg);
  color: var(--ink-muted);
  text-align: center;
  box-shadow: 0 0 0 var(--border-hairline) var(--line);
  transition: color var(--reveal-in) var(--ease),
              background var(--reveal-in) var(--ease);
}
.logo-strip__item:hover {
  color: var(--ink);
  --cell-bg: var(--surface-sunk);
}

/* Wordmark fallback, used until a real logo file lands. It has to
   read as a mark and not as body copy, so it takes the display face
   at optical size with tight tracking. Colour is currentColor, which
   is why it follows the item into full ink on hover and needs no
   dark-mode rule of its own. */
.logo-strip__mark {
  font-family: var(--font-display);
  font-size: var(--size-h4);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
  white-space: nowrap;
}

/* Drop-in image logo. Swap the <span class="logo-strip__mark"> for
   <img class="logo-strip__mark logo-strip__mark--img" src="assets/logos/…">
   and the colour is stripped so it sits in the monochrome plane; it
   lifts to full ink on hover. NOTE: a dark logo file needs an invert
   in dark mode — handle that per file when the real assets arrive,
   because some marks already ship white. */
.logo-strip__mark--img {
  max-height: 34px;
  width: auto;
  max-width: 100%;
  filter: invert(var(--logo-ink-invert)) opacity(0.72);
  transition: filter var(--reveal-in) var(--ease);
}
.logo-strip__item:hover .logo-strip__mark--img { filter: invert(var(--logo-ink-invert)) opacity(1); }

/* A logo that ships as SVG goes in the markup rather than through <img>: an
   <img> renders the file as its own document, where currentColor resolves to
   black and never reaches the cell. Inline, the mark inherits the cell's ink
   and the whole dark-mode question disappears. */
.logo-strip__mark--svg {
  height: 26px;
  width: auto;
  max-width: 100%;
  opacity: 0.72;
  transition: opacity var(--reveal-in) var(--ease);
}
.logo-strip__item:hover .logo-strip__mark--svg { opacity: 1; }

/* One mark drawn at the shared height can still read smaller than the rest:
   Lakeside Park is a light wordmark with a symbol and a wide gap, so at 26px
   it carries the same area as its neighbours and none of their presence.
   Optical size, not measured size, is what the eye compares. */
.logo-strip__mark--lg { height: 32px; }
/* Sits directly above a strip and names what it is a strip OF. It takes the
   air that would otherwise have gone above the strip, and the strip then
   takes none, so the label and the marks read as one block instead of the
   label drifting up against whatever is above them. */
.logo-strip__label { margin: var(--space-7) 0 var(--space-3); }
.logo-strip__label:first-child { margin-top: 0; }
.logo-strip__label + .logo-strip { margin-top: 0; }

.logo-strip__aside { margin: var(--space-5) 0 0; max-width: 58ch; }

/* Opening a section, the strip inherits the section-head's margin. Following
   anything else it has none of its own, and sat flush against the block above. */
* + .logo-strip { margin-top: var(--space-7); }

/* Captioned strip. Four marks that are not doing the same job cannot sit in
   a bare row — the row would claim they are equivalent. The caption is the
   smallest honest way to say what each one actually does. */
/* Grid rather than a column flex: the first row is a fixed 34px, so every
   caption sits on the same line no matter how tall its mark is. Constraining
   the marks to a common height instead would stretch a wide lockup and shrink
   a compact one — they have to stay optically balanced, not height-matched. */
.logo-strip--captioned .logo-strip__item {
  display: grid;
  grid-template-rows: 34px auto;
  align-items: center;
  justify-items: center;
  gap: var(--space-3);
  min-height: 132px;
}
.logo-strip--captioned .logo-strip__mark { max-height: 30px; }
/* A way out of a card that has more behind it. Pinned to the bottom with
   margin-top:auto so it sits on one line across a row of cards whose copy
   runs to different lengths. */
.card__more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-3);
  color: var(--ink);
  text-decoration: none;
  font-size: var(--size-body-sm);
  font-weight: 600;
}
.card__more .icon { transition: transform var(--reveal-in) var(--ease); }
.card__more:hover .icon { transform: translateX(3px); }

/* ============================================================
   SITE LAYER — SPEC LIST
   A <dl> of hardware facts. Two columns where there is room, so the
   labels form a readable rail down the left; stacked on a phone,
   because a 90px label column turns every value into a ladder.
   ============================================================ */
.spec-list {
  margin: 0;
  border-top: var(--border-hairline) solid var(--line);
}
.spec-list__row {
  display: grid;
  gap: var(--space-1) var(--space-5);
  padding-block: var(--space-4);
  border-bottom: var(--border-hairline) solid var(--line);
}
.spec-list dt {
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  text-transform: uppercase;
  color: var(--ink-muted);
}
.spec-list dd {
  margin: 0;
  color: var(--ink-secondary);
  font-size: var(--size-body-sm);
}
@media (min-width: 600px) {
  .spec-list__row { grid-template-columns: 148px minmax(0, 1fr); align-items: baseline; }
}

/* The kiosk render is a tall portrait object with transparent ground. It
   gets a sunk panel to stand on rather than floating in the section, and a
   cap so it cannot tower over the column of text beside it. */
.kiosk-shot {
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}
/* The hero pairs the sentence with the object. 7:5 while there is room, so
   the words lead and the unit sits beside them; stacked below the buttons on
   anything narrower, because a portrait render squeezed into half a phone is
   worth less than the copy it would be stealing from. */
.hero-split { display: grid; gap: var(--space-7) var(--grid-gutter); align-items: center; }
@media (min-width: 900px) {
  .hero-split { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
}
.kiosk-shot img {
  display: block;
  width: auto;
  max-width: 100%;
  margin-inline: auto;
  /* Close to the height of the text column beside it. Left at the render's
     own 876px the figure was taller than the text and stretched the section
     to fit; capped too short and the centring left a band of empty paper
     above and below it. */
  max-height: 540px;
}

/* ============================================================
   FIELD-OF-VIEW PANEL

   A themed screenshot, so two files and the same three-state swap
   the dashboard uses — the toggle in the header has to win, not just
   the OS setting.
   ============================================================ */

.fov { margin: 0; }

.fov img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--border-hairline) solid var(--line);
}

/* Scoped to .fov so it outranks `.fov img { display: block }` above — a class
   on its own loses to a class plus an element, and the dark shot would never
   hide. Same shape as the .theme-toggle icons. */
.fov .fov__dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .fov__light { display: none; }
  :root:not([data-theme="light"]) .fov__dark  { display: block; }
}
:root[data-theme="dark"]  .fov__light { display: none; }
:root[data-theme="dark"]  .fov__dark  { display: block; }
:root[data-theme="light"] .fov__light { display: block; }
:root[data-theme="light"] .fov__dark  { display: none; }

/* ============================================================
   LIMIT GRID

   Seven short negatives. As a list in a narrow column they were a
   screen of scrolling; in a grid they are one block the eye takes in
   at once, which is how a reader checks a list of things a product
   does not do.
   ============================================================ */

.limit-grid {
  display: grid;
  gap: var(--space-4) var(--grid-gutter);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  list-style: none;
  padding: 0;
  margin: 0;
  color: var(--ink-muted);
}

.limit-grid > li {
  padding-top: var(--space-3);
  border-top: var(--border-hairline) solid var(--line);
}

/* ============================================================
   DASHBOARD SHOT

   The audience page rests one claim — "every figure has a time on
   it" — on a screenshot, so the screenshot has to be readable rather
   than decorative. It takes the full content column instead of
   sitting beside the text; a dashboard squeezed into half a column is
   a picture of a dashboard, not one you can read.
   ============================================================ */

.dash-shot {
  margin: 0;
  max-width: 1100px;
  margin-inline: auto;
}

.dash-shot img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--border-hairline) solid var(--line);
  background: var(--surface);
}

/* Light by default, dark where the page is dark — the same three-state
   shape the colour tokens use at :root. */
/* Scoped for the same reason as .fov above. */
.dash-shot .dash-shot__dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .dash-shot__light { display: none; }
  :root:not([data-theme="light"]) .dash-shot__dark  { display: block; }
}
:root[data-theme="dark"]  .dash-shot__light { display: none; }
:root[data-theme="dark"]  .dash-shot__dark  { display: block; }
:root[data-theme="light"] .dash-shot__light { display: block; }
:root[data-theme="light"] .dash-shot__dark  { display: none; }

/* The colour tokens cross-fade over 260ms while data-switching is set, but
   `display` is not animatable, so the themed shot flipped in a single frame
   and sat near-black on still-cream paper for a quarter second — on the two
   largest objects on the page. Elsewhere the same instant swap only moves the
   16px glyph inside the toggle, where nobody sees it.

   An animation, not a transition: the `:root[data-switching] *` rule up in the
   motion section sets the transition shorthand with !important, and an opacity
   transition declared here would be discarded. The outgoing image still goes
   display:none, so exactly one of the pair stays in the accessibility tree.
   The global prefers-reduced-motion block cuts this to 0.01ms with the rest. */
@keyframes shot-fade-in { from { opacity: 0; } to { opacity: 1; } }
:root[data-switching] .fov img,
:root[data-switching] .dash-shot img {
  animation: shot-fade-in 260ms var(--ease);
}

.dash-shot figcaption {
  margin-top: var(--space-4);
  font-size: var(--size-spec);
  text-align: center;
  text-wrap: balance;
}

.logo-strip__caption {
  font-family: var(--font-mono);
  font-size: var(--size-mono-label);
  letter-spacing: var(--tracking-mono-label);
  color: var(--ink-muted);
  text-align: center;
}
