/* 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. */
@font-face {
  font-family: "Manrope";
  src: url("fonts/Manrope-variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* =========================================================
   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: "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.02em;
}

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); }
.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; }
@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.02em;
  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); }
main .section > p.note,
.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;
}
.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.

   The width is bounded three ways — 40rem, the viewport height (so the fully
   open panel never runs off the bottom of a laptop screen), and the screen
   width minus its gutters. Whichever is smallest wins. */
.reveal {
  /* Below the desktop breakpoint the panel is simply the column, exactly like
     every other screenshot on the site — no zoom, nothing special. Above it,
     the width is a share of the window rather than a fixed size, so narrowing
     the window narrows the panel with it. Fixed at 40rem it stayed 636px
     while the window shrank around it, which is what read as expanding. */
  --reveal-w: min(var(--measure), calc(100vw - 2.5rem));
  /* The panel's rendered height. Derived rather than guessed: 1.188 is the
     screenshot's own aspect ratio, 1997/1681. The sticky-stage offsets below
     are computed from it, and if it goes missing they silently evaluate to
     nothing — which is exactly what happened when an earlier edit dropped it. */
  --reveal-h: calc(var(--reveal-w) * 1.188);
}
.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; }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) and (min-height: 40rem) {
    /* The stage is exactly one viewport tall and sticks at the top, which
       makes the pinned duration (track - viewport) identical to the timeline's
       contain range (track - viewport). Get that wrong and the panel finishes
       opening while the page is still held, which reads as a stall. The image
       is centred inside the stage, so the collapsed panel sits still rather
       than drifting as it grows. */
    .reveal-track {
      view-timeline: --sting-reveal block;
      height: 130vh;
    }

    @media (min-width: 52rem) {
      /* The panel and the column are the same width, always. On a short window
         a panel at full column width would not fit the sticky viewport, and
         the old fix shrank the panel — which left it narrower than the text
         beside it. So the page follows the panel instead: --measure is
         redefined for the whole page, and the prose, the tiers, the spec list
         and the panel all narrow together and stay on one pair of edges.
         Scoped with :has() so it applies only to the page that has a reveal,
         and only while the reveal is actually running. */
      body:has(.reveal) { --measure: min(42rem, 48vw, calc((100vh - 5rem) / 1.188)); }
      .reveal { --reveal-w: var(--measure); --reveal-h: calc(var(--measure) * 1.188); }
    }
    /* The stage has to be exactly one viewport tall for the pin and the reveal
       to last the same distance, but the panel does not have to sit in the
       middle of it. Centred, a phone put half a screen of nothing between the
       title and the collapsed panel, because the panel is short and the stage
       is not. It now hangs from the top of the stage, with the offset only
       growing to a true centre once there is room for that to look
       deliberate. */
    .reveal-stage {
      position: sticky;
      top: 0;
      height: 100vh;
      display: flex;
      align-items: flex-start;
      padding-top: var(--reveal-top, 1.5rem);
    }
    @media (min-width: 52rem) {
      .reveal-stage { --reveal-top: calc(max(0px, 100vh - var(--reveal-h)) / 2); }
    }
        /* Pull the section up by whatever empty stage is left under the panel once
       the reveal has finished — which is the stage height less the top offset
       and the panel itself. */
    .reveal {
      margin-bottom: calc(-1 * max(0px, 100vh - var(--reveal-top, 1.5rem) - var(--reveal-h)));
    }
    @media (min-width: 52rem) {
      .reveal { --reveal-top: calc(max(0px, 100vh - var(--reveal-h)) / 2); }
    }
    .reveal-shot {
      /* Anchored at the top: the panel's title bar stays put while the window
         both shrinks and opens downward, which is the only combination of the
         two that reads as one movement rather than two. */
      transform-origin: top center;
      scale: var(--reveal-zoom, 1);
      clip-path: inset(0 0 66.35% 0 round 3px);
      animation: sting-reveal linear both;
      animation-timeline: --sting-reveal;
      animation-range: contain 0% contain 100%;
    }

    /* How much bigger the collapsed panel starts. Bounded by the window, not
       chosen freely: at 2x the image is 960px wide, so the zoom has to step
       down before that would reach the edge of the screen. Below 52rem the
       column is already 90% of the viewport and there is no room to grow into,
       so the panel just opens at its own size. */
    @media (min-width:  52rem) { .reveal-shot { --reveal-zoom: 1.1;  } }
    @media (min-width:  64rem) { .reveal-shot { --reveal-zoom: 1.15; } }
    @media (min-width:  90rem) { .reveal-shot { --reveal-zoom: 1.3;  } }
    @media (min-width: 120rem) { .reveal-shot { --reveal-zoom: 1.4;  } }
  }
}

@keyframes sting-reveal {
  from { clip-path: inset(0 0 66.35% 0 round 3px); scale: var(--reveal-zoom, 1); }
  to   { clip-path: inset(0 0 0% 0 round 3px);     scale: 1; }
}

/* --- 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.

   0.5% is shaved off so a title that fits to the pixel cannot wrap on a
   sub-pixel rounding difference. */
.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.995), 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); }

/* --- 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. */
.newsletter-form .consent {
  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. */
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; }

/* --- 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; }
}
