/* ═══ Fluid Lycoris ═══
   Material 3 Expressive motion with a red/teal colour code. Theme tokens
   (dark OLED default, light), the control metrics and the styles of every
   t-* primitive. Plain CSS: import it once, before the host's own rules.

   One radius scale, tied to the control heights the way --ctl-h is: a chip
   rounds less than a button, a button less than a panel, a panel less than
   a dialog. Hairline borders, flat fills, no glow; only dots, avatars and
   progress bars are fully round. */

/* ── Theme tokens ──
   Colour code: the accent (red, the Lycoris half) is live, new and the main
   action; accent-2 (teal, the Recoil half) is sync, matching, rules,
   explanations and airing times. A scheme may swap the two, never mix them.
   Dark is the default and OLED: a true black page, surfaces that carry only a
   shimmer of navy. Light is white with blush and mint. Every text colour
   clears 4.5:1 on bg, bg-2, card and hover in its theme; --line-strong is the
   3:1 edge of a field or an unchecked box (WCAG 1.4.11), --line the
   decorative hairline. --err leans orange because the accent is already red:
   an error always carries an icon and a word as well. */
:root,
[data-theme="dark"] {
  --bg: #000000;
  --bg-2: #05070d;
  --card: #0b1120;
  --hover: #131b2e;
  --line: #1f2a40;
  --line-strong: #6b819c;
  --fg: #eef2f7;
  --fg-2: #bccadb;
  --fg-3: #8ea2b9;
  --accent: #ef6177;
  --on-accent: #1a0509;
  --accent-2: #33c2b5;
  --on-accent-2: #03201d;
  --tint-1: color-mix(in oklab, #ef6177 16%, #000000);
  --tint-2: color-mix(in oklab, #33c2b5 14%, #000000);
  --ok: #5fd38d;
  --warn: #f2c46b;
  --err: #ff9a5c;
  --scrim: rgb(0 0 0 / 0.66);
  /* the art colours: the two halves of the code, the cream of the key visual
     and the ink of a printed line */
  --fl-ink: #000000;
  --fl-cream: #f4ead6;
  /* thumb = the 3:1 edge colour, so it clears 3:1 on bg, bg-2, card and hover;
     the track stays clear so the pair holds on whatever surface scrolls */
  --scrollbar-track: transparent;
  --scrollbar-thumb: #6b819c;
  --scrollbar-thumb-hover: #8ea2b9;
  /* glass for floating layers: tinted black, never grey, so on an OLED panel
     it reads as depth rather than a milky veil. 78% keeps grey text at 4.5:1
     even with a white poster behind it (66% fell to ~4.3:1). */
  --glass-bg: rgb(0 0 0 / 0.78);
  --glass-line: rgb(255 255 255 / 0.1);
  --glass-hi: rgb(255 255 255 / 0.12);
  color-scheme: dark;
}

/* light: set by [data-theme="light"], or by the OS when the host sets no
   theme at all (the two blocks carry the same values) */
[data-theme="light"] {
  --bg: #fdf8f8;
  --bg-2: #ffffff;
  --card: #ffffff;
  --hover: #fbe9ec;
  --line: #ecd3d8;
  --line-strong: #8a6f76;
  --fg: #1b2238;
  --fg-2: #3a4462;
  /* 4.5:1 also on the accent's 14% tint of a picked tab or row */
  --fg-3: #566079;
  --accent: #b3263f;
  --on-accent: #ffffff;
  --accent-2: #0c7069;
  --on-accent-2: #ffffff;
  --tint-1: #fcecf5;
  --tint-2: #e9faf8;
  --ok: #1d6b3d;
  --warn: #7d5200;
  --err: #a8440a;
  --scrim: rgb(27 34 56 / 0.32);
  --fl-ink: #1b2238;
  --scrollbar-track: transparent;
  --scrollbar-thumb: #8a6f76;
  --scrollbar-thumb-hover: #6b525a;
  --glass-bg: rgb(255 255 255 / 0.8);
  --glass-line: rgb(27 34 56 / 0.12);
  --glass-hi: rgb(255 255 255 / 0.7);
  color-scheme: light;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --bg: #fdf8f8;
    --bg-2: #ffffff;
    --card: #ffffff;
    --hover: #fbe9ec;
    --line: #ecd3d8;
    --line-strong: #8a6f76;
    --fg: #1b2238;
    --fg-2: #3a4462;
    --fg-3: #566079;
    --accent: #b3263f;
    --on-accent: #ffffff;
    --accent-2: #0c7069;
    --on-accent-2: #ffffff;
    --tint-1: #fcecf5;
    --tint-2: #e9faf8;
    --ok: #1d6b3d;
    --warn: #7d5200;
    --err: #a8440a;
    --scrim: rgb(27 34 56 / 0.32);
    --fl-ink: #1b2238;
    --scrollbar-track: transparent;
    --scrollbar-thumb: #8a6f76;
    --scrollbar-thumb-hover: #6b525a;
    --glass-bg: rgb(255 255 255 / 0.8);
    --glass-line: rgb(27 34 56 / 0.12);
    --glass-hi: rgb(255 255 255 / 0.7);
    color-scheme: light;
  }
}

/* Lycoris named explicitly: the default pair, for a preview of the scheme
   inside a page that wears the other one */
[data-scheme="lycoris"] {
  --accent: #ef6177;
  --on-accent: #1a0509;
  --accent-2: #33c2b5;
  --on-accent-2: #03201d;
}
[data-theme="light"][data-scheme="lycoris"],
[data-theme="light"] [data-scheme="lycoris"] {
  --accent: #b3263f;
  --on-accent: #ffffff;
  --accent-2: #0c7069;
  --on-accent-2: #ffffff;
}

/* Recoil: the scheme that swaps the halves. The accent turns teal and the
   accent-2 red, so the main action is teal and sync is red - the code stays
   a pair, only the leading colour changes. */
[data-scheme="recoil"] {
  --accent: #33c2b5;
  --on-accent: #03201d;
  --accent-2: #ef6177;
  --on-accent-2: #1a0509;
  --tint-1: color-mix(in oklab, #33c2b5 14%, #000000);
  --tint-2: color-mix(in oklab, #ef6177 16%, #000000);
}
[data-theme="light"][data-scheme="recoil"],
[data-theme="light"] [data-scheme="recoil"] {
  --accent: #0c7069;
  --on-accent: #ffffff;
  --accent-2: #b3263f;
  --on-accent-2: #ffffff;
  --tint-1: #e9faf8;
  --tint-2: #fcecf5;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme])[data-scheme="recoil"] {
    --accent: #0c7069;
    --on-accent: #ffffff;
    --accent-2: #b3263f;
    --on-accent-2: #ffffff;
    --tint-1: #e9faf8;
    --tint-2: #fcecf5;
  }
}

/* ── Glass: only for layers that float over content (menus, the player's
   bars). The page itself stays opaque - glass over plain black is just dark,
   and every blurred layer costs a phone GPU time. Never nest it. The top
   highlight is the "liquid" edge; no SVG refraction (Chromium only, costly).
   Without backdrop-filter, or when the user asks for less transparency, the
   layer turns solid. */
.t-glass {
  background: var(--glass-bg);
  border-color: var(--glass-line);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
  box-shadow: inset 0 1px 0 var(--glass-hi), 0 12px 32px -12px rgb(0 0 0 / 0.5);
}
@supports not (backdrop-filter: blur(1px)) {
  .t-glass { background: var(--bg-2); }
}
@media (prefers-reduced-transparency: reduce) {
  .t-glass { background: var(--bg-2); backdrop-filter: none; -webkit-backdrop-filter: none; }
  /* :root for weight - the player's own block comes later in the file */
  :root .t-player { --pl-surface: rgb(0 0 0 / 0.9); --pl-blur: none; }
}

/* ── Control metrics ──
   One height per control family, and a fixed line box inside each: without
   these an input, a select and a button in the same row come out at three
   different heights, because inputs inherit the surrounding font size while
   buttons carry their own. Touch overrides the values further down, so every
   rule can just reference the variable. */
:root {
  /* Material 3 Expressive's fast spatial motion, as a curve: for things
     that move or grow. It starts at the spring's speed but glides to a stop
     instead of overshooting: the overshoot and its settle read as a jolt at
     the end. Colour and opacity never use it */
  --m3-spatial: linear(0, 0.214 4%, 0.382 8%, 0.515 12%, 0.641 17%, 0.735 22%, 0.816 28%, 0.880 35%, 0.927 43%, 0.958 52%, 0.980 63%, 0.992 76%, 1);
  --m3-spatial-dur: 350ms;
  /* the hero morphs and presses: quick off the mark, gliding out, no
     overshoot to snap back from */
  --spring: linear(0, 0.182 4%, 0.332 8%, 0.454 12%, 0.576 17%, 0.672 22%, 0.759 28%, 0.832 35%, 0.890 43%, 0.932 52%, 0.964 63%, 0.984 76%, 1);
  /* a short entrance (a rise, a slide of a few pixels): the decelerating
     curve, but it lands once 6 % of the way is left. A move that slow is
     below a pixel, and a layer resting between pixels looks soft: the long
     tail made text blur just before an entrance ended. The jump is smaller
     than a pixel, so it doesn't show. Small moves only, never a sheet */
  --ease-enter: linear(0, 0.240 2%, 0.393 4%, 0.493 6%, 0.595 9%, 0.666 12%, 0.734 16%, 0.784 20%, 0.832 25%, 0.874 31%, 0.909 38%, 0.939 46%, 0.94 46.4%, 1 46.4%, 1);
  /* no overshoot: sheets, toasts, thumbs and other small parts */
  --spring-calm: linear(0, 0.1 3%, 0.32 8%, 0.6 15%, 0.8 22%, 0.91 30%, 0.97 40%, 0.99 52%, 1 70%, 1);
  /* type: Chakra Petch for display, IBM Plex for text and figures (the host
     loads the fonts, and may define a metric-matched "<family> Fallback"
     face for the moment before they arrive). Sentence case everywhere; only
     a brand mark tracks. */
  --f-display: "Chakra Petch", "Chakra Petch Fallback", "Bahnschrift", "DIN Alternate", system-ui, sans-serif;
  --f-sans: "IBM Plex Sans", "IBM Plex Sans Fallback", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
  --fs-tab: 0.6875rem; /* tab labels, badges */
  --fs-caption: 0.75rem; /* small captions */
  --fs-meta: 0.8125rem; /* a row's second line, counts */
  --fs-link: 0.875rem; /* text links, row actions */
  --fs-body: 0.9375rem; /* body and row titles */
  --fs-section: 1.0625rem; /* section headings */
  --fs-sheet: 1.25rem; /* sheet and dialog titles */
  --fs-title: 1.375rem; /* page title on a phone */
  --fs-title-lg: 1.75rem; /* page title on a desktop */
  --fs-big: 2rem; /* a big figure */
  /* 44px controls, 34px small ones; written in rem (px / 16) so they ride
     the host's root scale with everything else */
  --ctl-h: 2.75rem; /* input, select, button, icon button */
  /* touch hit area under every control on a coarse pointer (WCAG 2.5.8 asks
     24px, this is the comfortable size). px, not rem: the phone root is 18-22px */
  --hit: 44px;
  --ctl-h-sm: 2.125rem; /* small buttons and fields */
  --ctl-fs: 0.9375rem;
  --ctl-fs-sm: 0.8125rem;
  --ctl-fs-xs: 0.6875rem; /* chips; 11px is the legibility floor */
  --ctl-lh: 1.375rem;
  --ctl-lh-sm: 1.125rem;
  /* Radius per host height, roughly a sixth of the box: 24px chips 4, 26px
     small controls 4, 36px controls 6; surfaces step up from there. Touch
     raises the control steps with the control heights below. Explicit
     values rather than calc(): a calc() declared here would resolve here,
     and .t-toolbar's element-level --ctl-h override would not reach it. */
  --r-xs: 4px; /* the concentric floor: a cover inside a padded panel */
  --r-chip: 4px; /* .t-label, checkbox */
  --r-ctl-sm: 8px; /* .t-btn--sm, --sm inputs */
  --r-ctl: 8px; /* .t-btn, .t-input, .t-select, .t-tabs, notes */
  --r-menu: 10px; /* menus, dropdown lists, sub-boxes, chat bubbles */
  --r-item: 7px; /* an item inside a menu, the thumb of a segmented control */
  --r-panel: 12px; /* .t-panel, toast, composer, file browser */
  --r-dialog: 14px; /* a centred dialog */
  --r-cover: 6px; /* covers: rounded, never ringed */
  --r-sheet: 20px; /* the phone sheet's top corners, the hero banner */
  /* Motion: enter eases out, exit eases in and is shorter; nothing over
     300ms, nothing moves more than 8px or scales below 0.98, nothing
     transforms on hover. All of it sits under the reduced-motion gate. */
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --dur-1: 120ms; /* hover, tick, popover */
  --dur-2: 200ms; /* route, panel, toast, every exit */
  --dur-3: 280ms; /* dialog, sheet */
  /* the short names the component styles of the reference mock use */
  --d-1: var(--dur-1);
  --d-2: var(--dur-2);
  --d-3: var(--dur-3);
  /* height of the mobile tab bar - the bar sets it, <main> reserves it */
  --nav-h: 3.33rem;
  /* System-bar insets. Both engines report a bottom inset in a browser tab on a
     Pixel 8 Pro, but only one of them means it: with the padding removed,
     Chrome's gesture pill lands on top of the tab labels (its page really is
     drawn edge to edge), while Firefox's tab bar still ends above the pill -
     there the reported 28.8px are a band of dead space.
     `safe-area-max-inset-bottom` sorts them apart without sniffing: Chrome 135+
     ships it (and recommends it, because it does not jitter while the chin
     animates), Firefox does not, so the fallback chain lands on the plain inset
     - which is what iOS needs for its home indicator. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-max-inset-bottom, env(safe-area-inset-bottom, 0px));
  /* landscape notches and cutouts */
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

/* Root font-size is the master scale: every rem-based Tailwind utility
   (text, spacing, widths) grows from it. Desktop steps up from the 16px
   UA default; touch devices step up once more since phones are read at
   arm's length. Keeps the whole UI proportional instead of bumping every
   element by hand. */
html {
  font-size: 17px;
  /* reserve the scrollbar so locking the page behind a modal (below) does not
     shift the layout by its width. Desktop only: on a phone the document never
     scrolls (the shell is a fixed-height column, <main> is the scroller), so
     the gutter reserved a strip nothing could ever use - and since a fixed
     element is laid out against that narrowed box, the tab bar and the header
     ended up a scrollbar's width short of the right edge. */
  @media (width >= 64rem) {
    scrollbar-gutter: stable;
  }
  /* Tailwind's preflight only ships the -webkit- spelling, which Firefox has
     never aliased - without these two its font inflation stays on and scales
     text independently of the layout. */
  text-size-adjust: 100%;
  -moz-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}
@media (pointer: coarse) { html { font-size: 18px; } }
/* Phones only: tie the master scale to the viewport width. Browsers disagree
   on how many CSS pixels a phone screen is wide - Chrome on a Pixel 8 Pro
   reports 448, Firefox picks a lower device pixel ratio and reports around
   540 - so the identical CSS rendered ~17% smaller in Firefox. 4.0179vw is
   18px at 448 wide: Chrome keeps exactly the size it had, wider viewports
   scale up until both cover the same physical millimetres. Clamped at the
   bottom so a narrow phone never shrinks, capped at 640px so tablets keep
   the flat 18px above. */
@media (pointer: coarse) and (max-width: 640px) {
  html { font-size: clamp(18px, 4.0179vw, 22px); }
}

body {
  margin: 0;
  font-family: var(--f-sans);
  -webkit-font-smoothing: antialiased;
  background-color: var(--bg);
  color: var(--fg);
  line-height: 1.5;
  /* a token longer than its line (a path, a URL, an error's JSON) breaks
     instead of running out of its box and over whatever sits beside it */
  overflow-wrap: break-word;
}

/* Tailwind v4's preflight leaves buttons on the default arrow. Everything that
   acts on a click gets the hand, in the base layer so any component rule (a
   drag handle's grab, a busy state) still wins. */
@layer base {
  :is(button, [role='button'], [role='menuitem'], [role='menuitemradio'], [role='option'], [role='tab'], summary, label[for], select):not(:disabled, [aria-disabled='true']) {
    cursor: pointer;
  }
  :is(button, input, select, textarea, [role='button'], [role='menuitem'], [role='option'], [role='tab']):is(:disabled, [aria-disabled='true']) {
    cursor: not-allowed;
  }
  /* headings break into even lines, prose avoids a lone last word */
  :is(h1, h2, h3, h4) { text-wrap: balance; }
  p { text-wrap: pretty; }
}

/* Touch: the small controls keep their look but take a --hit (44px) hit area. The
   pseudo-element grows only on the axis that is short (100% is the control's
   own box), so a wide chip does not reach into its neighbours sideways. A
   parent with overflow: hidden clips it - give that parent padding instead.
   In the base layer, so an `absolute` utility on the control still wins. */
@layer base { @media (pointer: coarse) {
  :is(.t-iconbtn, .t-btn--sm, button.t-label, a.t-label, .t-pill, .t-seg-btn, button.t-link, a.t-link, input[type='checkbox'], input[type='radio']) { position: relative; }
  :is(.t-iconbtn, .t-btn--sm, button.t-label, a.t-label, .t-pill, .t-seg-btn, button.t-link, a.t-link, input[type='checkbox'], input[type='radio'])::before {
    content: '';
    position: absolute;
    inset: min(0px, calc((100% - var(--hit)) / 2));
  }
} }

/* ── Surfaces ── */

/* Panel: flat card, hairline border. position: relative stays - the update
   toast relies on the cascade it sits in. */
.t-panel {
  position: relative;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
}
/* A region that opened inside a panel (a search under a row, a disclosure's
   fields): a rule down its start edge instead of a second card in the card.
   Boxes inside boxes read as clutter; the indent alone says "belongs to the
   line above". */
.t-inset {
  border-inline-start: 2px solid var(--line);
  padding-block: 0.25rem;
  padding-inline-start: 0.75rem;
}

/* Rounded picker trigger (sort order, source, model): the current value plus a
   chevron, opening a Menu of kind listbox. One class for the three pickers that
   each spelled it out. In the components layer, so a caller's text utilities
   still win. */
@layer components {
  .t-pill {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    max-width: 100%;
    min-height: 2rem;
    padding-inline: 0.75rem;
    border: 1px solid var(--line-strong);
    border-radius: 9999px;
    font-size: 0.875rem;
    color: var(--fg);
    transition: border-color var(--dur-1) var(--ease-out);
  }
  @media (hover: hover) {
    .t-pill:hover { border-color: color-mix(in srgb, var(--accent) 60%, transparent); }
  }
}

/* Destructive area: the border itself carries the warning. The danger body
   of a dialog is a direct child without a border of its own, so the dialog
   takes the tint instead. */
.t-panel--danger { border-color: color-mix(in srgb, var(--accent) 65%, transparent); }
dialog:has(> .t-panel--danger) { border-color: color-mix(in srgb, var(--accent) 65%, transparent); }

/* Hatch texture: faint 135° diagonal hairlines. A placeholder texture for
   covers and skeletons, no longer the page background. */
.t-hatch {
  background-image: repeating-linear-gradient(
    135deg,
    transparent 0 7px,
    color-mix(in srgb, var(--fg) 3%, transparent) 7px 8px
  );
}

/* Chip (badge): sentence case as typed, no tracking, a 3:1 hairline. */
.t-label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--f-sans);
  font-size: var(--ctl-fs-xs);
  /* fixed line box: without it a chip is taller inside text-sm than inside
     text-xs, and chips in different sections stop matching */
  line-height: 1.4;
  font-weight: 500;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-chip);
  padding: 0 7px;
  color: var(--fg-2);
  /* one box for every chip: decorative ones then match the interactive ones,
     which need 24px to clear the target-size floor of WCAG 2.5.8 */
  min-height: 1.5rem;
  /* a chip never widens its panel; a chip that still cannot fit wraps inside
     its own box instead of pushing the row into a horizontal scroll. max-width
     alone only clips - a single long token (a slug, a path, an uppercase label
     whose letter-spacing pushes it past the box) needs somewhere to break. */
  max-width: 100%;
  overflow-wrap: anywhere;
  /* but it is not the item that gives way either: a chip is content-sized, and
     letting it shrink in a flex row squeezed the box below its own word, which
     then hung over the border with nothing to clip it. The flexible item in
     such a row is the path or the search field beside it, not the label. */
  flex-shrink: 0;
}
/* Dense chip: the size step below the default, for the chip rows inside a
   media tile, where several chips share one narrow column on a phone. The
   1.5rem box stays - that is the WCAG 2.5.8 floor, not a style choice. */
.t-label--sm {
  font-size: var(--ctl-fs-xs);
  padding-inline: 5px;
  gap: 3px;
}
/* Prose in a chip - a page subtitle, a status line with a server name. The
   fixed line box is what makes chips line up, and it is wrong for text that is
   meant to run over two lines. */
.t-label--multiline {
  align-items: flex-start;
  line-height: 1.4;
  padding-block: 3px;
}
.t-label--accent {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--accent);
}
.t-label--accent-2 {
  border-color: color-mix(in srgb, var(--accent-2) 55%, transparent);
  color: var(--accent-2);
}
.t-label--ok    { border-color: color-mix(in srgb, var(--ok) 55%, transparent); color: var(--ok); }
.t-label--warn  { border-color: color-mix(in srgb, var(--warn) 55%, transparent); color: var(--warn); }
.t-label--err   { border-color: color-mix(in srgb, var(--err) 55%, transparent); color: var(--err); }

/* An interactive chip has to look interactive. The gap badge is the only way
   into the episode list, and as a chip among decorative chips nothing said it
   could be clicked. currentColor picks up whichever tone the chip carries, so
   this works for every variant without repeating them. */
button.t-label,
a.t-label {
  cursor: pointer;
  transition: background-color var(--dur-1), border-color var(--dur-1);
}
/* Hover only where a pointer hovers: on a touch screen :hover sticks to the
   last tapped element until the next tap elsewhere. Tailwind's hover: variant
   already carries this gate, the hand-written rules here do it themselves. */
@media (hover: hover) {
  button.t-label:hover,
  a.t-label:hover {
    border-color: currentColor;
    background: color-mix(in srgb, currentColor 14%, transparent);
  }
}
@media (prefers-reduced-motion: reduce) {
  button.t-label,
  a.t-label { transition: none; }
}

/* Section divider with a trailing count: chip, hairline rule, number. One
   fixed row height so a divider reads the same in every list. */
.t-divider {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: var(--ctl-h-sm);
}
.t-divider-rule {
  flex: 1;
  height: 1px;
  background: var(--line);
}
.t-count {
  font-family: var(--f-mono);
  font-size: var(--ctl-fs-sm);
  line-height: var(--ctl-lh-sm);
  color: var(--fg-3);
  font-variant-numeric: tabular-nums;
}

/* Buttons */
.t-btn {
  /* inline-flex centers the label vertically whenever min-height (touch
     targets) exceeds the text height - a plain block leaves it top-aligned. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--f-sans);
  font-weight: 500;
  font-size: var(--ctl-fs);
  line-height: var(--ctl-lh);
  min-height: var(--ctl-h);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-ctl);
  background: transparent;
  color: var(--fg);
  padding: 0 1.125rem;
  cursor: pointer;
  transition: border-color var(--dur-1), background-color var(--dur-1), scale var(--dur-2) var(--spring);
}
/* the press sinks every button a little, on the spring */
@media (prefers-reduced-motion: no-preference) {
  .t-btn:active:not(:disabled, [aria-disabled='true']) { scale: 0.98; transition-duration: 90ms; }
}
.t-btn:is(:disabled, [aria-disabled='true']) { opacity: 0.45; cursor: not-allowed; }
.t-btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
/* destructive actions are Lycoris red, the design's alarm colour; the orange
   of --err is for error text only (as a button it read as the old theme) */
.t-btn--danger { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 60%, transparent); }
/* the destructive confirm: the red as a fill */
.t-btn--danger-fill { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
@media (hover: hover) {
  .t-btn:hover { border-color: color-mix(in srgb, var(--accent) 50%, transparent); background: var(--hover); }
  .t-btn--primary:hover { background: color-mix(in srgb, var(--accent) 85%, var(--bg)); }
  .t-btn--danger:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
  .t-btn--danger-fill:hover { background: color-mix(in srgb, var(--accent) 85%, var(--bg)); }
}
/* WCAG 2.5.8 target size; the shared variable keeps it level with every other
   small control in the same row */
.t-btn--sm {
  padding: 0 0.75rem;
  font-size: var(--ctl-fs-sm);
  line-height: var(--ctl-lh-sm);
  min-height: var(--ctl-h-sm);
  min-width: var(--ctl-h-sm);
  border-radius: var(--r-ctl-sm);
}
/* Dense step below --sm, for a labelled button inside a narrow card - the
   catalog tiles are ~110px wide and "Match ändern" needs 114px at the small
   size. Only type and side padding shrink: the box keeps the small button's
   height, so the touch target stays where WCAG 2.5.8 wants it. */
.t-btn--xs {
  font-size: var(--ctl-fs-xs);
  padding-inline: 0.4rem;
}

/* Round icon-only button: 44px, the icon in the secondary text colour; the
small step is 36px. Hover and press show a round ground. */
.t-iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* the control height, so a toolbar's narrower --ctl-h reaches it too */
  width: var(--ctl-h);
  height: var(--ctl-h);
  flex-shrink: 0;
  border-radius: 50%;
  font-size: 1.375rem;
  transition: background-color var(--dur-1), color var(--dur-1), scale var(--dur-2) var(--spring);
}
.t-iconbtn--sm { width: 2.25rem; height: 2.25rem; font-size: 1.125rem; }
/* colour and ground in the components layer: a caller's text-* or bg-*
   utility still wins */
@layer components {
  .t-iconbtn { color: var(--fg-2); }
  .t-iconbtn:active:not(:disabled) { background: var(--hover); }
  @media (hover: hover) {
    .t-iconbtn:hover:not(:disabled) { background: var(--hover); color: var(--fg); }
  }
}
@media (prefers-reduced-motion: no-preference) {
  .t-iconbtn:active:not(:disabled) { scale: 0.92; transition-duration: 90ms; }
}
.t-iconbtn:disabled { opacity: 0.45; }

/* Keyboard keys: a quiet outlined cap in the mono face */
kbd, .t-kbd {
  display: inline-block;
  font: 400 0.6875rem/1 var(--f-mono);
  color: var(--fg-3);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 2px 5px;
}

/* Tabs: segmented bar - display font, accent
   underline marks the active tab.
   The bar wraps rather than scrolling sideways. Scrolling hid the scrollbar
   too (by design), so a phone showed a tab cut off mid-word with nothing to
   say more existed - at 393px "Unvollständig" simply vanished past the edge.
   Wrapped, every tab is on screen and reachable with one tap. */
.t-tabs {
  display: flex;
  flex-wrap: wrap;
  border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  /* the bar rounds, the tabs stay square (their underline would curve): the
     corner tabs are clipped by the bar instead */
  overflow: clip;
  background: var(--bg-2);
}
/* the clip would cut the outer focus ring off - draw it inside the tab */
.t-tab:focus-visible { outline-offset: -2px; }
.t-tab {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--f-sans);
  font-weight: 500;
  font-size: 0.875rem;
  color: var(--fg-3);
  padding: 8px 14px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  cursor: pointer;
  min-height: 40px; /* WCAG 2.5.8 + comfortable touch target */
  transition: color var(--dur-1), background-color var(--dur-1), border-color var(--dur-1);
}
@media (hover: hover) {
  .t-tab:hover { color: var(--fg); background: var(--hover); }
}
.t-tab[aria-selected="true"],
/* a row of jump links marks the section in view the same way */
.t-tab[aria-current="true"] {
  color: var(--accent);
  background: var(--card);
  border-bottom-color: var(--accent);
}
/* The gliding selection (Tabs places it): the card fill and the underline
   move as one piece. The tabs sit above it; once it stands, the selected tab
   hands its own fill and underline over, so the first paint never lacks them.
   Only the indicator moves, and only on a change of tab - no hover motion. */
.t-tabs { position: relative; }
.t-tab { position: relative; z-index: 1; }
.t-tabs-ink {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  pointer-events: none;
  background: var(--card);
  border-bottom: 2px solid var(--accent);
  transition: transform var(--dur-2) var(--ease-out), width var(--dur-2) var(--ease-out), height var(--dur-2) var(--ease-out);
}
.t-tabs:not([data-ink]) .t-tabs-ink { display: none; }
.t-tabs[data-ink] .t-tab[aria-selected="true"] { background: transparent; border-bottom-color: transparent; }
/* A bar with more tabs than a phone is wide: one row that scrolls sideways
   instead of two or three wrapped rows above the content. Wrapping is still
   the default (see above); this is opt-in for the one bar that has seven.
   The caller scrolls the selected tab into view. No scroll snapping: Firefox
   snapped straight back to the start after scrollIntoView, which left the
   selected tab cut off at the edge again. */
.t-tabs--scroll {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}
/* only a fade and the arrow keys say there is more: a mouse keeps its bar */
@media (pointer: fine) {
  .t-tabs--scroll { scrollbar-width: auto; }
}
/* the cut edge fades out, so a tab sliced mid-word reads as "more this way"
   rather than a rendering fault; Tabs sets the attribute from the scroll
   position, and a bar that fits has none and stays crisp */
.t-tabs--scroll[data-overflow="end"] { mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent); }
.t-tabs--scroll[data-overflow="start"] { mask-image: linear-gradient(to right, transparent, #000 2rem); }
.t-tabs--scroll[data-overflow="both"] { mask-image: linear-gradient(to right, transparent, #000 2rem, #000 calc(100% - 2rem), transparent); }
/* Tabs that divide one thing into its parts (a series' overview, cast,
   similar titles) rather than pick between a few options: no box, the full
   width, a hairline under the row (drawn by the caller) and only the underline
   moving - the box of the default bar sat as a second frame inside a dialog. */
.t-tabs--line { border: 0; border-radius: 0; background: none; }
.t-tabs--line .t-tab { min-height: 44px; padding: 10px 12px; }
@media (hover: hover) {
  .t-tabs--line .t-tab:hover { background: none; }
}
.t-tabs--line .t-tab[aria-selected="true"],
.t-tabs--line .t-tab[aria-current="true"] { background: none; }
.t-tabs--line .t-tabs-ink { background: none; }

/* A dialog's docked title: a slim bar over the top of its scroller that is
   clear while the head is in view and fills in with the title as the head
   scrolls away (--q, written from the scroll). It never takes a click - the
   head under it keeps its own buttons. */
.t-dock {
  position: absolute;
  inset: 0 0 auto;
  z-index: 20;
  display: flex;
  align-items: center;
  height: 2.75rem;
  /* the close button sits over the end: the title stops short of it */
  padding: 0 3.5rem 0 1.25rem;
  pointer-events: none;
  background: color-mix(in srgb, var(--card) calc(var(--q, 0) * 100%), transparent);
  font: 600 0.9375rem/1.2 var(--f-display);
}
.t-dock > span { min-width: 0; opacity: var(--q, 0); }
/* the bar's hairline arrives with its fill */
.t-dock { border-bottom: 1px solid color-mix(in srgb, var(--line) calc(var(--q, 0) * 100%), transparent); }

/* The card's hero: the banner runs full bleed under the docked bar and fades
   into the card at its foot, the big title and its line sit on the picture.
   Scrolling, the picture lags (0.45) and the title sinks (0.25) and fades;
   the bar fills in and takes the title over. SeriesCard writes the
   transforms per frame, so only transform and opacity move. */
.t-hero {
  position: relative;
  height: 11rem;
  overflow: hidden;
  background: var(--hover);
}
.t-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(transparent 35%, var(--card));
}
.t-hero__art {
  position: absolute;
  inset: -20px 0 0;
  width: 100%;
  height: calc(100% + 20px);
  object-fit: cover;
  /* a 16:9 backdrop has its faces in the upper part; a banner strip barely
     moves */
  object-position: 50% 30%;
  will-change: transform;
}
.t-hero__title {
  position: absolute;
  inset: auto 1.25rem 0.75rem;
  z-index: 1;
  will-change: transform, opacity;
  /* a soft halo in the card's colour: a busy picture never swallows the line */
  text-shadow: 0 1px 12px color-mix(in srgb, var(--card) 85%, transparent);
}
/* over the picture the round buttons are dark discs with a white mark; as the
   bar fills in they turn into its plain icon buttons */
.t-hero-btn {
  background: color-mix(in srgb, #000 calc((1 - var(--q, 0)) * 45%), transparent) !important;
  color: color-mix(in srgb, #fff calc((1 - var(--q, 0)) * 100%), var(--fg));
}

/* Disclosure on the native details element: the marker is drawn as a chevron
   by the component, so the platform's own triangle goes. */
summary { list-style: none; }
summary::-webkit-details-marker { display: none; }

/* One field of a multi-column row: label on top, control pinned to the bottom,
   so the controls of a row still line up when one label wraps or carries a
   help button (which is taller than a bare text line). The caption must be a
   single element - every child becomes a flex item, so a bare text node next
   to a <Hint> would stack instead of sitting on one line. */
/* The caption/control spacing is a gap, not padding on the control: padding
   here would override the control's own and make it shorter than a <select>
   next to it. */
/* No height of its own: a field in a two-column row is a grid item and
   stretches to the row, which pushes its control down to line up. A
   percentage height filled any stretched parent instead, so the text after
   the field ran over the next one. */
.t-field { display: flex; flex-direction: column; gap: 4px; }
.t-field > :last-child { margin-top: auto; }

/* Inputs */
.t-input {
  font-family: inherit;
  font-size: var(--ctl-fs);
  line-height: var(--ctl-lh);
  min-height: var(--ctl-h);
  background: var(--bg-2);
  color: var(--fg);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-ctl);
  padding: 7px 10px;
  width: 100%;
  transition: border-color var(--dur-1);
}
.t-input { text-overflow: ellipsis; } /* also applies to a clipped placeholder */
/* Compact field for dense rows - same family as .t-btn--sm, so a small field
   and a small button in one row still line up. Replaces the ad-hoc
   "py-1 text-xs" pairs that were drifting apart. */
.t-input--sm, .t-select--sm {
  font-size: var(--ctl-fs-sm);
  line-height: var(--ctl-lh-sm);
  min-height: var(--ctl-h-sm);
  border-radius: var(--r-ctl-sm);
  padding-top: 2px;
  padding-bottom: 2px;
}
.t-input:focus {
  outline: none;
  /* the full accent: a half-strength edge fell to 2.4:1, below the 3:1 a
     focused field needs and below its own resting border (WCAG 1.4.11) */
  border-color: var(--accent);
}
.t-input::placeholder { color: var(--fg-3); }
/* every other field too (a search box inside its own label): the browser's
   grey failed 4.5:1 on the light page, and Firefox fades it on top */
::placeholder { color: var(--fg-3); opacity: 1; }
.t-input[aria-invalid='true'] { border-color: var(--err); }
/* env-locked and other read-only fields: visibly muted, value stays legible */
.t-input:disabled, .t-select:disabled {
  color: var(--fg-3);
  background: color-mix(in srgb, var(--bg-2) 60%, transparent);
  border-style: dashed;
  cursor: not-allowed;
}

/* Native select with hand-drawn chevron */
.t-select-wrap { position: relative; display: block; }
.t-select-wrap::after {
  content: "";
  position: absolute;
  right: 12px;
  top: 50%;
  width: 6px;
  height: 6px;
  border-right: 1px solid var(--fg-3);
  border-bottom: 1px solid var(--fg-3);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}
.t-select {
  font-family: inherit;
  font-size: var(--ctl-fs);
  line-height: var(--ctl-lh);
  min-height: var(--ctl-h);
  appearance: none;
  background: var(--bg-2);
  color: var(--fg);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-ctl);
  padding: 7px 30px 7px 10px;
  width: 100%;
  transition: border-color var(--dur-1);
}
.t-select:focus {
  outline: none;
  border-color: var(--accent); /* full strength, see .t-input:focus */
}
option { background-color: var(--bg-2); color: var(--fg); }

/* Accent surfaces carry dark text (WCAG 1.4.3) */
.bg-accent { color: var(--on-accent) !important; }

/* Focus rings follow the browser's :focus-visible (WCAG 2.4.7), so keyboard,
   voice and switch input all get one. The only thing that switches them off
   is html[data-input='pointer'], which main.tsx sets on a pointer press so a
   click does not leave a ring (a clicked text field matches :focus-visible).
   Custom rings (box-shadow, ::after) therefore gate on
   :root:not([data-input='pointer']), never on a key having been pressed;
   test/focusRing.test.ts scans every stylesheet for a rule that breaks this. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
:root[data-input='pointer'] :is(:focus-visible, :focus-within) {
  outline: none !important;
}
/* menu rows sit in a clipped surface: ring inside */
[role='menuitem']:focus-visible { outline-offset: -2px; }

/* ── Scrollbars ──
   One policy: the browser's full-width bar (scrollbar-width stays auto, a thin
   bar is hard to grab, WCAG 2.5.8), with a thumb that holds 3:1 on every
   surface (tokens above). thin/none only on horizontal rails that have a
   visible alternative (arrows, peek, chips). */
* { scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); }
/* Safari before 18.2 knows neither property: paint a classic bar there only */
@supports selector(::-webkit-scrollbar) and (not (scrollbar-color: red blue)) {
  ::-webkit-scrollbar { width: 12px; height: 12px; }
  ::-webkit-scrollbar-track { background: var(--scrollbar-track); }
  ::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border: 3px solid transparent;
    background-clip: content-box;
    border-radius: 6px;
  }
  ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); background-clip: content-box; }
  ::-webkit-scrollbar-corner { background: transparent; }
}

/* ── Scroller ──
   An inner vertical scroller whose content keeps its distance from the bar.
   A classic bar takes its gutter outside the padding, an overlay bar (phone,
   macOS) paints over the padding box: either way the end padding is the gap.
   The padding sits in the components layer, below Tailwind's utilities, so
   pr-* / px-* and any unlayered class with its own padding still win.
   The component form (tabIndex 0, labelled region) is for content without a
   focusable element, so the keyboard can still scroll it. */
.t-scroll { overflow-y: auto; scrollbar-gutter: stable; }
@layer components { .t-scroll { padding-inline-end: 1rem; } }
.t-scroll[tabindex]:focus-visible { outline-offset: -2px; }

/* ── Touch ergonomics ──
   Taller controls give comfortable thumb targets without changing the desktop
   density. Expressed in rem so they ride the viewport-relative master scale
   above and stay proportional to the text in every browser; at the 18px floor
   these are the same 48/40px as before. The 0.889rem input font is 16px there,
   which is what stops iOS Safari from zooming into a focused field - it must
   never resolve below 16px, hence the floor in the clamp. */
@media (pointer: coarse) {
  :root {
    --ctl-h: 2.667rem;
    --ctl-h-sm: 2.222rem;
    --ctl-fs: 0.889rem;
    --ctl-fs-sm: 0.75rem;
    --ctl-fs-xs: 0.639rem;
    --ctl-lh: 1.333rem;
    --ctl-lh-sm: 1.111rem;
  }
  .t-btn { padding-inline: 1rem; }
  .t-btn--sm { padding-inline: 0.667rem; }
  .t-tab { font-size: var(--ctl-fs); min-height: var(--ctl-h); padding: 0.556rem 0.889rem; }
}

/* ── Toolbar: one control row with a single normalized height ── */
.t-toolbar {
  /* one compact height for the whole row - the base rules read the variable,
     so nothing has to be overridden control by control */
  --ctl-h: 2rem;
  --ctl-h-sm: 2rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.t-toolbar .t-label { min-height: var(--ctl-h-sm); }
/* chips grown to toolbar height need matching horizontal room */
.t-toolbar .t-label { padding-inline: 0.625rem; border-radius: var(--r-ctl-sm); }
.t-toolbar .t-input,
.t-toolbar .t-select {
  padding-top: 0;
  padding-bottom: 0;
}
/* pointer:fine only - on touch the shrink would drop the box to 22.5px and
   break the 24px target size (WCAG 2.5.8) */
@media (pointer: fine) {
  /* stays at the 24px floor even in the compact row */
  .t-toolbar input[type='checkbox'] { width: 1.5rem; height: 1.5rem; }
}
/* The compact height is set on the element, so it outranks the :root touch
   values above and a toolbar on a phone came out at 36px. Same small step as
   every other touch control instead. */
@media (pointer: coarse) {
  .t-toolbar { --ctl-h: 2.222rem; --ctl-h-sm: 2.222rem; --r-ctl: var(--r-ctl-sm); }
}

/* ── Form controls ── */
/* Checkbox: the chip radius, hairline border, CSS-drawn check - the
   native control never matched the rest of the form. */
input[type='checkbox'] {
  appearance: none;
  /* 24px is the WCAG 2.5.8 floor; the box is author-styled, so the
     user-agent exception doesn't apply to it */
  width: 1.5rem;
  height: 1.5rem;
  flex-shrink: 0;
  position: relative;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-chip);
  background: var(--card);
  cursor: pointer;
}
@media (hover: hover) {
  input[type='checkbox']:hover { border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
}
input[type='checkbox']:checked,
input[type='checkbox']:indeterminate {
  background: var(--accent);
  border-color: var(--accent);
}
input[type='checkbox']:checked::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  /* fractions of the box, so the mark grows with the touch-sized box */
  width: 40%;
  height: 20%;
  border-left: 2px solid var(--on-accent);
  border-bottom: 2px solid var(--on-accent);
  transform: translate(-50%, -70%) rotate(-45deg);
  animation: t-tick var(--dur-1) var(--ease-out);
}
input[type='checkbox']:indeterminate::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 40%;
  height: 2px;
  background: var(--on-accent);
  transform: translate(-50%, -50%);
}
:root:not([data-input='pointer']) input[type='checkbox']:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 2px var(--bg),
    0 0 0 4px var(--accent);
}
input[type='checkbox']:disabled { opacity: 0.45; cursor: not-allowed; }
@media (pointer: coarse) {
  /* the small control step, so a box in a toolbar lines up with the search
     field and the icon buttons beside it instead of sitting 16px shorter.
     Toolbars only: a box in a list row stands next to text and a cover,
     and at 40px it was the heaviest thing on the line */
  .t-toolbar input[type='checkbox'] { width: var(--ctl-h-sm); height: var(--ctl-h-sm); border-radius: var(--r-ctl-sm); }
}

/* Radio: fully round, as a true indicator dot should be. Everything else (size, border, focus
   ring) matches the checkbox, so a mixed form reads as one set of controls. */
input[type='radio'] {
  appearance: none;
  /* same 24px WCAG 2.5.8 floor as the checkbox: author-styled, so the
     user-agent exception does not apply */
  width: 1.5rem;
  height: 1.5rem;
  flex-shrink: 0;
  position: relative;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--card);
  cursor: pointer;
}
@media (hover: hover) {
  input[type='radio']:hover { border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
}
input[type='radio']:checked { border-color: var(--accent); }
input[type='radio']:checked::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 50%;
  background: var(--accent);
  transform: translate(-50%, -50%);
  animation: t-tick var(--dur-1) var(--ease-out);
}
:root:not([data-input='pointer']) input[type='radio']:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 2px var(--bg),
    0 0 0 4px var(--accent);
}
input[type='radio']:disabled { opacity: 0.45; cursor: not-allowed; }

/* ── A season's episodes on a rail (EpisodeList): the marks are joined by
   a line coloured by what each episode is, so the holes in a season show as
   red stretches before a single title is read. Rows slide in as they enter
   the scroller: tied to the scroll for a mouse where the browser has view
   timelines, once as they come into view elsewhere (useReveal). ── */
.t-ep { position: relative; }
/* the rail runs through the middle of each mark: the mark is the 1em icon
   plus its 2px padding and 1.5px ring either side */
.t-ep::before {
  content: "";
  position: absolute;
  left: calc(0.5em + 3.5px - 1px);
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--rail, var(--line));
}
.t-ep:first-child::before { top: 50%; }
.t-ep:last-child::before { bottom: 50%; }
.t-ep:only-child::before { display: none; }
/* each episode is a node on the rail: its state icon in a ring of its colour,
   so the line reads as joined marks rather than broken dashes */
.t-ep > svg:first-child {
  position: relative;
  box-sizing: content-box;
  padding: 2px;
  border: 1.5px solid currentColor;
  border-radius: 9999px;
  background: var(--card);
}
.t-ep[data-state="have"] { --rail: color-mix(in srgb, var(--ok) 55%, transparent); }
.t-ep[data-state="gone"] { --rail: color-mix(in srgb, var(--err) 55%, transparent); }
@keyframes t-ep-in { from { opacity: 0.3; transform: translateX(8px); } }
@media (prefers-reduced-motion: no-preference) {
  /* the query is useReveal's SCROLL_REVEAL_MQ: a finger drags the content,
     and a row scrubbed by the scroll would lag behind it */
  @media (hover: hover) and (pointer: fine) {
    @supports (animation-timeline: view()) {
      :root:not([data-motion="off"]) .t-ep {
        animation: t-ep-in linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 80%;
      }
    }
  }
  /* elsewhere (touch, Firefox) rows slide in once as they come into view:
     useReveal marks them with data-in */
  :root:not([data-motion="off"]) .t-ep[data-in="0"] { opacity: 0.3; transform: translateX(8px); }
  :root:not([data-motion="off"]) .t-ep[data-in="1"] { transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out); }
}

/* The same entrance for any list or grid below the first screen (poster rows,
   calendar days, file lists): rise 8px and fade in as it scrolls into view.
   Tied to the scroll for a mouse where view timelines exist; on touch and
   elsewhere useReveal marks the items and they rise once. --i (set by
   useReveal) staggers items that arrive together, capped at 8 so a long grid
   never trails. */
@keyframes t-rise-in { from { opacity: 0; transform: translateY(8px); } }
@media (prefers-reduced-motion: no-preference) {
  @media (hover: hover) and (pointer: fine) {
    @supports (animation-timeline: view()) {
      :root:not([data-motion="off"]) .t-reveal {
        animation: t-rise-in linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 60%;
      }
    }
  }
  :root:not([data-motion="off"]) .t-reveal[data-in="0"] { opacity: 0; transform: translateY(8px); }
  :root:not([data-motion="off"]) .t-reveal[data-in="1"] {
    transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
    transition-delay: calc(min(var(--i, 0), 8) * 40ms);
  }
}

/* ── The page's large title and the app bar (PageHeader, PageScroll) ──
   On a desktop the page header is the heading over the content. On a phone a
   top-level page opens with it as a large title; scrolling shrinks and fades
   it while the app bar's own title fades in once it has gone (`--dock` 0..1
   and `data-docked`, both set by PageScroll). A stacked screen has a back link
   and its title in the bar, so the large title stays out of the way. */
.t-page-title { font-size: var(--fs-title-lg); line-height: 1.15; letter-spacing: 0.01em; }
.t-scroll-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0; /* inside the bar: a sticky heading in <main> stops right under it */
  height: 2px;
  background: var(--accent);
  transform-origin: left;
  transform: scaleX(0);
  pointer-events: none;
}
@media (width < 64rem) {
  .t-page-title { font-size: var(--fs-title); line-height: 1.1; }
  .t-page-heading {
    transform-origin: left bottom;
    transform: scale(calc(1 - var(--dock, 0) * 0.12));
    opacity: calc(1 - var(--dock, 0));
  }
  .t-appbar-title { transition: opacity var(--dur-2) var(--ease-out); }
}

/* ── Docked side panel (SidePanel): beside <main> on a desktop, the full
   height of the screen and sticky like the sidebar, so it stays put while
   the page scrolls. A dialog's content box (`dialog-body`) fills it instead
   of keeping the modal's 85dvh cap. ── */
.t-sidepanel {
  position: sticky;
  top: 0;
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  height: 100dvh;
  --surface: var(--bg-2);
  background: var(--bg-2);
  border-left: 1px solid var(--line);
}
.t-sidepanel .dialog-body {
  --dialog-max-h: none;
  flex: 1 1 auto;
  min-height: 0;
}
.t-sidepanel .overflow-y-auto {
  overflow-x: hidden;
}

/* ── Native dialog ── */

/* An open <dialog> makes the rest of the document inert, but inert does not
   stop scrolling - the page behind kept moving under the modal, and on a phone
   the touch gesture scrolled it instead of the dialog's own content. One rule
   instead of the usual JS body-lock, and it cannot leak: the selector is only
   true while a dialog is actually open. */
html:has(dialog[open]) {
  overflow: hidden;
}

dialog {
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--r-dialog);
  padding: 0;
  /* Preflight resets margin:0 - restore UA centering for :modal.
     width (not max-width) so per-dialog max-w-* utilities still cap it. */
  margin: auto;
  width: calc(100vw - 2rem);
  --dialog-max-h: calc(100dvh - 2rem);
  max-height: var(--dialog-max-h);
  /* The dialog itself never scrolls: at a device pixel ratio of 3 its 1px
     border resolves to 0.667px, and the rounding alone makes scrollHeight
     exceed clientHeight - a scrollbar down the side of a four-line confirm
     box. The content box scrolls instead, where the maths is whole. */
  overflow: hidden;
  opacity: 1;
  transform: none;
  /* enter: rise 8px and settle from 98%, ease-out; the exit below is shorter
     and eases in. No clip-path any more: it cut the shadow off, which now
     follows the rounded corners instead. */
  transition:
    opacity var(--dur-3) var(--ease-out),
    transform var(--dur-3) var(--ease-out),
    /* the phone sheet growing to its second height; a centred dialog's
       height is auto and never animates */
    height var(--dur-3) var(--ease-out),
    --lift var(--dur-3) var(--ease-out),
    overlay var(--dur-3) allow-discrete,
    display var(--dur-3) allow-discrete;
}
/* The sheet's lift (useSheetDrag, Dialog): the full height stands and the
   sheet sits this far down, sliding up as it drains. Registered so it
   animates, and inherited so the footer can ride against it - the footer
   stays at the bottom edge while the sheet rises past it, instead of
   dropping out of the screen for the length of the lift. */
@property --lift {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}
/* The box inside inherits the cap, so a dialog whose content outgrows the
   screen scrolls there instead of at the dialog (whose fractional border
   height would show a phantom bar). The length has to be repeated rather than
   written as `100%`: the dialog's own height is auto, and a percentage
   max-height against an auto height computes to none - the box then grew past
   the dialog and the overflow:hidden above simply cut the rest off. */
dialog > :only-child {
  max-height: var(--dialog-max-h);
}
/* Nothing in a dialog scrolls sideways on purpose, but `overflow-y: auto`
   turns the other axis into auto as well, and the content box measures one
   pixel wider than its client width (a cut button's outline ring reaches
   1px past its box, fractional layout at 125% or a phone's 2.625 does the
   rest): a horizontal scrollbar along the bottom of a four-line confirm. */
dialog .overflow-y-auto {
  overflow-x: hidden;
}
/* The box a dialog's content lives in: fixed header, scrolling middle, sticky
   footer, capped so the page behind never gains a scrollbar. One class instead
   of the `flex max-h-[85dvh] flex-col` every modal used to repeat - the cap has
   to be liftable from a single place when the dialog turns into a full-screen
   sheet, and it sits a level too deep for a child selector to reach. */
.dialog-body {
  display: flex;
  flex-direction: column;
  /* through the variable, so the `dialog > :only-child` rule above - which
     outranks this class - lands on the tighter cap too */
  --dialog-max-h: 85dvh;
  max-height: var(--dialog-max-h);
}

/* The phone layout: a bottom sheet, not a takeover (same cascade context as
   the base dialog rules above, so it wins without !important and the enter
   transition stays intact). It rises from the bottom edge and stops short of
   the top - the strip of page still showing is what makes it read as a sheet,
   and it is what a full-screen dialog took away along with the backdrop to
   tap. */
@media (max-width: 40rem) {
  dialog.dialog-sheet {
    width: 100vw;
    max-width: none;
    /* Open far enough for a form, short of the top. M3 caps a sheet's initial
       height at 50% of the screen and Apple's default is near-full; these
       dialogs carry rows of fields, so they sit between the two. */
    height: 70dvh;
    /* pinned to the bottom edge - `margin: auto` from the base rule would
       float it in the middle of the screen */
    margin: 0;
    inset: auto 0 0 0;
    border-radius: var(--r-sheet) var(--r-sheet) 0 0;
    border: 0;
    --surface: var(--bg-2);
    background: var(--bg-2);
    box-shadow: 0 -8px 32px rgb(0 0 0 / 0.16);
  }
  /* The second height. The first scroll inside the sheet is the content
     saying that 70dvh was not enough (the Dialog component sets the
     attribute), so it takes the rest of the screen and the reader carries on
     in the same movement. It still stops short of the top: that strip of page
     is the backdrop, and without it the sheet would be a takeover again. */
  dialog.dialog-sheet[data-expanded] {
    height: 92dvh;
  }
  /* A column, so the grabber sits above the content rather than over it. Only
     while open: the closed dialog has to stay `display: none` for the exit
     transition to run. */
  dialog.dialog-sheet[open] {
    display: flex;
    flex-direction: column;
  }
  /* the wrapper the Dialog component draws and the content box inside it take
     what is left under the grabber. `:not(button)` spares the grabber and the
     close button, whose own size utilities a blanket height would beat. */
  dialog.dialog-sheet > :not(button),
  dialog.dialog-sheet .dialog-body {
    flex: 1;
    min-height: 0;
    max-height: none;
    height: auto;
  }
  /* Who owns a vertical drag - the sheet or the scrolling middle - is decided
     in useSheetDrag, on the first move of the gesture. It cannot be expressed
     as `touch-action`: `pan-y` here let the browser claim every vertical
     gesture the moment it began, and on a sheet whose content fits there was
     nothing to scroll, so the drag died in the browser and the sheet never
     moved. `contain` stays: it keeps an over-scroll from reaching the page
     behind, which would scroll the app under the open sheet. */
  /* `none`, not just `contain`: iOS also bounces the scroller itself at its
     top, and that bounce would run under the hand-over from scroll to pull */
  dialog.dialog-sheet .overflow-y-auto {
    overscroll-behavior-y: none;
  }
  /* The backdrop dims in step with the pull: `--sheet-pull` (0..1, set by
     useSheetDrag) is how far the sheet has been dragged off the screen, and
     `::backdrop` inherits the property from the dialog. Off the finger the
     stylesheet's own transition fades it back; under the finger that
     transition is off, or the backdrop would lag a quarter second behind. */
  dialog.dialog-sheet::backdrop {
    opacity: calc(1 - var(--sheet-pull, 0));
  }
  dialog.dialog-sheet[data-dragging]::backdrop {
    transition: none;
  }
  /* the last row clears the home indicator */
  dialog.dialog-sheet .dialog-body > footer {
    padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
    translate: 0 calc(var(--lift) * -1);
    /* riding against a lift it stands over content that scrolls on under it:
       opaque, above it, and covering the stretch below it down to the edge */
    position: relative;
    z-index: 1;
    background: var(--bg-2);
  }
  dialog.dialog-sheet .dialog-body > footer::after {
    content: "";
    position: absolute;
    inset: 100% 0 auto;
    height: var(--lift);
    background: inherit;
  }
  dialog.dialog-sheet { translate: 0 var(--lift); }
  /* the grabber is the sheet's close button: a header one beside it would be
     a second control with the same name */
  dialog.dialog-sheet .t-modal-x { display: none; }
}
/* The page behind a dialog is blurred as well as dimmed: on a phone the box
   sits over content of the same colours and the hairline border alone did
   not separate the two (WCAG 1.4.11 asks 3:1 for a component's edge; the
   blur makes the boundary the shape, not the line). The box lifts with a
   shadow for the same reason. */
dialog::backdrop {
  background: var(--scrim);
  backdrop-filter: blur(6px);
  transition: opacity var(--dur-3) var(--ease-out);
  opacity: 1;
}
dialog[open] {
  box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6);
}
dialog:not([open]) {
  opacity: 0;
  transform: translateY(8px) scale(0.98);
  transition-duration: var(--dur-2);
  transition-timing-function: var(--ease-in);
}
dialog:not([open])::backdrop { opacity: 0; }
@starting-style {
  dialog[open] { opacity: 0; transform: translateY(8px) scale(0.98); }
  dialog[open]::backdrop { opacity: 0; }
}
/* the phone sheet slides up from the bottom edge instead of settling */
@media (max-width: 40rem) {
  dialog.dialog-sheet:not([open]) { opacity: 1; transform: translateY(100%); }
  @starting-style {
    dialog.dialog-sheet[open] { opacity: 1; transform: translateY(100%); }
  }
}

/* ── Popover entry: menus, dropdown lists, the toast ──
   These mount straight into the DOM, so @starting-style is the whole
   animation; an unmount is instant, which reads fine at this size. */
.t-pop {
  transform-origin: top;
  transition: opacity var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out);
}
.t-pop--up { transform-origin: bottom; }
@starting-style {
  .t-pop { opacity: 0; transform: translateY(-4px) scale(0.98); }
  .t-pop--up { transform: translateY(4px) scale(0.98); }
}

/* An anchored menu: a popover in the top layer, so no overflow clip between
   its wrapper and the viewport can cut it, placed by anchor positioning and
   flipped when it would leave the viewport. The UA sheet centres a popover
   (inset 0, margin auto) and gives it its own padding and colour - reset. */
.t-menu[popover] {
  position: fixed;
  inset: auto;
  margin: 0.25rem 0 0;
  padding-inline: 0;
  color: inherit;
  max-width: min(70vw, 20rem);
  position-area: bottom span-right;
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
.t-menu[popover][data-placement="bottom-end"] { position-area: bottom span-left; }
.t-menu[popover][data-placement^="top"] { margin: 0 0 0.25rem; }
.t-menu[popover][data-placement="top-start"] { position-area: top span-right; }
.t-menu[popover][data-placement="top-end"] { position-area: top span-left; }

/* A disclosure's body eases open where the engine can animate to auto
   (Chromium); elsewhere it still snaps, which is what it did before. */
details { interpolate-size: allow-keywords; }
details::details-content {
  height: 0;
  overflow: clip;
  transition: height var(--dur-2) var(--ease-out), content-visibility var(--dur-2) allow-discrete;
}
details[open]::details-content { height: auto; }

/* Squircle corners where the engine draws them (Chromium 139+): only on the
   large radii - at 4-6px a superellipse reads tighter, not softer. */
@supports (corner-shape: squircle) {
  .t-panel, dialog, .t-pop { corner-shape: squircle; }
}

/* ── Micro-transitions: a page rises 6px into place ── */
@keyframes t-reveal {
  from { transform: translateY(6px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

/* Slot roll: a value that changes rolls the old one out and the new one in,
   the way a split-flap or an odometer reads. The direction says whether the
   value moved forward or back. Both halves run inside a clipped box, so only
   one line is ever visible. Under the global reduced-motion gate the duration
   collapses and the swap is instant. */
@keyframes slot-in-up   { from { transform: translateY(100%); opacity: 0 } to { transform: none; opacity: 1 } }
@keyframes slot-out-up  { from { transform: none; opacity: 1 } to { transform: translateY(-100%); opacity: 0 } }
@keyframes slot-in-down  { from { transform: translateY(-100%); opacity: 0 } to { transform: none; opacity: 1 } }
@keyframes slot-out-down { from { transform: none; opacity: 1 } to { transform: translateY(100%); opacity: 0 } }
.t-slot { position: relative; display: block; overflow: clip; }
.t-slot > * { display: block; }
/* the leaving value is taken out of the flow, so the box keeps the height of
   the one that stays */
.t-slot-out { position: absolute; inset: 0; }
.t-slot--up   .t-slot-in  { animation: slot-in-up   var(--dur-2) var(--ease-out) both; }
.t-slot--up   .t-slot-out { animation: slot-out-up  var(--dur-2) var(--ease-out) both; }
.t-slot--down .t-slot-in  { animation: slot-in-down  var(--dur-2) var(--ease-out) both; }
.t-slot--down .t-slot-out { animation: slot-out-down var(--dur-2) var(--ease-out) both; }

/* The day band: one cell per day, the middle one picked. No CSS scroll snap -
   a snap container caps a fling at the next snap point, so however hard the
   band was thrown it stepped a single day. It centres the middle cell itself
   once the scrolling has gone quiet (see DayScroller). The cut edges fade like
   the tab bar's, and `overflow-y: clip` keeps the count chip from being sliced
   by the mask instead of the fade. */
.t-dayband {
  display: flex;
  overflow-x: auto;
  overflow-y: clip;
  scrollbar-width: none;
}
.t-dayband::-webkit-scrollbar { display: none; }
/* a mouse drags the band - a scroller gives it nothing but the wheel */
@media (pointer: fine) { .t-dayband { cursor: grab; } }
.t-dayband[data-drag] { user-select: none; cursor: grabbing; }
.t-dayband[data-overflow="end"] { mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent); }
.t-dayband[data-overflow="start"] { mask-image: linear-gradient(to right, transparent, #000 2rem); }
.t-dayband[data-overflow="both"] { mask-image: linear-gradient(to right, transparent, #000 2rem, #000 calc(100% - 2rem), transparent); }

/* A poster frame (Cover): the poster's colour, or the hover ground, until the
   file arrives, then the image fades in over it. */
.t-cover { display: block; overflow: hidden; background: var(--cover-tint, var(--hover)); }
.t-cover > img { display: block; opacity: 0; transition: opacity var(--dur-3) var(--ease-out); }
.t-cover > img[data-loaded] { opacity: 1; }

/* Odometer digits (Digits): each digit a column of 0-9 behind a one-line
   window, slid to its value. The column is ten lines tall, so a tenth of it
   is one digit. */
.t-digits { white-space: nowrap; }
.t-digit { display: inline-block; height: 1lh; overflow: clip; vertical-align: top; }
.t-digit > span { display: flex; flex-direction: column; transition: transform var(--dur-3) var(--ease-out); }

/* A long press (useLongPress): the element sinks while the finger rests, so
   the hold reads as building up to something, and comes back at once when
   the menu opens or the finger moves off. On a touch screen the text inside
   is not selectable, or the hold would select a word instead, and iOS keeps
   its image callout away. */
[data-longpress] { transition: scale var(--dur-1) var(--ease-out); -webkit-touch-callout: none; }
[data-longpress][data-pressing] { scale: 0.98; transition: scale 450ms linear; }
@media (pointer: coarse) { [data-longpress] { user-select: none; -webkit-user-select: none; } }

/* Releases as a band of posters (PosterBand). Centred by spacers at both
   ends, so the first and last poster can reach the middle; each poster turns
   and steps back by its distance from the middle (--d, --ad, written from
   the scroll position). Snapping per poster is wanted here: a poster is a
   choice, not a day to scrub through. */
.t-posterband {
  /* the posters' offsetLeft is measured from here, not from the page */
  --poster-w: 9rem;
  position: relative;
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  padding: 0.5rem calc(50% - var(--poster-w) / 2);
  outline-offset: 4px;
}
.t-posterband::-webkit-scrollbar { display: none; }
/* a phone shows the middle poster and a turned one either side */
@media (width < 64rem) { .t-posterband { --poster-w: 7.5rem; gap: 0.75rem; } }
.t-posterband[data-drag] { scroll-snap-type: none; cursor: grabbing; user-select: none; }
@media (pointer: fine) { .t-posterband { cursor: grab; } }
.t-posterband__item {
  position: relative;
  flex: 0 0 var(--poster-w);
  scroll-snap-align: center;
  border-radius: var(--r-menu);
  overflow: hidden;
  cursor: pointer;
  transform: perspective(800px) rotateY(calc(var(--d, 0) * -24deg)) scale(calc(1 - var(--ad, 0) * 0.14));
  opacity: calc(1 - var(--ad, 0) * 0.4);
}
.t-posterband__item[data-past] { filter: grayscale(0.6); }
.t-posterband__item[aria-current] { box-shadow: 0 16px 32px -16px rgb(0 0 0 / 0.6); }
.t-posterband__chip {
  position: absolute;
  top: 0.4rem;
  left: 0.4rem;
  padding: 0.2rem 0.4rem;
  border-radius: var(--r-chip);
  background: rgb(0 0 0 / 0.6);
  color: #fff;
  font: 500 0.6875rem/1 var(--f-mono);
}
:root[data-motion="off"] .t-posterband__item { transform: none; opacity: 1; }
@media (prefers-reduced-motion: reduce) { .t-posterband__item { transform: none; opacity: 1; } }

/* A row with buttons under each side (SwipeRow): swiped open, it rests on
   them and a tap runs one. The buttons sit in the ground at their side and
   are as tall as the row; the face slides off them by their width. */
.t-swiperow { position: relative; overflow: hidden; border-radius: var(--r-panel); }
/* a row of a flat, divided list keeps square corners */
li.t-swiperow { border-radius: 0; }
.t-swiperow__face { position: relative; }
.t-swiperow__ground {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  visibility: hidden;
}
.t-swiperow__ground--start { left: 0; }
.t-swiperow__ground--end { right: 0; flex-direction: row-reverse; }
.t-swiperow[data-side="start"] .t-swiperow__ground--start,
.t-swiperow[data-side="end"] .t-swiperow__ground--end { visibility: visible; }
.t-swiperow__action {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  width: 4.75rem;
  padding: 0 0.25rem;
  border: 0;
  cursor: pointer;
  font: 600 0.6875rem/1.2 var(--f-display);
  text-align: center;
  color: var(--on-accent);
  background: var(--tone, var(--accent));
}
.t-swiperow__action[data-tone="ok"] { --tone: var(--ok); }
.t-swiperow__action[data-tone="warn"] { --tone: var(--warn); }
.t-swiperow__action[data-tone="err"] { --tone: var(--err); }
.t-swiperow__action:focus-visible { outline: 2px solid var(--fg); outline-offset: -4px; }

/* A list row that folds away: its height runs to nothing and the list's gap
   with it, so the rows below close up instead of jumping. Exits ease in and
   stay short. The clip is on only while it folds, or it would cut focus rings
   and popovers of a row that stays. */
.t-collapse {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--dur-2) var(--ease-in), opacity var(--dur-2) var(--ease-in), margin var(--dur-2) var(--ease-in);
}
.t-collapse > * { min-height: 0; }
.t-collapse[data-gone] { grid-template-rows: 0fr; opacity: 0; margin-top: -0.75rem; }
.t-collapse[data-gone] > * { overflow: hidden; }

/* ── View transitions are for parts of a page only (a reorder, the poster
   morph, the accent wave); routes move with the wrapper animation above.
   The root pair does not animate, so whatever is not named stands still. ── */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
@keyframes t-fade-out { to { opacity: 0; } }
@keyframes t-spin { to { rotate: 360deg; } }

/* ── Loading indicator (LoadingIndicator): the duo. A red and a teal half
   turn and flow from shape to shape: two dots that run into a small yin and
   yang and straight back out, then a flower cut in two and two diamonds,
   the dots again between them (--a0..3 and --b0..3 from duo.ts). The yin
   and yang is a passing hint, the dots are where it rests. Underneath, the
   pair turns without a stop and breathes a little, like a liquid settling.
   The goo filter in the component rounds every edge. It fades in after
   400 ms, so a quick answer never shows it. Motion off: the dots, still. ── */
.t-loading { display: inline-flex; align-items: center; gap: 0.625rem; }
/* a placeholder that waits its turn the same way (skeleton rows) */
.t-loading, .t-late { animation: t-loading-in var(--dur-2) var(--ease-out) 400ms backwards; }
.t-loading--sm { --li-size: 1.25rem; }
.t-loading--md { --li-size: 1.75rem; }
.t-loading--lg { --li-size: 2.25rem; }
.t-loading-duo { display: block; flex: none; width: var(--li-size); height: var(--li-size); }
.t-duo-orbit { transform-box: view-box; transform-origin: 50px 50px; }
.t-duo-a { fill: var(--accent); clip-path: var(--a0); }
.t-duo-b { fill: var(--accent-2); clip-path: var(--b0); }
.t-loading-label { color: var(--fg-2); font-size: 0.875rem; }
@keyframes t-loading-in { from { opacity: 0; } }
@keyframes t-duo-a {
  0%, 8%, 32%, 42%, 90%, 100% { clip-path: var(--a0); }
  20% { clip-path: var(--a1); }
  54%, 60% { clip-path: var(--a2); }
  72%, 78% { clip-path: var(--a3); }
}
@keyframes t-duo-b {
  0%, 8%, 32%, 42%, 90%, 100% { clip-path: var(--b0); }
  20% { clip-path: var(--b1); }
  54%, 60% { clip-path: var(--b2); }
  72%, 78% { clip-path: var(--b3); }
}
@keyframes t-duo-breathe { 33% { scale: 1.04 0.96; } 66% { scale: 0.97 1.03; } }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="off"]) .t-duo-orbit { animation: t-spin 3000ms linear infinite, t-duo-breathe 1500ms ease-in-out infinite; }
  :root:not([data-motion="off"]) .t-duo-a { animation: t-duo-a 6400ms cubic-bezier(0.65, 0, 0.35, 1) infinite; }
  :root:not([data-motion="off"]) .t-duo-b { animation: t-duo-b 6400ms cubic-bezier(0.65, 0, 0.35, 1) infinite; }
}


/* A local view transition (localTransition in the design system): the
   children of the scope move as themselves, each named from its own --vt. */
[data-vt] > * { view-transition-name: var(--vt, none); }
/* The poster morph (morphTransition): the open dialog and the poster share a
   name, so the dialog's box grows out of the poster's and shrinks back into
   it. The poster's picture gives way quickly and the dialog fades in over the
   growth; both fill the moving box instead of stretching across it. Closing
   is the exit: ease-in and shorter. The dialog's own entry transition is off
   meanwhile, this is its entry. */
:root[data-vt-morph] dialog[open] { view-transition-name: morph-dialog; transition: none; }
:root[data-vt-morph] dialog[open]::backdrop { transition: none; }
/* clipped: a picture that covers the box would otherwise spill past it */
::view-transition-group(morph-dialog) {
  overflow: clip;
  border-radius: var(--r-dialog);
  animation-duration: var(--dur-3);
  animation-timing-function: var(--ease-out);
}
:root[data-vt-back]::view-transition-group(morph-dialog) { animation-duration: var(--dur-2); animation-timing-function: var(--ease-in); }
/* the hero morph (the live pill into its sheet): longer, on the spatial
   curve, both ways */
:root[data-vt-hero]::view-transition-group(morph-dialog) { animation-duration: 420ms; animation-timing-function: var(--m3-spatial); border-radius: var(--r-dialog); }
::view-transition-old(morph-dialog),
::view-transition-new(morph-dialog) { height: 100%; object-fit: cover; }
::view-transition-old(morph-dialog) { animation: t-fade-out var(--dur-1) var(--ease-in) both; }
::view-transition-new(morph-dialog) { animation: t-fade-in var(--dur-2) var(--ease-out) both; }
/* not paired - a phone's sheet, or a poster gone from the page: the dialog
   comes and goes on its own */
::view-transition-new(morph-dialog):only-child { animation: t-dialog-in var(--dur-3) var(--ease-enter) both; }
::view-transition-old(morph-dialog):only-child { animation: t-fade-out var(--dur-1) var(--ease-in) both; }
@media (width < 40rem) {
  ::view-transition-new(morph-dialog):only-child { animation-name: t-sheet-up; }
}
@keyframes t-dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } }
@keyframes t-sheet-up { from { transform: translateY(100%); } }
@keyframes t-fade-in { from { opacity: 0; } }
:root[data-vt-local]::view-transition-group(*) {
  animation-duration: var(--dur-3);
  animation-timing-function: var(--ease-out);
}

@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.animate-fadeIn { animation: fadeIn var(--dur-2) var(--ease-enter); }

/* a check mark grows in; `scale` is its own property, so the centring
   transform underneath stays untouched */
@keyframes t-tick { from { opacity: 0; scale: 0.6; } }

/* Progress bar stripes for running downloads. 25/50/75% stops tile
   seamlessly into the 28px background-size; shifting by exactly one tile
   per loop keeps the scroll continuous. */
@keyframes t-stripes { from { background-position: 28px 0; } to { background-position: 0 0; } }
/* Determinate progress: a rounded track in the secondary ground, the bar in
   the accent unless a tone says otherwise. Full radius, like every dot and
   bar on the scale. */
.t-progress {
  /* no width of its own: a block fills its line, and a utility narrows
     it to a meter beside a figure */
  height: 8px;
  overflow: hidden;
  border-radius: 9999px;
  /* the hover ground, not the secondary: on a card the secondary is a
     shade off the card itself, and an empty bar vanished */
  background: var(--hover);
}
.t-progress--sm { height: 4px; }
.t-progress-bar {
  height: 100%;
  border-radius: 9999px;
  background: var(--progress-color, var(--accent));
  transition: width var(--dur-3) var(--ease-out), background-color var(--dur-2) var(--ease-out);
}
/* the wave (Progress wave): the fill and the rest of the track are drawn
   paths, the track from right where the wave ends, no gap, and a dot on the
   wave's head marks where it stands. Taller than the bar, for the swing */
.t-progress--wave { position: relative; height: 12px; overflow: visible; background: none; border-radius: 0; --wave-w: 4px; }
.t-progress--wave.t-progress--sm { height: 10px; --wave-w: 2.6px; }
.t-progress-wave { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.t-progress-wave path {
  fill: none;
  stroke: var(--progress-color, var(--accent));
  stroke-width: var(--wave-w);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.t-progress-wave .t-progress-track { stroke: var(--hover); }
/* the dot on the wave's head: where the transfer stands */
.t-progress-wave .t-progress-head { r: calc(var(--wave-w) * 0.9); fill: var(--progress-color, var(--accent)); }
.t-progress--ok { --progress-color: var(--ok); }
.t-progress--warn { --progress-color: var(--warn); }
.t-progress--err { --progress-color: var(--err); }
.t-progress-running {
  background-image: linear-gradient(
    45deg,
    color-mix(in srgb, var(--bg) 18%, transparent) 25%,
    transparent 25% 50%,
    color-mix(in srgb, var(--bg) 18%, transparent) 50% 75%,
    transparent 75%
  );
  background-size: 28px 28px;
  animation: t-stripes 700ms linear infinite;
}

/* ── Reduced-motion gating (in-app toggle + OS preference).
   0.01ms instead of none so animationend handlers still fire. ── */
[data-motion="off"] *,
[data-motion="off"] *::before,
[data-motion="off"] *::after {
  animation-duration: 0.01ms !important;
  animation-delay: 0ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  transition-delay: 0ms !important;
  scroll-behavior: auto !important;
}
/* the view-transition pseudo-elements hang off <html>; `*` never reaches them */
:root[data-motion="off"]::view-transition-group(*),
:root[data-motion="off"]::view-transition-old(*),
:root[data-motion="off"]::view-transition-new(*) { animation: none !important; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ── A day as a time axis (DayTimeline): the releases in order, the time
   between them as height. Nothing is positioned absolutely - the gaps are real
   elements - so a cluster of releases stacks instead of overlapping, and the
   axis is one line down the left of the list. */
.t-timeline {
  /* one axis for the line, the filled stretch, every dot and the now line:
     its centre, measured from the list's left edge, and the list's indent */
  --axis: 0.35rem;
  --pad: 1.125rem;
  position: relative;
  display: flex;
  flex-direction: column;
  padding-left: var(--pad);
}
.t-timeline::before {
  content: '';
  position: absolute;
  left: calc(var(--axis) - 0.5px);
  top: 0;
  bottom: 0;
  width: 1px;
  /* the day has no hard ends: the axis softens at the top and below - from
     a faint start, never from nothing, so it reads as a tail, not a spike */
  background: linear-gradient(
    color-mix(in srgb, var(--line) 40%, transparent),
    var(--line) 2.5rem,
    var(--line) calc(100% - 2.5rem),
    color-mix(in srgb, var(--line) 40%, transparent)
  );
}
/* the filled stretch softens with it at the top */
.t-timeline > li:first-child[data-past]::after { mask-image: linear-gradient(rgb(0 0 0 / 0.4), #000 2.5rem); }
/* right above the now marker a length says nothing the marker's own time
   does not, and its chip sat on top of the marker's */
.t-timeline > .t-timeline__gap:has(+ .t-timeline__now) > span { display: none; }
.t-timeline > li { position: relative; }
/* the dot marking a release on the axis, on the row's vertical middle - a
   fixed offset from the top sat a third of the way down a two-line card */
.t-timeline__row::before {
  content: '';
  position: absolute;
  left: calc(var(--axis) - var(--pad));
  top: 50%;
  translate: -50% -50%;
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 999px;
  /* solid and on the axis, so the line runs into it, not through it */
  background: var(--line-strong);
  z-index: 1;
}
/* a stretch that had to be cut short says how long it really was, the way
   the now marker says the time: a quiet chip at the right, midway between the
   two releases. The axis itself runs on untouched - a caption sitting on it
   used to break the line in two. */
.t-timeline__gap > span {
  position: absolute;
  right: 0;
  top: 50%;
}
.t-timeline__gap > span::after {
  content: attr(data-label);
  position: absolute;
  right: 0;
  top: -0.55rem;
  padding: 0.15rem 0.4rem;
  border-radius: var(--r-chip);
  background: var(--hover);
  color: var(--fg-3);
  font: 500 0.625rem/1 var(--f-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* what has already aired: its stretch of the axis is filled, its rows step
   back and their dots are full */
.t-timeline > li[data-past]::after {
  content: '';
  position: absolute;
  left: calc(var(--axis) - var(--pad) - 1px);
  top: 0;
  bottom: 0;
  border-left: 2px solid var(--accent);
}
/* the row's content steps back, not the row: dimming the <li> dimmed its
   stretch of the filled axis and its dot too, and the line came out dark
   beside every card and bright between them, as if broken */
.t-timeline__row[data-past] > * { opacity: 0.6; }
.t-timeline__row[data-past]::before { background: var(--accent); }

/* now, on today's panel: it drifts down past the releases as the clock runs,
   the time beside it saying what the line is */
.t-timeline > .t-timeline__now {
  height: 0;
  border-top: 2px solid var(--accent);
  /* the line starts on the axis, in its dot, not left of it */
  margin-left: calc(var(--axis) - var(--pad));
  /* it marks a time on the axis, so it stays at that time: it used to stick
     to the screen edge and ride along with the scroll, standing at no time */
  position: relative;
  z-index: 2;
}
.t-timeline__now::after {
  content: attr(data-label);
  position: absolute;
  right: 0;
  top: -0.65rem;
  padding: 0.15rem 0.4rem;
  border-radius: var(--r-chip);
  background: var(--accent);
  color: var(--on-accent);
  font: 600 0.625rem/1 var(--f-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.t-timeline__now::before {
  content: '';
  position: absolute;
  left: 0;
  top: -1px;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 999px;
  background: var(--accent);
  translate: -50% -50%;
}

/* ── Content: section headings, hairline rows, text links, empty lines ──
   Lists carry hairlines instead of panels; covers are the only cards. */
/* centred, not on the baseline: a title with a fold button or a tall marker
   left the count or the link at its end hanging below the words */
.t-sec-h { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin: 0 0 0.5rem; }
.t-sec-title {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin: 0;
  min-width: 0;
  font: 600 var(--fs-section)/1.2 var(--f-display);
  color: var(--fg);
}
/* the marker: a small slanted panel in the colour code, before the words */
.t-sec-title::before {
  content: "";
  flex: none;
  width: 0.625rem;
  height: 1.125rem;
  background: var(--accent);
  clip-path: polygon(30% 0, 100% 0, 70% 100%, 0 100%);
}
.t-sec-title[data-tone="accent-2"]::before { background: var(--accent-2); }
/* a folding section: the title is the button, a chevron after it points
   down while open and to the side when folded */
.t-sec-fold {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  min-height: 2.75rem;
  margin-block: -0.75rem;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}
.t-sec-fold::after {
  content: "";
  flex: none;
  width: 0.45em;
  height: 0.45em;
  margin-top: -0.2em;
  border: solid var(--fg-3);
  border-width: 0 2px 2px 0;
  rotate: 45deg;
  transition: rotate var(--dur-2) var(--spring-calm), margin var(--dur-2) var(--spring-calm);
}
.t-sec-fold[aria-expanded="false"]::after { rotate: -45deg; margin-top: 0; }
.t-sec-fold:hover::after { border-color: var(--fg); }
.t-sec-fold:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 2px; }

.t-link {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.5rem 0;
  font-size: var(--fs-link);
  font-weight: 500;
  color: var(--accent);
  text-decoration: none;
  background: none;
  border: 0;
  cursor: pointer;
}
.t-link--2 { color: var(--accent-2); }
/* A link in running text ("... Retry") takes the paragraph's line, not a box
   of its own: the padding that makes a standalone link 24px+ tall would
   stretch the line it sits on. The target (WCAG 2.5.8) keeps its size through
   a hit area drawn around the text instead, which clicks the link and moves
   nothing. */
p > .t-link {
  position: relative;
  padding-block: 0;
  font-size: inherit;
  vertical-align: baseline;
}
p > .t-link::before {
  content: "";
  position: absolute;
  inset: -0.5rem 0;
}
@media (pointer: coarse) {
  p > .t-link::before { inset: min(-0.5rem, calc((100% - var(--hit)) / 2)) 0; }
}
@media (hover: hover) {
  .t-link:hover { text-decoration: underline; text-underline-offset: 3px; }
}

.t-rows { list-style: none; margin: 0; padding: 0; }
.t-row { position: relative; border-bottom: 1px solid var(--line); touch-action: pan-y; }
.t-row:last-child { border-bottom: 0; }
.t-row-in {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 3.75rem;
  padding: 0.5rem 0;
  background: var(--surface, var(--bg));
}
.t-rows--dense .t-row-in { min-height: 3.25rem; }
@media (hover: hover) {
  .t-row-in:hover { background: color-mix(in srgb, var(--fg) 4%, var(--surface, var(--bg))); }
}
.t-row-t { flex: 1; min-width: 0; }
.t-row-t > strong { display: block; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.t-row-t > span { display: block; color: var(--fg-3); font-size: var(--fs-meta); }
.t-glyph { width: 1.75rem; height: 1.75rem; display: grid; place-items: center; flex: none; color: var(--fg-2); }
.t-glyph[data-tone="err"] { color: var(--err); }
.t-glyph[data-tone="warn"] { color: var(--warn); }
.t-glyph[data-tone="ok"] { color: var(--ok); }
.t-glyph[data-tone="accent"] { color: var(--accent); }
.t-glyph[data-tone="accent-2"] { color: var(--accent-2); }
.t-row-act {
  flex: none;
  padding: 0.625rem 0 0.625rem 0.5rem;
  min-height: 2.75rem;
  font-size: var(--fs-link);
  font-weight: 500;
  color: var(--accent);
  background: none;
  border: 0;
  cursor: pointer;
}
.t-row-act--2 { color: var(--accent-2); }

.t-empty { color: var(--fg-3); padding: 2.5rem 0; text-align: center; }
.t-empty--inline { padding: 1rem 0; text-align: start; list-style: none; }
.t-empty-label { display: block; margin-bottom: 0.25rem; color: var(--fg-2); font-weight: 500; }

/* ── Segmented control: equal columns on a quiet track, a thumb gliding
   under the picked one. The columns are equal while every label fits
   (flex-basis 0, equal growth); a label that needs more keeps its own width
   (min-width: max-content) and what no longer fits wraps. Equal grid columns
   with min-width 0 used to squeeze a long label into a column narrower than
   its words, and the words ran over the next option. Segmented() measures
   the picked button and sets --seg-x/y/w/h; before that (first paint, no
   ResizeObserver) the thumb stands on the equal-column estimate from --n
   and --i. ── */
.t-seg {
  position: relative;
  display: inline-flex;
  flex-wrap: wrap;
  max-width: 100%;
  padding: 3px;
  border-radius: var(--r-menu);
  background: var(--bg-2);
  border: 1px solid var(--line);
}
.t-seg-btn {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  min-width: max-content;
  min-height: 2.25rem;
  padding: 0 0.75rem;
  font: inherit;
  font-size: 0.875rem;
  color: var(--fg-3);
  background: none;
  border: 0;
  border-radius: var(--r-item);
  white-space: nowrap;
  transition: color var(--dur-2);
}
.t-seg--md .t-seg-btn { min-height: 2.5rem; font-size: var(--ctl-fs); }
.t-seg-btn[aria-pressed="true"] { color: var(--fg); font-weight: 500; }
.t-seg-btn:disabled { opacity: 0.5; cursor: not-allowed; }
@media (hover: hover) {
  .t-seg-btn:hover:not(:disabled) { color: var(--fg); }
}
.t-seg-btn:focus-visible { outline-offset: -2px; }
.t-seg-thumb {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc((100% - 6px) / var(--n));
  border-radius: var(--r-item);
  background: var(--hover);
  /* the picked option is told apart by an edge at 3:1, not by a tint alone */
  box-shadow: inset 0 0 0 1px var(--line-strong);
  transform: translateX(calc(100% * var(--i)));
}
.t-seg[data-placed] > .t-seg-thumb {
  top: 0;
  bottom: auto;
  left: 0;
  width: var(--seg-w);
  height: var(--seg-h);
  transform: translate(var(--seg-x), var(--seg-y));
}
.t-seg[data-placed="glide"] > .t-seg-thumb {
  transition: transform var(--dur-3) var(--spring-calm), width var(--dur-3) var(--spring-calm), height var(--dur-3) var(--spring-calm);
}

/* ── Switch: a checkbox with the switch role. The global checkbox rules are
   overridden by weight (input[type].class), not by order. ── */
input[type='checkbox'].t-switch {
  width: 2.625rem;
  height: 1.5rem;
  border: 0;
  border-radius: 0.75rem;
  background: var(--line-strong);
  transition: background-color var(--dur-2);
}
input[type='checkbox'].t-switch::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: calc(1.5rem - 6px);
  height: calc(1.5rem - 6px);
  border: 0;
  border-radius: 50%;
  background: var(--fg);
  transform: none;
  animation: none;
  transition: translate var(--dur-2) var(--spring-calm), background-color var(--dur-2);
}
input[type='checkbox'].t-switch:checked { background: var(--accent); }
input[type='checkbox'].t-switch:checked::after { translate: 1.125rem 0; background: var(--on-accent); }
input[type='checkbox'].t-switch:disabled { opacity: 0.6; }
.t-switch-label { display: flex; align-items: center; gap: 0.75rem; min-height: var(--hit); cursor: pointer; }

/* Touch: what the ::before hit areas cannot reach. A field has no pseudo-element,
   so the compact field takes the real height outside a toolbar (a toolbar keeps
   its row height, the buttons beside it reach 44 through ::before). The text of
   every field is at least 16px, below that iOS zooms the page on focus. */
@media (pointer: coarse) {
  .t-input--sm, .t-select--sm { font-size: var(--ctl-fs); line-height: var(--ctl-lh); min-height: var(--hit); }
  .t-toolbar .t-input--sm, .t-toolbar .t-select--sm { min-height: var(--ctl-h-sm); }
  :where(input:not([type='checkbox'], [type='radio'], [type='range'], [type='file'], [type='button'], [type='submit'], [type='color']), select, textarea) {
    font-size: max(16px, var(--ctl-fs));
  }
}

/* ── SettingsField: label, chip, control, quiet hint ── */
/* one column that never grows past the field: an implicit column is as wide
   as its widest unbreakable line, and a long error pushed the row and its
   button out of the panel */
.t-sf { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.375rem; margin: 0.75rem 0 0; align-content: start; }
.t-sf--row { grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 1rem; }
.t-sf--row > .t-sf-h { grid-column: 1 / -1; }
.t-sf-l { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; font-weight: 500; color: var(--fg); }
.t-sf-h { margin: 0; font-size: var(--fs-meta); color: var(--fg-3); }

/* ── Covers: rounded, no ring. Without art, the title on a gradient in its
   own hue (--h). ── */
.t-cover { position: relative; }
.t-cover--fallback { display: block; background: linear-gradient(180deg, oklch(0.5 0.1 var(--h)) 0 55%, oklch(0.24 0.06 var(--h))); }
.t-cover--fallback > b {
  position: absolute;
  inset: auto 0.5rem 0.5rem;
  font: 700 var(--cover-fs, 0.8125rem)/1.02 var(--f-display);
  text-transform: uppercase;
  color: #fff;
  text-wrap: balance;
  overflow-wrap: anywhere;
  text-shadow: 0 1px 8px rgb(0 0 0 / 0.4);
}
/* every clickable cover answers the same way: the art zooms inside its frame
   on hover, the frame sinks on press. Both layers stay promoted, so neither
   movement ends in a raster jolt. */
:is(button, a, [role="button"], [data-open]) .t-cover { will-change: scale; }
:is(button, a, [role="button"], [data-open]) .t-cover > img { will-change: scale; transform-origin: 50% 40%; }
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  :is(button, a, [role="button"], [data-open]) .t-cover > img {
    transition: opacity var(--dur-3) var(--ease-out), scale 420ms var(--spring-calm);
  }
  :is(button, a, [role="button"], [data-open]):hover .t-cover > img { scale: 1.06; }
}
@media (prefers-reduced-motion: no-preference) {
  :is(button, a, [role="button"], [data-open]) .t-cover { transition: scale 260ms var(--spring); }
  :is(button, a, [role="button"], [data-open]):active .t-cover { scale: 0.97; transition-duration: 90ms; }
}
/* a row being dragged sideways is not a press */
:is(.dragging, [data-drag]) .t-cover,
:is(.dragging, [data-drag]) .t-cover > img { scale: none !important; }

/* ── Art: a banner or backdrop as one layer, picture and dot screen
   together, 5px past its frame on every side so an edge line baked into the
   source never shows. The frame (the host's) clips. ── */
.t-art {
  position: absolute;
  inset: -5px;
  display: block;
  overflow: hidden;
  will-change: scale;
  transform-origin: 50% 40%;
}
.t-art > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  /* --fx/--fy: a slice picked from the picture's content, where known */
  object-position: var(--fx, 50%) var(--fy, 35%);
}
.t-art-dots {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(circle, rgb(0 0 0 / 0.55) 1px, transparent 1.4px) 0 0 / 5px 5px;
  -webkit-mask-image: linear-gradient(var(--tone-dir, to top), #000, transparent 45%);
  mask-image: linear-gradient(var(--tone-dir, to top), #000, transparent 45%);
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  :is(button, a, [role="button"], [data-open]) .t-art { transition: scale 420ms var(--spring-calm); }
  :is(button, a, [role="button"], [data-open]):hover .t-art { scale: 1.06; }
}

/* ── Chips on a catalog cover: status top left, score top right, and a thin
   teal bar along the foot for what is on disk ── */
.t-cover-chip {
  position: absolute;
  top: 6px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 1.25rem;
  padding: 0 7px;
  font: 600 0.6875rem/1 var(--f-sans);
  border-radius: 4px;
  /* dark ground in both themes: the chip sits on cover art, not on the page */
  color: #fff;
  background: rgb(0 0 0 / 0.72);
}
.t-cover-status { left: 6px; }
.t-cover-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--fg-3); }
.t-cover-status[data-status="live"]::before { background: var(--accent-2); }
.t-cover-status[data-status="soon"]::before { background: var(--accent); }
.t-cover-score { right: 6px; font-variant-numeric: tabular-nums; }
.t-cover-score > svg { width: 11px; height: 11px; fill: #f2c46b; }
.t-cover-bar {
  position: absolute;
  inset: auto 0 0;
  z-index: 2;
  height: 4px;
  background: color-mix(in oklab, var(--bg) 70%, transparent);
}
.t-cover-bar > i { display: block; height: 100%; width: calc(var(--p, 0) * 100%); background: var(--accent-2); }

/* ── Rail: a horizontal shelf. It fades at an edge with more behind it and
   offers a round button there (hover devices only); a mouse drags it. The
   fades are two overlays in the ground colour (--rail-fade), not a mask on
   the scroller: a masked scroller is repainted on every frame it scrolls,
   which a phone's WebKit felt. --rail-bleed reaches them past a shelf that
   runs out to the screen edge. ── */
.t-rail { position: relative; }
.t-rail::before,
.t-rail::after {
  content: "";
  position: absolute;
  z-index: 1;
  top: 0;
  bottom: 0;
  width: 3rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
}
.t-rail::before { left: calc(-1 * var(--rail-bleed, 0px)); background: linear-gradient(to right, var(--rail-fade, var(--bg)), transparent); }
.t-rail::after { right: calc(-1 * var(--rail-bleed, 0px)); background: linear-gradient(to left, var(--rail-fade, var(--bg)), transparent); }
.t-rail[data-more-start]::before,
.t-rail[data-more-end]::after { opacity: 1; }
.t-shelf {
  display: flex;
  gap: 0.75rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  padding-block: 2px 6px;
}
.t-shelf::-webkit-scrollbar { display: none; }
.t-shelf > * { flex: none; scroll-snap-align: start; }
.t-shelf.dragging { cursor: grabbing; scroll-snap-type: none; user-select: none; }
.t-shelf.dragging * { pointer-events: none; }
.t-rail-btn {
  position: absolute;
  z-index: 2;
  top: calc(var(--cover-h, 9.75rem) / 2 - 1.375rem);
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 50%;
  color: var(--fg);
  background: color-mix(in oklab, var(--card) 88%, transparent);
  border: 1px solid var(--line);
  box-shadow: 0 6px 18px rgb(0 0 0 / 0.35);
  cursor: pointer;
}
.t-rail-btn[hidden] { display: none; }
.t-rail-btn--prev { left: -6px; }
.t-rail-btn--next { right: -6px; }
@media (hover: hover) {
  .t-rail-btn:hover { background: var(--hover); }
}
@media (prefers-reduced-motion: no-preference) {
  .t-rail-btn { transition: scale var(--dur-2) var(--spring), background-color var(--dur-1); }
  .t-rail-btn:active { scale: 0.9; }
}
/* a finger scrolls the shelf itself: no paging buttons, and no snap. Chrome
   caps a fling at the next snap point (measured on a phone: the throw braked
   after 60ms, then crept 400ms to the next tile instead of coasting on) */
@media (hover: none) {
  .t-rail-btn { display: none; }
  .t-shelf { scroll-snap-type: none; }
}

/* ── Wipe (the opening credits reveal): .t-wipe on a text line, played by
   [data-wipe="in"|"out"] on the line or an ancestor. The block is the line's
   ::after in --wipe-c (the text colour by default); a heading keeps ::before
   for its marker. Lines stagger by --i. The text stays hidden until the block
   has covered it. ── */
.t-wipe { position: relative; }
.t-wipe::after {
  content: "";
  position: absolute;
  inset: -1px -3px;
  z-index: 1;
  pointer-events: none;
  background: var(--wipe-c, var(--fg));
  transform: scaleX(0);
  transform-origin: left;
}
/* a heading's block starts after its marker, which stands from the start */
.t-sec-title.t-wipe::after { inset-inline-start: 1.25rem; }
@media (prefers-reduced-motion: no-preference) {
  /* "in" fills backwards only: each ends on the element's own look, and an
     animation that stays in effect keeps its layer, which was torn down (and
     the page re-rastered, its pictures decoded again) all at once when the
     mark came off a second later */
  :is([data-wipe="in"] .t-wipe, .t-wipe[data-wipe="in"]) { animation: t-wipe-text 560ms linear calc(var(--i, 0) * 110ms) backwards; }
  :is([data-wipe="in"] .t-wipe, .t-wipe[data-wipe="in"])::after { animation: t-wipe-block 560ms linear calc(var(--i, 0) * 110ms) backwards; }
  :is([data-wipe="out"] .t-wipe, .t-wipe[data-wipe="out"]) { animation: t-wipe-text-out 420ms linear calc(var(--i, 0) * 70ms) both; }
  :is([data-wipe="out"] .t-wipe, .t-wipe[data-wipe="out"])::after { animation: t-wipe-block-out 420ms linear calc(var(--i, 0) * 70ms) both; }
}
@keyframes t-wipe-text { 0%, 47% { color: transparent; } 48%, 100% { } }
@keyframes t-wipe-block {
  0% { transform: scaleX(0); transform-origin: left; }
  6% { transform: scaleX(0.012); transform-origin: left; }
  16% { transform: scaleX(0.012); transform-origin: left; animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1); }
  47% { transform: scaleX(1); transform-origin: left; }
  48% { transform: scaleX(1); transform-origin: right; animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1); }
  100% { transform: scaleX(0); transform-origin: right; }
}
@keyframes t-wipe-text-out { 0%, 49% { } 50%, 100% { color: transparent; } }
@keyframes t-wipe-block-out {
  0% { transform: scaleX(0); transform-origin: left; animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1); }
  49% { transform: scaleX(1); transform-origin: left; }
  50% { transform: scaleX(1); transform-origin: right; animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1); }
  100% { transform: scaleX(0); transform-origin: right; }
}

/* entering a page (enterPage): meters fill from zero, and every item rises
   14px with a fade in one left-to-right wave (--rd, 0..260ms from its place
   across the screen). Once per entry. */
@media (prefers-reduced-motion: no-preference) {
  [data-wipe="in"] .t-progress:not(.t-progress--wave) > .t-progress-bar:not(.t-progress-running) {
    transform-origin: left;
    animation: t-fill 900ms var(--spring-calm) 200ms backwards;
  }
  [data-wipe="in"] :is(.t-rows > .t-row, .t-shelf > *, [data-rise] > *, .t-rise):not(.t-reveal) {
    animation: t-enter-rise 520ms var(--ease-enter) var(--rd, 0ms) backwards;
  }
}
@keyframes t-fill { from { transform: scaleX(0); } }
@keyframes t-enter-rise { from { opacity: 0; translate: 0 14px; } }

/* ── Select: the trigger is a button (no native popup); the listbox is glass
   in the top layer, the picked option in the accent with a check ── */
button.t-select { display: flex; align-items: center; text-align: start; cursor: pointer; }
.t-select-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.t-listbox { list-style: none; margin-block: 0.25rem 0; padding-inline: 0.25rem; max-width: min(90vw, 28rem); }
.t-option {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: var(--ctl-h);
  padding: 0.5rem 0.75rem;
  border-radius: var(--r-item);
  font-size: 0.875rem;
  color: var(--fg-2);
  cursor: pointer;
}
.t-option[data-active] { background: var(--hover); color: var(--fg); }
/* walking the list by keys, the tint alone is the focus mark and sits near
   1.2:1 on the list: the same accent ring as every other focus (WCAG 1.4.11) */
/* focus stays on the trigger and hover sets data-active too: the list
   marks data-keys once the keys moved it */
:root:not([data-input='pointer']) .t-listbox[data-keys] .t-option[data-active] { outline: 2px solid var(--accent); outline-offset: -2px; }
.t-option[aria-selected="true"] { color: var(--accent); }
.t-option[aria-selected="true"] > svg { flex: none; font-size: 1.125rem; }
.t-option[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
button.t-select[aria-expanded="true"]:focus-visible { outline-offset: 2px; }

/* ── Range: a 4px track and a round thumb drawn by the theme ── */
input[type='range'] {
  appearance: none;
  -webkit-appearance: none;
  height: 1.5rem; /* the thumb's row is the touch target (WCAG 2.5.8) */
  background: transparent;
  cursor: pointer;
}
input[type='range']::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--line-strong); }
input[type='range']::-moz-range-track { height: 4px; border-radius: 2px; background: var(--line-strong); }
input[type='range']::-moz-range-progress { height: 4px; border-radius: 2px; background: var(--accent); }
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: calc(2px - 0.5625rem);
  border: 0;
  border-radius: 50%;
  background: var(--accent);
}
input[type='range']::-moz-range-thumb { width: 1.125rem; height: 1.125rem; border: 0; border-radius: 50%; background: var(--accent); }
input[type='range']:disabled { opacity: 0.45; cursor: not-allowed; }
:root:not([data-input='pointer']) input[type='range']:focus-visible { outline: none; }
:root:not([data-input='pointer']) input[type='range']:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }
:root:not([data-input='pointer']) input[type='range']:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }

/* ── Search fields: no UA clear button or decoration; a page that wants a
   clear button draws its own ── */
input[type='search'] { -webkit-appearance: none; appearance: none; }
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-decoration,
input[type='search']::-webkit-search-results-button,
input[type='search']::-webkit-search-results-decoration { -webkit-appearance: none; appearance: none; display: none; }

/* duotone art: the picture in grey under a multiply tint in the colour code;
   a hover eases both off together */
.t-art[data-duo] > img { filter: grayscale(1) contrast(1.25) brightness(1.05); }
.t-art[data-duo]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--accent);
  mix-blend-mode: multiply;
  pointer-events: none;
}
.t-art[data-duo="accent-2"]::before { background: var(--accent-2); }
.t-art-dots { z-index: 2; }
@media (hover: hover) {
  :is(button, a, [role="button"], [data-open]):hover .t-art[data-duo] > img { filter: grayscale(0.6) contrast(1.15) brightness(1.05); }
  :is(button, a, [role="button"], [data-open]):hover .t-art[data-duo]::before { opacity: 0.5; }
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .t-art[data-duo] > img { transition: filter 420ms var(--spring-calm); }
  .t-art[data-duo]::before { transition: opacity 420ms var(--spring-calm); }
}

/* ── Toast: inverted (the text colour as ground), a line and one action. The
   region is a popover in the top layer, open even while empty (it takes no
   room and lets clicks through). A phone shows it above the tab bar, a
   desktop at the bottom right, left of an open side panel (--sidepanel-w,
   written by SidePanel while it is mounted). ── */
.t-toast-region {
  position: fixed;
  inset: auto 0.75rem calc(var(--nav-h) + var(--safe-b) + 0.75rem);
  margin: 0 auto;
  width: auto;
  max-width: 28rem;
  padding: 0;
  border: 0;
  background: none;
  overflow: visible;
  pointer-events: none;
}
@media (width >= 64rem) {
  .t-toast-region {
    inset: auto calc(var(--sidepanel-w, 0px) + 2rem) 1.5rem auto;
    width: 23.75rem;
    margin: 0;
  }
}
.t-toast {
  pointer-events: auto;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0.5rem 0.75rem 1rem;
  border-radius: var(--r-panel);
  background: var(--fg);
  color: var(--bg);
  font-size: 0.875rem;
  box-shadow: 0 12px 32px -12px rgb(0 0 0 / 0.5);
  animation: t-toast-in var(--dur-2) var(--ease-enter) both;
}
.t-toast + .t-toast { margin-top: 0.5rem; }
.t-toast__text { flex: 1; min-width: 0; }
.t-toast__action {
  flex: none;
  min-height: 2.75rem;
  padding: 0 0.625rem;
  border: 0;
  border-radius: var(--r-ctl);
  background: none;
  font: 600 0.875rem/1 var(--f-sans);
  color: inherit;
  cursor: pointer;
}
@media (hover: hover) {
  .t-toast__action:hover { background: color-mix(in srgb, var(--bg) 14%, transparent); }
}
.t-toast__action:focus-visible { outline-color: var(--bg); outline-offset: -2px; }
.t-toast__time {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  background: var(--bg);
  opacity: 0.6;
  transform-origin: left;
  animation: t-toast-time linear both;
}
@keyframes t-toast-in { from { opacity: 0; translate: 0 1rem; } }
@keyframes t-toast-time { to { transform: scaleX(0); } }
/* the close button of a timed toast: its ring runs down the time left and
   stops with it while the toast is hovered, focused or the tab is hidden */
@property --t-toast-p { syntax: "<number>"; inherits: false; initial-value: 1; }
.t-toast__close {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}
@media (hover: hover) {
  .t-toast__close:hover { background: color-mix(in srgb, var(--bg) 14%, transparent); }
}
.t-toast__close:focus-visible { outline-color: var(--bg); outline-offset: -2px; }
.t-toast__ring {
  position: absolute;
  inset: 0.5rem;
  border-radius: 50%;
  background: conic-gradient(currentColor calc(var(--t-toast-p) * 1turn), color-mix(in srgb, currentColor 22%, transparent) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px));
  animation: t-toast-ring linear both;
}
.t-toast[data-paused] .t-toast__ring { animation-play-state: paused; }
@keyframes t-toast-ring { from { --t-toast-p: 1; } to { --t-toast-p: 0; } }
/* without motion the ring would show as already run out: a plain outline */
@media (prefers-reduced-motion: reduce) {
  .t-toast__ring { animation: none; --t-toast-p: 1; opacity: 0.4; }
}
:root[data-motion="off"] .t-toast__ring { animation: none; --t-toast-p: 1; opacity: 0.4; }

/* number fields: never the browser's stepper arrows, anywhere */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
