/* Manrope, self-hosted. One variable file covers 200-800, so no request
   leaves the site and no visitor IP reaches a third party.
   Copyright 2018 The Manrope Project Authors, SIL Open Font License 1.1.
   Licence text ships alongside the font at fonts/OFL.txt. */
/* Two faces, each doing what it is good at.

   Atkinson Hyperlegible Next — the Braille Institute's typeface — carries the
   reading: body copy, notes, shortcut tables, anything with an instruction or
   a value in it. It draws I l 1, b d p q and 0 O so they cannot be mistaken
   for one another, which matters in exactly the places a mistake costs
   something: an option string, a version number, a licence key.

   Manrope keeps the display: headings, the wordmark, the nav, the buttons —
   everything set in caps or large. At those sizes character confusion is not
   the risk, and Manrope is the better-looking face. Caps sidestep the b/d/p/q
   problem entirely, which is why the nav and buttons can stay with it.

   size-adjust on Atkinson is doing real work: its x-height is 0.496 em against
   Manrope's 0.540, so without it body copy would read a size smaller than the
   headings it sits under, and every clamp() on the site would need retuning.
   108% brings the two into line so they can be mixed freely.

   Roughly 90 KB of font in total, both self-hosted, both OFL. */
@font-face {
  font-family: "Manrope";
  src: url("fonts/Manrope-variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible Next";
  src: url("fonts/AtkinsonHyperlegibleNext-variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  size-adjust: 108%;
}

/* =========================================================
   iftah — "Signal"

   One colour field. One typeface, used at three widths.
   Every interactive element inverts, hard and instantly,
   the moment it takes hover or keyboard focus — so tabbing
   through the site is the thing you notice about it.
   ========================================================= */

:root {
  --page:   #ecd286;   /* sand */
  --page-2: #e3c46e;
  --ink:    #000000;
  --ink-2:  #403923;   /* the same black, thinned over the sand — 7.7:1, AAA */
  --accent: #000000;   /* there is no third colour; emphasis is inversion */
  --on-accent: #ecd286;
  --line:   rgba(0, 0, 0, 0.28);

  --font: "Atkinson Hyperlegible Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-display: "Manrope", "Helvetica Neue", Helvetica, Arial, sans-serif;

  --gutter: clamp(1.25rem, 5vw, 4.5rem);
  --stack: clamp(2.75rem, 6vw, 5.5rem);   /* space between sections */
}

*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  background: var(--page);
  /* Safari paints a translucent black rectangle over a tapped element's box.
     On round marks in a square hit area that reads as a flashing dark square,
     so it's off — the :active states below give the feedback instead. */
  -webkit-tap-highlight-color: transparent;
}

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font);
  font-size: 1.0625rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }
::selection { background: var(--ink); color: var(--page); }

:focus { outline: none; }

/* Touch feedback, since the tap highlight is off and hover does nothing on a
   phone. Brief and quiet: the element dims while your finger is down. */
a:active, button:active { opacity: 0.55; }
@media (hover: hover) { a:active, button:active { opacity: 1; } }
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 4px;
  border-radius: 1px;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip {
  position: absolute;
  left: var(--gutter);
  top: 0;
  transform: translateY(-140%);
  z-index: 30;
  background: var(--accent);
  color: var(--on-accent);
  padding: 0.85rem 1.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* :focus as well as :focus-visible — a skip link reached by a screen reader
   or by a browser that has not decided the focus is "visible" still has to
   appear, or the user is tabbing to something invisible.
   The matching half of this is tabindex="-1" on <main>: without it the browser
   scrolls but focus stays where it was, so the link looks like it does
   nothing. */
.skip:focus,
.skip:focus-visible { transform: translateY(0); outline-offset: -4px; }

/* main takes focus only programmatically, so it must not draw a focus ring
   as though it were an interactive control. */
main:focus { outline: none; }

.wrap { width: 100%; max-width: 58rem; margin-inline: auto; padding-inline: var(--gutter); }
.wrap--text { max-width: 78rem; margin-inline: auto; }

/* =========================================================
   Type — one family, three widths.
   Expanded shouts, normal speaks, condensed labels.
   ========================================================= */

h1, h2, h3 { margin: 0; text-wrap: balance; }

h1 {
  /* The floor matters more than the cap here: at 4.6vw a 390px phone fell to
     the 2rem minimum, so every device page opened with a 32px title.

     Set at 400. The tracking and line height move with the weight rather than
     staying put: -0.035em exists to stop heavy letterforms clogging, and at
     400 the same value just reads as cramped, so it opens to -0.02em and the
     leading goes from 1.02 to 1.06 to give the lighter strokes air. */
  font-size: clamp(3rem, 7.4vw, 5.75rem);
  font-weight: 400;
  line-height: 1.06;
  letter-spacing: -0.005em;
}

h2 {
  font-size: clamp(1.375rem, 2.4vw, 1.875rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.025em;
}

h3 { font-size: 1.125rem; font-weight: 600; }
h4 { font-size: 1rem; font-weight: 700; margin: 0; }

/* Long documents — the licence and the privacy policy — need real vertical
   rhythm, and they stay left-aligned: centring 2,500 words of policy with
   nested lists is unreadable. Everything else on the site is centred. */
/* Standfirst under a page title. The class was referenced but had no rule
   left in the stylesheet, so it was falling back to plain body text. */
.lede {
  max-width: 46ch;
  margin-inline: auto;
  color: var(--ink-2);
  font-size: clamp(0.9375rem, 1.05vw, 1.0625rem);
  line-height: 1.7;
}
.hero .lede { margin-top: clamp(0.75rem, 2vw, 1.25rem); }

.prose { max-width: 52ch; margin-inline: auto; }
.prose > :first-child { margin-top: 0; }
.prose h2 { margin: 2.75rem 0 0.8rem; }
.prose h3 { margin: 2rem 0 0.5rem; }
.prose h4 { margin: 1.4rem 0 0.4rem; }
.prose ul { margin: 0 0 1.15em; padding: 0; list-style: none; }
.prose li { margin-bottom: 0.45em; }

p { margin: 0 0 1.15em; max-width: 62ch; margin-inline: auto; }
p:last-child { margin-bottom: 0; }

.prose {
  font-size: clamp(0.9375rem, 1.05vw, 1.0625rem);
  line-height: 1.7;
  text-align: center;
}
.prose strong { font-weight: 700; }
.prose a, .text-link {
  color: var(--ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}
.prose a:hover, .text-link:hover { text-decoration-thickness: 3px; }

.prose--doc { text-align: left; max-width: 64ch; }
.prose--doc ul { list-style: disc; padding-left: 1.3em; }
.prose--doc p { margin-inline: 0; }

.label,
.site-nav a,
.wordmark,
.idx,
.price,
.site-footer a,
.field label,
.btn {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.note { font-size: 0.875rem; color: var(--ink-2); line-height: 1.65; max-width: 44ch; }

/* =========================================================
   Header
   ========================================================= */

/* The rule spans the viewport like the footer's does; the header sits outside
   the measure so its two ends reach the page edges rather than the column. */
/* Three columns of 1fr / auto / 1fr: the mark sits at the far left, the nav is
   centred in the page rather than pushed to the right, and the empty third
   column is what keeps it centred on the window and not on the space left over. */
.header-inner {
  max-width: none;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem 1rem;
  padding-block: 1.6rem 1.35rem;
}
.header-inner .wordmark { justify-self: start; }
.header-inner .site-nav { grid-column: 2; }

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.875rem;
  letter-spacing: 0.1em;
  text-decoration: none;
  color: var(--ink);
}
.wordmark:hover .mark { stroke-width: 6; }
.mark { transition: stroke-width 120ms ease; }
/* Sized to the cap height of the wordmark beside it, not to the line box —
   otherwise the disc towers over 12px uppercase. */
.mark {
  width: 30px;
  height: 30px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 5;
}

.site-nav ul { display: flex; gap: clamp(1rem, 2.5vw, 2rem); list-style: none; margin: 0; padding: 0; }
.site-nav a {
  font-size: 0.875rem;
  letter-spacing: 0.1em;
  text-decoration: none;
  color: var(--ink-2);
  padding: 0.6rem 0.5rem;
  display: inline-block;
  transition: background-color 120ms ease, color 120ms ease;
}
.site-nav a:hover { color: var(--ink); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 5px; }
.site-nav a[aria-current="page"] { color: var(--ink); }


/* =========================================================
   Hero
   ========================================================= */

/* One centred stack. The face sits above the headline at every width,
   so there is no gap to open up on a wide screen. */
.hero {
  display: grid;
  justify-items: center;
  gap: clamp(1.75rem, 4vw, 2.75rem);
  padding-block: clamp(3rem, 7vw, 5.5rem) 0;
  text-align: center;
}
.hero h1 { max-width: 20ch; }

/* The face, from FaceComponent. 1px strokes at any size. */
.face {
  display: block;
  width: clamp(112px, 17vw, 176px);
  aspect-ratio: 1;
}
.face svg { display: block; width: 100%; height: 100%; overflow: visible; }
/* The stroke scales with the mark rather than being pinned to 1 CSS pixel.
   A hairline is right in the plugin, where the face is small; at 260px on a
   1-times-density display it is a single physical pixel smeared along a curve,
   which is what aliases. 1.5 units against a 200 viewBox lands near 2px at
   full size and still reads as a fine line. */
.face .rim,
.face .eye,
.face .mouth,
.face .lids line {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.5;
  stroke-linecap: round;
  shape-rendering: geometricPrecision;
}
.face .pupil { shape-rendering: geometricPrecision; }
.face .pupil { fill: var(--ink); }
.face .pupil-wrap { transform-box: fill-box; transform-origin: center; }
.face .lids { opacity: 0; }
.face.blink .eyes { opacity: 0; }
.face.blink .lids { opacity: 1; }

/* =========================================================
   Icon behaviour on hover and keyboard focus.
   Each mark moves the way its device moves: the meter meters,
   the spectrum breathes, the cords carry signal, the face
   blinks. Nothing moves until the row is hovered or focused,
   and prefers-reduced-motion suspends all of it.
   ========================================================= */

.icon * { transform-box: fill-box; transform-origin: center; }
.icon .lids { opacity: 0; }   /* blink lids are hidden until a blink, on any icon that has them */

/* Sting — the blink from startBlinkShake, on a loop */
.index a:hover .icon--sting-2 .eyes,
.index a:focus-visible .icon--sting-2 .eyes,
.index a:hover .icon--sting .eyes,
.index a:focus-visible .icon--sting .eyes { animation: icon-blink 2.8s steps(1, end) infinite; }
.index a:hover .icon--sting-2 .lids,
.index a:focus-visible .icon--sting-2 .lids,
.index a:hover .icon--sting .lids,
.index a:focus-visible .icon--sting .lids { animation: icon-blink-lids 2.8s steps(1, end) infinite; }
@keyframes icon-blink      { 0%, 92%, 97% { opacity: 1 } 93%, 96% { opacity: 0 } }
@keyframes icon-blink-lids { 0%, 92%, 97% { opacity: 0 } 93%, 96% { opacity: 1 } }

/* and the eyes look around between blinks */
.index a:hover .icon--sting-2 .pup,
.index a:focus-visible .icon--sting-2 .pup,
.index a:hover .icon--sting .pup,
.index a:focus-visible .icon--sting .pup { animation: icon-look 2.8s ease-in-out infinite; }
@keyframes icon-look {
  0%, 8%   { transform: translate(0, 0); }
  22%, 38% { transform: translate(2.4px, -0.6px); }
  52%, 68% { transform: translate(-2.4px, 0.4px); }
  82%, 100%{ transform: translate(0, 0); }
}

/* Field Spectrum — the stems are full height and revealed by dash offset, so
   the peaks travel up and down the column and the dot rides the tip. Rest
   offsets reproduce the static skyline: drawn length = 66 - offset. */
.icon--field-spectrum .stem { stroke-dasharray: 66; }
.icon--field-spectrum .col:nth-of-type(1) .stem { stroke-dashoffset: 38; }
.icon--field-spectrum .col:nth-of-type(2) .stem { stroke-dashoffset: 22; }
.icon--field-spectrum .col:nth-of-type(3) .stem { stroke-dashoffset: 9; }
.icon--field-spectrum .col:nth-of-type(4) .stem { stroke-dashoffset: 22; }
.icon--field-spectrum .col:nth-of-type(5) .stem { stroke-dashoffset: 38; }

.index a:hover .icon--field-spectrum .col:nth-of-type(1) .stem,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(1) .stem { animation: fs-a 2.6s ease-in-out infinite; }
.index a:hover .icon--field-spectrum .col:nth-of-type(2) .stem,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(2) .stem { animation: fs-b 3.3s ease-in-out infinite; }
.index a:hover .icon--field-spectrum .col:nth-of-type(3) .stem,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(3) .stem { animation: fs-c 2.9s ease-in-out infinite; }
.index a:hover .icon--field-spectrum .col:nth-of-type(4) .stem,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(4) .stem { animation: fs-b 3.7s ease-in-out infinite reverse; }
.index a:hover .icon--field-spectrum .col:nth-of-type(5) .stem,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(5) .stem { animation: fs-a 3.1s ease-in-out infinite reverse; }

.index a:hover .icon--field-spectrum .col:nth-of-type(1) .dot,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(1) .dot { animation: fs-dot 2.6s ease-in-out infinite; }
.index a:hover .icon--field-spectrum .col:nth-of-type(2) .dot,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(2) .dot { animation: fs-dot 3.3s ease-in-out infinite; }
.index a:hover .icon--field-spectrum .col:nth-of-type(3) .dot,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(3) .dot { animation: fs-dot-s 2.9s ease-in-out infinite; }
.index a:hover .icon--field-spectrum .col:nth-of-type(4) .dot,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(4) .dot { animation: fs-dot 3.7s ease-in-out infinite reverse; }
.index a:hover .icon--field-spectrum .col:nth-of-type(5) .dot,
.index a:focus-visible .icon--field-spectrum .col:nth-of-type(5) .dot { animation: fs-dot 3.1s ease-in-out infinite reverse; }

@keyframes fs-a { 0%, 100% { stroke-dashoffset: 46 } 50% { stroke-dashoffset: 26 } }
@keyframes fs-b { 0%, 100% { stroke-dashoffset: 30 } 50% { stroke-dashoffset: 10 } }
@keyframes fs-c { 0%, 100% { stroke-dashoffset: 17 } 50% { stroke-dashoffset: 1 } }
@keyframes fs-dot   { 0%, 100% { transform: translateY(8px) } 50% { transform: translateY(-12px) } }
@keyframes fs-dot-s { 0%, 100% { transform: translateY(8px) } 50% { transform: translateY(-8px) } }

/* DripRog — the status indicator, doing what it does unattended */
.index a:hover .icon--driprog .led,
.index a:focus-visible .icon--driprog .led { animation: icon-led 1.6s ease-in-out infinite; }
@keyframes icon-led { 0%, 100% { transform: scale(1); opacity: 1 } 50% { transform: scale(1.4); opacity: 0.45 } }

/* Slippery Slope — the rings resonate, one cluster after another */
.index a:hover .icon--slipperyslope .clu,
.index a:focus-visible .icon--slipperyslope .clu { animation: icon-ring 1.8s ease-in-out var(--d, 0s) infinite; }
.icon--slipperyslope .clu:nth-of-type(2) { --d: 0.2s; }
.icon--slipperyslope .clu:nth-of-type(3) { --d: 0.4s; }
@keyframes icon-ring { 0%, 100% { transform: scale(1) } 50% { transform: scale(1.07) } }

/* Pyra — the strings vibrate. scaleY about each wave's own centre swings the
   amplitude through zero and out the other side, which is what a plucked
   string does; nothing moves horizontally and the scale never exceeds 1, so
   no line can leave the triangle at any point in the cycle. Each string runs
   at its own rate, so they drift in and out of phase.
   nth-of-type counts every path, and the first is the triangle outline. */
/* Without this the stroke scales with the shape, so each string thins to
   nothing as it crosses zero. */
.icon--pyra .w { vector-effect: non-scaling-stroke; }
.index a:hover .icon--pyra .w,
.index a:focus-visible .icon--pyra .w { animation: icon-string var(--dur, 3.2s) ease-in-out infinite; }
/* Slow, and no two the same — mutually prime-ish durations mean the strings
   take a long time to line up again. */
.icon--pyra .w:nth-of-type(2) { --dur: 2.9s; }
.icon--pyra .w:nth-of-type(3) { --dur: 3.7s; }
.icon--pyra .w:nth-of-type(4) { --dur: 3.1s; }
.icon--pyra .w:nth-of-type(5) { --dur: 4.3s; }
@keyframes icon-string {
  0%, 100% { transform: scaleY(1); }
  50%      { transform: scaleY(-0.85); }
}

/* Flufs — each bar is a segment sliding along a full-height track, the way a
   segment meter reads. The resting shape is unchanged; nothing stretches. */
.icon--flufs .bar:nth-of-type(1) { stroke-dasharray: 24 88; stroke-dashoffset: -32; }
.icon--flufs .bar:nth-of-type(2) { stroke-dasharray: 52 88; stroke-dashoffset: -18; }
.icon--flufs .bar:nth-of-type(3) { stroke-dasharray: 64 88; stroke-dashoffset: -12; }
.icon--flufs .bar:nth-of-type(4) { stroke-dasharray: 52 88; stroke-dashoffset: -18; }
.icon--flufs .bar:nth-of-type(5) { stroke-dasharray: 24 88; stroke-dashoffset: -32; }

.index a:hover .icon--flufs .bar:nth-of-type(1),
.index a:focus-visible .icon--flufs .bar:nth-of-type(1) { animation: fl-a 2.4s ease-in-out infinite; }
.index a:hover .icon--flufs .bar:nth-of-type(2),
.index a:focus-visible .icon--flufs .bar:nth-of-type(2) { animation: fl-b 3.1s ease-in-out infinite; }
.index a:hover .icon--flufs .bar:nth-of-type(3),
.index a:focus-visible .icon--flufs .bar:nth-of-type(3) { animation: fl-c 2.7s ease-in-out infinite; }
.index a:hover .icon--flufs .bar:nth-of-type(4),
.index a:focus-visible .icon--flufs .bar:nth-of-type(4) { animation: fl-b 3.5s ease-in-out infinite reverse; }
.index a:hover .icon--flufs .bar:nth-of-type(5),
.index a:focus-visible .icon--flufs .bar:nth-of-type(5) { animation: fl-a 2.9s ease-in-out infinite reverse; }

@keyframes fl-a { 0%, 100% { stroke-dashoffset: -24 } 50% { stroke-dashoffset: -40 } }
@keyframes fl-b { 0%, 100% { stroke-dashoffset: -11 } 50% { stroke-dashoffset: -25 } }
@keyframes fl-c { 0%, 100% { stroke-dashoffset: -6 }  50% { stroke-dashoffset: -18 } }

/* Pitch Pipe — the arcs themselves rock about their shared centre, inner
   ones swinging widest, like a tuner hunting for the note. Each arc's fill-box
   bottom centre IS the centre of its circle, so 50% 100% is the right pivot.
   Nothing scales and nothing fades; the shape is unchanged at rest. */
.icon--pitchpipe .arc { transform-box: fill-box; transform-origin: 50% 100%; }
.index a:hover .icon--pitchpipe .arc:nth-of-type(1),
.index a:focus-visible .icon--pitchpipe .arc:nth-of-type(1) { animation: pp-1 2.2s ease-in-out infinite; }
.index a:hover .icon--pitchpipe .arc:nth-of-type(2),
.index a:focus-visible .icon--pitchpipe .arc:nth-of-type(2) { animation: pp-2 2.7s ease-in-out infinite; }
.index a:hover .icon--pitchpipe .arc:nth-of-type(3),
.index a:focus-visible .icon--pitchpipe .arc:nth-of-type(3) { animation: pp-3 3.3s ease-in-out infinite; }
.index a:hover .icon--pitchpipe .arc:nth-of-type(4),
.index a:focus-visible .icon--pitchpipe .arc:nth-of-type(4) { animation: pp-4 4.1s ease-in-out infinite; }
@keyframes pp-1 { 0%, 100% { transform: rotate(-11deg) } 50% { transform: rotate(11deg) } }
@keyframes pp-2 { 0%, 100% { transform: rotate(7deg) }   50% { transform: rotate(-7deg) } }
@keyframes pp-3 { 0%, 100% { transform: rotate(-4.5deg) } 50% { transform: rotate(4.5deg) } }
@keyframes pp-4 { 0%, 100% { transform: rotate(2.5deg) }  50% { transform: rotate(-2.5deg) } }

/* 9 Tracks — a chase light. Each cell fills solid for a ninth of the cycle,
   then hands over to the next. steps(1) keeps the switch hard: a chase light
   snaps on and off, it doesn't fade. */
.index a:hover .icon--9tracks .cell,
.index a:focus-visible .icon--9tracks .cell { animation: nine-chase 2.7s steps(1, end) var(--d, 0s) infinite; }
.icon--9tracks .cell:nth-of-type(2) { --d: 0.3s; }
.icon--9tracks .cell:nth-of-type(3) { --d: 0.6s; }
.icon--9tracks .cell:nth-of-type(4) { --d: 0.9s; }
.icon--9tracks .cell:nth-of-type(5) { --d: 1.2s; }
.icon--9tracks .cell:nth-of-type(6) { --d: 1.5s; }
.icon--9tracks .cell:nth-of-type(7) { --d: 1.8s; }
.icon--9tracks .cell:nth-of-type(8) { --d: 2.1s; }
.icon--9tracks .cell:nth-of-type(9) { --d: 2.4s; }
@keyframes nine-chase {
  0%           { fill: currentColor; }
  11.2%, 100%  { fill: none; }
}

/* 13 Linear Tracks — signal running down the patch cords */
.index a:hover .icon--13tracks .cord,
.index a:focus-visible .icon--13tracks .cord {
  stroke-dasharray: 5 7;
  animation: icon-flow 0.8s linear var(--d, 0s) infinite;
}
.icon--13tracks .cord:nth-of-type(2) { --d: 0.4s; }
@keyframes icon-flow { to { stroke-dashoffset: -12; } }

/* Maxess — the dots read left to right, the way a braille cell is read */
.index a:hover .icon--maxess .dot,
.index a:focus-visible .icon--maxess .dot { animation: icon-dot 1.6s ease-in-out var(--d, 0s) infinite; }
.icon--maxess .dot:nth-of-type(2) { --d: 0.12s; }
.icon--maxess .dot:nth-of-type(3) { --d: 0.24s; }
.icon--maxess .dot:nth-of-type(4) { --d: 0.36s; }
@keyframes icon-dot { 0%, 100% { transform: scale(1) } 50% { transform: scale(1.32) } }

/* =========================================================
   Sections
   ========================================================= */

.section { padding-block: 0 var(--stack); }
.section + .section { padding-top: 0; }
.section-head { margin-bottom: clamp(1.5rem, 3vw, 2.5rem); text-align: center; }
main .section > p { text-align: center; }
.hero + .section { padding-top: clamp(2rem, 4.5vw, 3.25rem); }
.hero:last-child { padding-bottom: var(--stack); }

/* The home page is the only page whose job is impact rather than reading,
   so it runs a step larger than everything else. */
.hero--home { padding-block: clamp(3.5rem, 8vw, 6.5rem) 0; gap: clamp(2rem, 4.5vw, 3.25rem); }
.hero--home h1 { font-size: clamp(1.75rem, 4.2vw, 3.25rem); max-width: 22ch; letter-spacing: -0.035em; }
.hero--home .face { width: clamp(150px, 23vw, 250px); }
.hero--home { justify-items: stretch; }
.hero--home h1 { margin-inline: auto; }

/* =========================================================
   Index — full-bleed centred rows that invert on hover and focus
   ========================================================= */

.index {
  list-style: none;
  margin: 0;
  margin-inline: calc(var(--gutter) * -1);
  padding: 0;
}

.index a {
  display: block;
  text-align: center;
  padding: clamp(2rem, 4vw, 3rem) var(--gutter);
  text-decoration: none;
  transition: background-color 90ms linear, color 90ms linear;
}
/* No wash and no underline — the icon animation is the hover feedback.
   Keyboard focus still gets a ring, since the animation alone is not enough
   to tell you where you are, and is suppressed under reduced-motion. */
.index a .icon { transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1); }
.index a:hover .icon, .index a:focus-visible .icon { transform: translateY(-4px); }
.index a:focus-visible { outline: 3px solid var(--ink); outline-offset: -6px; }

/* Device icons. No colour of their own — stroke and fill are currentColor,
   so they follow the row's text rather than needing a second copy. */
.icon {
  display: block;
  width: clamp(84px, 13vw, 132px);
  height: clamp(84px, 13vw, 132px);
  margin: 0 auto 1.35rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
  overflow: visible;
}
.icon--thin { stroke-width: 2.7; }
.icon--hair { stroke-width: 2; }   /* the face is drawn at 1px in the plugin */
.icon .f { fill: currentColor; stroke: none; }

.idx { display: none; }

.name {
  display: block;
  font-size: clamp(1.25rem, 2.4vw, 1.75rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.03em;
}
.desc { display: block; margin-top: 0.35rem; color: var(--ink-2); font-size: 0.9375rem; }
.go { display: none; }


/* =========================================================
   Home page — the face is the link to the plugin
   ========================================================= */

.plug {
  display: block;
  max-width: 32rem;
  margin-inline: auto;
  padding: clamp(1.5rem, 3.5vw, 2.25rem) 1.25rem;
  text-decoration: none;
}
/* No rules, no hover wash, no underline — the face and the name carry it.
   Keyboard focus keeps its ring: without it there would be no way to tell
   where you are on a page whose only link is a picture. */
.plug:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }

.plug .face { margin: 0 auto clamp(1.25rem, 3vw, 1.75rem); }
.plug .name {
  display: block;
  font-size: clamp(3rem, 8vw, 5.5rem);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1;
}
.plug .desc {
  display: block;
  margin-top: 0.6rem;
  color: var(--ink-2);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* =========================================================
   Devices — category tabs
   ========================================================= */

.tabs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin-bottom: clamp(1.75rem, 3.5vw, 2.5rem);
}
.tab {
  font: inherit;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 0;
  padding: 0.7rem 1rem;
  min-height: 44px;
  cursor: pointer;
  transition: background-color 90ms linear, color 90ms linear, border-color 90ms linear;
}
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] { background: var(--ink); color: var(--page); border-color: var(--ink); }
[role="tabpanel"]:focus-visible { outline: 3px solid var(--ink); outline-offset: 4px; }

/* =========================================================
   Media
   ========================================================= */

/* Screenshots, video, prices and specs all share one block width, so
   everything lines up on the same vertical axis down the page. */
.shot { width: 100%; max-width: 30rem; margin-inline: auto; }
/* a wide, short screenshot needs more width to carry the same visual weight
   as a squarer one beside it */
.shot--wide { max-width: 40rem; }
.shot + .shot { margin-top: clamp(1rem, 2vw, 1.75rem); }
.bleed { margin-inline: calc(var(--gutter) * -1); }

.two-col { display: grid; gap: clamp(2rem, 5vw, 3rem); grid-template-columns: 1fr; justify-items: center; }

/* Deliberately NOT a grid. A grid item defaults to align-self: stretch, and
   stretch overrides aspect-ratio sizing — since the iframe inside is absolutely
   positioned it contributes no height, so every video collapsed to nothing.
   Plain block flow lets aspect-ratio derive the height from the width. */
.videos > * + * { margin-top: clamp(1rem, 2.5vw, 1.75rem); }
.video { position: relative; aspect-ratio: 16 / 9; max-width: 30rem; margin-inline: auto; background: var(--page-2); }
/* In the prose column the 30rem cap would make a video narrower than the
   paragraph above it. Here it takes the column. */
.prose .video { max-width: none; }
.video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* =========================================================
   Buttons, specs, tiers
   ========================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding: 0.85rem 1.75rem;
  background: transparent;
  color: var(--ink);
  border: 2px solid var(--ink);
  cursor: pointer;
  text-decoration: none;
  transition: background-color 120ms ease, color 120ms ease;
}
/* Outline at rest, filled on hover — the same direction as everything else
   on the site, where interaction adds weight rather than removing it. */
.btn:hover { background: var(--ink); color: var(--page); }

.spec { list-style: none; margin: 0; padding: 0; }
.spec li { padding: 0.3rem 0; color: var(--ink-2); }

.buy { max-width: 30rem; margin-inline: auto; text-align: center; }
.buy .note { margin-top: 1.25rem; }
.buy .note + .note { margin-top: 0.4rem; }
.spec { max-width: 30rem; margin-inline: auto; text-align: center; }
.tiers { list-style: none; margin: 0; padding: 0; }
.tier {
  cursor: pointer;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: 56px;
  padding: 0.85rem 0.5rem;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  transition: background-color 90ms linear;
}
.tier:focus-visible { outline: 3px solid var(--ink); outline-offset: -5px; }
.tiers li:last-child .tier { border-bottom: 0; }

/* the price is what you're choosing between, so it carries the weight */
.tier .price {
  color: var(--ink);
  white-space: nowrap;
  font-size: 1.25rem;
  font-weight: 800;
  letter-spacing: -0.03em;
}
.tier:hover .price, .tier:focus-visible .price { color: var(--ink); }

/* =========================================================
   Form
   ========================================================= */

form { max-width: 30rem; margin-inline: auto; text-align: center; }
.field {
  display: grid;
  gap: 0.6rem;
  margin: 0 auto 1.75rem;
  max-width: 30rem;
  text-align: center;
}
.field label { color: var(--ink-2); }
.field input, .field textarea {
  font-family: var(--font);
  font-size: 1.125rem;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: 0.7rem 0;
  min-height: 48px;
  width: 100%;
  text-align: center;
}
.field input:hover, .field textarea:hover { border-bottom-color: var(--accent); }
.field input:focus-visible, .field textarea:focus-visible { outline-offset: 2px; }
.field textarea { min-height: 9rem; resize: vertical; }

/* =========================================================
   Footer
   ========================================================= */

.site-footer { padding-block: clamp(2.5rem, 6vw, 4rem); }
.footer-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem clamp(1.25rem, 4vw, 2.5rem);
}

/* the marks sit on their own line, centred under the text links */
.footer-social {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: flex;
  justify-content: center;
  gap: clamp(0.5rem, 2vw, 1.25rem);
}
.site-footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem 1.25rem;
}
.site-footer a {
  color: var(--ink-2);
  text-decoration: none;
  padding: 0.35rem 0.4rem;
  margin-inline: -0.4rem;
  display: inline-block;
  transition: background-color 120ms ease, color 120ms ease;
}
.site-footer a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 4px; }

/* =========================================================
   Motion / contrast
   ========================================================= */

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

@media (forced-colors: active) {
  .index a, .tier, .btn { border: 1px solid; }
  :focus-visible { outline: 3px solid; }
}


/* Shortcut lists — real definition lists, so a screen reader announces
   key/action pairs and can navigate them as a list. */
.shortcuts {
  margin: 0 auto;
  padding: 0;
  max-width: 34rem;
  text-align: left;
}
.shortcut {
  display: grid;
  grid-template-columns: minmax(8rem, auto) 1fr;
  gap: 0.5rem 1.25rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line);
}
.shortcut:first-child { border-top: 1px solid var(--line); }
.shortcuts dt, .shortcuts dd { margin: 0; }
.shortcuts dd { color: var(--ink-2); font-size: 0.9375rem; }

/* The manual's control descriptions. Stacked rather than in two columns like
   .shortcuts, because a description here is a sentence rather than a key
   combination, and a narrow left column would leave the term wrapping over
   three lines while the sentence had nowhere to go.

   A definition list rather than paragraphs so that a name and its description
   are associated, not merely adjacent, and the reader can move term to term. */
.manual-list { margin: 1.25rem 0; padding: 0; text-align: left; }
.manual-list dt {
  margin: 1.1rem 0 0.25rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.manual-list dt:first-child { margin-top: 0; }
.manual-list dd { margin: 0; color: var(--ink-2); }
/* The contents list has empty definitions, so the gap under each line would
   otherwise read as a missing entry. */
.manual-list dd:empty { display: none; }
@media (max-width: 30rem) {
  .shortcut { grid-template-columns: 1fr; gap: 0.15rem; }
}

kbd {
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 3px;
  padding: 0.15em 0.45em;
  white-space: nowrap;
}

.kbd-hint {
  margin: 1.75rem auto 0;
  max-width: none;
  text-align: center;
  font-size: 0.75rem;
  color: var(--ink-2);
}
.kbd-hint kbd { font-size: 0.75rem; }
/* No keyboard, no point advertising a keyboard shortcut. The palette itself
   still works if a keyboard is attached later. */
@media (hover: none) {
  .kbd-hint { display: none; }
}

/* Social links: mark only, with the platform name supplied for screen
   readers. Drawn in the site's own line style rather than dropped-in brand
   assets, so they sit with the device icons. */
.social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  margin: 0;
}
.social svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  shape-rendering: geometricPrecision;
}
.social:hover { color: var(--ink); }

/* =========================================================
   Command palette — press / anywhere
   ========================================================= */

.palette {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: clamp(3rem, 12vh, 8rem) 1.25rem 2rem;
  background: rgba(0, 0, 0, 0.35);
}
.palette[hidden] { display: none; }

.palette-box {
  width: 100%;
  max-width: 30rem;
  background: var(--page);
  border: 2px solid var(--ink);
  padding: 0.75rem;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
}

.palette input {
  font-family: inherit;
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: 0.6rem 0.4rem 0.85rem;
  width: 100%;
}
/* Was outline:none with a 1px border colour change — the weakest focus
   indicator on the site, on the one control that is focused programmatically.
   Now it matches everything else. */
.palette input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.palette input::placeholder { color: var(--ink-2); font-weight: 500; }

.palette ul { list-style: none; margin: 0.5rem 0 0; padding: 0; overflow-y: auto; }
.palette li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.4rem;
  cursor: pointer;
}
.palette li[aria-selected="true"] { background: var(--ink); color: var(--page); }
.palette-title { font-weight: 700; letter-spacing: -0.02em; }
.palette-sec {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
}
.palette li[aria-selected="true"] .palette-sec { color: var(--page); }

.palette-hint {
  margin: 0.65rem 0 0.15rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-align: center;
}


/* =========================================================
   Home page — fits the window
   Sizes are the smaller of a width-based and a height-based value, so the
   composition shrinks when the window is short rather than pushing the footer
   off the bottom. Scoped to .home; no other page is affected.
   ========================================================= */

.home { min-height: 100dvh; display: flex; flex-direction: column; }
.home main { flex: 1 0 auto; display: grid; align-items: center; }
.home .site-footer { flex: none; }

.home .hero--home {
  padding-block: clamp(1.25rem, 3.5vh, 3rem) clamp(1rem, 3vh, 2.5rem);
  gap: clamp(1rem, 3vh, 2.25rem);
}
.home .hero--home h1 { font-size: clamp(1.375rem, min(4.2vw, 5.2vh), 3.25rem); }
.home .hero--home .face { width: clamp(96px, min(22vw, 26vh), 260px); }

.home .plug { padding-block: clamp(0.75rem, 2vh, 1.5rem); }
.home .plug .face { margin-bottom: clamp(0.75rem, 2vh, 1.5rem); }
.home .plug .name { font-size: clamp(1.75rem, min(7.5vw, 9.5vh), 5.5rem); }

.home .site-footer { padding-block: clamp(1.25rem, 3vh, 2.5rem); }
.home .kbd-hint { margin-top: clamp(0.75rem, 2vh, 1.5rem); }


/* =========================================================
   Phones
   ========================================================= */

/* The header is a space-between row, so once it wraps each line holds a single
   item and lands at the start — wordmark left, nav left, nothing centred.
   Below this width it becomes an explicit centred column. */
@media (max-width: 40rem) {
  .header-inner {
    grid-template-columns: auto 1fr;
    justify-items: start;
    padding-block: 1.1rem 1rem;
    gap: 0.5rem 1rem;
  }
  .header-inner .site-nav { grid-column: 2; justify-self: end; }
  .site-nav ul { gap: 1.1rem; }

  /* On a phone the width term dominates, so the face was collapsing to its
     96px floor while there was plenty of height going spare. */
  .home .hero--home h1   { font-size: clamp(1.375rem, min(6.4vw, 4.6vh), 2rem); }
  .home .hero--home .face { width: clamp(140px, min(46vw, 28vh), 240px); }
  .home .plug .name      { font-size: clamp(2rem, min(12vw, 8vh), 3.25rem); }
}


/* =========================================================
   Device page figures
   One or two per page, each sitting under the sentence it illustrates.
   Same line language as the icons; aria-hidden, since every one of them
   restates something the prose already says.
   ========================================================= */

.fig {
  width: clamp(160px, 28vw, 240px);
  margin: clamp(1.75rem, 3.5vw, 2.5rem) auto;
}
.prose p + .fig { margin-top: clamp(0.6rem, 1.6vw, 1.1rem); }
.fig svg {
  display: block;
  width: 100%;
  height: auto;
  fill: none;
  stroke: var(--ink);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  shape-rendering: geometricPrecision;
}
.fig .f { fill: var(--ink); stroke: none; }
.fig * { transform-box: fill-box; transform-origin: center; }

/* Sting's own rules at figure scale: gate is length, velocity is weight, and
   velocity 0 stays an outline. Each mark brightens as its step sounds and
   settles back — the light chases without a playhead. At rest the pattern is
   still fully legible, which is what a reduced-motion visitor sees. */
.fig--steps .grid { stroke-width: 1; stroke-opacity: 0.2; }
.fig--steps .n {
  fill: var(--ink);
  fill-opacity: var(--o, 0.5);
  stroke: var(--ink);
  stroke-opacity: var(--s, 0);
  stroke-width: 1.1;
  animation: fig-step 3.2s ease-out infinite;
}
.fig--steps .v0 { --o: 0;    --s: 0.5; }
.fig--steps .v1 { --o: 0.5;  --s: 0; }
.fig--steps .v2 { --o: 0.92; --s: 0; }
@keyframes fig-step {
  0%, 5%    { fill-opacity: 1; stroke-opacity: 1; }
  26%, 100% { fill-opacity: var(--o, 0.5); stroke-opacity: var(--s, 0); }
}
@keyframes fig-pad { 0% { fill: var(--ink); } 6.25%, 100% { fill: none; } }

.fig--spectrum .axis { stroke-opacity: 0.42; stroke-width: 1.4; }
.fig--spectrum .stem { animation: fig-col var(--dur, 2.8s) ease-in-out var(--d, 0s) infinite; }
.fig--spectrum .mark { animation: fig-peak var(--dur, 2.8s) ease-in-out var(--d, 0s) infinite; }
/* the stem's offset and the dot's travel are the same six units, on the same
   clock, so the dot cannot come off the tip */
@keyframes fig-col  { 0%, 100% { stroke-dashoffset: calc(var(--r) + 6px) } 50% { stroke-dashoffset: calc(var(--r) - 6px) } }
@keyframes fig-peak { 0%, 100% { transform: translateY(6px) } 50% { transform: translateY(-6px) } }

/* One button, and the same signal leaving it three ways. The three sectors
   carry the identical arc; only the stroke differs — solid seen, dashed heard,
   dotted felt. The pulse propagates outward by radius, so all three brighten
   together and none of them leads. */
.fig--pulse { width: clamp(150px, 24vw, 200px); }
.fig--pulse .ring { stroke-opacity: 0.34; stroke-width: 2.2; }
.fig--pulse .heard { stroke-dasharray: 3.4 2.8; }
.fig--pulse .felt  { stroke-dasharray: 0 3.8; stroke-linecap: round; }

.fig--pulse .r1 { animation: fig-wave 2.8s ease-out 0s infinite; }
.fig--pulse .r2 { animation: fig-wave 2.8s ease-out 0.13s infinite; }
.fig--pulse .r3 { animation: fig-wave 2.8s ease-out 0.26s infinite; }
.fig--pulse .core { animation: fig-core 2.8s ease-out infinite; }

@keyframes fig-wave {
  0%        { stroke-opacity: 0.26; }
  7%        { stroke-opacity: 1; }
  38%, 100% { stroke-opacity: 0.26; }
}
@keyframes fig-core {
  0%, 3%    { transform: scale(1); }
  9%        { transform: scale(1.16); }
  28%, 100% { transform: scale(1); }
}
@keyframes fig-seg { 0%, 46% { fill: var(--ink); } 54%, 100% { fill: none; } }

/* Pitch Pipe: the ring slides along the arc and settles onto the target.

   Sampled every 2% of the cycle rather than at a handful of points: keyframes
   interpolate in straight lines, so a few far-apart stops pulled the ring up to
   3.2 units off the curve on the long sweep. Easing is baked into the sampled
   positions, hence linear timing. */
.fig--cents .arc    { stroke-opacity: 0.42; stroke-width: 1.6; }
.fig--cents .target { stroke-opacity: 0.3; stroke-width: 2; }
.fig--cents .ring   { fill: var(--page); stroke-width: 3; animation: fig-tune 4.6s linear infinite; }
@keyframes fig-tune {
  0% { transform: translate(0.00px, 0.00px); }
  2% { transform: translate(-1.21px, 0.00px); }
  4% { transform: translate(-4.46px, 0.05px); }
  6% { transform: translate(-9.17px, 0.22px); }
  8% { transform: translate(-14.75px, 0.56px); }
  10% { transform: translate(-20.61px, 1.10px); }
  12% { transform: translate(-26.19px, 1.78px); }
  14% { transform: translate(-30.90px, 2.47px); }
  16% { transform: translate(-34.15px, 3.02px); }
  18% { transform: translate(-35.36px, 3.24px); }
  20% { transform: translate(-33.38px, 2.88px); }
  22% { transform: translate(-28.01px, 2.03px); }
  24% { transform: translate(-20.08px, 1.04px); }
  26% { transform: translate(-10.47px, 0.28px); }
  28% { transform: translate(0.00px, 0.00px); }
  30% { transform: translate(10.47px, 0.28px); }
  32% { transform: translate(20.08px, 1.04px); }
  34% { transform: translate(28.01px, 2.03px); }
  36% { transform: translate(33.38px, 2.88px); }
  38% { transform: translate(35.36px, 3.24px); }
  40% { transform: translate(33.29px, 2.87px); }
  42% { transform: translate(27.79px, 2.00px); }
  44% { transform: translate(19.91px, 1.03px); }
  46% { transform: translate(10.70px, 0.30px); }
  48% { transform: translate(1.24px, 0.00px); }
  50% { transform: translate(-7.44px, 0.14px); }
  52% { transform: translate(-14.26px, 0.53px); }
  54% { transform: translate(-18.18px, 0.86px); }
  56% { transform: translate(-18.31px, 0.87px); }
  58% { transform: translate(-15.37px, 0.61px); }
  60% { transform: translate(-10.36px, 0.28px); }
  62% { transform: translate(-4.21px, 0.05px); }
  64% { transform: translate(2.17px, 0.01px); }
  66% { transform: translate(7.86px, 0.16px); }
  68% { transform: translate(11.95px, 0.37px); }
  70% { transform: translate(13.52px, 0.47px); }
  72% { transform: translate(12.13px, 0.38px); }
  74% { transform: translate(8.67px, 0.19px); }
  76% { transform: translate(4.16px, 0.04px); }
  78% { transform: translate(-0.35px, 0.00px); }
  80% { transform: translate(-3.81px, 0.04px); }
  82% { transform: translate(-5.20px, 0.07px); }
  84% { transform: translate(-4.44px, 0.05px); }
  86% { transform: translate(-2.64px, 0.02px); }
  88% { transform: translate(-0.48px, 0.00px); }
  90% { transform: translate(1.32px, 0.00px); }
  92% { transform: translate(2.08px, 0.01px); }
  94% { transform: translate(1.76px, 0.01px); }
  96% { transform: translate(1.04px, 0.00px); }
  98% { transform: translate(0.33px, 0.00px); }
  100% { transform: translate(0.00px, 0.00px); }
}

.fig--strings .str { vector-effect: non-scaling-stroke; animation: fig-str 3.2s ease-in-out infinite; }
.fig--strings .str:nth-of-type(1) { animation-duration: 2.9s; }
.fig--strings .str:nth-of-type(2) { animation-duration: 3.7s; }
.fig--strings .str:nth-of-type(3) { animation-duration: 3.1s; }
.fig--strings .str:nth-of-type(4) { animation-duration: 4.3s; }
@keyframes fig-str { 0%, 100% { transform: scaleY(1) } 50% { transform: scaleY(-0.85) } }

@keyframes fig-sweep { 0%, 100% { transform: translateX(0) } 50% { transform: translateX(62px) } }

/* Each pin holds its own schedule: raised in the frames where the letter under
   that cell needs it. steps(1) so pins snap — a braille display refreshes, it
   does not fade. Base state is frame 0, which is what shows under
   reduced-motion. */
.fig--braille .pin { fill: none; stroke: var(--ink); stroke-width: 1.1; stroke-opacity: 0.4; }
.fig--braille .pin.up { fill: var(--ink); stroke-opacity: 0; }
.fig--braille .pin { animation-duration: 6s; animation-timing-function: steps(1, end); animation-iteration-count: infinite; }
@keyframes bd1 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p1 { animation-name: bd1; }
@keyframes bd2 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p2 { animation-name: bd2; }
@keyframes bd3 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p3 { animation-name: bd3; }
@keyframes bd4 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p4 { animation-name: bd4; }
@keyframes bd5 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p5 { animation-name: bd5; }
@keyframes bd7 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p7 { animation-name: bd7; }
@keyframes bd8 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p8 { animation-name: bd8; }
@keyframes bd9 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p9 { animation-name: bd9; }
@keyframes bd10 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p10 { animation-name: bd10; }
@keyframes bd11 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p11 { animation-name: bd11; }
@keyframes bd13 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p13 { animation-name: bd13; }
@keyframes bd14 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p14 { animation-name: bd14; }
@keyframes bd15 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p15 { animation-name: bd15; }
@keyframes bd16 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p16 { animation-name: bd16; }
@keyframes bd17 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p17 { animation-name: bd17; }
@keyframes bd19 { 0% { fill: var(--ink); stroke-opacity: 0; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: none; stroke-opacity: 0.4; } }
.fig--braille .p19 { animation-name: bd19; }
@keyframes bd20 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p20 { animation-name: bd20; }
@keyframes bd21 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p21 { animation-name: bd21; }
@keyframes bd22 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: none; stroke-opacity: 0.4; } 40% { fill: var(--ink); stroke-opacity: 0; } 60% { fill: var(--ink); stroke-opacity: 0; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p22 { animation-name: bd22; }
@keyframes bd23 { 0% { fill: none; stroke-opacity: 0.4; } 20% { fill: var(--ink); stroke-opacity: 0; } 40% { fill: none; stroke-opacity: 0.4; } 60% { fill: none; stroke-opacity: 0.4; } 80% { fill: var(--ink); stroke-opacity: 0; } }
.fig--braille .p23 { animation-name: bd23; }

/* Flufs: the loudness curve of a track drawn as it is analysed, with the
   target held steady across it. pathLength="100" makes the dash maths exact
   regardless of the path's real length. */
.fig--meter .axis   { stroke-opacity: 0.42; stroke-width: 1.4; }
.fig--meter .target { stroke-opacity: 0.42; stroke-width: 1.4; stroke-dasharray: 3 4; }
.fig--meter .curve  { stroke-dasharray: 100; stroke-dashoffset: 100; animation: fig-draw 5s ease-in-out infinite; }
.fig--meter .head   { animation: fig-head 5s ease-in-out infinite; }
@keyframes fig-draw { 0% { stroke-dashoffset: 100 } 55%, 85% { stroke-dashoffset: 0 } 100% { stroke-dashoffset: 100 } }
@keyframes fig-head { 0% { transform: translateX(0) } 55%, 85% { transform: translateX(112px) } 100% { transform: translateX(0) } }

.fig--filter .axis  { stroke-opacity: 0.42; stroke-width: 1.4; }
.fig--filter .curve { transform-origin: 50% 100%; animation: fig-sweep 6s ease-in-out infinite; }
@keyframes fig-sweep {
  0%, 100% { transform: translateX(-14px) scaleY(0.72); }
  35%      { transform: translateX(26px)  scaleY(1.12); }
  65%      { transform: translateX(6px)   scaleY(0.92); }
}

/* Counterpoint: four voices that gather into one line and part again. They
   rest apart, so the still state still says four. */
.fig--split .voice { animation: fig-split 5.2s cubic-bezier(0.5, 0, 0.3, 1) infinite; }
.fig--split .v1 { --x: 36px; }
.fig--split .v2 { --x: 12px; }
.fig--split .v3 { --x: -12px; }
.fig--split .v4 { --x: -36px; }
@keyframes fig-split {
  0%, 12%   { transform: translateX(0); }
  40%, 52%  { transform: translateX(var(--x)); }
  84%, 100% { transform: translateX(0); }
}
.fig--steps .n:nth-of-type(1) { animation-delay: 0.00s; }
.fig--steps .n:nth-of-type(2) { animation-delay: 0.40s; }
.fig--steps .n:nth-of-type(3) { animation-delay: 0.80s; }
.fig--steps .n:nth-of-type(4) { animation-delay: 1.20s; }
.fig--steps .n:nth-of-type(5) { animation-delay: 1.60s; }
.fig--steps .n:nth-of-type(6) { animation-delay: 2.00s; }
.fig--steps .n:nth-of-type(7) { animation-delay: 2.40s; }
.fig--steps .n:nth-of-type(8) { animation-delay: 2.80s; }

/* The pad grid: square, and sized so a single pad stays legible on a phone.
   Unlit pads sit as faint outlines so the grid reads as hardware; the lit ones
   carry the face, and the eyes blink. */
.fig--pads { width: clamp(150px, 24vw, 205px); }
.fig--pads .pad.on  { fill: var(--ink); stroke: none; }
/* The empty cells are scaffolding, not content. At 0.6 units and full black
   they weighed exactly as much as the lit ones and the face disappeared into
   the grid — 64 equally heavy squares. Thinner and lightened, they read as a
   grid you look past, and the smile is the first thing you see, which is the
   only reason the figure exists. Still just over a device pixel at the 150px
   floor, so it does not vanish on a phone. */
.fig--pads .pad.off {
  fill: none;
  stroke: var(--ink);
  stroke-width: 0.34;
  stroke-opacity: 0.4;
}
.fig--pads .eye { animation: fig-blink 3.4s steps(1, end) infinite; }
@keyframes fig-blink {
  0%, 90%   { fill: var(--ink); }
  90.1%, 95% { fill: none; }
  95.1%, 100% { fill: var(--ink); }
}


/* =========================================================
   REBALANCE PASS — August 2026

   One appended block, so it can be reviewed or deleted whole.
   Nothing above this line was edited. Every rule that changes
   layout is either global-safe or gated behind a min-width, so
   the phone layout is untouched except where it was explicitly
   broken (the tab row).
   ========================================================= */

/* --- 1. One width instead of five ----------------------------------------
   Prose was 52ch (468px), screenshots 30rem (480px), video 30rem, specs
   30rem, shortcuts 34rem. Nearly the same, never the same, so nothing ever
   quite lined up — text and screenshots sat 12px apart, which reads as a
   mistake rather than a decision.

   Now everything with a left edge shares one 30rem column: prose, shots,
   video, spec lists, price tiers, forms. 30rem is the width the screenshots
   were always at, and at 17px it works out to about 55 characters, which is
   a good measure. The column is deliberately narrower than the window — the
   screenshots are UI, and blowing UI up past its natural size makes it look
   like a slide rather than a piece of software. */
:root {
  /* One column for text, screenshots, video, spec lists and forms. It grows
     with the window rather than sitting at a fixed 30rem: at 480px inside a
     1440px screen the page was a third of the width with two thirds of empty
     sand either side. The floor keeps phones unchanged, and the cap stops the
     line getting too long to read comfortably. */
  --measure: clamp(30rem, 48vw, 42rem);
  --media-wide: 52rem;   /* the one exception, if a shot ever needs it    */
}

.wrap { max-width: 64rem; }
/* The header and footer rules deliberately opt out of the measure so their
   ends reach the window edges. Re-asserted here because this block sits
   after them in the file and .wrap would otherwise win on source order. */
.header-inner { max-width: none; }

.prose,
.shortcuts,
.spec,
.buy,
form,
.field       { max-width: var(--measure); }
/* The legal pages used to be wider than the device pages because they had no
   media to line up against. Now that the shared column has grown, that extra
   would put them past 90 characters a line, so they share the column too. */
.prose--doc  { max-width: var(--measure); }
.shot,
.video       { max-width: var(--measure); }
.shot--wide  { max-width: var(--media-wide); }

/* --- 2. Prose reads left, sits centred ------------------------------------
   The block stays on the page's centre line, so the symmetry survives; the
   text inside it gets a hard left edge, so the eye has somewhere to return
   to on a seven-line paragraph. The legal pages already did this — now the
   whole site agrees with itself. Headings, ledes and the hero stay centred. */
.prose { text-align: left; }
.prose p { margin-inline: 0; max-width: none; }
/* main .section > p stays centred: on the Contact page that paragraph is a
   single button, and on device pages it is a one-line note. Only running
   prose changes alignment. */
.spec { text-align: left; }
.spec li, .buy .note { margin-inline: 0; }
/* The terms note sat centred in a 44ch column under a left-aligned spec
   list, so nothing on the lower half of a device page shared an edge. */
.note { max-width: var(--measure); }

/* The terms note sits alone under System Requirements, where everything else
   — the heading and the requirements block — is centred on the page axis. Left
   across the full column made it the one thing hanging off to one side. It is
   a single sentence, so it centres cleanly and reads as a caption to the
   section rather than as body copy.
   The note inside .buy keeps its left edge: there it sits under the price
   tiers, which are justified to their two edges, and a centred line would
   share an edge with nothing. */
main .section > p.note {
  text-align: center;
  max-width: 46ch;
  margin-inline: auto;
}
.buy .note { text-align: left; }

.hero, .hero h1, .hero .lede, .lede,
.section-head,
.prose h2, .prose h3 { text-align: center; }
.prose--doc,
.prose--doc h2, .prose--doc h3, .prose--doc h4 { text-align: left; }

/* --- 3. Body copy one step up --------------------------------------------
   15.1px over a 468px measure was under-sized for a site whose whole subject
   is legibility, and it left a 4.2:1 hole between the 64px h1 and the text. */
.prose,
.lede       { font-size: clamp(1rem, 0.94rem + 0.28vw, 1.0625rem); line-height: 1.72; }
.prose--doc { font-size: clamp(1rem, 0.94rem + 0.35vw, 1.125rem); }
.shortcuts dd { font-size: 1rem; }
.desc       { font-size: 1rem; }

/* --- 4. Sticky footer on every page, not just the home page ---------------
   Contact ended with its footer at y=583 and 317px of empty sand beneath it.
   Newsletter, the Plug-ins tab and several device pages did the same. */
body { min-height: 100dvh; display: flex; flex-direction: column; }
body > main { flex: 1 0 auto; }
body > .site-footer { flex: none; margin-top: auto; }

/* --- 5. Header: no empty third column -------------------------------------
   1fr auto 1fr centred the nav on the window with the mark far left and
   nothing on the right — 72px of margin one side, 565px the other. Mark
   left, nav right, which is what the phone layout already did.
   Gated at 40rem so the existing phone rules keep priority. */
@media (min-width: 40rem) {
  .header-inner { grid-template-columns: auto 1fr; }
  .header-inner .site-nav { grid-column: 2; justify-self: end; }
}

/* --- 6. Devices index: a grid, not a 2,551px column -----------------------
   Six devices stacked one per row meant the collection could never be seen
   at once, with a thousand pixels of empty page either side of a 130px icon.
   Single column below 34rem — the phone is unchanged. */
@media (min-width: 34rem) {
  .index {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: clamp(1rem, 2vw, 1.75rem);
    align-items: start;
  }
  .index a { padding: clamp(1.5rem, 2.5vw, 2.25rem) 1rem; height: 100%; }
  .index .icon { margin-bottom: 1.1rem; }

  /* A tab holding one device shouldn't stretch it across three columns, and
     shouldn't leave it looking abandoned either. One wider card, centred. */
  .index:has(> li:only-child) {
    grid-template-columns: minmax(0, 24rem);
    justify-content: center;
  }
  .index:has(> li:only-child) .icon {
    width: clamp(132px, 17vw, 190px);
    height: clamp(132px, 17vw, 190px);
  }
  .index:has(> li:only-child) .name { font-size: clamp(1.75rem, 3vw, 2.5rem); }
}

/* --- 7. Category tabs: an even block on a phone ---------------------------
   Four pills of four different widths wrapping 2 + 2 read as an accident. */
@media (max-width: 30rem) {
  .tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
  .tab { padding-inline: 0.5rem; }
}

/* --- 8. The form ----------------------------------------------------------
   A centred label floating over a full-width rule, with the typed address
   recentring itself keystroke by keystroke, read as decoration rather than
   a field. Label and value now share the field's left edge; the block
   itself stays centred on the page. */
form  { text-align: left; }
.field { text-align: left; }
.field input, .field textarea { text-align: left; }
.field label { letter-spacing: 0.14em; }
/* 48px of box with the rule at the bottom left the label floating well clear
   of the line it belonged to. */
.field { gap: 0.35rem; }
.field input { min-height: 44px; padding: 0.45rem 0 0.4rem; }

/* --- 9. A little more line in the marks -----------------------------------
   Between an 800-weight headline and an 800-weight "Sting", a 1.5-unit
   stroke read as a lighter, different design rather than a quieter one. */
.face .rim,
.face .eye,
.face .mouth,
.face .lids line { stroke-width: 2; }

/* --- 10. Shortcut tables: an even gutter ----------------------------------
   The keys are a definition list, not a table. Left-aligned in a fixed
   column, a one-letter key sat 107px from its description while
   "Shift Control V" sat 30px away. Right-aligning the key column makes the
   gutter constant. Only above 30rem — below that the list is one column
   and the key belongs at the left. */
@media (min-width: 30.0625rem) {
  .shortcuts dt { text-align: right; }
}

/* --- 11b. Long unbreakable tokens -----------------------------------------
   The DripRog licence list carries filenames and a git commit hash. On a
   320px screen `buildroot-2024.02.9.tar.gz` is wider than the column, and the
   whole page scrolled sideways because of it. Also pre-existing. */
.prose, .note, .spec, .shortcuts dd { overflow-wrap: anywhere; }

/* --- 13. The home headline ------------------------------------------------
   Bigger, and back to 800 weight. The tracking and line height go back with
   it: -0.035em and 1.02 exist because heavy letterforms clog at default
   spacing, and loosening them was only ever compensation for the lighter
   weight. Same for the measure — 800-weight characters are wider, so 22ch is
   what keeps this to two lines, not the 24ch a 400 weight needed.

   The size is bounded by height, not width: the home page is one screen, and
   the headline, the face and the plug share it. So growing the headline means
   the face yields — 26vh to 22vh, about 36px back at 1440x900, which is what
   pays for the larger type. The headline is the message; the face is grace
   notes. Both terms keep a vh component so a short laptop screen still fits
   without scrolling. */
.home .hero--home h1 {
  /* Inherits 400 from h1 now, so the measure widens with it: 400-weight
     characters are narrower, and 22ch was tuned for 800 — at the new weight
     it broke the line to three. */
  font-size: clamp(1.875rem, min(6vw, 7vh), 5rem);
  max-width: 25ch;
}
/* Atkinson is wider than the face it replaced, so the headline takes more
   height at the same size. The face gives it back — 22vh to 18vh — rather
   than shrinking the sentence that is the point of the page. */
.home .hero--home .face { width: clamp(96px, min(20vw, 22vh), 230px); }

/* The plug was the loudest thing on the page. At 1440x900 "Sting" set at
   85.5px against a 57.6px headline is a hierarchy inversion — the product
   name shouting over the sentence that says who you are — and the wordmark
   also ran wider than the face above it, so the two read as competing
   elements rather than as one mark with its name under it.

   At 0.92 of the headline it sits inside the face's width and reads as its
   caption, and the headline is unambiguously the first thing you see. The vh
   term stays, because this shares the one screen with everything else. */
.home .plug .name { font-size: clamp(1.75rem, min(5vw, 5.9vh), 4.125rem); }

/* The block was centred but the ink was not. main centres the hero between
   header and footer, but the hero and the plug each carry bottom padding, and
   the footer carries top padding on top of that — so the measured gap under
   the plug ran about 1.5x the gap above the headline (127px against 86px at
   1440x900) and the whole composition sat high. Dropping both bottom paddings
   lets the centring do its job on the ink rather than on the boxes: 109 above
   against 105 below, and the same ratio holds from 1280 to 2560. A hair
   top-heavy is deliberate — optical centre sits slightly above the
   mathematical one. */
.home .hero--home { padding-bottom: 0; }
.home .plug { padding-bottom: 0; }

/* The phone rules from the earlier breakpoint have to be restated here: this
   block sits later in the file, so a bare .face selector above would otherwise
   collapse the phone face to its 96px floor. On a narrow screen the width term
   dominates, and the headline has to stay modest or it takes three lines and
   pushes the plug off the one screen. */
@media (max-width: 40rem) {
  .home .hero--home h1 { font-size: clamp(1.625rem, min(7.2vw, 5.2vh), 2.375rem); }
  .home .hero--home .face { width: clamp(140px, min(46vw, 28vh), 240px); }
  /* On a phone the headline is two lines and the name is one word, so the
     headline's block still dominates at a nominally larger name size. Set
     explicitly rather than inherited: 47px here was the old inversion, 23px
     leaves the word stranded under a 179px face. */
  .home .plug .name { font-size: clamp(1.5rem, min(7.7vw, 5.6vh), 2.5rem); }
}

/* --- 14. Figures and prose on device pages -------------------------------
   The drawings sit inside the reading column, centred on the same axis as the
   screenshots, the video and the price table. Everything on a device page
   therefore shares one centre line and one pair of margins — which is the
   whole reason the column exists.

   Hanging them in the margin was tried and reads well in isolation, but it
   put one element on the page off the axis that every other element shares,
   and that asymmetry is louder than the problem it solved.

   What actually fixes the arrangement is the block structure, not the
   position: each drawing lives inside the titled block it belongs to, bound
   tight to the text above it, with the block's own margin below. It annotates
   a named idea rather than floating between two paragraphs that both have a
   claim on it.

   Width is back near the size the figures were drawn for. Their stroke widths
   are absolute units inside a 120-unit viewBox, so scale is not free: at
   176px the lines came out thin and the drawings read as small and timid, and
   at 480 they would be four times heavier than the body text.
   ========================================================================= */
.prose .block { margin-bottom: clamp(2.25rem, 4.5vw, 3.25rem); }
.prose .block:last-child { margin-bottom: 0; }
/* An unheaded intro paragraph is a plain <p>, so without this the first
   heading sits one paragraph-gap below it instead of one block-gap, and reads
   as belonging to the intro rather than starting something new. */
.prose > p + .block { margin-top: clamp(2.25rem, 4.5vw, 3.25rem); }
.prose .block h3 {
  font-size: 1.0625rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.3;
  text-align: left;
  margin: 0 0 0.3rem;
}
.prose .block p { margin: 0; }
/* A block can hold more than one paragraph, so sibling paragraphs inside one
   need their own gap — smaller than the gap between blocks, or the heading
   stops looking like it governs both of them. */
.prose .block p + p { margin-top: 0.85em; }

.prose .block > .fig {
  /* 264px was competing with the screenshots above it. These are annotations
     on a sentence, so they should read as smaller than the thing they annotate. */
  width: clamp(170px, 20vw, 212px);
  margin: clamp(1rem, 2vw, 1.5rem) auto 0;
}
/* Every drawing is optically centred inside its own viewBox except the Pyra
   strings, whose ink sits 4 units right of centre in a 120-unit box. Centring
   the container would leave it visibly off the page axis. */
.fig--strings { transform: translateX(-3.3%); }

/* --- 16. The Sting plug-in reveal ----------------------------------------
   The two screenshots of Sting are the same window collapsed and expanded,
   and the first 672 of the taller image's 1997 pixels are byte-identical to
   the shorter one. So this is not a crossfade between two pictures faking
   continuity — it is one picture, revealed. The panel grows downward exactly
   the way it does in the plug-in.

   The stage sticks while the track scrolls past it, and the image's bottom
   clip travels from 66.35% (= 1 - 672/1997) to 0 over that distance. Nothing
   else on the page moves during it. `round 3px` keeps the cut edge rounded to
   match the corners baked into the image, so the collapsed state has the same
   silhouette the real window does.

   Everything is inside @supports: with no scroll-driven timelines the track
   is an ordinary block, the stage is not sticky, and the image is simply
   shown in full. Same for reduced motion — a pinned viewport is precisely
   what that setting exists to prevent. Both fallbacks are the plain page.
   ========================================================================= */
/* The reveal is the one image on the site allowed to break the 30rem column:
   it is the page's opening statement, and the expanded panel has four times
   the content of the collapsed one, so at column width the sequencer rows
   were unreadable by the time it finished.

   Bounded by the text column and by the screen less its gutters, whichever is
   smaller. The viewport height used to bound it too, back when the panel had
   to fit inside a pinned stage; it scrolls in the page now, so its height is
   its own business. */
.reveal {
  --reveal-w: min(var(--measure), calc(100vw - 2.5rem));
}
.reveal-stage { max-width: var(--reveal-w); margin-inline: auto; }
/* max-width: none because .shot caps every image at the 30rem column — the
   stage above is what bounds this one. */
.reveal-shot { display: block; width: 100%; max-width: none; height: auto; }

/* Not a scroll-scrubbed animation. A scrub maps progress to scroll position,
   which needs scroll distance belonging to the panel — and the panel sits near
   the top of the page with none. The previous version manufactured it with a
   130vh track and a pinned stage, and that manufactured distance was the dead
   space above and below the image. Removing the pin removed the distance, and
   the reveal was then already complete before the page had been scrolled at
   all.

   So it is a transition instead. The panel is collapsed on arrival, opens once
   when it has been scrolled to, and stays open. No extra height, nothing
   pinned, nothing to scrub, and no way to play it backwards.

   Both classes are set by script, which means the default state — no
   JavaScript, or reduced motion — is the plain image, fully shown. */
.reveal-armed .reveal-shot {
  clip-path: inset(0 0 66.35% 0 round 3px);
  /* Anchored at the top: the panel's title bar stays put while it opens
     downward, the way it does in the plug-in. */
  transform-origin: top center;
  transition: clip-path 900ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.reveal-armed.reveal-done .reveal-shot {
  clip-path: inset(0 0 0% 0 round 3px);
}
}

/* --- 17. The free tier, which has to explain itself -----------------------
   Every other tier on the site is a link straight to checkout. This one has a
   condition attached, so it opens a panel first. It is a <details>, which
   means Enter, Space, Tab and Escape all behave before a line of JavaScript
   is written, and it degrades to an open panel if anything fails.

   The summary is styled as an ordinary tier row so the two read as one list.
   Removing the default marker takes two rules, since Safari still uses the
   old pseudo-element. */
.tier-more > summary { list-style: none; }
.tier-more > summary::-webkit-details-marker { display: none; }
.tier-more > summary::marker { content: ""; }
.tier-more > summary { cursor: pointer; }
.tier-more[open] > summary { border-bottom-color: transparent; }

/* The caret is drawn rather than typed, so it matches the site's line work
   instead of arriving with a system font. */
.tier-more > summary .price::after {
  content: "";
  display: inline-block;
  width: 0.5em;
  height: 0.5em;
  margin-left: 0.6em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-0.15em) rotate(45deg);
  transition: transform 140ms ease;
}
.tier-more[open] > summary .price::after { transform: translateY(0.1em) rotate(-135deg); }

.tier-panel {
  /* Left-aligned like all running prose on the site. The tier rows above it
     are justified to their two edges, so a centred paragraph underneath had
     no edge in common with anything. */
  text-align: left;
  padding: 0.25rem 0.5rem 1.25rem;
  border-bottom: 1px solid var(--line);
}
.tier-panel p { margin: 0 0 1.15rem; }
.tier-panel p:last-child { margin-bottom: 0; }
.tiers li:last-child .tier-panel { border-bottom: 0; }

/* One row of platform buttons, wrapping rather than shrinking — a button that
   narrows to fit stops looking like a button. */
.platforms {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}
/* Platform names are proper nouns, so they keep their own casing — .btn
   uppercases everything, which would render macOS as MACOS. Dropping the
   transform also removes the centring problem it caused: letter-spacing adds
   its space after the last character too, so a centred uppercase label sits
   half a tracking unit left of true centre. */
.platforms .btn {
  min-width: 8rem;
  justify-content: center;
  text-align: center;
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 0.875rem;
}

@media (prefers-reduced-motion: reduce) {
  .tier-more > summary .price::after { transition: none; }
}

/* --- 19. The screen reader guide -----------------------------------------
   Reuses the shortcut list from the keyboard pages. The two additions are a
   quotation style for what the software actually says out loud, and inline
   code for the Live option string. */
.say {
  margin: 0.9rem 0 0;
  padding-left: 1rem;
  border-left: 2px solid var(--line);
}
.say p { margin: 0; font-style: italic; }

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  /* A command you have to type by hand should not be broken across a line —
     the reader cannot tell whether the break is part of it. Long strings get
     to overhang the measure instead, and only break as a last resort. */
  white-space: nowrap;
  overflow-wrap: normal;
}
/* The Live option string is longer than the column on a phone, so there it
   has to be allowed to wrap after all. */
@media (max-width: 30rem) {
  code { white-space: normal; overflow-wrap: anywhere; }
}

/* --- 20. Link lists -------------------------------------------------------
   A stack of links needs more room between them than lines of prose do, so
   each is a target rather than a line to read past. */
.links {
  list-style: none;
  margin: 0;
  padding: 0;
}
.links li + li { margin-top: 0.85rem; }
.links .meta {
  margin-left: 0.5rem;
  font-size: 0.75em;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.55;
  text-decoration: none;
}

/* --- 21. Device titles fitted to the column ------------------------------
   Each device title is set to the exact width of the screenshot beneath it.
   build.py measures the string in the real font and writes its advance width
   in em into --title-adv; the column width divided by that is the size at
   which the word spans the column exactly.

   The clamp is what keeps it from becoming a stunt. Short names like Pyra
   would want 340px and long ones like the 13 Tracks title would want 39px;
   both are past the point where the effect stops reading as typography. Names
   that fall outside the band keep a sensible size and simply are not full
   width, which is the right trade.

   2% is shaved off the fitted size. A title measured to land exactly on the
   column edge is at the mercy of text-wrap: balance, which will break a line
   it judges too full even when it technically fits — Pitch Pipe did exactly
   that at 0.5%. The slack is invisible and the wrap is not. */
.title-fit {
  /* width: 100% is required, not decoration: container-type stops the element
     sizing from its contents, and inside the hero's centred grid that leaves
     it zero wide — which makes 100cqw zero and every title fall to the
     floor. */
  container-type: inline-size;
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
}
.title-fit h1 {
  font-size: clamp(2.75rem, calc(100cqw / var(--title-adv, 4) * 0.98), 13rem);
}
/* Without an advance — a build without fontTools — nothing changes. */
.title-fit:not([style*="--title-adv"]) h1 { font-size: clamp(3rem, 7.4vw, 5.75rem); }

/* Uppercase display, mixed-case markup. All-caps typed into the HTML gets read
   out letter by letter by VoiceOver and NVDA, which is the wrong thing to do to
   a plug-in name on this site of all sites. text-transform leaves the
   accessible name alone.

   Tracking follows weight everywhere else on the site by getting tighter as the
   weight goes up. Caps want the opposite correction: they sit further apart
   optically to begin with, so the negative tracking that suits mixed-case
   display type closes them up too far. Both values below are the mixed-case
   ones, opened back up.

   --title-adv on anything using .title-caps has to be measured on the uppercase
   string, since that is what actually gets drawn. */
.title-caps h1 {
  text-transform: uppercase;
  /* Uppercase at 600. The home page link is 800, which reads as too heavy
     once the letters are this large on the device page, so the two are close
     rather than identical. h1 is 400 across the site; this is the one that
     is not.

     title_advance() in build.py instantiates the font at weight 400, so an
     advance for anything using this rule has to be measured at 600 instead or
     the title comes out too small for its box. STING at 600 with this tracking
     is 2.866. */
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1;
}
.plug .name,
.name--caps { text-transform: uppercase; letter-spacing: -0.015em; }

/* --- 22. A section that is only a button ---------------------------------
   Normal section spacing assumes a heading or a paragraph follows. A lone
   button under a title reads as attached to it, so it gets its own step. */
.cta { margin-top: clamp(1.5rem, 4vw, 2.75rem); }

/* --- 23. Underlines that survive descenders -------------------------------
   Browsers default to skip-ink, which breaks the underline around every p, q,
   g and y. On a link that is one or two words the gaps read as damage rather
   than as a nicety, and a broken rule is harder to see as a link at all. The
   offset is already large enough to clear the descenders cleanly, so the
   skipping buys nothing. */
a,
.prose a,
.text-link,
.site-nav a:hover {
  text-decoration-skip-ink: none;
}

/* --- 24. The spec list, centred as a block --------------------------------
   Its items stay left-aligned to each other — a ragged centre on a list of
   requirements is hard to scan — but the block itself is only as wide as its
   longest line and sits on the page's centre line, so it reads as belonging
   to the heading above it rather than to the left edge of the column. */
.spec {
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
}

/* --- 25. A category with nothing in it yet -------------------------------
   Sits where a device would, at the same weight as a device name but quiet,
   so the tab reads as deliberate rather than broken. */
.index .soon {
  grid-column: 1 / -1;
  padding: clamp(2rem, 5vw, 3.5rem) 1rem;
  text-align: center;
  opacity: 0.55;
}
.index .soon .name { font-size: clamp(1.125rem, 2vw, 1.375rem); font-weight: 800; }

/* --- 26. The newsletter form ---------------------------------------------
   A single email field does not need the full 42rem column: a 672px rule for
   an address looked like a mistake, and the button sitting at its left end
   left the page weighted to one side. The form takes a width of its own,
   centred, and the button matches the field it belongs to — so the block
   reads as one shape on the page's centre line rather than three elements of
   three different widths. */
.newsletter-form {
  max-width: min(var(--measure), 22rem);
  margin-inline: auto;
}
.newsletter-form .field { max-width: none; }
.newsletter-form .btn {
  display: flex;
  width: 100%;
  justify-content: center;
  margin-top: 0.35rem;
}

/* --- 27. Newsletter consent line and error note --------------------------
   The consent line is a promise, not fine print, so it is quiet but not tiny.
   The error note sits above the form rather than below the button, where it
   would be off-screen on a phone once the keyboard is up. */
/* Fine print under a form. Renamed off ".consent" — the cookie notice took
   that class later and is position: fixed, which dropped this to the bottom
   corner of the page on every form that used it. */
.form-note {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.5;
  opacity: 0.7;
  text-align: left;
}
.form-error {
  max-width: min(var(--measure), 22rem);
  margin: 0 auto 1.25rem;
  padding: 0.7rem 0.9rem;
  border: 2px solid var(--ink);
  font-size: 0.9375rem;
  text-align: left;
}

/* --- 28. Consent notice ---------------------------------------------------
   A bar along the bottom rather than a full-screen overlay: the page stays
   readable while the choice is open, which is both kinder and what the notice
   is claiming — that the site works either way.

   Two real buttons, equally weighted. A grey "decline" next to a bright
   "accept" is a dark pattern, and on a site about accessibility it would be a
   poor look as well as legally weaker. */
.consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  background: var(--page);
  border-top: 2px solid var(--ink);
  /* The notice sits over the page, so it needs to be opaque and to read as a
     layer above rather than a band printed on it. */
  box-shadow: 0 -12px 28px rgba(0, 0, 0, 0.14);
}
.consent-inner {
  max-width: var(--measure);
  margin-inline: auto;
  padding: clamp(1.25rem, 3vw, 1.75rem) clamp(1.25rem, 4vw, 2rem);
}
.consent h2 {
  font-size: 1.0625rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  margin: 0 0 0.4rem;
  text-align: left;
}
.consent h2:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.consent p {
  margin: 0 0 0.6rem;
  font-size: 0.9375rem;
  line-height: 1.55;
  text-align: left;
}
.consent-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 1rem;
}
.consent-actions { align-items: center; }
.consent-actions .btn { min-width: 7.5rem; justify-content: center; }
/* Same size, same weight, same target. Only the fill differs, so "Allow" reads
   as the primary action without "No thanks" being hidden. */
/* Same size, same border, same target — only the fill differs, so "Allow"
   leads without "No thanks" being demoted to a grey afterthought. */
.btn--quiet { background: none; color: var(--ink); }

/* The notice covers the foot of the page while it is open, so the footer gets
   room to be scrolled clear of it — the home page included. Excluding it kept
   that page to one screen, but on a 667px phone the notice then sat on top of
   the product name, which is worse than a page that scrolls for as long as the
   notice is open. Once dismissed the home page is one screen again. */
body:has(.consent) .site-footer { padding-bottom: 12rem; }

/* A quiet third action beside the two buttons: dismiss without agreeing.
   Deliberately not a button-shaped control — it is the "leave me alone" exit,
   and giving it the same weight as Accept would make the row read as a choice
   between three equal things. */
.consent-close {
  background: none;
  border: 0;
  padding: 0.5rem 0.25rem;
  font: inherit;
  font-size: 0.875rem;
  color: inherit;
  opacity: 0.65;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.consent-close:hover { opacity: 1; }

/* --- 29. The home page on a short screen ---------------------------------
   The home page is one screen by design, and the fixed costs — header, footer,
   the plug — do not shrink. On anything under about 800px tall the hero's own
   spacing is the only slack left, so it tightens rather than letting the page
   scroll. Keyed to height, not width: a 1440x700 laptop and a 375x667 phone
   have the same problem and neither is caught by a width breakpoint. */
@media (max-height: 50rem) {
  .home .hero--home {
    padding-block: clamp(0.5rem, 1.6vh, 1.25rem) 0;
    gap: clamp(0.6rem, 1.8vh, 1.25rem);
  }
  .home .plug { gap: 0.3rem; }
  /* The face follows the height down, but only as far as it has to. The
     earlier version took it to 72px on a 667px phone while a quarter of the
     screen sat empty — it was solving for a footer that has since been
     tightened, and kept solving after the problem was gone. */
  .home .plug .face { width: clamp(120px, min(38vw, 26vh), 200px); }
  .home .plug .name { font-size: clamp(1.375rem, min(6vw, 4.6vh), 2.125rem); }
  .site-footer { padding-block: clamp(0.75rem, 2vh, 1.5rem) clamp(0.5rem, 1.5vh, 1rem); }
  .footer-grid { gap: 0.35rem 0.9rem; }
  .footer-social { margin-top: 0.4rem; }
  .footer-social svg { width: 18px; height: 18px; }
  .footer-grid a { font-size: 0.75rem; letter-spacing: 0.08em; }
}

/* The palette hint tells you to press a key. On a narrow screen there is no
   keyboard to press it on, and at 320px it wraps to two lines and takes 40px
   of a footer that is already the tallest thing on the page. */
@media (max-width: 30rem) {
  .kbd-hint { display: none; }
}

/* --- 30. Which face goes where -------------------------------------------
   Everything is Atkinson by default. These take Manrope back: the headings,
   and the small-caps furniture — nav, buttons, prices, footer links, tabs,
   field labels, the palette's chrome. All of it is either large or in caps,
   so the character confusions Atkinson exists to prevent do not arise.

   Deliberately left in Atkinson: <kbd>, the shortcut tables, <code>, the spec
   lists and all body copy — every place a reader has to tell one character
   from another rather than read a word. */
h1, h2, h3,
.label,
.site-nav a,
.wordmark,
.idx,
.price,
.site-footer a,
.field label,
.btn,
.tab,
.name,
.plug .desc,
.palette-title,
.palette-sec,
.palette-hint,
.links .meta,
.skip,
.consent h2,
.blocked-title,
.consent-close {
  font-family: var(--font-display);
}

/* The tier row is a control, not reading matter — you click it. Every other
   control on the site is small caps in Manrope: the nav, the buttons, the
   footer links, the field labels. Sentence-case Atkinson bold made this the
   one clickable thing that looked like a paragraph, and Atkinson's bold is
   wide enough to fight the hairline rules it sits between.

   The price is in the same row and the same face, so name and price read as
   one object rather than two. Digits are the one thing Atkinson would do
   better here, but "20€ + VAT" in a labelled row is not where anyone misreads
   a zero. */
.tier {
  font-family: var(--font-display);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.tier .price { font-size: inherit; }

/* Manrope's tighter tracking comes back with it — the headings are its type
   again, and -0.005em was a concession to Atkinson's open spacing. */
h1 { letter-spacing: -0.02em; }

/* --- 31. The notice on a small screen ------------------------------------
   At full size it took nearly half of a 568px screen. Tighter padding, tighter
   leading and a smaller heading give most of that back without cutting a word,
   since the two facts it carries — which companies, and no tracking — are the
   whole reason it is worth reading. */
@media (max-height: 44rem), (max-width: 26rem) {
  .consent-inner { padding: 0.9rem 1.1rem 1rem; }
  .consent h2 { font-size: 0.9375rem; margin-bottom: 0.3rem; }
  .consent p { font-size: 0.8125rem; line-height: 1.45; margin-bottom: 0.4rem; }
  .consent-actions { gap: 0.5rem; margin-top: 0.7rem; }
  .consent-actions .btn { min-width: 6.5rem; padding: 0.6rem 0.9rem; }
}

/* --- 32. The free download count -----------------------------------------
   Sits above the platform buttons so it is read before the click, not after.
   Empty until the endpoint answers — an unknown number shown as a guess is
   worse than no number. */
.dl-count {
  margin: 0.75rem 0 0;
  font-size: 0.875rem;
  font-weight: 700;
}
.dl-count:empty { display: none; }

/* When today's are gone the links stay in the page and stay focusable — they
   still lead somewhere that explains why — but they stop looking like the
   thing to do. */
.btn--spent {
  opacity: 0.45;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

/* --- 11. Very narrow phones ----------------------------------------------
   Pre-existing, not introduced above: at 320px (iPhone SE, older Android)
   the three nav items plus the mark were 27px wider than the window and the
   whole page scrolled sideways. Tighter gaps rather than a burger — three
   links do not need a menu. Tap targets keep their 44px height. */
@media (max-width: 22.5rem) {
  .header-inner { gap: 0.5rem 0.5rem; }
  .site-nav ul  { gap: 0.55rem; }
  .site-nav a   { padding-inline: 0.2rem; font-size: 0.8125rem; letter-spacing: 0.06em; }
}
