/**
 * Rin — the site.
 *
 * Every value here comes from src/theme.ts. Four brand colours, three deliberate
 * exceptions (green, red, fire), one card recipe, one reveal recipe. Labels are
 * all-caps and widely tracked; body copy is sentence case and untracked. That
 * contrast is the whole look.
 *
 * Things the app does not have, and this must not either: backdrop-filter,
 * pill-shaped buttons, hover lifts, colour transitions on the theme flip.
 */

/* ------------------------------------------------------------------- fonts */
/* Self-hosted from node_modules/@expo-google-fonts — the same files the app
   ships. The privacy section prints a live request ledger, and a page arguing
   "nothing leaves your phone" while calling fonts.gstatic.com argues against
   itself. This is what makes "third-party requests: 0" true. */

/* Four faces, subset to Latin and General Punctuation and compressed to WOFF2:
   45 kB in place of 303 kB of unsubsetted TTF. SemiBold is gone — it was 60 kB
   serving two rules, both of which read just as well at 500. */
@font-face {
  font-family: 'Josefin Sans';
  src: url('../fonts/JosefinSans_400Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Josefin Sans';
  src: url('../fonts/JosefinSans_400Regular_Italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'Josefin Sans';
  src: url('../fonts/JosefinSans_500Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Josefin Sans';
  src: url('../fonts/JosefinSans_700Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ------------------------------------------------------------------ tokens */

:root {
  /* light palette — src/theme.ts lightPalette */
  --bg: #ffffe3;
  --card: rgba(255, 255, 250, 0.8);
  --chip: #ececdb;
  --ink: #4a4a4a;
  --ink-soft: #5f6b78;
  --line: #cbcbcb;
  --line-faint: #e3e3d3;
  --accent: #57697c;
  --accent-soft: rgba(109, 129, 150, 0.18);
  --on-accent: #ffffe3;
  --on-state: #2e2e2e;
  --done: #7fb32e;
  --done-soft: rgba(147, 198, 63, 0.24);
  --missed: #e5405a;
  --missed-soft: rgba(239, 76, 99, 0.18);
  --cell-empty: #e5e5d5;
  --gh-0: #e5e5d5;
  --gh-1: #d9ecb2;
  --gh-2: #b7dc7a;
  --gh-3: #9ccd4d;
  --gh-4: #7fb32e;
  --gh-missed: #eb8a99;
  --shadow-card: 0 12px 20px rgba(74, 74, 74, 0.16);
  --mark-empty: #e4e4d2;

  /* The state colours are tuned to be read as large blocks in the tracker, not
     as text: light --done is 2.46:1 on ivory. These are the same three hues
     pushed until they clear 4.5:1 on the ground, the card and the chip, for the
     places where they carry words rather than area. */
  --done-ink: #53741e;
  --missed-ink: #c0364c;
  --accent-ink: #57697c;

  /* geometry — RADIUS in src/theme.ts */
  --r-card: 18px;
  --r-control: 10px;
  --r-chip: 8px;
  --r-pill: 999px;

  /* Easings. The app samples every curve into 13 keyframes and interpolates
     linearly, so the piecewise result *is* the motion — these tables are the
     app's curves, not approximations of them. */
  --ease-out: linear(0, .2297, .4213, .5781, .7037, .8015, .875, .9277, .963, .9844, .9954, .9994, 1);
  --bloom-out: linear(0, .3488, .6181, .8172, .9556, 1.0425, 1.0875, 1.0998, 1.0889, 1.0641, 1.0347, 1.0102, 1);
  --drop-out: linear(0, .3838, .6759, .8875, 1.0296, 1.1134, 1.15, 1.1505, 1.1259, 1.0875, 1.0463, 1.0134, 1);
  --lift: linear(0, .0139, .0556, .125, .2222, .3472, .5, .6528, .7778, .875, .9444, .9861, 1);
  --ios: cubic-bezier(0.32, 0.72, 0, 1);
  --pop: cubic-bezier(0.34, 1.56, 0.64, 1);
  /* Animated.timing's default is Easing.inOut(Easing.ease) — not CSS ease-in-out,
     which is what cubic-bezier(.42,0,.58,1) actually is */
  --rn-inout: linear(0, .0222, .0781, .1577, .2558, .3698, .5, .6302, .7442, .8423, .9219, .9778, 1);

  --gutter: clamp(16px, 4vw, 32px);
  --measure: 1120px;
  /* ~68 real characters in Josefin Sans, whose zero glyph is much wider than
     its average lowercase, so `ch` overstates the measure by about a third */
  --measure-text: 58ch;

  color-scheme: light;
}

/* Dark is the CSS default and what a fresh visitor gets — App.tsx does the same
   with `settings?.theme ?? 'dark'`. A light system preference is honoured by the
   inline script, which stamps data-theme before first paint. */
:root:not([data-theme='light']),
:root[data-theme='dark'] {
  --bg: #0d0d0d;
  --card: rgba(24, 24, 24, 0.78);
  --chip: rgba(40, 40, 40, 0.9);
  --ink: #ffffe3;
  --ink-soft: #cbcbcb;
  --line: #4a4a4a;
  --line-faint: #242424;
  --accent: #8fa5ba;
  --accent-soft: rgba(143, 165, 186, 0.2);
  --on-accent: #0d0d0d;
  --on-state: #0d0d0d;
  --done: #93c63f;
  --done-soft: rgba(147, 198, 63, 0.2);
  --missed: #ef4c63;
  --missed-soft: rgba(239, 76, 99, 0.18);
  --cell-empty: #1a1a1a;
  --gh-0: #2a2a2a;
  --gh-1: #386021;
  --gh-2: #497b2d;
  --gh-3: #6da939;
  --gh-4: #93c63f;
  --gh-missed: #7d2f3c;
  --shadow-card: 0 12px 20px rgba(0, 0, 0, 0.5);
  --mark-empty: #363636;

  --done-ink: #93c63f;
  --missed-ink: #f58e9d;
  --accent-ink: #9aaec1;

  color-scheme: dark;
}

/* -------------------------------------------------------------- foundation */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Josefin Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img,
svg {
  max-width: 100%;
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* Press feedback is opacity only — everywhere, without exception. Nothing on
   this page shifts colour, lifts on a shadow, scales up or slides an underline. */
a,
button,
summary {
  transition: opacity 120ms ease;
}
a:hover,
button:hover,
summary:hover {
  opacity: 0.85;
}
a:active,
button:active,
summary:active {
  opacity: 0.7;
}

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

::selection {
  background: var(--accent-soft);
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 10px 16px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: var(--r-control);
}
.skip:focus {
  left: 8px;
  top: 8px;
}

.sr {
  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;
}

/* ------------------------------------------------------------------ ground */
/* AuroraBackground.tsx: five soft blobs, drifting, never looping visibly. The
   only thing on this page that never stops. */

#ground {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  background: var(--bg);
}

.blob {
  position: absolute;
  border-radius: 50%;
  /* `will-change` is set by initGround() only while a blob is actually drifting,
     and cleared when it stops — see the note in stop(). */
}

/* --------------------------------------------------------------- structure */

.page {
  position: relative;
  z-index: 1;
}

.wrap {
  width: 100%;
  max-width: var(--measure);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

section {
  padding-block: clamp(56px, 9vw, 96px);
}

/* ---------------------------------------------------------------- typography */

/* The one large type on the page that is neither caps nor tracked. That
   contrast against the tiny wide-tracked labels is the entire look. */
h1 {
  margin: 0 0 18px;
  font-size: clamp(32px, 5.2vw, 52px);
  font-weight: 700;
  letter-spacing: 0;
  line-height: 1.15;
  color: var(--ink);
}

h2 {
  margin: 0 0 14px;
  font-size: clamp(24px, 3.4vw, 29px);
  font-weight: 700;
  line-height: 1.21;
  letter-spacing: 0;
  color: var(--ink);
}

.eyebrow {
  display: block;
  margin: 0 0 10px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--accent);
}

.lede,
p {
  margin: 0 0 14px;
  color: var(--ink-soft);
  font-weight: 400;
}

.lede {
  font-size: 15px;
  line-height: 1.6;
  max-width: 46ch;
}

.measure {
  max-width: var(--measure-text);
}
/* a section that is one column of prose rather than a column inside a grid */
.measure-wrap {
  max-width: calc(var(--measure-text) + var(--gutter) * 2);
}

.caption {
  margin: 8px 0 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-soft);
}

.defn {
  font-size: 12px;
  font-style: italic;
  color: var(--ink-soft);
}

.num {
  color: var(--done-ink);
  font-weight: 700;
}

/* SectionHeader.tsx — the inline 4×15 accent bar, an element and never a border. */
.shead {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}
.shead::before {
  content: '';
  flex: none;
  width: 4px;
  height: 15px;
  border-radius: 2px;
  background: var(--accent);
}
.shead h3,
.shead > .shead-title {
  flex: 1;
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--ink);
}

/* ------------------------------------------------------------------ surfaces */

/* cardSurface() — the one recipe. Translucent so the drifting ground reads
   through. No backdrop-filter: the app has none. */
.card {
  background: var(--card);
  border: 1px solid var(--line-faint);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  padding: 18px;
}

.chip-box {
  background: var(--chip);
  border-radius: var(--r-control);
  padding: 14px 16px;
}

.tag {
  display: inline-block;
  padding: 3px 8px;
  border-radius: var(--r-chip);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  line-height: 1.6;
}

/* ------------------------------------------------------------------ buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  /* Optical centring, measured rather than guessed. Josefin Sans sets its
     capitals high in the line box, so a centred line leaves them 3.5px nearer
     the top; and letter-spacing adds its 2px after the last letter too, which
     pushes the word 1px left. The uneven padding puts the ink in the middle. */
  padding: 5px 19px 0 21px;
  border: 0;
  border-radius: var(--r-control);
  background: var(--accent);
  color: var(--on-accent);
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
}

.btn.ghost {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--ink-soft);
}

.btn[disabled] {
  opacity: 0.4;
  cursor: default;
}

/* the quiet action — a caps label in accent, no chrome at all */
.quiet {
  border: 0;
  background: none;
  padding: 4px 2px;
  font-family: inherit;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 22px;
}

/* -------------------------------------------------------- segmented control */
/* SegmentedControl.tsx — one pill that slides, labels cross-fading underneath. */

.seg {
  position: relative;
  display: flex;
  padding: 3px;
  border-radius: var(--r-control);
  background: var(--chip);
}

.seg-thumb {
  position: absolute;
  left: 3px;
  top: 3px;
  bottom: 3px;
  border-radius: var(--r-chip);
  background: var(--accent);
  transition: transform 240ms var(--ios);
  will-change: transform;
}

.seg button {
  position: relative;
  flex: 1;
  border: 0;
  background: none;
  padding: 6px 10px;
  font-family: inherit;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
  white-space: nowrap;
}

/* the filled copy, stacked exactly over the resting one */
.seg button .on {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--on-accent);
  opacity: 0;
  transition: opacity 240ms var(--ios);
}
.seg button[aria-pressed='true'] .on {
  opacity: 1;
}
.seg button .off {
  transition: opacity 240ms var(--ios);
}
.seg button[aria-pressed='true'] .off {
  opacity: 0;
}
.seg button:hover {
  opacity: 1;
}

/* -------------------------------------------------------------- the masthead */

#masthead {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid var(--line-faint);
  /* opaque enough that scrolling content does not read through it, translucent
     enough that the drifting ground still tints it */
  background: color-mix(in srgb, var(--bg) 96%, transparent);
}

.masthead-in {
  display: flex;
  align-items: center;
  gap: 16px;
  height: 56px;
  max-width: var(--measure);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  text-decoration: none;
  color: var(--ink);
  margin-right: auto;
}
.brand svg {
  width: 28px;
  height: 28px;
  display: block;
}
.brand b {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 2px;
}

.mast-nav {
  display: flex;
  align-items: center;
  gap: 4px;
}
/* Each link is a chip-sized target rather than a bare word, so the row reads as
   separate things: the chip ground on hover, and the section heads' accent bar
   on whichever section is on screen. */
.mast-nav a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 30px;
  /* the same optical centring as .btn: capitals sit high, tracking trails */
  padding: 3px 9px 0 11px;
  border-radius: var(--r-chip);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--ink-soft);
  text-decoration: none;
  transition: background-color 200ms var(--ease-out), color 200ms var(--ease-out);
}
.mast-nav a:hover,
.mast-nav a:focus-visible {
  background: var(--chip);
  color: var(--ink);
}
.mast-nav a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* where you are: the .shead marker, at the nav's size */
.mast-nav a[aria-current='true'] {
  background: var(--chip);
  color: var(--ink);
}
.mast-nav a[aria-current='true']::before {
  content: '';
  flex: none;
  width: 3px;
  height: 11px;
  margin-top: -3px;
  border-radius: 2px;
  background: var(--accent);
}
/* the one link that leaves the page says so, and stands a hairline apart */
.mast-nav a.keep {
  margin-left: 8px;
  border: 1px solid var(--line-faint);
}
.mast-nav a.keep::after {
  content: '\2197';
  margin-top: -3px;
  font-size: 12px;
  letter-spacing: 0;
  color: var(--accent);
}

#theme-top {
  width: 132px;
  flex: none;
}

@media (max-width: 719px) {
  .mast-nav a:not(.keep) {
    display: none;
  }
}

/* ----------------------------------------------------------------- the hero */

.hero-grid {
  display: grid;
  gap: 48px;
  align-items: center;
}

@media (min-width: 900px) {
  .hero-grid {
    grid-template-columns: minmax(0, 1fr) minmax(320px, 460px);
  }
}

.hero-eyebrow {
  letter-spacing: 4px;
  font-size: 11px;
}

/* --------------------------------------------------------- the tracker card */

.tcard-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 14px;
}

/* StreakBadge.tsx — the only capsule on the page. */
.streak {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 36px;
  min-width: 36px;
  padding-inline: 11px;
  border: 1px solid var(--line-faint);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--ink);
}
.streak span {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.3px;
  line-height: 1;
}
.streak svg {
  display: block;
}

.monthnav {
  display: flex;
  align-items: center;
  gap: 10px;
}
.monthnav .label {
  text-align: center;
  min-width: 108px;
}
.monthnav .label b {
  display: block;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--ink);
  line-height: 1.2;
}
.monthnav .label i {
  display: block;
  font-style: normal;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 1px;
  color: var(--ink-soft);
  line-height: 1.2;
}
.navbtn {
  width: 30px;
  height: 30px;
  flex: none;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--chip);
  color: var(--ink);
  font-family: inherit;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}

.radial-slot {
  display: grid;
  place-items: center;
  margin-bottom: 12px;
}
.radial {
  width: 100%;
  max-width: 420px;
  height: auto;
  display: block;
  touch-action: manipulation;
}

/* the cells. Fill snaps with no transition — data changing should look like
   data changing — while the pop is on transform only. */
.radial .cell {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  stroke-width: 0.7;
}
.radial .cell.live {
  cursor: pointer;
}
.radial .cell.live:focus-visible {
  outline: none;
  stroke: var(--ink);
  stroke-width: 2;
}
.radial .cell.pop {
  animation: cellpop 320ms var(--pop) both;
}
@keyframes cellpop {
  from {
    transform: scale(0.72);
  }
  to {
    transform: scale(1);
  }
}

.radial .daylabel {
  font-size: 10px;
  font-weight: 500;
  fill: var(--ink-soft);
}
.radial .daylabel.pick {
  cursor: pointer;
}
.radial .daylabel.selected {
  font-size: 12px;
  font-weight: 700;
  fill: var(--ink);
}
.radial .daylabel.today {
  font-size: 12px;
  font-weight: 700;
  fill: var(--accent);
}

/* TrackerCard.tsx — the bar eases to its new width rather than jumping there. */
.progress {
  height: 6px;
  border-radius: 3px;
  background: var(--chip);
  overflow: hidden;
}
.progress i {
  display: block;
  height: 6px;
  border-radius: 3px;
  background: var(--done);
  transform-origin: left;
  transform: scaleX(0);
  transition: transform 500ms var(--rn-inout);
}
.stats {
  margin: 6px 0 0;
  text-align: center;
  font-size: 12px;
  color: var(--ink-soft);
}

.legend {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 10px;
  font-size: 12px;
  color: var(--ink-soft);
}
.legend i {
  width: 12px;
  height: 12px;
  border-radius: 3px;
  display: inline-block;
}
.legend i.e {
  background: var(--cell-empty);
  border: 1px solid var(--line);
}
.legend span {
  margin-right: 8px;
}

/* ------------------------------------------------------------- daily check */

.checklist {
  border-top: 1px solid var(--line-faint);
}
.checkrow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line-faint);
}
.checkrow p {
  flex: 1;
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
}

/* MarkButton.tsx — 1.5px ring, soft ground at rest, filled when set. */
.mark {
  width: 38px;
  height: 38px;
  flex: none;
  display: grid;
  place-items: center;
  border: 1.5px solid transparent;
  border-radius: var(--r-control);
  cursor: pointer;
  padding: 0;
}
.mark svg {
  width: 20px;
  height: 20px;
  display: block;
}
/* MarkButton.tsx keeps an unset button neutral — a faint ring and soft ink over
   the tinted ground — and only takes the state colour once it is actually set.
   Colouring both at rest reads as two answers already given. */
.mark.done,
.mark.miss {
  border-color: var(--line-faint);
  color: var(--ink-soft);
}
.mark.done {
  background: var(--done-soft);
}
.mark.done[aria-pressed='true'] {
  background: var(--done);
  border-color: var(--done);
  color: var(--on-state);
}
.mark.miss {
  background: var(--missed-soft);
}
.mark.miss[aria-pressed='true'] {
  background: var(--missed);
  border-color: var(--missed);
  color: var(--on-state);
}

.today-badge {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--accent);
  text-transform: uppercase;
}

/* DailyCheck.tsx's day stepper, and the note under it on a day that isn't today */
.daynav {
  display: flex;
  align-items: center;
  gap: 6px;
}
.daynav .label {
  min-width: 70px;
  text-align: center;
  line-height: 1.2;
}
.daynav .label b {
  display: block;
  font-size: 13px;
  font-weight: 700;
  color: var(--ink);
}
.daynav .label .today-badge {
  display: block;
  font-style: normal;
}
.daynav .navbtn:disabled {
  opacity: 0.35;
  cursor: default;
}
.daynote {
  margin: 0 0 8px;
  font-size: 11px;
  color: var(--ink-soft);
}
/* a locked day still shows its marks, muted, as MarkButton.tsx does */
.mark:disabled {
  opacity: 0.28;
  cursor: default;
}
.mark:disabled[aria-pressed='true'] {
  opacity: 0.5;
}

.demo-note {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-top: 10px;
}
.demo-note p {
  margin: 0;
  font-size: 11px;
}

/* ---------------------------------------------------------------- refusals */

.two-up {
  display: grid;
  /* minmax(0, 1fr), not 1fr: a grid track's default minimum is its content, so
     a long code block or an unbroken backup code pushes the track wider than
     the page rather than scrolling inside its own box */
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  margin-top: 28px;
}
.two-up > * {
  min-width: 0;
}
@media (min-width: 820px) {
  .two-up {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}

.refusals {
  list-style: none;
  margin: 0;
  padding: 0;
}
.refusals li {
  position: relative;
  display: inline-block;
  padding: 4px 0;
  margin-right: 14px;
  font-size: 15px;
  font-weight: 500;
  color: var(--ink);
}
/* a rule that can animate — text-decoration cannot */
.refusals li::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 52%;
  height: 1px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: left;
}
.refusals.struck li::after {
  animation: strike 240ms var(--ios) forwards;
  animation-delay: calc(var(--i) * 60ms);
}
@keyframes strike {
  to {
    transform: scaleX(1);
  }
}

.hairline {
  border: 0;
  border-top: 1px solid var(--line-faint);
  margin: 14px 0;
}

.linkrow {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
.linkrow a {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  text-decoration: none;
}

/* ------------------------------------------------------------- the ledger */

.ledger {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  color: var(--ink-soft);
}
.ledger th {
  text-align: left;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 0 8px 6px 0;
  border-bottom: 1px solid var(--line-faint);
}
.ledger td {
  padding: 5px 8px 5px 0;
  border-bottom: 1px solid var(--line-faint);
  vertical-align: top;
  animation: fadein 200ms var(--ease-out) both;
}
.ledger td:last-child {
  text-align: right;
  white-space: nowrap;
}
@keyframes fadein {
  from {
    opacity: 0;
  }
}
.ledger-scroll {
  max-height: 208px;
  overflow-y: auto;
}
.bigstat {
  font-size: 30px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--done-ink);
}
.bigstat + p {
  margin-top: 2px;
}

.aside {
  display: flex;
  gap: 8px;
  margin-top: 28px;
}
.aside::before {
  content: '';
  flex: none;
  width: 4px;
  border-radius: 2px;
  background: var(--accent);
}
.aside p {
  margin: 0;
  max-width: 58ch;
  font-size: 13px;
  font-style: italic;
  line-height: 1.6;
}

/* --------------------------------------------------------------- fade grid */

.fadegrid rect {
  transition: fill 0ms;
}
.fadegrid.play rect {
  animation: fadein 180ms var(--ease-out) both;
  animation-delay: calc(var(--c) * 40ms);
}

/* ------------------------------------------------------- diagram + callouts */

.diagram {
  display: grid;
  gap: 28px;
  align-items: center;
}
@media (min-width: 900px) {
  .diagram {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  }
}

.callouts {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
}
.callouts li {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.callouts b {
  width: 24px;
  height: 24px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-pill);
  background: var(--chip);
  font-size: 11px;
  font-weight: 700;
  color: var(--ink-soft);
}
.callouts li[data-tone='accent'] b {
  background: var(--accent-soft);
  /* the badge sits on the bare ground, not in a card, where raw --accent on
     --accent-soft lands at 4.25:1 */
  color: var(--accent-ink);
}
.callouts li[data-tone='done'] b {
  background: var(--done-soft);
  color: var(--done-ink);
}

/* ------------------------------------------------------------ views panels */

.panel {
  min-height: 0;
}
.panel[hidden] {
  display: none !important;
}
.panel.enter {
  animation: panelin 260ms var(--ease-out) both;
}
@keyframes panelin {
  from {
    opacity: 0;
    transform: translateX(var(--from, 34px)) scale(0.97);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.yearscroll {
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 4px;
}
/* The grid is a fixed 13px-cell drawing that scrolls; letting the global
   `max-width:100%` shrink it to fit would squash the cells to about 8px and
   lose the whole texture of the year. */
.yearbody {
  display: flex;
  align-items: flex-start;
}
.yeargutter {
  flex: none;
  max-width: none;
}
.yeargrid {
  display: block;
  max-width: none;
  flex: none;
}
.yeargrid .g {
  clip-path: inset(0 0 0 0);
}
.yeargrid.wipe .g {
  animation: wipe 900ms var(--ease-out) both;
}
@keyframes wipe {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}
.yeargrid text {
  font-size: 10px;
  font-weight: 700;
  fill: var(--ink-soft);
}
.yeargrid text.wd {
  font-size: 9px;
  font-weight: 500;
}

.yearfoot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 8px;
  font-size: 11px;
  color: var(--ink-soft);
}
.ramp {
  display: flex;
  align-items: center;
  gap: 3px;
}
.ramp i {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
}
.ramp span {
  font-size: 10px;
  margin-inline: 3px;
}

/* history */
.histrow {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line-faint);
}
/* the chips wrap inside their own column, so a fourth habit lines up under the
   first rather than under the date */
.hchips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
}
.histrow > b {
  width: 76px;
  flex: none;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.hchip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px 4px 7px;
  border-radius: var(--r-chip);
  border: 1px solid;
  font-size: 12px;
  font-weight: 500;
}
.hchip.done {
  border-color: var(--done);
  background: var(--done-soft);
  color: var(--ink);
}
.hchip.miss {
  border-color: var(--missed);
  background: var(--missed-soft);
  color: var(--ink);
}
.hchip svg {
  width: 11px;
  height: 11px;
  display: block;
}
.hchip.done svg {
  color: var(--done-ink);
}
.hchip.miss svg {
  color: var(--missed-ink);
}

/* ------------------------------------------------------------- deadlines */

.task {
  background: var(--chip);
  border-radius: var(--r-control);
  padding: 8px 12px 12px;
}
.task-top {
  display: flex;
  align-items: center;
  gap: 10px;
}
.checkbox {
  width: 22px;
  height: 22px;
  flex: none;
  display: grid;
  place-items: center;
  /* --line on --chip is 1.36:1; a control's own boundary needs 3:1 */
  border: 1.5px solid var(--ink-soft);
  border-radius: var(--r-pill);
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
  padding: 0;
}
.checkbox svg {
  width: 13px;
  height: 13px;
  display: block;
  opacity: 0;
}
.checkbox[aria-pressed='true'] {
  background: var(--done);
  border-color: var(--done);
  color: var(--on-state);
}
.checkbox[aria-pressed='true'] svg {
  opacity: 1;
}
/* Deadlines.tsx files a finished task away rather than leaving it in the list */
.task.done #task-text {
  text-decoration: line-through;
  color: var(--ink-soft);
}
.task-top p {
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
}
.task-meta {
  margin-left: 32px;
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.task-meta b {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.3px;
}
.pressure {
  margin-left: 32px;
  height: 3px;
  border-radius: 1.5px;
  background: var(--cell-empty);
  overflow: hidden;
}
.pressure i {
  display: block;
  height: 3px;
  border-radius: 1.5px;
  transform-origin: left;
  transform: scaleX(0);
}
.pressure.ease i {
  transition: transform 500ms var(--rn-inout);
}

.slider {
  width: 100%;
  margin: 18px 0 6px;
  accent-color: var(--accent);
  background: transparent;
}
.sliderlabels {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* the escalation ladder — spacing tightens toward the deadline */
.ladder {
  position: relative;
  margin: 26px 0 0;
  padding: 0;
  list-style: none;
  /* tall enough for the three-line label at the far end, which is the one that
     matters — the morning after is the last thing the app says about a deadline */
  height: 96px;
  border-top: 1px solid var(--line-faint);
}
.ladder li {
  position: absolute;
  top: 0;
  left: var(--x);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: 64px;
  text-align: center;
}
/* The outermost two hang inside the track rather than straddling its ends: the
   tick still lands exactly on --x, but the label grows inward from it instead of
   bleeding off the card. */
.ladder li:first-child {
  transform: translateX(0);
  align-items: flex-start;
  text-align: left;
}
.ladder li:last-child {
  transform: translateX(-100%);
  align-items: flex-end;
  text-align: right;
}
.ladder i {
  display: block;
  width: 2px;
  height: 12px;
  background: var(--line);
  transform-origin: top;
  transform: scaleY(1);
}
.ladder.play i {
  animation: rung 200ms var(--ease-out) both;
  animation-delay: var(--d);
}
@keyframes rung {
  from {
    transform: scaleY(0.3);
  }
  to {
    transform: scaleY(1);
  }
}
.ladder span {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--ink-soft);
  line-height: 1.25;
  transition: color 120ms ease;
}
.ladder li.lit i {
  background: var(--accent);
}
.ladder li.lit span {
  color: var(--accent);
}

/* On a phone the six rungs are closer together than their labels are wide, and
   the two that collide are the two the diagram is about. Alternating the tick
   length drops every second label onto its own row — the spacing still tightens
   toward the deadline, which is the whole point, and nothing overlaps. */
@media (max-width: 720px) {
  .ladder {
    height: 124px;
  }
  .ladder li {
    width: 58px;
  }
  .ladder li:nth-child(even) i {
    height: 34px;
  }
  .ladder span {
    font-size: 8.5px;
    letter-spacing: 0.6px;
  }
}

/* Narrower still, and two rows are not enough: the last two rungs are 14% apart
   and their labels are wider than that. Three tick lengths means no two
   neighbours ever share a row, and the ones that do are a third of the track
   apart. */
@media (max-width: 480px) {
  .ladder {
    height: 152px;
  }
  .ladder li {
    width: 54px;
  }
  .ladder li:nth-child(3n + 2) i {
    height: 32px;
  }
  .ladder li:nth-child(3n) i {
    height: 52px;
  }
}

.notif {
  margin-top: 22px;
  border: 1px solid var(--line-faint);
  border-radius: var(--r-control);
  background: var(--card);
  padding: 12px 14px;
}
.notif.arrive {
  animation: notifin 300ms var(--ios) both;
}
@keyframes notifin {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.98);
  }
}
.notif b {
  display: block;
  font-size: 13px;
  font-weight: 700;
  color: var(--ink);
}
.notif p {
  margin: 2px 0 0;
  font-size: 13px;
}

/* ------------------------------------------------------------- widget rail */

.rail {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: 10px;
  margin-top: 20px;
}
.rail figure {
  margin: 0;
  flex: none;
  scroll-snap-align: start;
  width: min(58vw, 232px);
}
/* printed onto the page, not floating above it: a hairline, and no shadow */
.frame {
  border: 1px solid var(--line-faint);
  border-radius: var(--r-card);
  overflow: hidden;
  display: block;
  background: var(--card);
}
.frame picture {
  display: block;
}
/* every capture is exported at the same 570 x 1238, so one ratio holds the
   frames level whether or not the image has arrived yet */
.frame img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 570 / 1238;
  object-fit: cover;
}
.rail figcaption {
  margin-top: 8px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---------------------------------------------------------------- showcase */
/* Only ever present once site.js has mounted it, which it does not under
   reduced motion. The page scroll drives the rail: the wrapper is as tall as the
   scrolling it takes, and the pin holds the rail in view the whole way down.
   Nothing here declares an animation. site.js builds the keyframes and, where
   the browser can, hands them to the compositor on a view timeline, so a
   browser without one can never run them off the wrong clock. */

.showcase {
  position: relative;
  margin-top: 20px;
  /* one screen for the pin, then half a screen more per capture after the first */
  height: calc(100svh - 57px + (var(--slides, 1) - 1) * 50svh);
}
.showcase-pin {
  position: sticky;
  top: 57px;
  height: calc(100svh - 57px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 18px;
  /* not overflow: hidden, which would make the pin its own scroller and unstick it */
  overflow-x: clip;
  /* No mask, filter or backdrop-filter here or on anything above the figures. A
     mask is a screen-sized texture redrawn every scroll frame, and a filter
     would pull the scroll-driven animation back onto the main thread. The
     depth table fades a capture out before it reaches the edge instead. */
}
.rail.scrolly {
  /* sized by the screen's height too, so a short window still fits a whole frame.
     One expression for the width and for the padding that centres capture 0, so
     position j is exactly j steps to the left and nothing is measured to centre it. */
  --fig-w: min(62vw, 260px, calc((100svh - 57px - 110px) * 0.4604));
  overflow: visible;
  scroll-snap-type: none;
  margin-top: 0;
  padding-bottom: 0;
  padding-inline-start: calc(50% - var(--fig-w) / 2);
}
.rail.scrolly:focus-visible {
  outline: none;
}
.rail.scrolly figure {
  width: var(--fig-w);
  /* the neighbours shrink toward their baseline, so the smaller one reads as
     further back with no shadow to say so */
  transform-origin: 50% 100%;
}
.rail.scrolly figcaption {
  text-align: center;
}
/* Promoted only while the stage is within a screen — site.js sets `.live` the
   way initGround() sets will-change on the blobs, and clears it the same way.
   Opacity lives on the frame and on the caption, never on the figure, so a
   fading capture is two flat layers rather than a group that has to be
   composited on its own first. */
.rail.scrolly.live figure {
  will-change: transform;
}
.rail.scrolly.live .frame {
  will-change: opacity;
}
.rail.scrolly.live figcaption {
  will-change: transform, opacity;
}
.showcase-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
}
.showcase-dots button {
  width: 8px;
  height: 8px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  /* the hairline colour vanishes against the ground in both themes */
  background: color-mix(in srgb, var(--ink-soft) 40%, transparent);
  cursor: pointer;
  transition: transform 200ms var(--ease-out), background-color 200ms var(--ease-out);
}
.showcase-dots button[aria-current='true'] {
  background: var(--accent);
  transform: scale(1.35);
}
.showcase-dots button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.showcase-pin:has(.rail.scrolly:focus-visible) .showcase-dots button[aria-current='true'] {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ---------------------------------------------------------------- code */

.code {
  margin: 0;
  padding: 16px 18px;
  border-radius: var(--r-control);
  background: var(--chip);
  color: var(--ink-soft);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.65;
  overflow-x: auto;
  tab-size: 2;
}
.code.numbered {
  /* "printed in full rather than excerpted" has to be true of the layout too —
     at 340px this showed twelve of eighty-one lines */
  max-height: 70vh;
  overflow-y: auto;
  counter-reset: ln;
}
.code.numbered span.l {
  display: block;
  white-space: pre-wrap;
  counter-increment: ln;
}
.code.numbered span.l::before {
  content: counter(ln);
  display: inline-block;
  width: 2.4em;
  margin-right: 12px;
  text-align: right;
  opacity: 0.8;
  user-select: none;
}

.codehead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}

.codebox {
  text-align: center;
  font-size: clamp(13px, 2.6vw, 16px);
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--ink);
  word-break: break-all;
}

/* --------------------------------------------------------------- colophon */

#colophon {
  border-top: 1px solid var(--line-faint);
  /* <footer> is not a <section>, so the section rhythm never reached it and the
     page ended flush on its last baseline */
  padding-block: clamp(56px, 9vw, 96px);
}
.colo > :last-child {
  margin-bottom: 0;
}
.colo > .mark-lg:first-child {
  margin-top: 0;
}
.colo {
  max-width: 560px;
  margin: 0 auto;
  text-align: center;
}
.colo .mark-lg {
  width: 76px;
  height: 76px;
  margin: 26px auto 14px;
  display: block;
}
.colo h4 {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--ink);
}
.colo .fine {
  font-size: 11px;
  line-height: 1.55;
}
.quotecard {
  text-align: left;
}
.quotecard p {
  margin: 0;
  font-size: 13px;
  font-style: italic;
  line-height: 1.5;
}

details.proof {
  margin-top: 26px;
  text-align: left;
  border-top: 1px solid var(--line-faint);
  padding-top: 14px;
}
details.proof summary {
  cursor: pointer;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--accent);
  list-style: none;
}
details.proof summary::-webkit-details-marker {
  display: none;
}
details.proof summary::after {
  content: ' +';
}
details.proof[open] summary::after {
  content: ' –';
}
.proof-body {
  padding-top: 14px;
  animation: fadein 240ms ease-in-out both;
}
.proof-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  align-items: center;
}
.proof-grid > * {
  min-width: 0;
}
@media (min-width: 720px) {
  .proof-grid {
    grid-template-columns: 132px minmax(0, 1fr);
  }
}
.verdict {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--done-ink);
}

.colo-appearance {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin: 26px 0 18px;
  text-align: left;
}
#theme-foot {
  width: 132px;
  flex: none;
}

/* -------------------------------------------------------------- the reveal */
/* One recipe. Fires once, unobserved immediately, never replayed — a printed
   page does not redraw when you scroll back up it. Content is visible by
   default; these styles only apply once JS has confirmed it is running. */

.js .reveal {
  opacity: 0;
  transform: translateY(12px);
}
/* the boot fallback: if the module never ran, nothing stays hidden */
.js.no-reveal .reveal {
  opacity: 1;
  transform: none;
}
.js .reveal.in {
  opacity: 1;
  transform: none;
  transition: opacity 260ms var(--ease-out), transform 260ms var(--ease-out);
  transition-delay: var(--rd, 0ms);
}

/* --------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  .js .reveal {
    transform: none;
  }
  .js .reveal.in {
    transition: opacity 150ms linear;
  }
  .radial .cell.pop,
  .fadegrid.play rect,
  .ladder.play i,
  .yeargrid.wipe .g,
  .notif.arrive,
  .ledger td,
  .proof-body {
    animation: none !important;
  }
  .refusals.struck li::after {
    animation: none;
    transform: scaleX(1);
  }
  .panel.enter {
    animation: fadein 150ms linear both;
  }
  .seg-thumb {
    transition: none;
  }
  .progress i,
  .pressure.ease i {
    transition: none;
  }
}

/* ------------------------------------------------------------ document page */
/* privacy.html and anything else that is mostly prose. */

.doc {
  max-width: 72ch;
  margin: 0 auto;
}
.doc h2 {
  margin-top: 44px;
  font-size: clamp(20px, 2.6vw, 24px);
  /* the masthead is sticky and 57px tall, so a bare anchor jump parks the
     heading underneath it. Clear it, with room to read as a heading. */
  scroll-margin-top: 76px;
}
.doc h3 {
  margin: 28px 0 8px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--ink);
}
.doc p,
.doc li {
  font-size: 15px;
  line-height: 1.65;
}
.doc ul,
.doc ol {
  margin: 0 0 14px;
  padding-left: 20px;
}
/* the deletion steps: numerals carry the same accent as the bullets */
.doc ol {
  margin-bottom: 12px;
}
.doc li {
  margin-bottom: 6px;
  color: var(--ink-soft);
}
.doc li::marker {
  color: var(--accent);
}
.doc code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: var(--chip);
  border-radius: 4px;
  padding: 1px 5px;
}
.doc .updated {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.dtable {
  width: 100%;
  border-collapse: collapse;
  margin: 8px 0 14px;
  font-size: 14px;
}
.dtable th,
.dtable td {
  text-align: left;
  vertical-align: top;
  padding: 9px 12px 9px 0;
  border-bottom: 1px solid var(--line-faint);
  color: var(--ink-soft);
}
.dtable th {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}
.dtable td:first-child {
  color: var(--ink);
  font-weight: 500;
}
.dtable-scroll {
  overflow-x: auto;
}

.yes {
  color: var(--done-ink);
  font-weight: 700;
}
.no {
  color: var(--ink-soft);
  font-weight: 700;
}

/* The three-up view switcher does not fit beside its title on a phone; it drops
   to its own full-width line rather than being squeezed or clipped. */
.seg-inline {
  width: 240px;
  flex: none;
}
@media (max-width: 620px) {
  .seg-inline {
    width: 100%;
    margin-top: 4px;
  }
}
