/* Lantern — a design language for the web.
 *
 * The CSS sibling of github.com/lapfelix/Lantern (UIKit). Same model, same
 * tokens, same banned list. No dependencies, no build step, no @import.
 *
 * THE MODEL
 * Everything is one sheet of material. A surface is in one of three physical
 * states: raised (a card), sunken (a well you put content into), lit (an accent
 * surface that emits). Depth comes from tone and edge, not from blur: a few
 * percent of tonal gradient, a 1px specular lip on the top edge, a hairline
 * border, a tight neutral contact shadow, and a pixel-fine grain.
 *
 * WHAT IS BANNED, AND WHY — this list is the load-bearing part.
 *   - No white gloss sweeping half a control's height. A specular lip is 1px.
 *   - No shadow tinted with the control's own colour. Contact shadows are neutral.
 *   - No glow behind anything that isn't genuinely the light source on its screen.
 *   - No saturated fills. Status hues are muted on purpose — four saturated grade
 *     keys side by side read as a toy.
 * If you find yourself widening a blur or brightening a gloss to make something
 * "pop", the fix is almost always tone or spacing instead.
 *
 * TWO TRAPS, both of which shipped once on iOS
 *   - Fade white into transparent WHITE, never `transparent`/black. Interpolating
 *     white→rgb(0 0 0/0) passes through a grey veil across the upper half of
 *     every card. Every gradient here spells out `rgb(255 255 255 / 0)`.
 *   - Build a pattern tile at the device pixel ratio. A tile sized in CSS px
 *     magnifies on a 2x/3x screen and reads as static, not grain — hence the
 *     `--ln-grain-size` overrides under `min-resolution` below.
 *
 * THEMING
 * Tokens resolve through `light-dark()`, so switching a subtree is one property:
 *   <html data-theme="dark">  ·  data-theme="light"  ·  omit for the OS setting.
 *
 * YOUR OWN ACCENT
 * Everything here is neutral except the accent. Set one hue and the wash, the
 * ember and the fill gradients follow:
 *   :root { --ln-accent-light: #1478CE; --ln-accent-dark: #57ADEA; }
 * `--ln-on-accent` is the one thing worth overriding by hand — the ink over a
 * filled control has to be decided per interface style, and the default pair
 * (white in light, near-black in dark) assumes an accent that darkens for light
 * mode the way Lantern's own azure does.
 */

/* ========================================================================== *
 * Tokens
 * ========================================================================== */

:root {
  color-scheme: light dark;

  /* -- Accent ------------------------------------------------------------- */
  /* The two literals an app substitutes. Everything accent-derived below
     reads `--ln-accent`, so overriding these two is enough. */
  --ln-accent-light: #1478ce;
  --ln-accent-dark: #57adea;
  --ln-accent: light-dark(var(--ln-accent-light), var(--ln-accent-dark));
  /* Ink on a filled accent surface. */
  --ln-on-accent: light-dark(#ffffff, #07131c);
  /* A tinted wash for selected rows and quiet accent chips. Heavier in dark:
     a 10% tint that reads as a wash on paper disappears against a dark page. */
  --ln-accent-wash: light-dark(
    color-mix(in srgb, var(--ln-accent) 10%, transparent),
    color-mix(in srgb, var(--ln-accent) 16%, transparent)
  );
  /* The warm counterweight — progress, streaks, mastery. */
  --ln-ember: light-dark(#a5701f, #d8a05c);
  --ln-on-ember: light-dark(#ffffff, #1c1206);

  /* -- Page --------------------------------------------------------------- */
  /* Both modes are a vertical wash rather than a flat fill, so a card floating
     on the page has something to be lit against. */
  --ln-canvas-top: light-dark(#f5f4f1, #18181b);
  --ln-canvas-bottom: light-dark(#eae9e5, #0c0c0e);
  /* Flat stand-in, for anything that can't host a gradient. */
  --ln-canvas: light-dark(#f0efeb, #121214);

  /* -- Surfaces ----------------------------------------------------------- */
  /* A hair off white, so the lip on the top edge has something to be brighter than. */
  --ln-surface: light-dark(#fcfcfb, #1d1d21);
  /* A second surface nested inside the first (a chip inside a card). */
  --ln-surface-quiet: light-dark(#e9e7e1, #26262b);
  /* A recessed well. Barely off the page tone — a field you type into should
     read as a dip in the surface, not a dark trough cut out of it. */
  --ln-well: light-dark(#efeeea, #17171b);

  /* -- Ink ---------------------------------------------------------------- */
  --ln-ink: light-dark(#17171a, #f3f3f1);
  --ln-ink-secondary: light-dark(rgb(23 23 26 / 0.58), rgb(243 243 241 / 0.56));
  --ln-ink-tertiary: light-dark(rgb(23 23 26 / 0.36), rgb(243 243 241 / 0.34));

  /* -- Status. Muted on purpose. ------------------------------------------ */
  /* These are tuned as INK — a status word, a glyph, a hairline. */
  --ln-positive: light-dark(#2e7a5a, #63be95);
  --ln-caution: light-dark(#99721e, #d1a65b);
  --ln-critical: light-dark(#af4a42, #dd7c72);
  /* And these as FILLS, which is a different job: the dark-mode hues above are
     lightened so they read as ink on a dark page, and at that lightness they
     cannot carry white. So a lit status surface starts from the light-mode hue
     in both modes, deepened toward the media recess rather than desaturated, and
     each gets its own ink. Reaching for `--ln-positive` as a background is the
     mistake this pair exists to prevent.

     Positive and critical carry white unchanged (5.2:1 and 5.4:1). Caution does
     not — `--ln-caution` is the lightest of the three and white on it measures
     4.4:1 — so its fill is darkened until it does (5.3:1). The rule is "the
     status hue, dark enough for its own ink", and only caution needs the second
     half of that. */
  --ln-positive-fill: light-dark(#2e7a5a, #2a6b50);
  --ln-caution-fill: light-dark(#8a6519, #7d5c18);
  --ln-critical-fill: light-dark(#af4a42, #963f38);
  --ln-on-positive: #ffffff;
  --ln-on-caution: #ffffff;
  --ln-on-critical: #ffffff;

  /* Grading a flash card, worst → best. Easy lands on the brand accent so the
     happy path is the app's own colour. */
  --ln-grade-again: var(--ln-critical);
  --ln-grade-hard: var(--ln-caution);
  --ln-grade-good: var(--ln-positive);
  --ln-grade-easy: var(--ln-accent);

  /* -- Edges -------------------------------------------------------------- */
  --ln-hairline: light-dark(rgb(23 23 26 / 0.09), rgb(255 255 255 / 0.07));
  --ln-hairline-faint: light-dark(rgb(23 23 26 / 0.055), rgb(255 255 255 / 0.045));
  /* Opaque, for the cases where a hairline is a *fill* rather than an edge — a
     divider drawn as a block, a keyline over an unknown backdrop. The
     translucent tokens above vanish there. */
  --ln-hairline-strong: color-mix(in srgb, var(--ln-ink) 14%, var(--ln-surface));

  /* -- Over media --------------------------------------------------------- */
  /* Video and artwork are their own ground: the neutral ink tokens flip with
     the interface style, so over a frame that does not they go unreadable in
     one mode. These do not flip. */
  --ln-media: #0e0e10;
  --ln-on-media: #ffffff;
  --ln-on-media-secondary: rgb(255 255 255 / 0.72);
  /* A well over artwork is invisible — the recess reads as part of the frame —
     so a track laid on media is a wash of the over-media ink instead. */
  --ln-on-media-track: rgb(255 255 255 / 0.22);
  --ln-media-chip: rgb(0 0 0 / 0.55);
  /* The veil under a modal. Lighter in light mode: a near-black scrim over a
     paper page reads as the lights going out rather than as a layer above it. */
  --ln-scrim: light-dark(rgb(23 23 26 / 0.28), rgb(0 0 0 / 0.6));
  /* A translucent floating material — a bar or a panel over content it has to
     stay legible against. Paired with a backdrop-filter at the use site; on its
     own it is just a tinted wash of the page. */
  --ln-glass: color-mix(in srgb, var(--ln-canvas) 82%, transparent);

  /* -- Spacing. A 4px grid. ---------------------------------------------- */
  --ln-hair: 2px; /* between two lines of the same thought (a word and its gloss) */
  --ln-xs: 4px;
  --ln-sm: 8px; /* the default gap between siblings */
  --ln-md: 12px;
  --ln-lg: 16px; /* page margins, card padding */
  --ln-xl: 24px; /* between sections */
  --ln-xxl: 32px;

  /* -- Radius. Nothing is ever sharp. ------------------------------------ */
  --ln-r-xs: 8px; /* chips, small controls */
  --ln-r-sm: 12px; /* artwork, nested surfaces */
  --ln-r-md: 16px; /* the standard card */
  --ln-r-lg: 22px; /* a large panel, a now-playing bar */
  --ln-r-xl: 28px; /* a hero card */
  --ln-r-pill: 999px;

  /* -- Elevation ---------------------------------------------------------- */
  /* Deliberately small: depth comes from tone and edge, and the shadow is only
     the contact with the page. Blur is 2x the UIKit shadowRadius, which is how
     the two rasterisers agree. A dark page swallows a black shadow, so dark
     mode needs a heavier one to read at all. */
  --ln-e-resting: 0 1px 8px light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.28));
  --ln-e-raised: 0 3px 20px light-dark(rgb(0 0 0 / 0.075), rgb(0 0 0 / 0.4));
  --ln-e-floating: 0 9px 44px light-dark(rgb(0 0 0 / 0.12), rgb(0 0 0 / 0.52));

  /* -- Surface finish ---------------------------------------------------- */
  /* The full-height tonal wash — a few percent, never a highlight sweep. In
     dark mode light gathers on the top edge; in light mode a white sheen over a
     near-white card is invisible, so the cue is occlusion gathering at the
     bottom instead. Same physics from the other side. */
  --ln-sheen: linear-gradient(
    light-dark(rgb(255 255 255 / 0), rgb(255 255 255 / 0.05)),
    rgb(255 255 255 / 0) 45%,
    light-dark(rgb(0 0 0 / 0.035), rgb(0 0 0 / 0.05))
  );
  --ln-sheen-quiet: linear-gradient(
    light-dark(rgb(255 255 255 / 0), rgb(255 255 255 / 0.03)),
    rgb(255 255 255 / 0) 45%,
    light-dark(rgb(0 0 0 / 0.021), rgb(0 0 0 / 0.03))
  );
  --ln-sheen-lit: linear-gradient(
    light-dark(rgb(255 255 255 / 0.1), rgb(255 255 255 / 0.05)),
    rgb(255 255 255 / 0) 45%,
    light-dark(rgb(0 0 0 / 0.07), rgb(0 0 0 / 0.05))
  );
  /* The 1px lip where the light lands. */
  --ln-lip: light-dark(rgb(255 255 255 / 0.75), rgb(255 255 255 / 0.1));
  --ln-lip-lit: light-dark(rgb(255 255 255 / 0.34), rgb(255 255 255 / 0.26));
  /* A well's shadow falls inward. It has no lip of its own — the light lands
     on the rim above it. */
  --ln-inner: inset 0 1px 6px light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.34));

  /* A fine monochrome dither, tiled. Real materials are not perfectly smooth,
     and a couple of percent of grain is most of the difference between a
     surface and a coloured rectangle. */
  --ln-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='96' height='96' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
  --ln-grain-size: 96px 96px;

  /* -- Motion ------------------------------------------------------------- */
  --ln-t-tap: 0.18s; /* a press-down / release. Short enough to feel mechanical. */
  --ln-t-move: 0.32s; /* something appearing or moving on screen */
  --ln-ease: cubic-bezier(0.32, 0.72, 0.24, 1);
  --ln-ease-spring: cubic-bezier(0.2, 0.9, 0.24, 1.02);

  /* -- Type --------------------------------------------------------------- */
  /* Sizes are rem, so they scale with the reader's root font size — the web's
     Dynamic Type. Never freeze a px size for body text. */
  --ln-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --ln-font-serif: ui-serif, Georgia, 'Times New Roman', serif;
  --ln-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Arabic is the subject, not a translation of it — its own stack and, below,
     its own scale and leading. */
  --ln-font-ar:
    -apple-system, 'SF Arabic', 'Geeza Pro', 'Noto Naskh Arabic', 'Segoe UI', Tahoma, serif;

  --ln-fs-display: clamp(1.75rem, 1.35rem + 2vw, 2.125rem); /* 34 */
  --ln-fs-title: 1.375rem; /* 22 */
  --ln-fs-headline: 1.0625rem; /* 17 semibold */
  --ln-fs-body: 1.0625rem; /* 17 */
  --ln-fs-callout: 0.9375rem; /* 15 */
  --ln-fs-caption: 0.8125rem; /* 13 */
  --ln-fs-micro: 0.6875rem; /* 11 semibold — badges, timestamps, tags */

  --ln-fs-ar-hero: clamp(2rem, 1.5rem + 2.4vw, 2.375rem); /* 38 */
  --ln-fs-ar-title: 1.875rem; /* 30 */
  --ln-fs-ar-row: 1.375rem; /* 22 */
  --ln-fs-ar-body: 1.1875rem; /* 19 */
  --ln-fs-ar-caption: 1rem; /* 16 */
  --ln-lh-ar: 1.22; /* Arabic ascenders and descenders collide at Latin leading */
  --ln-lh: 1.45;
}

[data-theme='light'] {
  color-scheme: light;
}
[data-theme='dark'] {
  color-scheme: dark;
}

/* One device pixel per speck. A 96px tile on a 3x screen is 3px blocks. */
@media (min-resolution: 2dppx) {
  :root {
    --ln-grain-size: 48px 48px;
  }
}
@media (min-resolution: 3dppx) {
  :root {
    --ln-grain-size: 32px 32px;
  }
}

/* The one rule the web needs that iOS does not: a visible keyboard focus.
   Accent, outside the box, so it reads on every surface state. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--ln-accent);
  outline-offset: 2px;
  border-radius: inherit;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --ln-t-tap: 0.001s;
    --ln-t-move: 0.001s;
  }
}

/* ========================================================================== *
 * Canvas
 * ========================================================================== */

/* Every Lantern component is measured border-box, because several of them are
   `width: 100%` with padding and would otherwise overflow their container by
   exactly that padding — which is what happened to the gallery's rows on a page
   that had no reset of its own. Scoped to our own class prefix rather than `*`:
   nothing else in this file selects a bare element, so linking the language can
   never silently re-measure a consumer's markup. `[class^=]` plus `[class*=" "]`
   matches `ln-*` as a whole token, not as a substring of someone else's name. */
[class^='ln-'],
[class^='ln-']::before,
[class^='ln-']::after,
[class*=' ln-'],
[class*=' ln-']::before,
[class*=' ln-']::after {
  box-sizing: border-box;
}

.ln-canvas {
  min-height: 100%;
  background-color: var(--ln-canvas);
  background-image: linear-gradient(var(--ln-canvas-top), var(--ln-canvas-bottom));
  background-attachment: fixed;
  color: var(--ln-ink);
  font-family: var(--ln-font);
  font-size: var(--ln-fs-body);
  line-height: var(--ln-lh);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.ln-canvas ::selection {
  background: var(--ln-accent-wash);
  color: var(--ln-ink);
}

/* ========================================================================== *
 * Surface — the single container primitive
 * ========================================================================== */

.ln-surface {
  position: relative;
  border-radius: var(--ln-radius, var(--ln-r-md));
  border: 1px solid var(--ln-hairline);
  background-color: var(--ln-surface);
  background-image: var(--ln-grain), var(--ln-sheen);
  background-size: var(--ln-grain-size), auto;
  background-repeat: repeat, no-repeat;
  box-shadow: var(--ln-e-raised);
  color: var(--ln-ink);
}

/* The specular lip: 1px of light caught on the top edge.
 *
 * It is an inset shadow on a full-size overlay, NOT a straight bar inset from
 * the corners. A bar has to guess how far to stop short of the arc, and on
 * anything round the guess is visibly wrong — on a circle it reads as a chord
 * floating inside the top of the shape, and on a pill its flat ends read as a
 * second, smaller shape. An inset shadow follows `border-radius: inherit`
 * exactly, so one recipe is correct for a card, a pill and a circle.
 *
 * The mask is the other half of it: a real specular highlight is brightest where
 * the surface faces the light and dies off as it turns away, so the line fades
 * out towards the sides instead of ringing the whole top edge. */
.ln-surface::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 var(--ln-lip);
  mask-image: linear-gradient(to right, transparent, #000 22%, #000 78%, transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 22%, #000 78%, transparent);
  pointer-events: none;
  transition: opacity var(--ln-t-tap) var(--ln-ease);
}

/* A tinted panel with no depth of its own — for nesting inside another
   surface, where a second shadow would read as mud. */
.ln-surface.ln-quiet {
  background-color: var(--ln-surface-quiet);
  background-image: var(--ln-grain), var(--ln-sheen-quiet);
  border-color: var(--ln-hairline-faint);
  box-shadow: none;
}

/* A well content sits *in*. No grain, no lip; the shadow falls inward. */
.ln-surface.ln-sunken {
  background-color: var(--ln-well);
  background-image: none;
  border-color: var(--ln-hairline-faint);
  box-shadow: var(--ln-inner);
}
.ln-surface.ln-sunken::before {
  display: none;
}

/* An accent surface that emits. */
.ln-surface.ln-lit {
  background-color: var(--ln-accent);
  background-image: var(--ln-grain), var(--ln-sheen-lit);
  border-color: rgb(0 0 0 / 0.1);
  color: var(--ln-on-accent);
}
.ln-surface.ln-lit::before {
  box-shadow: inset 0 1px 0 var(--ln-lip-lit);
}

.ln-flat {
  box-shadow: none;
}
.ln-resting {
  box-shadow: var(--ln-e-resting);
}
.ln-raised {
  box-shadow: var(--ln-e-raised);
}
.ln-floating {
  box-shadow: var(--ln-e-floating);
}

.ln-r-xs {
  --ln-radius: var(--ln-r-xs);
}
.ln-r-sm {
  --ln-radius: var(--ln-r-sm);
}
.ln-r-md {
  --ln-radius: var(--ln-r-md);
}
.ln-r-lg {
  --ln-radius: var(--ln-r-lg);
}
.ln-r-xl {
  --ln-radius: var(--ln-r-xl);
}

/* Padding inside a card. */
.ln-card {
  padding: var(--ln-lg);
}
.ln-card-lg {
  padding: var(--ln-xl);
}

/* A soft radial glow, behind an accent control so it looks like it is emitting
   rather than painted on. Reserved for a control that is genuinely the light
   source on its screen — a glow behind every primary button is what reads as
   amateur. */
.ln-bloom {
  position: relative;
  isolation: isolate;
}
.ln-bloom::after {
  content: '';
  position: absolute;
  inset: calc(var(--ln-bloom-spread, 22px) * -1);
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--ln-bloom-color, var(--ln-accent)) 45%, transparent),
    color-mix(in srgb, var(--ln-bloom-color, var(--ln-accent)) 15%, transparent) 55%,
    rgb(255 255 255 / 0) 100%
  );
  pointer-events: none;
  transition: opacity var(--ln-t-tap) var(--ln-ease);
}

/* ========================================================================== *
 * Type
 * ========================================================================== */

.ln-display {
  font-size: var(--ln-fs-display);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--ln-ink);
  margin: 0;
}
.ln-title {
  font-size: var(--ln-fs-title);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--ln-ink);
  margin: 0;
}
.ln-headline {
  font-size: var(--ln-fs-headline);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ln-ink);
  margin: 0;
}
.ln-body {
  font-size: var(--ln-fs-body);
  line-height: var(--ln-lh);
  color: var(--ln-ink);
  margin: 0;
}
.ln-callout {
  font-size: var(--ln-fs-callout);
  line-height: 1.4;
  color: var(--ln-ink);
  margin: 0;
}
.ln-caption {
  font-size: var(--ln-fs-caption);
  line-height: 1.35;
  color: var(--ln-ink-secondary);
  margin: 0;
}
/* The smallest legible tier. */
.ln-micro {
  font-size: var(--ln-fs-micro);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.02em;
  color: var(--ln-ink-secondary);
  margin: 0;
}
/* A section title above a group. Sentence case, quiet — the content should
   carry the weight, not its label. */
.ln-section-header {
  font-size: var(--ln-fs-micro);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ln-ink-secondary);
  margin: 0;
}

.ln-secondary {
  color: var(--ln-ink-secondary);
}
.ln-tertiary {
  color: var(--ln-ink-tertiary);
}
.ln-accent-ink {
  color: var(--ln-accent);
}

/* A gloss / literal reading. */
.ln-gloss {
  font-style: italic;
  color: var(--ln-ink-secondary);
}
/* A translation — a serif italic reads as "this is the meaning, not the words". */
.ln-meaning {
  font-family: var(--ln-font-serif);
  font-style: italic;
  color: var(--ln-ink-secondary);
}
.ln-mono {
  font-family: var(--ln-font-mono);
  font-size: var(--ln-fs-caption);
}
/* Timers, intervals, offsets — anything whose digits must not jitter. */
.ln-digits {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum';
}

/* Arabic. `direction` is the load-bearing half: right-aligning a block leaves
   the writing direction LTR, which mis-orders mixed content like "عندي 3 كتب".
   `unicode-bidi: isolate` keeps a run from reordering its neighbours. */
.ln-ar {
  font-family: var(--ln-font-ar);
  direction: rtl;
  unicode-bidi: isolate;
  text-align: right;
  line-height: var(--ln-lh-ar);
  color: var(--ln-ink);
}
.ln-ar-hero {
  font-size: var(--ln-fs-ar-hero);
  font-weight: 600;
}
.ln-ar-title {
  font-size: var(--ln-fs-ar-title);
  font-weight: 600;
}
.ln-ar-row {
  font-size: var(--ln-fs-ar-row);
}
.ln-ar-body {
  font-size: var(--ln-fs-ar-body);
}
.ln-ar-caption {
  font-size: var(--ln-fs-ar-caption);
}
.ln-ar.ln-center {
  text-align: center;
}

a.ln-link,
.ln-prose a {
  color: var(--ln-accent);
  text-decoration-color: color-mix(in srgb, var(--ln-accent) 40%, transparent);
  text-underline-offset: 0.2em;
}
a.ln-link:hover,
.ln-prose a:hover {
  text-decoration-color: var(--ln-accent);
}

/* ========================================================================== *
 * Button
 * ========================================================================== */

/* One class, six roles — so "the primary action" looks the same on every screen
   instead of being re-invented per file. A filled button is a key cut from the
   same material as everything else: a barely-there tonal gradient, a 1px
   specular lip, and a tight neutral contact shadow. Pressing it sinks it and
   dims the lip. Explicitly not: a coloured glow, or a white gloss sweeping half
   the height. */
.ln-btn {
  --ln-btn-fill: var(--ln-accent);
  --ln-btn-ink: var(--ln-on-accent);
  --ln-btn-radius: var(--ln-r-md);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ln-sm);
  min-height: 52px;
  padding: 14px 22px;
  border: 1px solid rgb(0 0 0 / 0.12);
  border-radius: var(--ln-btn-radius);
  font-family: inherit;
  font-size: var(--ln-fs-headline);
  font-weight: 600;
  line-height: 1.2;
  color: var(--ln-btn-ink);
  background-color: var(--ln-btn-fill);
  /* A few percent from top to bottom. Any more and it stops looking machined
     and starts looking moulded. */
  background-image:
    var(--ln-grain),
    linear-gradient(
      color-mix(in srgb, var(--ln-btn-fill), light-dark(#fff 5.5%, #fff 7%)),
      color-mix(in srgb, var(--ln-btn-fill), light-dark(#000 4.5%, #000 5%))
    );
  background-size: var(--ln-grain-size), auto;
  background-repeat: repeat, no-repeat;
  box-shadow: var(--ln-e-raised);
  cursor: pointer;
  text-decoration: none;
  text-align: center;
  transition:
    transform var(--ln-t-tap) var(--ln-ease),
    box-shadow var(--ln-t-tap) var(--ln-ease),
    opacity var(--ln-t-tap) var(--ln-ease);
  -webkit-tap-highlight-color: transparent;
}

/* Same recipe as the surface lip — see the note there for why it is a shadow on
   an overlay rather than a bar. A pill button is exactly the case a bar breaks. */
.ln-btn::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 light-dark(rgb(255 255 255 / 0.3), rgb(255 255 255 / 0.2));
  mask-image: linear-gradient(to right, transparent, #000 22%, #000 78%, transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 22%, #000 78%, transparent);
  pointer-events: none;
  transition: opacity var(--ln-t-tap) var(--ln-ease);
}

.ln-btn:active:not(:disabled) {
  transform: scale(0.985);
  box-shadow: var(--ln-e-resting);
}
.ln-btn:active:not(:disabled)::before {
  opacity: 0.2;
}
.ln-btn:active:not(:disabled) .ln-bloom::after,
.ln-btn.ln-bloom:active:not(:disabled)::after {
  opacity: 0.5;
}

/* Everything else: a chip, a toolbar action. */
.ln-btn.ln-secondary-btn {
  --ln-btn-fill: var(--ln-surface-quiet);
  --ln-btn-ink: var(--ln-ink);
  border-color: var(--ln-hairline);
  background-image: linear-gradient(
    color-mix(in srgb, var(--ln-btn-fill), light-dark(#fff 5.5%, #fff 7%)),
    color-mix(in srgb, var(--ln-btn-fill), light-dark(#000 4.5%, #000 5%))
  );
  box-shadow: var(--ln-e-resting);
}

/* Reserved for the accent's warm counterpart (streaks, mastery). */
.ln-btn.ln-ember-btn {
  --ln-btn-fill: var(--ln-ember);
  --ln-btn-ink: var(--ln-on-ember);
}

/* A lit critical fill, for the one destructive action a screen is about. */
.ln-btn.ln-destructive {
  --ln-btn-fill: var(--ln-critical-fill);
  --ln-btn-ink: var(--ln-on-critical);
}

/* The other two thirds of the same pair. A status *fill* is for a button that
   commits to that status — "Mark as known", "Overwrite anyway" — not for a
   button that merely mentions it; that one is `.ln-quiet-btn` with the ink. */
.ln-btn.ln-positive {
  --ln-btn-fill: var(--ln-positive-fill);
  --ln-btn-ink: var(--ln-on-positive);
}
.ln-btn.ln-caution {
  --ln-btn-fill: var(--ln-caution-fill);
  --ln-btn-ink: var(--ln-on-caution);
}

/* Text only, no surface. */
.ln-btn.ln-quiet-btn,
.ln-btn.ln-quiet-destructive {
  --ln-btn-ink: var(--ln-accent);
  background: none;
  border-color: transparent;
  box-shadow: none;
  font-weight: 500;
}
.ln-btn.ln-quiet-btn::before,
.ln-btn.ln-quiet-destructive::before {
  display: none;
}
.ln-btn.ln-quiet-btn:hover:not(:disabled),
.ln-btn.ln-quiet-destructive:hover:not(:disabled) {
  background: color-mix(in srgb, currentColor 8%, transparent);
}
/* Text in the critical hue, for a destructive action that must not be the
   loudest thing on its screen: a lit red button invites the tap it is warning
   about, while the accent's blue makes "delete everything" read as an ordinary
   link. */
.ln-btn.ln-quiet-destructive {
  --ln-btn-ink: var(--ln-critical);
}

/* A chip in a row of chips. Under 44px it resolves to a true capsule. */
.ln-btn.ln-compact {
  --ln-btn-radius: var(--ln-r-pill);
  min-height: 40px;
  padding: 9px 14px;
  font-size: var(--ln-fs-callout);
  font-weight: 500;
}
.ln-btn.ln-compact.ln-squared {
  --ln-btn-radius: var(--ln-r-sm);
}
.ln-btn.ln-block {
  display: flex;
  width: 100%;
}

/* A disabled control is inert, not a faded version of an active one: dimming a
   lit fill leaves white text on a pale tint, the least legible thing on the
   screen. */
.ln-btn:disabled,
.ln-btn[aria-disabled='true'] {
  --ln-btn-fill: var(--ln-surface-quiet);
  --ln-btn-ink: var(--ln-ink-tertiary);
  background-image: none;
  border-color: var(--ln-hairline-faint);
  box-shadow: none;
  cursor: default;
  transform: none;
}
.ln-btn:disabled::before,
.ln-btn[aria-disabled='true']::before {
  display: none;
}

/* A circular symbol button: a bar action, a transport control. `ln-bare` for a
   button in bar chrome — a contact shadow is the contact between an object and
   the surface under it, and a bar button has no surface under it. */
.ln-icon-btn {
  --ln-btn-fill: var(--ln-surface-quiet);
  --ln-btn-ink: var(--ln-ink);
  --ln-icon-size: 38px;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ln-icon-size);
  height: var(--ln-icon-size);
  padding: 0;
  border: 1px solid var(--ln-hairline);
  border-radius: 50%;
  color: var(--ln-btn-ink);
  background-color: var(--ln-btn-fill);
  background-image: linear-gradient(
    color-mix(in srgb, var(--ln-btn-fill), light-dark(#fff 5.5%, #fff 7%)),
    color-mix(in srgb, var(--ln-btn-fill), light-dark(#000 4.5%, #000 5%))
  );
  box-shadow: var(--ln-e-resting);
  font-family: inherit;
  font-size: calc(var(--ln-icon-size) * 0.42);
  line-height: 1;
  cursor: pointer;
  transition:
    transform var(--ln-t-tap) var(--ln-ease),
    box-shadow var(--ln-t-tap) var(--ln-ease);
  -webkit-tap-highlight-color: transparent;
}
/* A circle is where the old straight-bar lip was worst: it sat as a visible
   chord across the top of the disc. As a shadow on a radius-inheriting overlay it
   is an arc, and the mask is tighter here because a circle turns away from the
   light much sooner than a rectangle does. */
.ln-icon-btn::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 light-dark(rgb(255 255 255 / 0.42), rgb(255 255 255 / 0.18));
  mask-image: linear-gradient(to right, transparent, #000 34%, #000 66%, transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 34%, #000 66%, transparent);
  pointer-events: none;
  transition: opacity var(--ln-t-tap) var(--ln-ease);
}
.ln-icon-btn.ln-primary-icon {
  --ln-btn-fill: var(--ln-accent);
  --ln-btn-ink: var(--ln-on-accent);
  border-color: rgb(0 0 0 / 0.12);
  box-shadow: var(--ln-e-raised);
}
.ln-icon-btn.ln-bare {
  --ln-btn-fill: transparent;
  background-image: none;
  border-color: transparent;
  box-shadow: none;
}
.ln-icon-btn.ln-bare::before {
  display: none;
}
.ln-icon-btn.ln-bare:hover:not(:disabled) {
  background-color: color-mix(in srgb, currentColor 8%, transparent);
}
.ln-icon-btn:active:not(:disabled) {
  transform: scale(0.92);
}
.ln-icon-btn:active:not(:disabled)::before {
  opacity: 0.2;
}
.ln-icon-btn:disabled {
  --ln-btn-fill: var(--ln-surface-quiet);
  --ln-btn-ink: var(--ln-ink-tertiary);
  background-image: none;
  box-shadow: none;
  cursor: default;
}
.ln-icon-btn.ln-lg {
  --ln-icon-size: 56px;
}
.ln-icon-btn.ln-sm {
  --ln-icon-size: 30px;
}

/* ========================================================================== *
 * Pill
 * ========================================================================== */

/* A badge. One implementation, four styles — it replaced seven hand-rolled
   capsules that agreed on nothing. */
.ln-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--ln-xs);
  padding: 3px 9px;
  border: 1px solid
    color-mix(in srgb, var(--ln-pill-color, var(--ln-ink-secondary)) 42%, transparent);
  border-radius: var(--ln-r-pill);
  background: transparent;
  color: var(--ln-pill-color, var(--ln-ink-secondary));
  font-size: var(--ln-fs-micro);
  font-weight: 600;
  line-height: 1.45;
  white-space: nowrap;
}
.ln-pill.ln-filled {
  background: var(--ln-pill-color, var(--ln-accent));
  border-color: transparent;
  color: var(--ln-on-accent);
}
.ln-pill.ln-wash {
  background: color-mix(in srgb, var(--ln-pill-color, var(--ln-accent)) 14%, transparent);
  border-color: color-mix(in srgb, var(--ln-pill-color, var(--ln-accent)) 24%, transparent);
  color: var(--ln-pill-color, var(--ln-accent));
}
/* Over video or artwork, where `filled`'s ink would flip to near-black in dark
   mode and vanish into the frame behind it. */
.ln-pill.ln-on-media {
  background: var(--ln-media-chip);
  border-color: rgb(255 255 255 / 0.16);
  color: var(--ln-on-media);
}
.ln-pill.ln-accent-pill {
  --ln-pill-color: var(--ln-accent);
}
.ln-pill.ln-positive-pill {
  --ln-pill-color: var(--ln-positive);
}
.ln-pill.ln-caution-pill {
  --ln-pill-color: var(--ln-caution);
}
.ln-pill.ln-critical-pill {
  --ln-pill-color: var(--ln-critical);
}

/* A pill you can press — a filter, a toggle chip. */
button.ln-pill,
a.ln-pill {
  min-height: 40px;
  padding: 6px 14px;
  cursor: pointer;
  font-family: inherit;
  transition:
    transform var(--ln-t-tap) var(--ln-ease),
    background-color var(--ln-t-tap) var(--ln-ease);
}
button.ln-pill:active {
  transform: scale(0.96);
}
button.ln-pill[aria-pressed='true'],
.ln-pill.ln-selected {
  background: var(--ln-accent-wash);
  border-color: color-mix(in srgb, var(--ln-accent) 34%, transparent);
  color: var(--ln-accent);
}

/* ========================================================================== *
 * Rows and groups
 * ========================================================================== */

/* A hairline between two rows of the same group. */
.ln-separator {
  height: 1px;
  border: 0;
  margin: 0;
  background: var(--ln-hairline);
}

/* A group of rows inside one raised card, hairlines between them. */
.ln-group {
  display: flex;
  flex-direction: column;
  border-radius: var(--ln-radius, var(--ln-r-md));
  overflow: hidden;
}
.ln-group > * + * {
  border-top: 1px solid var(--ln-hairline);
}

/* A tappable line in a group: an optional accent glyph, a title, a value, an
   accessory. It dims rather than sinks on press — a row is part of a card's
   surface, not an object sitting on it, so scaling it would peel it off. */
.ln-row {
  display: flex;
  align-items: center;
  gap: var(--ln-md);
  width: 100%;
  padding: var(--ln-sm) var(--ln-lg);
  border: 0;
  background: none;
  color: var(--ln-ink);
  font: inherit;
  text-align: left;
  text-decoration: none;
  transition: opacity var(--ln-t-tap) var(--ln-ease);
}
.ln-row > .ln-row-title {
  flex: 1;
  min-width: 0;
}
.ln-row > .ln-row-value {
  color: var(--ln-ink-secondary);
  font-size: var(--ln-fs-callout);
  white-space: nowrap;
}
.ln-row > .ln-row-icon {
  color: var(--ln-accent);
  flex: none;
  width: 24px;
  text-align: center;
}
.ln-row[href],
.ln-row[type],
button.ln-row {
  cursor: pointer;
}
.ln-row[href]:active,
button.ln-row:active {
  opacity: 0.55;
}
.ln-row.ln-selected {
  background: var(--ln-accent-wash);
}
/* Pushes a screen. */
.ln-row-chevron::after {
  content: '›';
  color: var(--ln-ink-tertiary);
  font-size: 1.15em;
  line-height: 1;
}

/* A label on the leading side and its control on the trailing side. The label
   holds its width and the control yields — a label that yields instead just
   disappears, and then the row is a control with nothing saying what it does. */
.ln-control-row {
  display: flex;
  align-items: center;
  gap: var(--ln-md);
}
.ln-control-row > .ln-control-label {
  flex: none;
  color: var(--ln-ink-secondary);
  font-size: var(--ln-fs-callout);
}
.ln-control-row > :last-child {
  flex: 1;
  min-width: 0;
}

/* ========================================================================== *
 * Fields
 * ========================================================================== */

/* Icons inherit ink and remain secondary to the visible action label. */
.ln-icon { display: inline-block; width: 20px; height: 20px; flex: 0 0 20px; vertical-align: middle; }
.ln-sidebar-nav { display: grid; gap: 4px; }
.ln-sidebar-nav > a { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 10px 12px; border-radius: var(--ln-r-xs); color: var(--ln-ink-secondary); text-decoration: none; font-size: var(--ln-fs-body); }
.ln-sidebar-nav > a:hover { background: var(--ln-well); color: var(--ln-ink); }
.ln-sidebar-nav > a[aria-current="page"] { background: var(--ln-accent-wash); color: var(--ln-accent); font-weight: 600; }

/* Local navigation is a row of destinations, not a row of raised buttons. */
.ln-subnav { display: flex; flex-wrap: wrap; gap: 4px 20px; border-block-end: 1px solid var(--ln-hairline-faint); }
.ln-subnav > a { display: inline-flex; align-items: center; min-height: 44px; padding-block: 8px; color: var(--ln-ink-secondary); text-decoration: none; border-block-end: 2px solid transparent; }
.ln-subnav > a:hover, .ln-subnav > a[aria-current] { color: var(--ln-accent); border-block-end-color: currentColor; }

/* Plain data shares one set of rules, without a raised surface per row. */
.ln-table { width: 100%; border-collapse: collapse; font-size: var(--ln-fs-body); text-align: start; }
.ln-table th, .ln-table td { padding: 12px 16px; border-block-end: 1px solid var(--ln-hairline-faint); vertical-align: top; text-align: start; }
.ln-table th { color: var(--ln-ink-secondary); font-weight: 600; background: var(--ln-well); }
.ln-table tbody tr:last-child td { border-block-end: 0; }

/* Form rhythm groups labels with controls without adding another surface. */
.ln-form { display: grid; gap: 16px; }
.ln-field { display: grid; gap: 6px; min-width: 0; }
.ln-field > label { font-size: var(--ln-fs-body); font-weight: 600; }
.ln-form-divider { display: flex; align-items: center; gap: 12px; color: var(--ln-ink-secondary); font-size: var(--ln-fs-micro); }
.ln-form-divider::before, .ln-form-divider::after { content: ""; flex: 1; height: 1px; background: var(--ln-hairline-faint); }

/* Editable text in a well. */
.ln-input,
.ln-textarea,
.ln-select {
  display: block;
  width: 100%;
  padding: var(--ln-sm) var(--ln-md);
  border: 1px solid var(--ln-hairline-faint);
  border-radius: var(--ln-r-sm);
  background: var(--ln-well);
  box-shadow: var(--ln-inner);
  color: var(--ln-ink);
  font-family: inherit;
  font-size: var(--ln-fs-body);
  line-height: var(--ln-lh);
  transition:
    border-color var(--ln-t-tap) var(--ln-ease),
    box-shadow var(--ln-t-tap) var(--ln-ease);
}
.ln-textarea {
  min-height: 72px;
  resize: vertical;
}
.ln-input::placeholder,
.ln-textarea::placeholder {
  color: var(--ln-ink-tertiary);
}
.ln-input:focus,
.ln-textarea:focus,
.ln-select:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--ln-accent) 55%, transparent);
  box-shadow:
    var(--ln-inner),
    0 0 0 3px var(--ln-accent-wash);
}
.ln-input:disabled,
.ln-textarea:disabled,
.ln-select:disabled {
  color: var(--ln-ink-tertiary);
  cursor: default;
}
.ln-select {
  appearance: none;
  padding-right: var(--ln-xxl);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ln-ink-tertiary) 50%),
    linear-gradient(135deg, var(--ln-ink-tertiary) 50%, transparent 50%);
  background-position:
    right var(--ln-lg) center,
    right var(--ln-md) center;
  background-size:
    5px 5px,
    5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}
.ln-field-label {
  display: block;
  margin-bottom: var(--ln-xs);
  font-size: var(--ln-fs-micro);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ln-ink-secondary);
}
.ln-field-hint {
  margin-top: var(--ln-xs);
  font-size: var(--ln-fs-caption);
  color: var(--ln-ink-secondary);
}
.ln-field-hint.ln-error {
  color: var(--ln-critical);
}

/* A toggle in the app's accent. */
.ln-switch {
  position: relative;
  display: inline-flex;
  flex: none;
  align-items: center;
  width: 51px;
  height: 31px;
  border-radius: var(--ln-r-pill);
  background: var(--ln-well);
  border: 1px solid var(--ln-hairline);
  box-shadow: var(--ln-inner);
  cursor: pointer;
  transition:
    background-color var(--ln-t-move) var(--ln-ease),
    border-color var(--ln-t-move) var(--ln-ease);
  -webkit-tap-highlight-color: transparent;
}
.ln-switch > input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}
.ln-switch > .ln-switch-knob {
  position: absolute;
  left: 2px;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  background: light-dark(#fff, #f3f3f1);
  box-shadow: var(--ln-e-resting);
  transition: transform var(--ln-t-move) var(--ln-ease-spring);
  pointer-events: none;
}
.ln-switch:has(> input:checked) {
  background: var(--ln-accent);
  border-color: rgb(0 0 0 / 0.12);
  box-shadow: none;
}
.ln-switch:has(> input:checked) > .ln-switch-knob {
  transform: translateX(20px);
}
.ln-switch:has(> input:focus-visible) {
  outline: 2px solid var(--ln-accent);
  outline-offset: 2px;
}
.ln-switch:has(> input:disabled) {
  opacity: 0.5;
  cursor: default;
}

/* A row of choices sunk into the surface, the selected one raised back out of
   it. Radio labels are the no-JS form; buttons are the dynamic form used when
   a page owns the state. Both stay one component so a selector does not grow a
   page-specific visual dialect. */
.ln-segmented {
  display: inline-flex;
  align-items: stretch;
  padding: 2px;
  border: 1px solid var(--ln-hairline-faint);
  border-radius: var(--ln-r-xs);
  background: var(--ln-well);
  box-shadow: var(--ln-inner);
}
/* Equal widths, not width-by-content: a two-segment control whose labels are
   "Spotlight" and "Study" should not have one segment twice the other. */
.ln-segmented > label,
.ln-segmented > button {
  position: relative;
  flex: 1;
  display: inline-flex;
  box-sizing: border-box;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  padding: 8px 14px;
  border: 1px solid transparent;
  border-radius: calc(var(--ln-r-xs) - 3px);
  background: none;
  color: var(--ln-ink-secondary);
  font-family: inherit;
  font-size: var(--ln-fs-callout);
  cursor: pointer;
  text-align: center;
  white-space: nowrap;
  transition:
    background-color var(--ln-t-tap) var(--ln-ease),
    color var(--ln-t-tap) var(--ln-ease);
}
.ln-segmented > button:hover:not(:disabled),
.ln-segmented > label:hover {
  color: var(--ln-ink);
}
.ln-segmented input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.ln-segmented > label:has(input:checked),
.ln-segmented > button[aria-pressed='true'] {
  background: var(--ln-surface);
  border: 1px solid var(--ln-hairline);
  color: var(--ln-ink);
  box-shadow: var(--ln-e-resting);
}
.ln-segmented > label:has(input:focus-visible) {
  outline: 2px solid var(--ln-accent);
  outline-offset: 1px;
}
.ln-segmented > button:disabled {
  color: var(--ln-ink-tertiary);
  cursor: default;
}

/* A value track: a well with a lit fill and a machined cap. */
.ln-slider {
  appearance: none;
  width: 100%;
  height: 26px;
  background: transparent;
  cursor: pointer;
}
.ln-slider::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 3px;
  background: var(--ln-well);
  border: 1px solid var(--ln-hairline);
  box-shadow: var(--ln-inner);
}
.ln-slider::-moz-range-track {
  height: 6px;
  border-radius: 3px;
  background: var(--ln-well);
  border: 1px solid var(--ln-hairline);
  box-shadow: var(--ln-inner);
}
.ln-slider::-moz-range-progress {
  height: 6px;
  border-radius: 3px;
  background: var(--ln-accent);
}
/* A transport, not a setting: the part already played is lit. Gecko fills a
   range on its own (above); WebKit has no track-fill pseudo-element, so the fill
   is painted into the track and needs the value handed to it — CSS cannot read
   an input's value. Set `--ln-progress` (0…1) wherever you set `.value`. Not
   `Lantern.setProgress`: that one also writes progressbar ARIA, which would lie
   about a control the reader can drag. */
.ln-slider.ln-filled {
  --ln-progress: 0;
}
.ln-slider.ln-filled::-webkit-slider-runnable-track {
  background: linear-gradient(
      to right,
      var(--ln-accent) 0 calc(var(--ln-progress) * 100%),
      transparent calc(var(--ln-progress) * 100%) 100%
    )
    var(--ln-well);
}
.ln-slider.ln-on-media::-webkit-slider-runnable-track,
.ln-slider.ln-on-media::-moz-range-track {
  background-color: var(--ln-on-media-track);
  border-color: transparent;
  box-shadow: none;
}
.ln-slider::-webkit-slider-thumb {
  appearance: none;
  width: 26px;
  height: 26px;
  margin-top: -11px;
  border-radius: 50%;
  border: 1px solid var(--ln-hairline);
  background-color: var(--ln-surface);
  background-image: linear-gradient(
    light-dark(rgb(255 255 255 / 0), rgb(255 255 255 / 0.08)),
    light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.06))
  );
  box-shadow: var(--ln-e-resting);
}
.ln-slider::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--ln-hairline);
  background: var(--ln-surface);
  box-shadow: var(--ln-e-resting);
}

/* A determinate bar: a well with a lit fill. */
.ln-progress {
  --ln-progress: 0;
  position: relative;
  height: 6px;
  border-radius: 3px;
  background: var(--ln-well);
  border: 1px solid var(--ln-hairline);
  box-shadow: var(--ln-inner);
  overflow: hidden;
}
.ln-progress > .ln-progress-fill {
  height: 100%;
  border-radius: 3px;
  /* Never narrower than its own height: a 2% sliver drawn into a capsule corner
     reads as a rendering glitch rather than as "just started". */
  min-width: 6px;
  width: calc(var(--ln-progress) * 100%);
  background-color: var(--ln-progress-tint, var(--ln-accent));
  background-image: linear-gradient(
    color-mix(in srgb, var(--ln-progress-tint, var(--ln-accent)), light-dark(#fff 5.5%, #fff 7%)),
    color-mix(in srgb, var(--ln-progress-tint, var(--ln-accent)), light-dark(#000 4.5%, #000 5%))
  );
  transition: width var(--ln-t-move) var(--ln-ease);
}
.ln-progress[data-progress='0'] > .ln-progress-fill {
  min-width: 0;
  width: 0;
}
/* Over video or artwork the well disappears and the neutral tokens go with it. */
.ln-progress.ln-on-media {
  background: var(--ln-on-media-track);
  border-color: transparent;
  box-shadow: none;
}

/* A determinate ring with its own reading in the middle, for a wait long enough
   that a bar would sit at the bottom of an otherwise empty screen. */
.ln-ring {
  --ln-progress: 0;
  --ln-ring-size: 96px;
  position: relative;
  width: var(--ln-ring-size);
  height: var(--ln-ring-size);
  border-radius: 50%;
  background: conic-gradient(var(--ln-accent) calc(var(--ln-progress) * 360deg), var(--ln-well) 0);
  display: grid;
  place-items: center;
}
.ln-ring::before {
  content: '';
  position: absolute;
  inset: 8px;
  border-radius: 50%;
  background: var(--ln-surface);
}
.ln-ring > .ln-ring-readout {
  position: relative;
  font-variant-numeric: tabular-nums;
  font-size: var(--ln-fs-title);
  font-weight: 600;
}

/* A colour, as a tappable chip. A transparent colour and a white one both leave
   nothing to see on a light card, so the chip is always outlined and
   transparency is drawn as a checker. */
.ln-swatch {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 56px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--ln-hairline);
  border-radius: var(--ln-r-xs);
  /* The checker is the element's own background and the colour is a layer on top
     of it, not the reverse: an element's background paints *below* its ::before,
     so a checker in the pseudo-element would cover every opaque colour instead
     of showing through a transparent one. */
  background-image:
    linear-gradient(
      45deg,
      rgb(0 0 0 / 0.14) 25%,
      transparent 25%,
      transparent 75%,
      rgb(0 0 0 / 0.14) 75%
    ),
    linear-gradient(
      45deg,
      rgb(0 0 0 / 0.14) 25%,
      transparent 25%,
      transparent 75%,
      rgb(0 0 0 / 0.14) 75%
    );
  background-size: 8px 8px;
  background-position:
    0 0,
    4px 4px;
  cursor: pointer;
  font-size: var(--ln-fs-micro);
  color: var(--ln-ink-secondary);
  transition: transform var(--ln-t-tap) var(--ln-ease);
  overflow: hidden;
}
.ln-swatch::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--ln-swatch-color, transparent);
}
/* Above the colour layer, so the "None" label survives an opaque swatch. */
.ln-swatch > * {
  position: relative;
}
.ln-swatch:active {
  transform: scale(0.95);
}

/* ========================================================================== *
 * States — empty, loading, failure
 * ========================================================================== */

/* One vocabulary for the three states every list can be in, so that every
   failure in the app reads the same. */
.ln-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ln-sm);
  padding: var(--ln-xxl) var(--ln-lg);
  text-align: center;
  color: var(--ln-ink-secondary);
}
.ln-state > .ln-state-glyph {
  font-size: 2rem;
  line-height: 1;
  color: var(--ln-ink-tertiary);
}
.ln-state > .ln-state-title {
  font-size: var(--ln-fs-headline);
  font-weight: 600;
  color: var(--ln-ink);
}
.ln-state > .ln-state-detail {
  font-size: var(--ln-fs-callout);
  max-width: 34ch;
}
.ln-state.ln-failure > .ln-state-glyph {
  color: var(--ln-caution);
}

.ln-spinner {
  display: inline-block;
  width: var(--ln-spinner-size, 18px);
  height: var(--ln-spinner-size, 18px);
  border: 2px solid color-mix(in srgb, currentColor 22%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: ln-spin 0.7s linear infinite;
}
@keyframes ln-spin {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .ln-spinner {
    animation-duration: 1.6s;
  }
}

/* A shape standing in for content that is on its way. */
.ln-skeleton {
  border-radius: var(--ln-r-xs);
  background: linear-gradient(
    90deg,
    var(--ln-surface-quiet) 25%,
    color-mix(in srgb, var(--ln-surface-quiet), var(--ln-ink) 5%) 37%,
    var(--ln-surface-quiet) 63%
  );
  background-size: 400% 100%;
  animation: ln-shimmer 1.4s ease-in-out infinite;
  color: transparent;
  user-select: none;
}
@keyframes ln-shimmer {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: 0 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .ln-skeleton {
    animation: none;
  }
}

/* ========================================================================== *
 * Over media
 * ========================================================================== */

/* A media tile: the dark recess fill, a continuous radius, clipped content, and
   — the load-bearing part — a hairline edge. Without it a tile disappears in
   dark mode: `--ln-media` is a hair off `--ln-canvas` by design, so a black
   rectangle on a black page has nothing to be an edge against, and a thumbnail
   that has not loaded reads as a hole in the screen rather than as a frame
   waiting for a frame. */
.ln-media-tile {
  background: var(--ln-media);
  border: 1px solid var(--ln-hairline);
  border-radius: var(--ln-radius, var(--ln-r-sm));
  overflow: hidden;
}
.ln-media-tile > img,
.ln-media-tile > video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The gradient that lets text sit over media. Both stops are transparent black
   rather than one being `transparent`, for the same reason a card's sheen fades
   white into white. */
.ln-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(rgb(0 0 0 / 0), rgb(0 0 0 / 0.33) 55%, rgb(0 0 0 / 0.78));
}
.ln-on-media-ink {
  color: var(--ln-on-media);
}
.ln-on-media-ink-secondary {
  color: var(--ln-on-media-secondary);
}

/* ========================================================================== *
 * Chrome — bars, dialogs, sheets, toasts
 * ========================================================================== */

/* Scrolled-to-top is the page wash with no edge, so a card scrolling under the
   bar is the only edge on screen; scrolled-away gains the hairline the rest of
   the app separates things with. Toggle `data-scrolled` from a scroll listener. */
.ln-bar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--ln-md);
  padding: var(--ln-md) var(--ln-lg);
  background: var(--ln-glass);
  backdrop-filter: saturate(140%) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--ln-t-move) var(--ln-ease);
}
.ln-bar[data-scrolled='true'] {
  border-bottom-color: var(--ln-hairline);
}
.ln-bar > .ln-bar-title {
  flex: 1;
  min-width: 0;
  font-size: var(--ln-fs-headline);
  font-weight: 600;
}

/* A native <dialog>, in the language. */
dialog.ln-dialog {
  max-width: min(560px, calc(100vw - var(--ln-xl) * 2));
  max-height: min(80vh, 720px);
  padding: 0;
  border: 1px solid var(--ln-hairline);
  border-radius: var(--ln-r-lg);
  background-color: var(--ln-surface);
  background-image: var(--ln-grain), var(--ln-sheen);
  background-size: var(--ln-grain-size), auto;
  box-shadow: var(--ln-e-floating);
  color: var(--ln-ink);
  overflow: hidden;
}
dialog.ln-dialog::backdrop {
  background: var(--ln-scrim);
  backdrop-filter: blur(3px);
}
dialog.ln-dialog[open] {
  animation: ln-dialog-in var(--ln-t-move) var(--ln-ease-spring);
}
@keyframes ln-dialog-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.985);
  }
}
.ln-dialog-head,
.ln-dialog-foot {
  display: flex;
  align-items: center;
  gap: var(--ln-sm);
  padding: var(--ln-md) var(--ln-lg);
}
.ln-dialog-head {
  border-bottom: 1px solid var(--ln-hairline);
  font-size: var(--ln-fs-headline);
  font-weight: 600;
}
.ln-dialog-head > :first-child {
  flex: 1;
}
.ln-dialog-body {
  padding: var(--ln-lg);
  overflow-y: auto;
}
.ln-dialog-foot {
  border-top: 1px solid var(--ln-hairline);
  justify-content: flex-end;
}

/* A sheet: the same surface, docked to an edge. Bottom on narrow screens,
   side on wide ones — the web's equivalent of a drawer and a rail being the
   same component in two places. */
dialog.ln-sheet {
  max-width: none;
  width: min(420px, 100vw);
  max-height: 100vh;
  height: 100vh;
  margin: 0 0 0 auto;
  border-radius: var(--ln-r-lg) 0 0 var(--ln-r-lg);
}
dialog.ln-sheet[open] {
  animation: ln-sheet-in var(--ln-t-move) var(--ln-ease);
}
@keyframes ln-sheet-in {
  from {
    transform: translateX(24px);
    opacity: 0;
  }
}
@media (max-width: 560px) {
  dialog.ln-sheet {
    width: 100vw;
    height: auto;
    max-height: 88vh;
    margin: auto 0 0;
    border-radius: var(--ln-r-lg) var(--ln-r-lg) 0 0;
  }
  dialog.ln-sheet[open] {
    animation: ln-sheet-up var(--ln-t-move) var(--ln-ease);
  }
  @keyframes ln-sheet-up {
    from {
      transform: translateY(24px);
      opacity: 0;
    }
  }
}
/* The grab handle a bottom sheet needs to look draggable. */
.ln-sheet-grip {
  width: 36px;
  height: 4px;
  margin: var(--ln-sm) auto 0;
  border-radius: var(--ln-r-pill);
  background: var(--ln-ink-tertiary);
}

/* A transient message over the content. A toast states something and leaves. */
.ln-toast-host {
  position: fixed;
  left: 50%;
  bottom: var(--ln-xl);
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ln-sm);
  transform: translateX(-50%);
  pointer-events: none;
}
.ln-toast {
  max-width: min(88vw, 420px);
  padding: 10px 18px;
  border: 1px solid var(--ln-hairline);
  border-radius: 18px;
  background-color: var(--ln-surface);
  background-image: var(--ln-grain), var(--ln-sheen);
  background-size: var(--ln-grain-size), auto;
  box-shadow: var(--ln-e-floating);
  color: var(--ln-ink);
  font-size: var(--ln-fs-callout);
  text-align: center;
  animation: ln-toast-in var(--ln-t-move) var(--ln-ease-spring);
}
.ln-toast.ln-leaving {
  animation: ln-toast-out 0.24s var(--ln-ease) forwards;
}
.ln-toast.ln-positive-toast {
  color: var(--ln-positive);
}
.ln-toast.ln-warning-toast {
  color: var(--ln-caution);
}
@keyframes ln-toast-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
}
@keyframes ln-toast-out {
  to {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* A snackbar is an offer, not a statement: it carries up to two actions, so it
   waits long enough to read and stays reachable. A floating raised surface with
   ink on it, not a dark bar — a bar in its own palette is a second material on
   the page. */
.ln-snackbar {
  position: fixed;
  left: var(--ln-md);
  right: var(--ln-md);
  bottom: var(--ln-md);
  z-index: 100;
  max-width: 520px;
  margin: 0 auto;
  padding: var(--ln-lg);
  border: 1px solid var(--ln-hairline);
  border-radius: var(--ln-r-md);
  background-color: var(--ln-surface);
  background-image: var(--ln-grain), var(--ln-sheen);
  background-size: var(--ln-grain-size), auto;
  box-shadow: var(--ln-e-floating);
  animation: ln-toast-in var(--ln-t-move) var(--ln-ease-spring);
}
.ln-snackbar > .ln-snackbar-message {
  font-size: var(--ln-fs-callout);
  color: var(--ln-ink);
}
/* Stacked, not side by side: a term, an arrow, its replacement and two verbs
   never fit one line at a large text size. */
.ln-snackbar > .ln-snackbar-actions {
  display: flex;
  gap: var(--ln-sm);
  margin-top: var(--ln-sm);
}

/* A banner that stays: a persistent, dismissible notice at the top of a view. */
.ln-banner {
  display: flex;
  align-items: center;
  gap: var(--ln-sm);
  padding: var(--ln-sm) var(--ln-md);
  border: 1px solid color-mix(in srgb, var(--ln-banner-color, var(--ln-caution)) 30%, transparent);
  border-radius: var(--ln-r-sm);
  background: color-mix(in srgb, var(--ln-banner-color, var(--ln-caution)) 12%, transparent);
  color: var(--ln-banner-color, var(--ln-caution));
  font-size: var(--ln-fs-callout);
}
.ln-banner.ln-positive-banner {
  --ln-banner-color: var(--ln-positive);
}
.ln-banner.ln-critical-banner {
  --ln-banner-color: var(--ln-critical);
}
.ln-banner.ln-neutral-banner {
  --ln-banner-color: var(--ln-ink-secondary);
}
.ln-banner > :first-child {
  flex: 1;
}

/* A tooltip on hover/focus, from a `data-ln-tip` attribute. Web-only: there is
   no hover on iOS, so the language gains this one rather than porting it. */
[data-ln-tip] {
  position: relative;
}
[data-ln-tip]:hover::after,
[data-ln-tip]:focus-visible::after {
  content: attr(data-ln-tip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  padding: 4px 8px;
  border: 1px solid var(--ln-hairline);
  border-radius: var(--ln-r-xs);
  background: var(--ln-surface);
  box-shadow: var(--ln-e-resting);
  color: var(--ln-ink);
  font-size: var(--ln-fs-micro);
  font-weight: 500;
  white-space: nowrap;
  pointer-events: none;
}

/* ========================================================================== *
 * Lists, tables, prose — web surfaces iOS has no equivalent for
 * ========================================================================== */

/* A list with no background of its own: the page wash is already behind it. */
.ln-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.ln-list > li + li {
  border-top: 1px solid var(--ln-hairline);
}

.ln-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--ln-fs-callout);
}
.ln-table th {
  text-align: left;
  padding: var(--ln-sm) var(--ln-md);
  border-bottom: 1px solid var(--ln-hairline);
  font-size: var(--ln-fs-micro);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ln-ink-secondary);
}
.ln-table td {
  padding: var(--ln-sm) var(--ln-md);
  border-bottom: 1px solid var(--ln-hairline-faint);
}
.ln-table tbody tr:last-child td {
  border-bottom: 0;
}
/* Wide content scrolls inside its own box; the page never scrolls sideways. */
.ln-scroll-x {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.ln-prose {
  color: var(--ln-ink);
  line-height: 1.6;
}
.ln-prose h2 {
  font-size: var(--ln-fs-title);
  font-weight: 600;
  margin: var(--ln-xl) 0 var(--ln-sm);
}
.ln-prose h3 {
  font-size: var(--ln-fs-headline);
  font-weight: 600;
  margin: var(--ln-lg) 0 var(--ln-xs);
}
.ln-prose p {
  margin: 0 0 var(--ln-md);
}
.ln-prose code {
  font-family: var(--ln-font-mono);
  font-size: 0.9em;
  padding: 1px 5px;
  border-radius: 5px;
  background: var(--ln-well);
  border: 1px solid var(--ln-hairline-faint);
}
.ln-prose pre {
  padding: var(--ln-md);
  border-radius: var(--ln-r-sm);
  background: var(--ln-well);
  border: 1px solid var(--ln-hairline-faint);
  box-shadow: var(--ln-inner);
  overflow-x: auto;
}
.ln-prose pre code {
  padding: 0;
  border: 0;
  background: none;
}

/* A collapsible section. The marker is ours: the system triangle is a fifth
   grey nobody chose. */
details.ln-details > summary {
  display: flex;
  align-items: center;
  gap: var(--ln-sm);
  padding: var(--ln-sm) 0;
  cursor: pointer;
  list-style: none;
  font-size: var(--ln-fs-callout);
  font-weight: 600;
  color: var(--ln-ink);
}
details.ln-details > summary::-webkit-details-marker {
  display: none;
}
details.ln-details > summary::before {
  content: '›';
  color: var(--ln-ink-tertiary);
  transition: transform var(--ln-t-tap) var(--ln-ease);
}
details.ln-details[open] > summary::before {
  transform: rotate(90deg);
}

/* ========================================================================== *
 * Layout helpers
 * ========================================================================== */

/* Each size is a complete stack, not a modifier that needs `.ln-stack` beside
   it — a gap with no flex container is a silent no-op, and it was one. */
.ln-stack,
.ln-stack-sm,
.ln-stack-md,
.ln-stack-xl {
  display: flex;
  flex-direction: column;
}
.ln-stack {
  gap: var(--ln-lg);
}
.ln-stack-sm {
  gap: var(--ln-sm);
}
.ln-stack-md {
  gap: var(--ln-md);
}
.ln-stack-xl {
  gap: var(--ln-xl);
}
.ln-hstack {
  display: flex;
  align-items: center;
  gap: var(--ln-sm);
}
.ln-hstack-wrap {
  flex-wrap: wrap;
}
.ln-spacer {
  flex: 1;
}
/* Page margins for a scrolling reading surface. */
.ln-page {
  max-width: var(--ln-page-width, 860px);
  margin: 0 auto;
  padding: var(--ln-xl) var(--ln-lg);
}
.ln-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Scrollbars, so a dark page does not get a light gutter. */
.ln-canvas * {
  scrollbar-color: var(--ln-ink-tertiary) transparent;
  scrollbar-width: thin;
}
