/* Byggradarn.
 *
 * The design language is docs/design-language.md and it is binding. This file is its only
 * implementation, and DesignLanguageTest fails the build when the two disagree.
 *
 * The whole system, so it fits in one head:
 *
 *     five type sizes · three radii · two depths · one accent
 *
 * The smallness of those numbers is the design. A sixth size is one of the five plus a weight; a
 * third depth is a surface that has not decided whether it floats. Everything below is either a
 * token or a component built only from tokens — a value invented in a rule is how a design system
 * stops being one, and the test refuses it.
 *
 * The idea the components serve: one decision at a time, large, on a quiet sheet. The page is
 * near-monochrome so that the accent can mean exactly one thing — the answer — and the primary
 * button is ink rather than accent so the two never compete.
 */

/* ── the font ───────────────────────────────────────────────────────────────
   Self-hosted, not from a CDN: no third-party request on any page view, which is the same rule
   the vendored htmx follows. One variable file per subset covers 400–800, so a weight costs
   nothing after the first. 40KB for the pair.
   Manrope by Mikhail Sharanda, SIL Open Font License 1.1 — see fonts/README.md. */
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/fonts/manrope-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/fonts/manrope-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                 U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --font-ui: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;

  /* ── type ────────────────────────────────────────────────────────────────
     Five, and the smallest is 15. The scale this replaced said seven and the stylesheet used
     eleven, of which the two commonest were 12 and 13 — which is why the product read as small
     and tight. Body is 17/28 and carries most of the interface. */
  --fs-display: 44px; --lh-display: 50px;
  --fs-title:   30px; --lh-title:   38px;
  --fs-heading: 21px; --lh-heading: 30px;
  --fs-body:    17px; --lh-body:    28px;
  --fs-caption: 15px; --lh-caption: 22px;

  /* Tracking tightens as type grows, and stays at zero below the heading, where negative
     tracking reads as cramped rather than as crisp. */
  --track-display: -0.03em;
  --track-title: -0.02em;
  --track-heading: -0.01em;
  --track-caps: 0.06em;

  /* ── space ───────────────────────────────────────────────────────────────
     8pt. The large steps are where the air is; a screen feels generous because of --sp-6 and
     --sp-8, not because every gap grew. */
  --sp-1: 8px;  --sp-2: 16px; --sp-3: 24px; --sp-4: 32px;
  --sp-6: 48px; --sp-8: 64px; --sp-12: 96px;
  --sp-half: 4px;

  /* ── shape ───────────────────────────────────────────────────────────────
     Three. Controls, surfaces, capsules. Radii are concentric where they nest: a 20px surface
     with 24px of padding holds 12px controls, and the two curves stay parallel. */
  --r-control: 12px;
  --r-surface: 20px;
  --r-full: 999px;

  /* ── measure ─────────────────────────────────────────────────────────────
     What makes a screen calm is a column narrow enough to take in at a glance, not more space
     between things. A list stretched across a 27" monitor puts the action half a screen from the
     heading it belongs to. Tables, charts and a screen whose halves sit side by side opt out —
     they use the window honestly.

     These are the phone-and-laptop values. On a desk — 1200px and up, at the end of this file —
     both step up once, so a monitor is not a phone column with white on either side; what does
     not step up is prose, which keeps a reading measure whatever the window is. */
  --measure: 760px;
  --measure-wide: 1080px;

  /* ── motion ──────────────────────────────────────────────────────────────
     Things settle rather than stop. The curve decelerates late, which is what makes a swap read
     as material moving instead of a repaint. */
  --dur-fast: 140ms;
  --dur-base: 280ms;
  --dur-slow: 480ms;
  --ease: cubic-bezier(0.32, 0.72, 0, 1);

  /* ── colour ──────────────────────────────────────────────────────────────
     Almost none. The page is near-monochrome, the primary button is ink, and the accent means
     one thing: the answer — the figure, the date, the thing we found out. An accent spent on
     decoration is an interface that can no longer point at anything. */
  --ground: #F6F7F9;
  --surface: #FFFFFF;
  --sunk: #F1F3F6;
  --hairline: #E8EBEF;

  --ink: #101317;
  --ink-2: #6A7280;
  --ink-3: #9BA3AF;

  --accent: #4B44E0;
  --accent-ink: #4B44E0;
  --accent-soft: #EEEDFD;
  --on-accent: #FFFFFF;
  --on-ink: #FFFFFF;
  /* The aurora: three lights behind the sales page's one action, and nothing else — ADR 0050.
     Named here so the glow moves with the palette; the accent is the first of them. */
  --aurora-1: #4B44E0;
  --aurora-2: #2DD4BF;
  --aurora-3: #A78BFA;
  --aurora-angle: 0deg;
  /* The front door's closing band, on ink — ADR 0051. Five values for one band on one page: the
     ground, two text colours on it, a surface and a hairline drawn in white at low opacity so
     they sit on the ink without a second palette. Nothing else in the product is dark. */
  --band: #101317;
  --on-band: #FFFFFF;
  --on-band-2: rgba(255, 255, 255, 0.64);
  --band-surface: rgba(255, 255, 255, 0.06);
  --band-hairline: rgba(255, 255, 255, 0.12);
  /* Text on a filled state colour. White in both themes, because the fill is saturated in both. */
  --on-bad: #FFFFFF;

  /* State, separate from the accent on purpose: a red that is also the brand colour cannot warn. */
  --ok: #0E9F6E;   --ok-soft: #DFF5EC;
  --warn: #B45309; --warn-soft: #FCEFD9;
  --bad: #DC3A30;  --bad-soft: #FCE5E3;

  /* ── depth ───────────────────────────────────────────────────────────────
     Two. A surface rests on the page or floats over it, and there is no third state because
     there is no third reason. Each is a hairline ring plus shadow: the ring draws the edge in
     both themes, which is why depth is never "a bigger blur". */
  --rest: 0 0 0 1px var(--hairline), 0 2px 4px rgba(16, 19, 23, 0.03),
          0 12px 32px -12px rgba(16, 19, 23, 0.10);
  --float: 0 0 0 1px var(--hairline), 0 8px 16px rgba(16, 19, 23, 0.06),
           0 40px 80px -24px rgba(16, 19, 23, 0.26);

  /* ── material ────────────────────────────────────────────────────────────
     Glass, on the chrome and the sheet and nowhere else. Content sits on an opaque surface: a
     table of figures over a live blur shifts as the page scrolls behind it, and small text over
     varying luminance loses contrast exactly where somebody is checking a number.
     It is four things at once — tint, blur, saturation, and a specular edge. Any subset is a
     grey rectangle. */
  --glass: rgba(250, 251, 252, 0.72);
  --glass-edge: rgba(16, 19, 23, 0.08);
  --glass-sheen: rgba(255, 255, 255, 0.7);
  --glass-blur: saturate(180%) blur(24px);

  /* Light, and only light — ADR 0023. Declared rather than left to the browser, because native
     widgets (scrollbars, the select menu, the checkbox) are painted by it and follow this rather
     than our tokens. Undeclared, a select on a light page drops a dark menu on a reader whose
     machine is in dark mode, which is the one part of the interface we do not otherwise control
     and exactly the way this leaked in. */
  color-scheme: light;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
}

/* ── type ───────────────────────────────────────────────────────────────────
   Five sizes as five classes, so a template names the role rather than a number. Headings are
   the same five — an h2 is the heading size because that is what a section heading is. */

h1, h2, h3, h4 { margin: 0; font-weight: 700; }
h1, .t-title {
  font-size: var(--fs-title); line-height: var(--lh-title);
  letter-spacing: var(--track-title); text-wrap: balance;
}
h2, .t-heading {
  font-size: var(--fs-heading); line-height: var(--lh-heading);
  font-weight: 600; letter-spacing: var(--track-heading);
}
h3, h4 { font-size: var(--fs-body); line-height: var(--lh-body); font-weight: 600; }

.t-display {
  font-size: var(--fs-display); line-height: var(--lh-display);
  font-weight: 800; letter-spacing: var(--track-display); text-wrap: balance;
}
.t-body { font-size: var(--fs-body); line-height: var(--lh-body); }
.t-cap { font-size: var(--fs-caption); line-height: var(--lh-caption); }

p { margin: 0; }
.lede { font-size: var(--fs-heading); line-height: 32px; color: var(--ink-2); max-width: 46ch; }
.sub { color: var(--ink-2); }
.cap { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--ink-3); }
.eyebrow {
  font-size: var(--fs-caption); line-height: var(--lh-caption);
  font-weight: 700; letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--ink-3);
}
.mono { font-family: var(--font-mono); font-size: var(--fs-caption); }
.num { font-variant-numeric: tabular-nums; }

a { color: var(--accent-ink); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Announced, not shown. For something a sighted reader already has from the shape of the page. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ── the shell ──────────────────────────────────────────────────────────────
   One rail across the top, on glass, and the page under it. Not a sidebar: navigation here is
   six destinations, and a 228px column spent on six words is 228px not spent on the letter.
   On a phone the rail keeps the brand and the menu; the destinations move into a sheet. */

.shell { min-height: 100dvh; display: flex; flex-direction: column; }

.rail {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-1) var(--sp-3);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-edge);
  box-shadow: inset 0 1px 0 var(--glass-sheen);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .rail { background: var(--surface); }
}
.rail .grow { flex: 1; min-width: 0; }

.wordmark {
  font-size: var(--fs-caption); font-weight: 800; letter-spacing: -0.01em;
  color: var(--ink); text-decoration: none; white-space: nowrap;
}
.wordmark span { color: var(--accent-ink); }
.wordmark:hover { text-decoration: none; }

.nav { display: flex; align-items: center; gap: var(--sp-half); flex-wrap: wrap; }
.nav a {
  padding: 6px 12px; border-radius: var(--r-control);
  font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: 600;
  color: var(--ink-2); text-decoration: none; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.nav a:hover { background: var(--sunk); color: var(--ink); text-decoration: none; }
.nav a[aria-current="page"] { background: var(--sunk); color: var(--ink); }

/* A count worth interrupting for. Never on a destination that merely has rows in it. */
.nav-count {
  display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: var(--r-full);
  background: var(--accent); color: var(--on-accent);
  font-size: var(--fs-caption); font-weight: 700; line-height: var(--lh-caption);
}

.page { flex: 1; width: 100%; max-width: var(--measure); margin: 0 auto; padding: var(--sp-6) var(--sp-3) var(--sp-12); }
.page-wide { max-width: var(--measure-wide); }

/* The head of a screen: what it is, and at most one control. */
.head { display: flex; align-items: flex-start; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.head .grow { flex: 1; min-width: 0; }
.head p { margin-top: var(--sp-half); }
.head-centred { flex-direction: column; align-items: center; text-align: center; }

.back {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: 600;
  color: var(--ink-2); text-decoration: none;
}
.back:hover { color: var(--ink); text-decoration: none; }

/* ── surfaces ───────────────────────────────────────────────────────────────
   One surface, one radius, one depth. A list of like things is ONE surface with hairline rows —
   nine cards with gaps is nine shadows and nine edges for one list, and the eye counts
   containers instead of reading rows. Separate surfaces are for separate subjects. */

.surface { background: var(--surface); border-radius: var(--r-surface); box-shadow: var(--rest); }
.pad { padding: var(--sp-3); }
.stack { display: flex; flex-direction: column; gap: var(--sp-3); }
.stack-tight { display: flex; flex-direction: column; gap: var(--sp-1); }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.row-tight { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-half); }
.grow { flex: 1; min-width: 0; }
.scroll { overflow-x: auto; }
.section { margin-bottom: var(--sp-6); }

.list { display: flex; flex-direction: column; }
.item {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3);
  border-top: 1px solid var(--hairline);
  color: inherit; text-decoration: none;
}
.item:first-child { border-top: 0; }
.item .grow { flex: 1; min-width: 0; }
/* Block, whatever element carries them. Written on a span — which half the templates do — they
   run together on one line and wrap into each other, which is what the kommun page looked like:
   a heading and its evidence as one paragraph with a dot beside the second line. */
.item-title { display: block; font-weight: 600; letter-spacing: -0.005em; }
.item-sub {
  display: block;
  font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--ink-3);
}
/* Two lines, then an ellipsis. For a summary whose whole text is behind the link it stands on:
   unclamped, one long body turns a row of a list into a paragraph and the rows stop being rows. */
.clamp {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}

/* A tag on the evidence line sits with the words rather than above them. */
.item-sub .tag { vertical-align: middle; margin-right: 4px; }
/* A sub on the trailing edge — a state and its detail — shrinks before the title does. As a flex
   sibling with a sentence in it, it took its content width and the name beside it was wrapped one
   word per line; half the row is what it may have. */
.item > .item-sub { flex: 0 1 auto; min-width: 0; max-width: 50%; }
.item-quiet .item-title { color: var(--ink-2); font-weight: 500; }
/* Four real columns: state, date, what it is, what to do about it.
   A date given a minimum width inside a paragraph is not a column — the prose beside it wraps
   underneath and the second line starts where the date does, which is what this replaced.

   Aligned to the top rather than centred, and every column's first line shares the title's line
   box, so the date, the heading and the button all sit on one line however long the address in
   the middle turns out to be. */
.item-cols {
  display: grid; align-items: start;
  grid-template-columns: auto 4.5rem minmax(0, 1fr) auto;
  gap: var(--sp-2);
}
/* Half of an 8px dot below the middle of a 28px line. */
.item-cols > .dot { margin-top: 10px; }
.item-cols > .working, .item-cols > .btn, .item-cols > form { justify-self: end; }
/* The spinner and its sentence share the title's line box, so the two columns start level. */
.item-cols > .working { padding: 0; line-height: var(--lh-body); }
.item-cols > .item-body { min-width: 0; }

.item-when {
  color: var(--ink-2); font-weight: 700; font-variant-numeric: tabular-nums;
  line-height: var(--lh-body); white-space: nowrap;
}

/* On a phone the four columns become two lines: the date keeps the state beside it, and what to
   do about it goes under. Four columns in 360px gives every one of them two words. */
@media (max-width: 640px) {
  .item-cols { grid-template-columns: auto minmax(0, 1fr); }
  .item-cols > .item-body { grid-column: 1 / -1; }
  .item-cols > .working, .item-cols > .btn, .item-cols > form { grid-column: 1 / -1; justify-self: start; }
}
/* Two controls on a row — a person's verify and remove — sit beside the name on a desk; on a
   phone they go under it. Side by side they took the row's width and the name beside them was
   wrapped one word per line. */
@media (max-width: 640px) {
  .item:has(> .actions) { flex-wrap: wrap; }
  .item > .actions { flex-basis: 100%; }
}
/* A column a phone can do without. The table keeps the two that answer the question and
   scrolls for nothing; on a wider screen the column is back. */
@media (max-width: 640px) { .narrow-hidden { display: none; } }
/* A row that navigates says so before the pointer arrives. */
a.item:hover { background: var(--sunk); text-decoration: none; }
a.item:hover .chevron { transform: translateX(2px); color: var(--ink-2); }
.item-top { align-items: flex-start; }

.chevron { width: 8px; height: 14px; flex-shrink: 0; color: var(--ink-3);
           transition: transform var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }

/* Nothing here, said plainly. An empty list that renders as nothing looks broken. */
.empty { padding: var(--sp-6) var(--sp-3); text-align: center; color: var(--ink-2); }
.empty p + p { margin-top: var(--sp-1); }

/* ── controls ───────────────────────────────────────────────────────────────
   Four weights, and the choice between them is about consequence, not looks. One filled button
   per screen: it is the action the screen exists for. Filled is INK, not accent — the accent is
   the answer, and a button wearing it competes with every figure on the page. */

.btn {
  font-family: inherit; font-size: var(--fs-caption); font-weight: 700; line-height: var(--lh-caption);
  padding: 11px 20px;
  border: 1px solid transparent; border-radius: var(--r-control);
  background: var(--ink); color: var(--on-ink);
  cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
.btn:hover { opacity: 0.88; text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn-plain { background: var(--surface); color: var(--ink); border-color: var(--hairline); }
.btn-plain:hover { background: var(--sunk); opacity: 1; }
.btn-ghost { background: none; color: var(--ink-2); }
.btn-ghost:hover { background: var(--sunk); color: var(--ink); opacity: 1; }
/* Only inside a sheet that has already asked. */
.btn-bad { background: var(--bad); color: var(--on-bad); }
.btn-sm { padding: 7px 14px; font-size: var(--fs-caption); line-height: var(--lh-caption); }
.btn[disabled] { opacity: 0.4; pointer-events: none; }

/* ── the aurora ─────────────────────────────────────────────────────────────
   The sales page's call to action and nothing else — ADR 0050. Two layers behind and over an
   ordinary ink button: a blurred ring of three lights turning slowly behind it, and a highlight
   that sweeps across it now and then. Both are decoration that the rest of the system refuses
   (design-guidelines.md, "Deliberately unused"), allowed on this one element because a sales
   page has one job and this is the button that does it. The button itself is unchanged — the
   label, the fill, the size — so the glow can be taken away without the button moving. */
@property --aurora-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
/* The fill moves to ::after, because a stacking context paints its own background above its
   negative-z children: with the ink on the button itself the lights showed through it. */
.btn-aurora { position: relative; isolation: isolate; overflow: visible; background: transparent; }
.btn-aurora::before {
  content: ""; position: absolute; inset: -5px; z-index: -2; border-radius: inherit;
  background: conic-gradient(from var(--aurora-angle), var(--aurora-1), var(--aurora-2), var(--aurora-3), var(--aurora-1));
  filter: blur(14px); opacity: 0.9;
  transition: opacity var(--dur-fast) var(--ease);
}
.btn-aurora::after {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; pointer-events: none;
  background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, 0.34) 50%, transparent 65%), var(--ink);
  background-size: 250% 100%, auto; background-position: 150% 0, 0 0;
}
.btn-aurora:hover { opacity: 1; }
.btn-aurora:hover::before { opacity: 1; }
.btn-aurora.btn-sm::before { inset: -3px; filter: blur(10px); }
@media (prefers-reduced-motion: no-preference) {
  .btn-aurora::before { animation: aurora-turn 8s linear infinite; }
  .btn-aurora::after { animation: aurora-sweep 3600ms var(--ease) infinite; }
}
@media (prefers-reduced-motion: reduce) {
  /* The finished state, not a frozen frame: the glow stays, the sweep is gone. The layer stays,
     because it carries the fill; hiding it took the button away with the animation. */
  .btn-aurora::after { background: var(--ink); animation: none; }
}
@keyframes aurora-turn { to { --aurora-angle: 360deg; } }
@keyframes aurora-sweep { 0%, 60% { background-position: 150% 0, 0 0; } 100% { background-position: -50% 0, 0 0; } }

.actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.actions .grow { flex: 1; min-width: 0; }

/* A field is a well: recessed, so it reads as somewhere to put something before anything is in
   it. Flat against a flat surface, only the fill distinguishes an input from a label. */
.field {
  font-family: inherit; font-size: var(--fs-body); line-height: var(--lh-body);
  width: 100%; padding: 12px 16px;
  color: var(--ink); background: var(--sunk);
  border: 1px solid transparent; border-radius: var(--r-control);
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}
.field::placeholder { color: var(--ink-3); }
.field:focus {
  outline: none; background: var(--surface); border-color: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent);
}
textarea.field { resize: vertical; min-height: 120px; }
select.field { appearance: none; padding-right: 40px;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%),
                    linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
  background-position: right 18px center, right 13px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

label, .label {
  display: block;
  font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--ink-3);
  margin-bottom: 6px;
}
.check { display: flex; align-items: center; gap: var(--sp-1); margin: 0; color: var(--ink); }
.check input { width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; }
/* A checkbox against a two-line label sits on the first line, not in the middle of the block —
   centred, it points at the gap between the name and the explanation. */
.check-block { align-items: flex-start; }
.check-block input { margin-top: 5px; }
.check-block .item-title, .check-block .item-sub { display: block; }

/* One choice from closely related options that change the view. */
.switch { display: inline-flex; gap: 2px; padding: 3px; background: var(--sunk); border-radius: var(--r-control); }
.switch a, .switch button {
  font-family: inherit; font-size: var(--fs-caption); font-weight: 600; line-height: var(--lh-caption);
  /* Concentric: the track is --r-control with 3px of padding, so the segment is 3px tighter
     and the two curves stay parallel. */
  padding: 6px 12px; border: 0; border-radius: calc(var(--r-control) - 3px);
  background: none; color: var(--ink-2); cursor: pointer; text-decoration: none;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.switch a:hover, .switch button:hover { color: var(--ink); text-decoration: none; }
.switch [aria-pressed="true"], .switch [aria-current="page"] {
  background: var(--surface); color: var(--ink);
  box-shadow: 0 1px 2px rgba(16, 19, 23, 0.10);
}

/* ── data ───────────────────────────────────────────────────────────────────
   The three things a row can carry on its trailing edge, and they are not interchangeable. */

/* The answer: the figure, the date, the thing we found out. The only fill of the accent in the
   product, and one per row — a second beside it makes neither of them the answer. The dot
   carries a qualification without spending a word on it. */
/* The answer, on the trailing edge.
   It wraps. It used to be nowrap, which is right for the values this was written for — a sum, a
   date, a CPV code — and wrong for the ones it also has to carry: a TED notice title is ninety
   characters, and as a flex item that can neither wrap nor shrink it ran straight out of the card
   and over the label beside it.
   Numbers are protected at the source instead, by ProjectRowFormatter: money and dates are grouped
   with non-breaking spaces, so nothing here can split 8 000 000 kr across two lines. */
.value {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 6px 14px; border-radius: var(--r-full);
  background: var(--accent); color: var(--on-accent);
  font-size: var(--fs-caption); font-weight: 700; line-height: var(--lh-caption);
  font-variant-numeric: tabular-nums;
  /* anywhere rather than break-word: an upphandlings-ID is one unbroken token wider than the card,
     and break-word would leave it overflowing exactly as before. */
  overflow-wrap: anywhere; text-align: right;
}
/* Two rules, and the second is not optional.
   min-width: 0 — a flex item defaults to min-width:auto, its min-content width, which is the whole
   reason the pill could not shrink.
   max-width — .grow has flex-basis 0 and the pill's basis is its own content width, so a long value
   consumes the line before any free space exists to distribute. Without a cap the pill fits inside
   the card and the label column measures 0px: the row's own heading disappears rather than
   overflowing, which is a quieter version of the same bug. */
.item > .value { min-width: 0; max-width: 60%; }
.value-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.75; }
.value-quiet { background: var(--sunk); color: var(--ink-2); }

/* A state the system reports. Nobody can remove it, and it is never the accent. */
.tag {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 12px; border-radius: var(--r-full);
  background: var(--sunk); color: var(--ink-2);
  font-size: var(--fs-caption); font-weight: 600; line-height: var(--lh-caption); white-space: nowrap;
}
.tag-ok { background: var(--ok-soft); color: var(--ok); }
.tag-warn { background: var(--warn-soft); color: var(--warn); }
.tag-bad { background: var(--bad-soft); color: var(--bad); }

/* A value somebody put in and can take out again: a filter, an address, a file. The add
   affordance is the same capsule as a dashed outline — an empty slot waiting, not another
   button. */
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.chip {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 9px 16px; border-radius: var(--r-full);
  border: 1px solid transparent; background: var(--sunk); color: var(--ink);
  font-family: inherit; font-size: var(--fs-caption); font-weight: 600; line-height: var(--lh-caption);
  text-decoration: none;
}
.chip em { font-style: normal; font-weight: 400; color: var(--ink-3); }
.chip:hover { text-decoration: none; }
.chip-x {
  border: 0; background: none; color: var(--ink-3); cursor: pointer;
  padding: 0 0 0 2px; font-size: var(--fs-body); line-height: 1; font-family: inherit;
}
.chip-x:hover { color: var(--bad); }
.chip-add {
  background: none; cursor: pointer;
  border: 1px dashed color-mix(in srgb, var(--ink-3) 55%, transparent);
  color: var(--ink-2);
}
.chip-add:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.chip[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-ink); }
/* A chip that is a button: "Följ" on a project, chip-add before and chip-on after. The pressed
   state answers the pointer too — a control with no hover reads as a label. */
button.chip { cursor: pointer; }
button.chip-on:hover { border-color: var(--accent); }

/* Urgency, where a page of similar rows cannot show it by order. */
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--ink-3); }
/* Scoped to .dot on purpose. The tone is decided in Java — one enum knows what a row's state is
   called and what colour says so — and a class name arriving on the wrong element must do
   nothing rather than turn a row into an 8px circle. */
.dot.dot-warn { background: var(--warn); }
.dot.dot-ok { background: var(--ok); }
.dot.dot-bad { background: var(--bad); }
.dot.dot-accent { background: var(--accent); }
/* Ready for you, right now. Green because the row is go rather than because something is well, and
   it breathes: a ring that expands out of the dot and fades, once every two seconds.

   Discreet on purpose — it is the only moving thing on a page of still rows, and a pulse loud
   enough to notice from across the desk is one you stop seeing by lunchtime. The ring is a
   pseudo-element so the dot itself never changes size and the row cannot shift under it. */
.dot.dot-ready { position: relative; background: var(--ok); }
.dot.dot-ready::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: var(--ok);
  animation: dot-pulse 2.2s var(--ease) infinite;
}
@keyframes dot-pulse {
  0%   { transform: scale(1); opacity: 0.5; }
  70%  { transform: scale(2.6); opacity: 0; }
  100% { transform: scale(2.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* Not merely shortened. An infinite animation with a millisecond duration is a repaint loop,
     which is worse for the reader who asked for less motion than the motion was. */
  .dot.dot-ready::after { animation: none; display: none; }
}

/* A ladder of stages: filled behind, hollow ahead, and the accent on the one it is at. */
/* Read, as opposed to unread. Hollow rather than absent: the column has to stay a column, or
   every read row shifts left by eight pixels. */
.dot.dot-read { background: none; box-shadow: inset 0 0 0 1.5px var(--hairline); }
.dot.filled { background: var(--ink-3); }
.dot.here { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }

/* One figure that answers one question. No tile, no gradient, no icon: the number is the thing. */
.stats { display: grid; gap: var(--sp-2); grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.stat { padding: var(--sp-3); }
.stat-value {
  font-size: var(--fs-title); line-height: var(--lh-title); font-weight: 800;
  letter-spacing: var(--track-title); font-variant-numeric: tabular-nums;
}
.stat-label { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--ink-3); margin-top: var(--sp-half); }
/* A figure that opens the rows it counts. Ink, not accent: the number is a door, not the answer. */
a.stat { display: block; color: inherit; border-radius: var(--r-control); }
a.stat:hover { background: var(--sunk); text-decoration: none; }

.avatar {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--sunk); color: var(--ink-2);
  font-size: var(--fs-caption); font-weight: 700;
}
.avatar-lg { width: 64px; height: 64px; font-size: var(--fs-heading); }

/* ── the chart ──────────────────────────────────────────────────────────────
   Drawn the way Stocks draws one: a filled area under a single stroke, no axes, no gridlines, no
   legend. The figures worth reading exactly are written out at display size above it; what the
   chart is for is the shape.

   It inherits colour from the accent, which is the one place a chart is allowed to use it — the
   line is the answer. The stroke does not scale with the viewBox, so a wide window gets a
   hairline rather than a smear. */
.chart-frame { color: var(--accent); }
.chart { display: block; width: 100%; height: 220px; }
.chart-foot { margin-top: var(--sp-1); }

/* A share, as a bar. A percentage in a column of percentages is read four times; a bar once. */
.bar {
  display: block; height: 4px; margin-top: var(--sp-1); max-width: 320px;
  background: var(--sunk); border-radius: var(--r-full); overflow: hidden;
}
.bar span { display: block; height: 100%; background: var(--accent); border-radius: var(--r-full); }

/* Movement, in the two directions it can go. Green up is not a rule anywhere else in the product;
   here the reader is watching a cost, so up is the direction worth noticing and it is amber. */
.up { color: var(--warn); font-weight: 700; }
.down { color: var(--ok); font-weight: 700; }

/* The chip that is currently doing the pricing. */
.chip-on { background: var(--accent-soft); color: var(--accent-ink); }

/* ── tables ─────────────────────────────────────────────────────────────────
   For rows that are genuinely compared column by column. Everything else is a list: a table of
   four columns where one is a title and three are prose is a list wearing a grid. */

.table { width: 100%; border-collapse: collapse; }
.table th {
  text-align: left; white-space: nowrap;
  padding: 0 var(--sp-2) var(--sp-1) 0;
  font-size: var(--fs-caption); line-height: var(--lh-caption);
  font-weight: 700; letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--ink-3);
}
.table td {
  padding: var(--sp-2) var(--sp-2) var(--sp-2) 0;
  border-top: 1px solid var(--hairline);
  vertical-align: middle;
}
.table tbody tr:hover { background: var(--sunk); }
.table .num { text-align: right; }
.table a { color: inherit; font-weight: 600; }
/* A table that is the whole surface gets room at its edges, or the first column sits on the
   card's edge and the header on its top. */
.surface.scroll > .table th { padding-top: var(--sp-2); }
.surface.scroll > .table th:first-child, .surface.scroll > .table td:first-child { padding-left: var(--sp-3); }
.surface.scroll > .table th:last-child, .surface.scroll > .table td:last-child { padding-right: var(--sp-3); }
.table th button {
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
  background: none; border: 0; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px;
}
.table th button:hover { color: var(--ink); }
.sort-arrow { font-size: var(--fs-caption); }
.sort-arrow-idle { opacity: 0; }
.table th button:hover .sort-arrow-idle { opacity: 0.5; }

/* The head of a surface: a heading, and at most one control on the trailing edge. */
.card-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); margin-bottom: var(--sp-3); }
.card-row .grow { flex: 1; min-width: 0; }
/* A surface whose children are its own padding rather than a list. */
.card-inner { display: flex; flex-direction: column; gap: var(--sp-1); }

/* A row of controls that narrow a list. Wraps rather than scrolls: a filter that has gone off
   the edge of its own bar is a filter nobody knows is set. */
.filters { display: grid; gap: var(--sp-2); grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

/* ── the sheet ──────────────────────────────────────────────────────────────
   Asking before something cannot be taken back. Native, so the browser supplies what a
   hand-built overlay gets wrong: the page behind goes inert, focus is trapped and restored, Esc
   closes it. The question is the title and the cost is the line under it, because somebody
   reading one line reads the title. */

.sheet {
  padding: 0; border: 1px solid var(--glass-edge); border-radius: var(--r-surface);
  background: var(--surface); color: var(--ink);
  box-shadow: var(--float);
  max-width: min(440px, calc(100vw - 32px));
}
.sheet::backdrop {
  background: rgba(11, 13, 17, 0.32);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
.sheet[open] { animation: sheet-in var(--dur-base) var(--ease); }
@keyframes sheet-in {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
.sheet-form { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-3); }
/* A sheet for reading rather than for answering. Wide enough for a prompt to keep its own line
   breaks, and tall enough to scroll inside instead of pushing the page around behind it. */
.sheet-wide { max-width: min(860px, calc(100vw - 48px)); }
.underlag {
  margin: 0; padding: var(--sp-2);
  max-height: 60vh; overflow: auto;
  background: var(--sunk); border-radius: var(--r-control);
  font-family: var(--font-mono); font-size: var(--fs-caption); line-height: var(--lh-caption);
  white-space: pre-wrap; word-break: break-word;
}
.sheet-actions { display: flex; justify-content: flex-end; gap: var(--sp-1); margin-top: var(--sp-2); }

/* ── feedback ───────────────────────────────────────────────────────────── */

.note {
  padding: var(--sp-2); border-radius: var(--r-control);
  font-size: var(--fs-caption); line-height: var(--lh-caption);
  background: var(--sunk); color: var(--ink-2);
}
.note-bad { background: var(--bad-soft); color: var(--bad); }
.note-warn { background: var(--warn-soft); color: var(--warn); }
.note-accent { background: var(--accent-soft); color: var(--accent-ink); }
.note ul { margin: var(--sp-half) 0 0; padding-left: var(--sp-2); }

.spinner {
  display: inline-block; width: 16px; height: 16px; flex-shrink: 0;
  border: 2px solid color-mix(in srgb, var(--accent) 26%, transparent);
  border-top-color: var(--accent); border-radius: 50%;
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* A wait somebody chose to leave running: the spinner sits with the sentence rather than
   replacing it, and it occupies exactly where the button will be so the row does not jump. */
.working { display: inline-flex; align-items: center; gap: var(--sp-1); color: var(--ink-2);
           font-size: var(--fs-caption); line-height: var(--lh-caption); padding: 7px 0; }

/* Live: a small mark that the page is following along, and when it last heard anything. */
.live { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-3);
        font-size: var(--fs-caption); line-height: var(--lh-caption); }
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); }

/* The button that submits and then reloads gives nothing back in between, which is what makes
   people press it twice — and a second POST is a second letter, not a no-op. */
.btn.is-busy { position: relative; color: transparent; pointer-events: none; }
.btn.is-busy > * { visibility: hidden; }
.btn.is-busy::after {
  content: ""; position: absolute; width: 16px; height: 16px;
  border: 2px solid color-mix(in srgb, var(--on-ink) 40%, transparent);
  border-top-color: var(--on-ink); border-radius: 50%;
  animation: spin 700ms linear infinite;
}
.btn-plain.is-busy::after, .btn-ghost.is-busy::after {
  border-color: color-mix(in srgb, var(--ink) 25%, transparent); border-top-color: var(--ink);
}
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-flex; }

/* ── the second rail ────────────────────────────────────────────────────────
   The operational destinations, under the main one. Quieter, and not glass a second time: two
   blurred layers over each other is a smear rather than two sheets. */
.rail-second {
  position: static;
  background: var(--surface);
  backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: none;
  border-bottom: 1px solid var(--hairline);
  padding-top: var(--sp-half); padding-bottom: var(--sp-half);
}
.rail-second .nav a { font-size: var(--fs-caption); padding: 4px 10px; }

/* An icon-only control. Square, so three of them make a row rather than a ragged edge, and the
   target is 36px because the rail is 44px tall and a 44px target would fill it.
   position: relative for the dot, which has nowhere else to sit. */
.icon-btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 36px; height: 36px; padding: 0;
  border: 0; border-radius: var(--r-control);
  background: none; color: var(--ink-2); cursor: pointer; text-decoration: none;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.icon-btn:hover { background: var(--sunk); color: var(--ink); text-decoration: none; }
.icon-btn[aria-current="page"] { background: var(--sunk); color: var(--ink); }
.icon-btn svg { width: 20px; height: 20px; }

/* Something is waiting, said without a number. The ring is the rail's own colour, so the dot reads
   as sitting on top of the bell rather than being part of it. */
.icon-dot {
  position: absolute; top: 5px; right: 5px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--glass);
}

.nav-quiet {
  font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: 600;
  color: var(--ink-3); text-decoration: none; padding: 6px 12px; border-radius: var(--r-control);
  white-space: nowrap;
}
.nav-quiet:hover { color: var(--ink); background: var(--sunk); text-decoration: none; }
.nav-quiet[aria-current="page"] { color: var(--ink); background: var(--sunk); }
/* A rail link that carries a dot beside its word — Kö, when something waits. */
.nav-dotted { display: inline-flex; align-items: center; gap: 6px; }

/* The same statement as the bell's dot, inline beside a word instead of over an icon. */
.inline-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }

/* ── signing in ─────────────────────────────────────────────────────────────
   The one screen with nothing else on it. Centred on the ground rather than in a card: a card
   implies something around it, and there is nothing around it. */
.auth {
  min-height: 100dvh; display: flex; align-items: center; justify-content: center;
  padding: var(--sp-3);
}
.auth-inner { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: var(--sp-3); }
.auth-mark { font-size: var(--fs-body); }
.auth .btn { width: 100%; }

/* ── the developer portal ───────────────────────────────────────────────────
   docs/api.md rendered as it is, so the elements arrive without classes and are dressed here.
   The same five sizes and the same tokens as everywhere else; a code block is the underlag
   treatment without the height cap, because here the text is the page. */
.developers-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2);
  margin-bottom: var(--sp-6);
}
/* A reading measure, in the unit the rule is stated in: it does not widen with the page on a
   desk, because a line of 110 characters is a line nobody finds the start of again. */
.prose { max-width: 80ch; }
.prose > * + * { margin-top: var(--sp-2); }
.prose h1 { margin-bottom: var(--sp-3); }
.prose h2 { margin-top: var(--sp-6); }
.prose h3 { margin-top: var(--sp-4); }
.prose ul, .prose ol { margin: 0; padding-left: var(--sp-3); }
.prose li + li { margin-top: var(--sp-half); }
.prose code {
  font-family: var(--font-mono); font-size: var(--fs-caption);
  background: var(--sunk); border-radius: var(--r-control); padding: 0 var(--sp-half);
}
.prose pre {
  margin: 0; padding: var(--sp-2);
  background: var(--sunk); border-radius: var(--r-control);
  font-family: var(--font-mono); font-size: var(--fs-caption); line-height: var(--lh-caption);
  overflow-x: auto;
}
.prose pre code { background: none; padding: 0; }
.prose table { width: 100%; border-collapse: collapse; }
.prose th {
  text-align: left; padding: 0 var(--sp-2) var(--sp-1) 0;
  font-size: var(--fs-caption); line-height: var(--lh-caption);
  font-weight: 700; letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--ink-3);
}
.prose td { padding: var(--sp-1) var(--sp-2) var(--sp-1) 0; vertical-align: top; }
.prose tbody tr:hover { background: var(--sunk); }
.prose strong { font-weight: 700; }
/* A screenshot in the design guide is a surface: it shows one, and it rests like one. Wide ones
   scale down rather than run off the measure. */
.prose img {
  display: block; max-width: 100%; height: auto;
  border-radius: var(--r-surface); box-shadow: var(--rest);
}
/* The guide's own nav between its documents, on the head beside the wordmark. */
.developers-head .nav { flex: 1; }

/* ── the front door ─────────────────────────────────────────────────────────
   The one page for somebody not signed in — ADR 0046. The console's rail without its
   destinations, the measure, and sections a screen apart: what the platform reads, where it reads
   it, and how to build on it. Nothing here is a component of its own; a table, a grouped list
   and the buttons carry it, which is the point of having a system. */
.front { padding-top: var(--sp-8); }
.front-hero { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3); }
.front-hero .t-display { max-width: 26ch; }
/* A section's intro and what it introduces are two groups so a desk can put them beside each
   other; on a phone they read in the order they are written. */
.front-hero-copy, .front-intro { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3); width: 100%; }
/* scroll-margin, because the rail's destinations here are the page's own sections: a heading
   scrolled to the top of the window is a heading under the glass. */
.front-section { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: var(--sp-12); scroll-margin-top: var(--sp-8); }
/* The line under a figure is a fact the page must not get wrong, so it reads at --ink-2:
   --ink-3 on the ground is under the contrast the guidelines ask of 15px text. */
.front-section .cap { color: var(--ink-2); }
/* ── app frame ──────────────────────────────────────────────────────────────
   The product as its own picture — ADR 0054 as amended. A window bar and, inside it, the
   application's actual rows drawn by the same components and the same stylesheet: what a
   screenshot would show, without a screenshot to go stale. Floats, because it is a window over
   the page. The rows inside are the ordinary grouped list; the frame is their surface. */
.frame { background: var(--surface); border-radius: var(--r-surface); box-shadow: var(--float); overflow: hidden; }
.frame-bar { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-1) var(--sp-2); background: var(--sunk); border-bottom: 1px solid var(--hairline); }
.frame-dots { display: inline-flex; gap: 5px; margin-right: var(--sp-half); }
.frame-dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--ink-3); opacity: 0.45; }
.frame-title { flex: 1; min-width: 0; font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: 600; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.frame-head { padding: var(--sp-3) var(--sp-3) var(--sp-1); }
.frame .item { padding: var(--sp-2) var(--sp-3); }
.frame .cap { padding: var(--sp-1) var(--sp-3) var(--sp-2); }

/* ── the sky ────────────────────────────────────────────────────────────────
   Behind the hero: signals drifting, drawn by front.js in the language's greys with the accent
   on a few. The copy sits over it on the left; the dots lean right, where the copy is not.

   The hero is most of the first screen. On a desk the whole of it less the rail — the claim
   alone, with the sky given room to be a scene, and the applications one scroll down where
   the button leads. On a phone it stops short of the fold, so the cards show under it and the
   page is seen to go on. The rail's height is its padding and a row of small buttons, ~52px;
   the page's own top padding is taken off as well. */
.front-hero { position: relative; min-height: calc(64dvh - 52px - var(--sp-6)); justify-content: center; }
.front-sky { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
/* Only as wide as its words: the lens rests while the pointer is over the copy, and a copy box
   the width of the hero would rest it across the whole band. */
.front-hero-copy { position: relative; width: auto; max-width: 100%; }

/* An application's card (.surface.pad.stack). Its three benefits are a list, in words; its
   example is the application's own list in a frame. The band's eyebrow reads on ink. */
.front-points { margin: 0; padding-left: var(--sp-2); display: flex; flex-direction: column; gap: var(--sp-half); color: var(--ink-2); }
.front-band .eyebrow { color: var(--on-band-2); }
.front-band .frame { box-shadow: 0 0 0 1px var(--band-hairline); }

/* ── the reveal ─────────────────────────────────────────────────────────────
   A section below the fold arrives when it is scrolled to, and the rows in a frame arrive one
   after another — front.js adds .front-reveal once it runs and .is-in as each section enters,
   so a page without the script shows everything at once. The load-time entrance stays for the
   hero, which is on screen already. */
@media (prefers-reduced-motion: no-preference) {
  .front-reveal .front-section, .front-reveal .front-band-inner {
    animation: none; opacity: 0; transform: translateY(12px);
    transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
  }
  .front-reveal .front-section.is-in, .front-reveal .front-band-inner.is-in { opacity: 1; transform: none; }
  .front-reveal .frame .item { opacity: 0; transform: translateY(6px); transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease); }
  .front-reveal .is-in .frame .item { opacity: 1; transform: none; }
  .front-reveal .frame .item:nth-child(2) { transition-delay: 120ms; }
  .front-reveal .frame .item:nth-child(3) { transition-delay: 240ms; }
  .front-reveal .frame .item:nth-child(4) { transition-delay: 360ms; }
  .front-reveal .frame .item:nth-child(n+5) { transition-delay: 480ms; }
}
/* Five steps in a row on a desk, two-up on a phone: the stat tile, with the number as the step. */
.front-steps .stat-label { color: var(--ink-2); }
.front-steps strong { color: var(--ink); }
/* The closing band: the developer offer on ink, full width — ADR 0051, the one dark thing in a
   product that is light (ADR 0023). The components inside are the same ones,
   recoloured with the five band tokens: text in white, the quieter text at 64%, the surface and
   its hairlines as white at low opacity. No shadow on the surface — a shadow on ink is invisible
   and the ring does the edge. */
.front-band { background: var(--band); color: var(--on-band); padding: var(--sp-8) 0 var(--sp-6); }
.front-band-inner { max-width: var(--measure); margin: 0 auto; padding: 0 var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-3); }
.front-band .lede, .front-band .item-sub, .front-band .chevron { color: var(--on-band-2); }
.front-band .surface { background: var(--band-surface); box-shadow: 0 0 0 1px var(--band-hairline); }
.front-band .item { color: var(--on-band); }
.front-band .item { border-top-color: var(--band-hairline); }
.front-band a.item:hover { background: var(--band-hairline); }
.front-band a.item:hover .chevron { color: var(--on-band); }
/* The footer under the band, on the sunk ground: the page comes back to light to end. */
.foot.foot-sunk { background: var(--sunk); border-top: 0; }

/* ── the footer ─────────────────────────────────────────────────────────────
   Where everything else is: link groups under eyebrows, in caption size, on the ground under a
   hairline. Links only. A footer that explains is a page that did not. */
.foot { border-top: 1px solid var(--hairline); padding: var(--sp-6) 0 var(--sp-8); }
/* The same measure and side padding as .page, so the groups sit on the page's own left edge. */
.foot-inner { max-width: var(--measure); margin: 0 auto; padding: 0 var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-4); }
.foot-groups { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(160px, max-content)); }
.foot-group { display: flex; flex-direction: column; gap: var(--sp-1); }
.foot-group .eyebrow { margin-bottom: var(--sp-half); }
.foot-group a { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--ink-2); width: fit-content; }
.foot-group a:hover { color: var(--ink); }

/* ── two lanes ──────────────────────────────────────────────────────────────
   The thing being done, and what accompanies it. The action and its trail go in the main lane;
   anything that qualifies it — what the agent was unsure of, what the conversation points at —
   goes in the panel beside it, where it can be read without being stepped over on the way to the
   button.

   One column until there is room for two. Below that width the panel would be a 200px lane beside
   a 300px letter, and the letter is the reason the page is open.

   The panel sticks while the letter scrolls: it is context for the whole letter, not for the part
   of it that happens to be on screen. Offset by the rail, which is what it would otherwise slide
   under. */
.lanes { display: grid; gap: var(--sp-3); grid-template-columns: minmax(0, 1fr); }
@media (min-width: 1000px) {
  .lanes { grid-template-columns: minmax(0, 1fr) 300px; align-items: start; }
  .lane-aside { position: sticky; top: 76px; }
}
.lane-aside { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
/* Left, and only as wide as they need. Stretched by the column they sit in, three buttons read as
   three centred banners rather than as a list of things you can do. */
.lane-aside .btn { justify-content: flex-start; }
/* A surface in the panel fills it; an identifier in it wraps rather than pushing the panel wider
   than its lane. */
.lane-aside > .surface { width: 100%; }
.lane-aside .mono { overflow-wrap: anywhere; }
.lane-aside .note { align-self: stretch; }
.lane-aside .eyebrow { margin-bottom: var(--sp-half); }
/* A second group in the panel stands off the first: the gap is the space between rows, and an
   eyebrow starting a new group wants a little more than a row's worth. */
.lane-aside .eyebrow:not(:first-child) { margin-top: var(--sp-1); }

/* ── split ──────────────────────────────────────────────────────────────────
   Two things of equal standing: what waits and whether the platform runs, a list and the form that
   adds to it, the groups of a health page. One under the other until a desk, where they sit side
   by side and the screen is read in a glance rather than a scroll. Not lanes — nothing here is
   the aside of anything else. A note, or anything marked .span, takes the whole row. */
.split { display: flex; flex-direction: column; gap: var(--sp-3); }
.split > * { min-width: 0; }
@media (min-width: 1200px) {
  .split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
  .split > .note, .split > .span { grid-column: 1 / -1; }
}

/* ── a document and its contents ───────────────────────────────────────────
   The developer documents: prose at a reading measure, and on a desk the headings beside it,
   sticky, so a forty-screen document has a map. Below a desk there is no column to put it in and
   the document reads top to bottom, which is what a phone does anyway. The head of these pages is
   not sticky, so the list sticks to the top of the window itself. */
.docs { display: grid; gap: var(--sp-6); grid-template-columns: minmax(0, 1fr); }
.contents { display: none; }
@media (min-width: 1200px) {
  .docs { grid-template-columns: 240px minmax(0, 1fr); align-items: start; }
  /* Sticky, and scrolling on its own when the document has more headings than the window has
     height — the design guide names every component, and a list cut at the fold is a list that
     lies about what is on the page. */
  .contents { display: flex; flex-direction: column; gap: var(--sp-half); position: sticky; top: var(--sp-3); max-height: calc(100vh - var(--sp-6)); overflow-y: auto; }
  /* overflow clips the focus ring on a link at the edge; the padding is the ring's room, and the
     margin gives it back so the links still sit on the column's edge. */
  .contents { padding: 0 var(--sp-1); margin: 0 calc(-1 * var(--sp-1)); }
  .contents .eyebrow { margin-bottom: var(--sp-1); }
  .contents a { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--ink-2); width: fit-content; }
  .contents a:hover { color: var(--ink); }
}

/* ── the letter ─────────────────────────────────────────────────────────────
   Written where it is read. One form, so every control submits what is on screen. */

.letter-body {
  white-space: pre-wrap; font-size: var(--fs-body); line-height: var(--lh-body);
}
.mail-edit { font-family: inherit; min-height: 260px; }

/* ── a conversation ─────────────────────────────────────────────────────── */

.turn-body { white-space: pre-wrap; margin-top: var(--sp-1); color: var(--ink-2); }

/* ── the ladder ─────────────────────────────────────────────────────────────
   Where a thing moves through named stages and the stage is the fact. */

.ladder { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.rung {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--ink-3);
}
.rung-now { color: var(--ink); font-weight: 700; }
.rung-done { color: var(--ink-2); }
.rung::after { content: "›"; color: var(--ink-3); margin-left: 6px; }
.rung:last-child::after { content: ""; margin: 0; }

/* ── entrance ───────────────────────────────────────────────────────────────
   A screen arrives rather than being suddenly there. Applied to the direct children of .page,
   which htmx never replaces: a fragment swaps INSIDE a wrapper the page keeps, so filtering a
   table does not replay the entrance. Replacing one of these is how a list blinks and slides
   back into place because a badge moved — it has been shipped twice. */
@media (prefers-reduced-motion: no-preference) {
  .page > * { opacity: 0; animation: rise-in var(--dur-base) var(--ease) forwards; }
  .page > *:nth-child(2) { animation-delay: 40ms; }
  .page > *:nth-child(3) { animation-delay: 80ms; }
  .page > *:nth-child(n+4) { animation-delay: 120ms; }

  /* A sheet is not page content, and this rule nearly broke every one of them.
     The entrance sets opacity to 0 and relies on `forwards` to hold the element visible after it
     finishes. Opening a dialog replaces the animation property with sheet-in, which has no
     forwards — so the sheet faded in, and the moment its animation ended the fill was gone and
     the base opacity: 0 applied again. The dialog stayed open and the backdrop stayed blurred
     over a page with nothing on it.

     Specificity is doing the work here: .page > dialog outranks .page > * on opacity, and
     .sheet[open] outranks it on animation, so the sheet still animates in. */
  .page > dialog { opacity: 1; animation: none; }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ── a list that updates in front of you ────────────────────────────────────
   When the queue re-reads itself the whole list is replaced, so nothing in the markup says which
   row is new — every row simply appears, and an arrival looks the same as a repaint.

   The rows carry a view-transition-name, which is what lets the browser pair them across the
   swap: a row that was already there is recognised and glides to its new position, and a row with
   no partner has nothing to glide from, so it arrives. That pairing is the effect. There is
   deliberately no keyframe of our own on the new snapshot — applied to every row it would slide
   the four that did not change as well, which is the blink this was meant to remove.

   The page behind it must hold still. A view transition snapshots the whole document, and the
   root's default cross-fade would dim the header and the rest of the page every twenty seconds
   for one row that moved. */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none; }
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-control); }

/* ── phone ──────────────────────────────────────────────────────────────────
   The measure is already narrower than the window, so the layout barely changes. What changes is
   the display size, which at 44px would take three lines of a phone to say two words. */
@media (max-width: 640px) {
  :root {
    --fs-display: 34px; --lh-display: 40px;
    --fs-title: 25px;   --lh-title: 32px;
  }
  .page { padding: var(--sp-3) var(--sp-2) var(--sp-8); }
  /* The front door's own values on a phone, said here rather than lost to the rule above; the
     rail wraps to two rows there, so a section needs more room above it than on a desk. */
  .front { padding-top: var(--sp-6); }
  /* The copy at the top and the region under it, drawn by front.js in the lower half. */
  .front-hero { justify-content: flex-start; }
  .front-section { scroll-margin-top: calc(var(--sp-12) + var(--sp-2)); }
  .front-band { padding: var(--sp-6) 0 var(--sp-3); }
  .front-band-inner { padding: 0 var(--sp-2); }
  .foot-inner { padding: 0 var(--sp-2); }
  .item { padding: var(--sp-2); }
  .pad { padding: var(--sp-2); }
  .rail { padding: var(--sp-1) var(--sp-2); }
  .nav { order: 3; width: 100%; overflow-x: auto; padding-bottom: var(--sp-half); flex-wrap: nowrap; }
  .nav a { white-space: nowrap; }
}

/* ── desk ───────────────────────────────────────────────────────────────────
   Mobile first, and then the room a monitor has is used rather than left white on either side.
   The measure steps up once, and stays a measure: a column of rows still has its action within
   reach of its heading. The front door is where the width does real work — its hero, sections and
   band become two columns, the copy beside what it introduces, because there a screen of one
   thing after another is a page somebody scrolls past. Prose does not widen; see .prose. */
@media (min-width: 1200px) {
  :root {
    --measure: 960px;
    --measure-wide: 1400px;
  }
  .front, .front-band-inner, .front-foot .foot-inner { max-width: var(--measure-wide); }
  .front-hero { min-height: calc(100dvh - 52px - var(--sp-8)); }
  .front-section, .front-band-inner { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--sp-8); align-items: start; }
  /* An intro with nothing beside it takes the row rather than leaving half of it empty. */
  .front-section > .front-intro:only-child { grid-column: 1 / -1; }
  /* A section whose content wants the whole width — two application cards, five steps, two
     lists of examples — keeps the intro above it and the content across. */
  .front-section-full { display: flex; align-items: stretch; }
  .front-steps { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

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