/* Trackline design system: the shared foundation every face loads first.
 *
 * Source of truth: DESIGN-BRIEF.md. Values here are transcribed from it, not
 * invented. If a number looks arbitrary, it came from the brief.
 *
 * How a face consumes this:
 *   <link rel="stylesheet" href="/assets/base.css">      <- always first
 *   <link rel="stylesheet" href="/assets/<face>.css">    <- face-specific
 *
 * Naming: every class this file ships is prefixed `tl-`. Faces own every
 * unprefixed class. That keeps the shared layer from colliding with markup it
 * has never seen, and makes it obvious at a glance which layer a rule lives in.
 *
 * Theme: light tokens sit on :root. Dark arrives via prefers-color-scheme, and
 * a data-theme="light|dark" attribute on <html> overrides the OS either way.
 * No visible toggle ships; the hook exists so adding one is one line.
 *
 * `color-scheme` is deliberately NOT set here. It has to live on :root, and
 * setting it globally would repaint form controls and scrollbars on faces that
 * have no dark palette yet. Instead this file publishes --scheme, and a face
 * opts in with one line in its own stylesheet:
 *     :root { color-scheme: var(--scheme); }
 *
 * The brand accent is per-client and injected at runtime by JS
 * (document.documentElement.style.setProperty('--accent', ...)). The value
 * below is a neutral fallback, never a client colour.
 */

/* ==========================================================================
   1. Tokens: light theme
   ========================================================================== */

:root {
  --scheme: light;

  /* --- Type families ---------------------------------------------------
     -apple-system leads deliberately. system-ui first resolves to Segoe UI or
     Roboto on some engines and silently loses San Francisco. */
  --font-text: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-round: ui-rounded, -apple-system, BlinkMacSystemFont, system-ui, sans-serif;

  /* --- The ramp ---------------------------------------------------------
     Each token is a `font` shorthand, so `font: var(--t-body)` sets weight,
     size, leading and family in one declaration. Tracking rides alongside in
     the matching --tr-* token because the shorthand cannot carry it. */
  --t-large-title: 700 34px/41px var(--font-text);
  --t-title1: 700 28px/34px var(--font-text);
  --t-title2: 600 22px/28px var(--font-text);
  --t-title3: 600 20px/25px var(--font-text);
  --t-headline: 600 17px/22px var(--font-text);
  --t-body: 400 17px/22px var(--font-text);
  --t-callout: 400 16px/21px var(--font-text);
  --t-subhead: 400 15px/20px var(--font-text);
  --t-footnote: 400 13px/18px var(--font-text);
  --t-caption: 400 12px/16px var(--font-text);

  --tr-large-title: -0.022em;
  --tr-title1: -0.020em;
  --tr-title2: -0.017em;
  --tr-title3: -0.014em;
  --tr-headline: -0.011em;
  --tr-body: -0.011em;
  --tr-callout: -0.009em;
  --tr-subhead: -0.007em;
  --tr-footnote: -0.003em;
  --tr-caption: 0;

  /* --- Ground and surfaces ---------------------------------------------
     --surface is pure #FFFFFF on purpose. The white sheet against the
     #F2F2F7 ground is the inset-grouped signature; tinting it breaks the
     single most load-bearing relationship in the design. */
  --bg-grouped: #F2F2F7;
  --surface: #FFFFFF;
  --surface-2: #F4F4F5;
  --surface-3: #EEEEF0;

  /* --- Label ramp -------------------------------------------------------
     --label-3 is restricted: placeholders and disclosure chevrons only, 15px
     or larger, desk face only. It is banned outright on /field. */
  --label: #111114;
  --label-2: #6C6C70;
  --label-3: #8E8E93;

  --separator: rgba(60, 60, 67, 0.29);
  --separator-op: #C6C6C8;

  /* --- Accent lanes -----------------------------------------------------
     Lane 1 --accent: fills only. Never text, ever.
     Lane 2 --tint-text: the accent with its lightness pinned in OKLCH, so an
       arbitrary client hex is always legible. Hue survives, contrast is
       guaranteed.
     Lane 3 --tint-soft: the accent at low alpha, for washes and chip fills.
     --accent-ink auto-flips between near-black and near-white depending on
       how light the injected client colour is. */
  --accent: #111114;
  --accent-ink: oklch(from var(--accent) clamp(0.12, (0.65 - l) * 1000, 0.98) 0.008 h);
  --tint-text: oklch(from var(--accent) 0.47 min(c, 0.16) h);
  --tint-soft: color-mix(in srgb, var(--accent) 14%, var(--surface));

  /* --- Semantics --------------------------------------------------------
     Three states, not four. There is no warning colour in this product: the
     client tint owns orange, and a tint that also reads as a status is a tint
     that lies. The middle state is carried by weight and type instead. */
  --positive: #34C759;
  --positive-text: #17752D;
  --positive-soft: #E2F9E8;

  --critical: #FF3B30;
  --critical-text: #D70015;
  --critical-soft: #FFE5E3;

  --scrim: rgba(0, 0, 0, 0.32);
  --nav-bg: color-mix(in srgb, var(--bg-grouped) 80%, transparent);

  /* --- systemGray, for anything the ramp above does not cover ----------- */
  --gray-1: #8E8E93;
  --gray-2: #AEAEB2;
  --gray-3: #C7C7CC;
  --gray-4: #D1D1D6;
  --gray-5: #E5E5EA;
  --gray-6: #F2F2F7;

  /* --- Elevation --------------------------------------------------------
     Cards are flat. Elevation exists in exactly three places: the sheet-modal
     top edge, the pushed detail view's leading edge, and the toast. The
     segmented-control thumb carries the one soft shadow the HIG gives it. */
  --shadow-lead: -8px 0 24px rgba(0, 0, 0, 0.12);
  /* --shadow-modal biases upward: it lifts the top edge of a sheet that has
     slid up from the bottom of a phone. --shadow-sheet biases downward, for
     the same component in its centred desktop presentation. */
  --shadow-modal: 0 -2px 28px rgba(0, 0, 0, 0.16);
  --shadow-sheet: 0 12px 40px rgba(0, 0, 0, 0.14);
  --shadow-toast: 0 8px 28px rgba(0, 0, 0, 0.18);
  --shadow-thumb: 0 3px 8px rgba(0, 0, 0, 0.12);

  /* --- Spacing. Nothing off-scale. ------------------------------------- */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 20px;
  --s6: 24px;
  --s7: 32px;
  --s8: 44px;

  /* --- Radii ------------------------------------------------------------ */
  --r-sheet: 10px;
  --r-sheet-modal: 16px;
  --r-button: 12px;
  --r-button-lg: 14px;
  --r-button-sm: 8px;
  --r-photo: 8px;
  --r-input: 10px;
  --r-chip: 999px;
  --r-alert: 14px;

  /* --- Rail-and-Sheet geometry ------------------------------------------
     --inset is the sheet's distance from the viewport edge. --rail is the
     fixed leading column inside every row. Override --rail per sheet, never
     per row: the alignment is the whole point of the pattern. */
  --inset: var(--s4);
  --rail: 68px;
  --rail-wide: 84px;

  /* --- Motion -----------------------------------------------------------
     No bounce, no elastic, no overshoot anywhere. A spring that overshoots on
     a work-order app reads as a toy. */
  --ease-out: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-in: cubic-bezier(0.40, 0.00, 1, 1);
  --ease-glide: cubic-bezier(0.25, 1.00, 0.5, 1);

  --dur-press: 120ms;
  --dur-release: 260ms;
  --dur-micro: 160ms;
  --dur-fade: 200ms;
  --dur-enter: 260ms;
  --dur-view: 380ms;
  --dur-view-out: 340ms;
  --dur-sheet: 420ms;
  --dur-sheet-out: 300ms;
  /* Row wash release. Instant on, slow off: the single most recognisable iOS
     touch behaviour, and the one most often built symmetrical by mistake. */
  --dur-wash-out: 320ms;

  /* --- Safe areas ------------------------------------------------------- */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
}

@media (min-width: 768px) {
  :root {
    --inset: var(--s5);
  }
}

/* ==========================================================================
   2. Tokens: dark theme
   Ground and surface measure only 1.18:1 apart. That is correct: in iOS dark
   mode cards separate from the ground by radius and hairline, not luminance.
   The block is written twice because a selector list cannot straddle a media
   query. Keep the two copies identical.
   ========================================================================== */

:root[data-theme="dark"] {
  --scheme: dark;

  --bg-grouped: #08080A;
  --surface: #1C1C1E;
  --surface-2: #27272A;
  --surface-3: #2C2C30;

  --label: #F5F5F7;
  --label-2: #98989F;
  --label-3: #6C6C70;

  --separator: rgba(84, 84, 88, 0.60);
  --separator-op: #3E3E41;

  --accent: #F5F5F7;
  /* The chroma cap is 0.09 here against 0.16 in light, tighter than the brief's
     first cut of 0.14. Reason: the two lanes were inverting the hierarchy. The
     light lane resolves dark and RECEDES behind the primary label; the dark
     lane resolves bright and ADVANCES in front of it, so the same layout put
     the eye on the customer name in one theme and on the buttons in the other.
     Lightness is untouched at 0.80, so every contrast guarantee the brief
     measured still holds; only the vividness comes down. */
  --tint-text: oklch(from var(--accent) 0.80 min(c, 0.09) h);
  --tint-soft: color-mix(in srgb, var(--accent) 20%, var(--surface));

  --positive: #30D158;
  --positive-text: #30D158;
  --positive-soft: color-mix(in srgb, var(--positive) 20%, var(--surface));

  --critical: #FF453A;
  --critical-text: #FF453A;
  --critical-soft: color-mix(in srgb, var(--critical) 20%, var(--surface));

  --scrim: rgba(0, 0, 0, 0.48);
  --nav-bg: color-mix(in srgb, var(--bg-grouped) 72%, transparent);

  --gray-1: #8E8E93;
  --gray-2: #636366;
  --gray-3: #48484A;
  --gray-4: #3A3A3C;
  --gray-5: #2C2C2E;
  --gray-6: #1C1C1E;

  /* The dark shadow leads with a light hairline. A 0.44 black cast onto a
     #08080A ground darkens it by about 3.5 of 255, which is nothing: the
     boundary between the dimmed list and the incoming detail pane was an
     unlit cut, in the theme built for 6am and 9pm truck use. */
  --shadow-lead: -0.5px 0 0 rgba(255, 255, 255, 0.12), -8px 0 24px rgba(0, 0, 0, 0.50);
  --shadow-modal: 0 -2px 28px rgba(0, 0, 0, 0.52);
  --shadow-sheet: 0 12px 40px rgba(0, 0, 0, 0.55);
  --shadow-toast: 0 8px 28px rgba(0, 0, 0, 0.56);
  --shadow-thumb: 0 3px 8px rgba(0, 0, 0, 0.40);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --scheme: dark;

    --bg-grouped: #08080A;
    --surface: #1C1C1E;
    --surface-2: #27272A;
    --surface-3: #2C2C30;

    --label: #F5F5F7;
    --label-2: #98989F;
    --label-3: #6C6C70;

    --separator: rgba(84, 84, 88, 0.60);
    --separator-op: #3E3E41;

    --accent: #F5F5F7;
    --tint-text: oklch(from var(--accent) 0.80 min(c, 0.09) h);
    --tint-soft: color-mix(in srgb, var(--accent) 20%, var(--surface));

    --positive: #30D158;
    --positive-text: #30D158;
    --positive-soft: color-mix(in srgb, var(--positive) 20%, var(--surface));

    --critical: #FF453A;
    --critical-text: #FF453A;
    --critical-soft: color-mix(in srgb, var(--critical) 20%, var(--surface));

    --scrim: rgba(0, 0, 0, 0.48);
    --nav-bg: color-mix(in srgb, var(--bg-grouped) 72%, transparent);

    --gray-1: #8E8E93;
    --gray-2: #636366;
    --gray-3: #48484A;
    --gray-4: #3A3A3C;
    --gray-5: #2C2C2E;
    --gray-6: #1C1C1E;

    /* Light hairline first: see the note in the data-theme block above. */
    --shadow-lead: -0.5px 0 0 rgba(255, 255, 255, 0.12), -8px 0 24px rgba(0, 0, 0, 0.50);
    --shadow-modal: 0 -2px 28px rgba(0, 0, 0, 0.52);
    --shadow-sheet: 0 12px 40px rgba(0, 0, 0, 0.55);
    --shadow-toast: 0 8px 28px rgba(0, 0, 0, 0.56);
    --shadow-thumb: 0 3px 8px rgba(0, 0, 0, 0.40);
  }
}

/* Relative colour syntax is the live derivation of the text lane from the
   client accent. Engines without it degrade to the neutral label colour, which
   reads as "no tint" rather than as some other client's brand. Never hardcode a
   specific client's measured hex here: this file ships to every client, and a
   wrong-brand tint is a worse failure than an absent one. */
@supports not (color: oklch(from red l c h)) {
  :root {
    --accent-ink: #111114;
    --tint-text: #111114;
  }
  :root[data-theme="dark"] {
    --tint-text: #F5F5F7;
  }
}
@media (prefers-color-scheme: dark) {
  @supports not (color: oklch(from red l c h)) {
    :root:not([data-theme="light"]) {
      --tint-text: #F5F5F7;
    }
  }
}

/* ==========================================================================
   3. Reset
   ========================================================================== */

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

/* Load-bearing, do not drop in a restyle: author rules elsewhere in this
   codebase set `display` on elements that ship with the `hidden` attribute
   (form, #quote-builder, #toast), and an author declaration outranks the UA
   stylesheet's [hidden] rule. Without this, hidden = true does nothing.
   Nothing in this file may reintroduce a `display` rule that defeats it. */
[hidden] {
  display: none !important;
}

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

body {
  margin: 0;
  font: var(--t-body);
  letter-spacing: var(--tr-body);
  background: var(--bg-grouped);
  color: var(--label);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Kills the 300ms tap delay without disabling pinch-zoom. */
  touch-action: manipulation;
  /* Replaced by our own row wash, which we control the timing of. */
  -webkit-tap-highlight-color: transparent;
}

h1, h2, h3, h4, p, figure, blockquote, dl, dd {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

img, svg, video, canvas {
  display: block;
  max-width: 100%;
}

button, input, select, textarea {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
}

button {
  cursor: pointer;
}

a {
  color: var(--tint-text);
  text-decoration: none;
}

/* :where() keeps specificity at zero so any face can override the ring. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--tint-text);
  outline-offset: 2px;
}

/* ==========================================================================
   4. Type ramp utilities
   Each class sets the `font` shorthand plus its tracking. The shorthand also
   resets font-family and font-variant-numeric, which is why .tl-round and
   .tl-tabular are declared AFTER the ramp: equal specificity, later wins.
   ========================================================================== */

.tl-large-title { font: var(--t-large-title); letter-spacing: var(--tr-large-title); }
.tl-title1 { font: var(--t-title1); letter-spacing: var(--tr-title1); }
.tl-title2 { font: var(--t-title2); letter-spacing: var(--tr-title2); }
.tl-title3 { font: var(--t-title3); letter-spacing: var(--tr-title3); }
.tl-headline { font: var(--t-headline); letter-spacing: var(--tr-headline); }
.tl-body { font: var(--t-body); letter-spacing: var(--tr-body); }
.tl-callout { font: var(--t-callout); letter-spacing: var(--tr-callout); }
.tl-subhead { font: var(--t-subhead); letter-spacing: var(--tr-subhead); }
.tl-footnote { font: var(--t-footnote); letter-spacing: var(--tr-footnote); }
.tl-caption { font: var(--t-caption); letter-spacing: var(--tr-caption); }

/* SF Pro Rounded on Apple platforms. Restricted to three uses: the rail
   token, money amounts, and count badges. Everywhere else uses --font-text. */
.tl-round { font-family: var(--font-round); }

/* Mandatory on rail tokens, money, ages, counts, times and quote lines.
   Nothing that updates on a 60s poll may reflow because a digit changed. */
.tl-tabular { font-variant-numeric: tabular-nums; }

.tl-balance { text-wrap: balance; }

/* ==========================================================================
   5. Colour utilities
   ========================================================================== */

.tl-c-label { color: var(--label); }
.tl-c-label-2 { color: var(--label-2); }
/* Placeholders and disclosure chevrons only, 15px or larger, desk face only. */
.tl-c-label-3 { color: var(--label-3); }
.tl-c-tint { color: var(--tint-text); }
.tl-c-positive { color: var(--positive-text); }
.tl-c-critical { color: var(--critical-text); }

/* Chrome does not select. Content the user copies does. */
.tl-chrome {
  -webkit-user-select: none;
  user-select: none;
}

/* Phone numbers, addresses, quote line items. Micah copies those. */
.tl-selectable {
  -webkit-user-select: text;
  user-select: text;
}

/* ==========================================================================
   6. Scroll containers and safe areas
   ========================================================================== */

.tl-scroll {
  overflow-y: auto;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
}

.tl-safe-x {
  padding-inline: max(var(--inset), var(--safe-l)) max(var(--inset), var(--safe-r));
}

.tl-safe-b {
  padding-bottom: max(var(--s4), var(--safe-b));
}

.tl-safe-t {
  padding-top: var(--safe-t);
}

/* ==========================================================================
   7. Rail-and-Sheet
   The alignment law: inside any sheet, the row's content column, the group
   header's text, and the left edge of every hairline separator all sit at the
   same x, `16px + var(--rail)`. The last row of a sheet has no separator.
   ========================================================================== */

.tl-group {
  padding-inline: max(var(--inset), var(--safe-l)) max(var(--inset), var(--safe-r));
  margin-block-end: var(--s6);
}

.tl-group-header {
  font: var(--t-footnote);
  letter-spacing: var(--tr-footnote);
  color: var(--label-2);
  /* Sentence case. Uppercase letterspaced headers are the loudest dated tell
     in the old CSS; modern iOS abandoned them. */
  text-transform: none;
  padding-inline-start: calc(var(--s4) + var(--rail));
  margin-block-end: 6px;
}

.tl-sheet {
  background: var(--surface);
  border-radius: var(--r-sheet);
  /* No border and no box-shadow. Separation is ground versus surface plus
     0.5px internal hairlines. */
  overflow: hidden;
}

@supports (corner-shape: squircle) {
  .tl-sheet { corner-shape: squircle; }
}

/* Wide rail for money amounts over $9,999. Applied to the whole sheet, never
   to one row: truncation inside the rail is forbidden. */
.tl-sheet[data-rail="wide"] { --rail: var(--rail-wide); }
/* Money sheets have no rail. The amount lives in the trailing slot instead. */
.tl-sheet[data-rail="none"] { --rail: 0px; }

.tl-row {
  position: relative;
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr) auto;
  grid-template-areas: "rail content trailing";
  align-items: center;
  min-height: 44px;
  padding-block: 11px;
  padding-inline: var(--s4);
  background: transparent;
  /* Instant on, slow off. The transition governs the release only; :active
     sets duration 0 so the wash lands the moment the finger does. */
  transition: background-color var(--dur-wash-out) var(--ease-glide);
}

.tl-row[data-lines="2"] { min-height: 60px; }
.tl-row[data-size="field"] { min-height: 72px; }

/* Hairline at the top of every row except the first, so the last row of a
   sheet has no separator by construction rather than by an :last-child rule
   that a re-render can get wrong. */
.tl-row + .tl-row::before {
  content: "";
  position: absolute;
  top: 0;
  inset-inline: calc(var(--s4) + var(--rail)) 0;
  height: 0.5px;
  background: var(--separator);
  pointer-events: none;
}

/* Engines that round 0.5px up to a whole pixel get the scaled-box hairline
   instead. A 1px border on a 3x display is a 1990s rule. */
@media (resolution: 1dppx) {
  .tl-row + .tl-row::before {
    height: 1px;
    transform: scaleY(0.5);
    transform-origin: top;
  }
}

.tl-rail {
  grid-area: rail;
  font: var(--t-headline);
  letter-spacing: var(--tr-headline);
  font-family: var(--font-round);
  font-variant-numeric: tabular-nums;
  color: var(--label);
  text-align: start;
  align-self: start;
  padding-inline-end: var(--s2);
}

.tl-row[data-size="field"] .tl-rail {
  font: var(--t-title3);
  letter-spacing: var(--tr-title3);
  font-family: var(--font-round);
  font-variant-numeric: tabular-nums;
}

.tl-row-content {
  grid-area: content;
  min-width: 0;
}

.tl-row-primary {
  font: var(--t-headline);
  letter-spacing: var(--tr-headline);
  color: var(--label);
}

/* The middle state that would have been orange is encoded in weight, not hue.
   Fresh reads secondary regular, warming reads primary semibold. */
.tl-row-secondary {
  font: var(--t-subhead);
  letter-spacing: var(--tr-subhead);
  color: var(--label-2);
}

.tl-row-trailing {
  grid-area: trailing;
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding-inline-start: var(--s3);
}

/* Rows with actions. The action group sits below the two content lines,
   inside the content column, as 44px iOS text buttons. Never behind hover:
   this face ships to a phone. */
.tl-row[data-expanded="1"] {
  grid-template-areas:
    "rail content trailing"
    ".    actions actions";
  align-items: start;
}

.tl-row-actions {
  grid-area: actions;
  display: flex;
  gap: var(--s4);
  margin-block-start: var(--s1);
}

/* Interactive rows take the wash. Rows never scale. */
.tl-row-tap {
  -webkit-user-select: none;
  user-select: none;
  cursor: pointer;
}

.tl-row-tap:active {
  background: var(--surface-2);
  transition-duration: 0ms;
}

/* ==========================================================================
   8. Chips
   Status chips are the one contrast exemption, and only where the same
   information is also present in the row's text.
   ========================================================================== */

.tl-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  padding: 3px var(--s2);
  border-radius: var(--r-chip);
  font: var(--t-footnote);
  letter-spacing: var(--tr-footnote);
  font-weight: 600;
  background: var(--surface-2);
  color: var(--label-2);
  white-space: nowrap;
}

.tl-chip[data-tone="tint"] { background: var(--tint-soft); color: var(--tint-text); }
.tl-chip[data-tone="positive"] { background: var(--positive-soft); color: var(--positive-text); }
.tl-chip[data-tone="critical"] { background: var(--critical-soft); color: var(--critical-text); }

.tl-chip-count {
  font-family: var(--font-round);
  font-variant-numeric: tabular-nums;
}

/* A filled dot, for status that needs a mark rather than a word. */
.tl-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--r-chip);
  background: var(--accent);
  flex: none;
}

.tl-dot[data-tone="positive"] { background: var(--positive); }
.tl-dot[data-tone="critical"] { background: var(--critical); }
.tl-dot[data-tone="neutral"] { background: var(--gray-2); }

/* ==========================================================================
   9. Buttons and press states
   Buttons scale. The amount is size-dependent, because a large element
   scaling 0.96 looks like it is collapsing.
   ========================================================================== */

.tl-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 44px;
  padding-inline: var(--s4);
  border: none;
  border-radius: var(--r-button);
  background: transparent;
  color: var(--tint-text);
  font: var(--t-headline);
  letter-spacing: var(--tr-headline);
  -webkit-user-select: none;
  user-select: none;
  /* No translateZ(0) here. It put 34 of the desk face's 39 buttons on a
     permanent compositor layer for nothing (a none-to-scale() transition
     composites on its own the moment it starts) and changed how their text
     rasterised the whole time they sat still. */
  transition:
    transform var(--dur-release) var(--ease-glide),
    opacity var(--dur-release) var(--ease-glide),
    background-color var(--dur-micro) var(--ease-glide);
}

@supports (corner-shape: squircle) {
  .tl-btn { corner-shape: squircle; }
}

.tl-btn:active {
  transform: scale(0.96);
  transition-duration: var(--dur-press), var(--dur-press), var(--dur-micro);
  transition-timing-function: var(--ease-out), var(--ease-out), var(--ease-glide);
}

/* iOS text buttons DIM. They do not scale, and the reason is arithmetic: 0.96
   on a 17px label with no fill moves its edge by seven tenths of a pixel, which
   is nothing. Every callback action and every money action is one of these, so
   until now the most-pressed controls in the product gave back almost nothing.
   The scale stays for the tiers that have a box to see move; the opacity is
   what actually answers the finger, on the same asymmetric clock as everything
   else: down in 120ms, back over 260ms. */
.tl-btn-text:not(.tl-btn-filled):not(.tl-btn-tinted):active,
.tl-btn-quiet:active,
.tl-btn-sm:not(.tl-btn-filled):not(.tl-btn-tinted):active {
  opacity: 0.35;
}

.tl-btn:disabled {
  opacity: 0.4;
  transform: none;
}

/* Lane 1: the accent is a fill, and the label on it is --accent-ink. */
.tl-btn-filled {
  background: var(--accent);
  color: var(--accent-ink);
}

.tl-btn-filled:active {
  opacity: 0.86;
}

.tl-btn-tinted {
  background: var(--tint-soft);
  color: var(--tint-text);
}

/* A control with a fill dims as well as scales, which is the brief's rule for
   filled buttons read for what it is about: the fill is the thing the eye
   watches, and 0.86 is what makes it look pressed rather than merely smaller. */
.tl-btn-tinted:active {
  opacity: 0.86;
}

/* A plain text button. No fill, no border, no chrome. */
.tl-btn-text {
  padding-inline: 0;
  border-radius: var(--r-button-sm);
}

/* The quiet tier, and the reason it has to exist: .tl-btn paints --tint-text at
   headline weight, so without a step below it EVERY action in the product
   renders at maximum emphasis. Four actions in one row all shouted equally, and
   the accent covered a quarter to a third of the text on screen against a
   Restrained strategy that allows a tenth. iOS puts one tinted control in a
   row, not four. Emphasis steps down by colour and weight together, which is
   the same mechanism the palette already uses for lead heat, so the tier
   survives a colour-blind reader and direct sun. */
.tl-btn-quiet {
  color: var(--label);
  font: var(--t-body);
  letter-spacing: var(--tr-body);
}

/* Small: under 44px tall, so it scales further. Desk face only, and only for
   money-row mini buttons. */
.tl-btn-sm {
  min-height: 36px;
  padding-inline: var(--s3);
  border-radius: var(--r-button-sm);
  font: var(--t-subhead);
  letter-spacing: var(--tr-subhead);
  font-weight: 600;
}

.tl-btn-sm:active { transform: scale(0.94); }

/* Large: the 56px field Done button. */
.tl-btn-lg {
  width: 100%;
  min-height: 56px;
  border-radius: var(--r-button-lg);
  font-weight: 600;
}

.tl-btn-lg:active { transform: scale(0.97); }

/* A 44x44 hit area around a 22px glyph, for nav trailing slots. */
.tl-btn-glyph {
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  padding-inline: 0;
  border-radius: var(--r-chip);
}

/* Every control is at least 44x44 on a phone. Desk at >= 768px may drop to
   36px for money-row mini buttons only, which is what .tl-btn-sm is for. */
@media (max-width: 767px) {
  .tl-btn-sm { min-height: 44px; }
}

/* ==========================================================================
   10. Icons
   Geometry lives in icons.js. This layer only sizes and colours them.
   ========================================================================== */

.tl-icon {
  width: 22px;
  height: 22px;
  flex: none;
  color: currentColor;
}

.tl-icon[data-size="glyph"] { width: 22px; height: 22px; }
.tl-icon[data-size="empty"] { width: 44px; height: 44px; }

/* The disclosure chevron is the one glyph heavier than the family, because
   iOS's is too. It never takes the tint: it is chrome, not an action. */
.tl-chevron {
  width: 8px;
  height: 13px;
  flex: none;
  color: var(--label-3);
}

:root[data-theme="dark"] .tl-chevron { color: var(--separator-op); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tl-chevron { color: var(--separator-op); }
}

/* ==========================================================================
   11. Empty states
   One 44px glyph, a title3 headline, one subhead line. No illustration.
   ========================================================================== */

.tl-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s2);
  padding: var(--s7) var(--s4);
  text-align: center;
  color: var(--label-3);
}

.tl-empty-title {
  font: var(--t-title3);
  letter-spacing: var(--tr-title3);
  color: var(--label);
}

.tl-empty-note {
  font: var(--t-subhead);
  letter-spacing: var(--tr-subhead);
  color: var(--label-2);
  max-width: 34ch;
}

/* A section-level empty: one group inside an otherwise populated screen. Same
   idiom, sized for a sheet rather than for a whole face, and without the glyph,
   which at 44px would outweigh the fact it is reporting. Its reason to exist is
   that one screen was carrying three different ways of saying "nothing here". */
.tl-empty[data-size="compact"] {
  padding: var(--s5) var(--s4);
}

/* ==========================================================================
   12. List-item entrance and exit
   Motion lives on interaction and on genuine state change, never on render.
   A render function marks only nodes whose id it has not painted before, and
   removes the attribute on animationend. A poll that finds an unchanged id
   set must produce zero animation and zero attribute writes.

   The exit is the same rule read backwards: it plays only for a removal the
   user just caused, never for a repaint that happens to be missing a row. It
   arrives over --dur-enter and leaves over --dur-fade, which is the asymmetry
   the whole motion system is built on. Transform and opacity only, so the
   layout-property ban holds: the rows below close the gap in one frame, which
   is the honest trade for never animating height.
   ========================================================================== */

@keyframes tl-enter {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes tl-exit {
  from { opacity: 1; transform: translateY(0); }
  to { opacity: 0; transform: translateY(-4px); }
}

[data-enter="1"] {
  animation: tl-enter var(--dur-enter) var(--ease-out) both;
}

/* A departing node stops being a target the moment it starts leaving, so a
   second press cannot fire an action against something already gone. */
[data-exit="1"] {
  animation: tl-exit var(--dur-fade) var(--ease-in) both;
  pointer-events: none;
}

/* ==========================================================================
   13. Reduced motion
   All transform-based motion is removed. Durations clamp to 1ms except
   opacity fades, which cap at 100ms. Colour and opacity survive, because
   they carry meaning.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-press: 1ms;
    --dur-release: 1ms;
    --dur-enter: 1ms;
    --dur-view: 1ms;
    --dur-view-out: 1ms;
    --dur-sheet: 1ms;
    --dur-sheet-out: 1ms;
    --dur-fade: 100ms;

    /* These two are held, not clamped. Both drive colour: the row wash and the
       checkbox fill. Clamping --dur-wash-out to 1ms turned the most
       recognisable iOS touch in the product from instant-on/slow-off into
       instant-on/instant-off, which is a hard flash rather than reduced motion.
       This document's own rule says colour changes survive under reduce
       because they carry meaning, and a background fade is paint, not
       movement. Everything --dur-micro drives that IS movement (the segmented
       thumb, the nav hairline, the checkmark scale) is pinned separately, here
       and in each face's own reduced block. */
    --dur-wash-out: 200ms;
    --dur-micro: 200ms;
  }

  .tl-btn:active,
  .tl-btn-sm:active,
  .tl-btn-lg:active {
    transform: none;
  }

  /* Press states fall back to the background wash alone. */
  .tl-btn:active {
    background-color: var(--surface-2);
  }

  .tl-btn-filled:active {
    background-color: var(--accent);
  }

  [data-enter="1"] {
    animation-name: none;
  }

  /* The exit keeps its opacity fade, clamped with every other fade: a row
     vanishing between frames is the confusion this animation exists to stop,
     and an opacity change is not movement. */
  [data-exit="1"] {
    animation-name: tl-exit-reduced;
  }

  @keyframes tl-exit-reduced {
    from { opacity: 1; }
    to { opacity: 0; }
  }
}
