/* ===== src/styles/tokens.css ===== */
/* ==========================================================================
   Design tokens — Apple Human Interface Guidelines inspired
   --------------------------------------------------------------------------
   Colour values follow the iOS system palette so the app feels native next to
   Apple's own apps. Every token is declared on bare :root (light), then
   redefined for dark in two places: the system-preference media query and the
   explicit [data-theme] attribute, so a manual toggle wins in both directions.
   ========================================================================== */

:root {
  /* --- Interface scale ---------------------------------------------------- */
  /* One number, set from Settings, that carries the whole interface with it.
     The type ramp and the spacing scale below are both expressed in rem, so
     moving the root font size moves padding, gaps and control heights in step
     with the letters — enlarging only the glyphs would leave them cramped
     inside rows still sized for the old ramp. */
  --text-scale: 1;

  /* WHAT 100% MEANS. The percentage above is the reader's own choice and 100
     is its middle; this is the size the app draws at when they have not asked
     for anything — and it is the size the app used to draw at 90%, which is
     the density the owner settled on after living with both. Moving it here
     rather than re-basing the percentage keeps every stored preference
     honest: somebody who chose 110% still gets a tenth more than the default,
     and the number under the slider still means what it says.

     One definition, multiplied into the root font size in base.css and
     nowhere else, so there is no second copy to drift. */
  --text-scale-base: 0.9;

  /* --- Typography -------------------------------------------------------- */
  /* San Francisco when available, falling back through the platform stack.
     No webfont is loaded: system fonts render instantly and stay offline. */
  --font-text: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
    "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial,
    sans-serif;
  --font-rounded: ui-rounded, -apple-system, BlinkMacSystemFont, "SF Pro Rounded",
    "Segoe UI Variable Display", "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  /* Tabular figures matter in a money app: columns of numbers must align. */
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono", Menlo,
    Consolas, "Liberation Mono", monospace;

  /* iOS type ramp (size / line-height / weight / tracking) */
  --type-large-title: 700 2.125rem/1.2 var(--font-text);   /* 34px */
  --type-title1: 700 1.75rem/1.25 var(--font-text);        /* 28px */
  --type-title2: 700 1.375rem/1.27 var(--font-text);       /* 22px */
  --type-title3: 600 1.25rem/1.3 var(--font-text);         /* 20px */
  --type-headline: 600 1.0625rem/1.4 var(--font-text);     /* 17px */
  --type-body: 400 1.0625rem/1.47 var(--font-text);        /* 17px */
  --type-callout: 400 1rem/1.5 var(--font-text);           /* 16px */
  --type-subhead: 400 0.9375rem/1.4 var(--font-text);      /* 15px */
  --type-footnote: 400 0.8125rem/1.38 var(--font-text);    /* 13px */
  --type-caption1: 400 0.75rem/1.33 var(--font-text);      /* 12px */
  --type-caption2: 400 0.6875rem/1.27 var(--font-text);    /* 11px */

  --tracking-tight: -0.022em;
  --tracking-normal: -0.01em;
  --tracking-wide: 0.02em;
  /* Uppercase section headers in grouped lists */
  --tracking-caps: 0.06em;

  /* --- Spacing ----------------------------------------------------------- */
  /* 4pt grid, matching Apple's layout rhythm. */
  --space-0: 0;
  --space-1: 0.25rem;   /* 4  */
  --space-2: 0.5rem;    /* 8  */
  --space-3: 0.75rem;   /* 12 */
  --space-4: 1rem;      /* 16 — standard iOS content inset */
  --space-5: 1.25rem;   /* 20 */
  --space-6: 1.5rem;    /* 24 */
  --space-8: 2rem;      /* 32 */
  --space-10: 2.5rem;   /* 40 */
  --space-12: 3rem;     /* 48 */
  --space-16: 4rem;     /* 64 */

  /* Layout rhythm that answers the viewport instead of a fixed grid.
     A narrow phone keeps its gutters tight so a table still has room for its
     columns; a wide desktop is given air. `clamp` interpolates across the
     whole range rather than jumping at a breakpoint, so a window being
     dragged wider breathes out smoothly, and because the endpoints are in
     rem the interface scale carries them too. */
  --gutter: clamp(1rem, 0.55rem + 1.6vw, 2rem);
  --section-gap: clamp(1.5rem, 1rem + 1.8vw, 2.75rem);
  --card-pad: clamp(0.875rem, 0.7rem + 0.7vw, 1.375rem);

  /* --- Radii ------------------------------------------------------------- */
  /* Apple's continuous ("squircle") corners read softer than a plain arc, so
     the radii run a little larger than a typical web scale. */
  --radius-xs: 6px;
  --radius-sm: 8px;
  --radius-md: 10px;
  --radius-lg: 14px;    /* grouped list container */
  --radius-xl: 18px;    /* cards */
  --radius-2xl: 22px;   /* sheets / modals */
  --radius-full: 999px; /* pills, capsules */

  /* --- Layout ------------------------------------------------------------ */
  --nav-height: 44px;          /* iOS navigation bar */
  --nav-height-large: 96px;    /* with large title */
  --tabbar-height: 49px;       /* iOS tab bar */
  --sidebar-width: 220px;
  --content-max: 1200px;
  --hairline: 0.5px;           /* iOS separators are sub-pixel */

  /* Safe-area insets so the app clears the notch and home indicator. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  /* --- Motion ------------------------------------------------------------ */
  /* Apple's default curve is a gentle ease-out; springs for interactive bits. */
  --ease-out: cubic-bezier(0.25, 0.1, 0.25, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --duration-fast: 150ms;
  --duration-base: 250ms;
  --duration-slow: 350ms;

  /* --- Z-index ----------------------------------------------------------- */
  --z-base: 0;
  --z-sticky: 10;
  --z-nav: 100;
  --z-tabbar: 100;
  --z-sheet: 200;
  --z-toast: 300;

  /* ======================================================================
     LIGHT THEME (default)
     iOS system colours, light appearance.
     ====================================================================== */

  /* Accents — the iOS system palette */
  --color-blue: #007aff;
  --color-green: #34c759;
  --color-indigo: #5856d6;
  --color-orange: #ff9500;
  --color-pink: #ff2d55;
  --color-purple: #af52de;
  --color-red: #ff3b30;
  --color-teal: #30b0c7;
  --color-yellow: #ffcc00;
  --color-mint: #00c7be;
  --color-brown: #a2845e;
  --color-cyan: #32ade6;
  --color-graphite: #6e6e78;

  /* Backgrounds. iOS "grouped" backgrounds invert the relationship between
     page and card: the page is grey and cards are white. */
  --bg-base: #f2f2f7;            /* systemGroupedBackground */
  --bg-elevated: #ffffff;        /* secondarySystemGroupedBackground (cards) */
  --bg-elevated-2: #f2f2f7;      /* tertiary — nested inside a card */
  --bg-input: #ffffff;
  --bg-hover: rgba(0, 0, 0, 0.04);
  --bg-active: rgba(0, 0, 0, 0.08);
  --bg-selected: rgba(0, 122, 255, 0.1);
  --bg-scrim: rgba(0, 0, 0, 0.4);

  /* Chrome (nav bar / tab bar / sidebar) uses a translucent material. */
  --material-chrome: rgba(249, 249, 249, 0.94);
  --material-sidebar: rgba(242, 242, 247, 0.8);
  --material-thick: rgba(255, 255, 255, 0.92);
  --blur-chrome: saturate(180%) blur(20px);

  /* Labels — the four iOS label levels */
  --text-primary: #000000;
  --text-secondary: rgba(60, 60, 67, 0.6);
  --text-tertiary: rgba(60, 60, 67, 0.3);
  --text-quaternary: rgba(60, 60, 67, 0.18);
  --text-on-accent: #ffffff;
  --text-link: var(--color-blue);

  /* Separators */
  --separator: rgba(60, 60, 67, 0.29);      /* non-opaque, hairline */
  --separator-opaque: #c6c6c8;
  --border-subtle: rgba(60, 60, 67, 0.12);

  /* Fills — for controls that sit on a background */
  --fill-primary: rgba(120, 120, 128, 0.2);
  --fill-secondary: rgba(120, 120, 128, 0.16);
  --fill-tertiary: rgba(118, 118, 128, 0.12);
  --fill-quaternary: rgba(116, 116, 128, 0.08);

  /* --- Cells ---------------------------------------------------------- */
  /* One size and one inset for everything inside a table, so a figure you
     typed and a figure the sheet came with are the same size, the same font
     and on the same vertical line. Read by both the table and the fields
     inside it; changing either alone is what made typed text look imported
     from somewhere else.

     `--cell-pad-x` is the field's own horizontal padding. The cell holding a
     field subtracts it, so the *text* lines up with the text of a plain cell
     beside it while the field keeps its own visible box. */
  --cell-size: 0.9375rem;          /* 15px, the subhead step */
  --cell-pad-x: var(--space-2);
  --cell-gutter: var(--space-3);   /* a table cell's own horizontal padding */

  /* What a field needs *around* its digits before any of them show: its own
     padding on both sides and its hairline border. A field sized to the digits
     alone clips the last one off every figure in it — which is what a long
     number in the monthly log used to do.

     The field's own chrome and nothing else. The padding between the column's
     edge and the field is the table's business, and a table asked to fit its
     contents already adds it when working out how wide the column has to be. */
  --num-fit-slack: calc(2 * var(--cell-pad-x) + 2px);

  /* Editable cells.
     Grey by default: it says "type here" without colouring a page that is
     mostly boxes. Written in terms of the fill scale, so it follows the theme
     without either dark block having to restate it. Yellow — the workbook's
     own legend colour — is one of the choices below. */
  --editable-bg: var(--fill-quaternary);
  --editable-bg-hover: var(--fill-tertiary);
  --editable-border: var(--border-subtle);

  /* Semantic — money in, money out, warnings */
  --color-positive: #34c759;
  --color-negative: #ff3b30;
  --color-warning: #ff9500;
  --color-neutral: rgba(60, 60, 67, 0.6);
  /* Tinted backgrounds for badges and pills */
  --tint-positive: rgba(52, 199, 89, 0.14);
  --tint-negative: rgba(255, 59, 48, 0.12);
  --tint-warning: rgba(255, 149, 0, 0.14);
  --tint-accent: rgba(0, 122, 255, 0.12);

  /* Accent role — swap this one token to re-tint the whole app */
  --accent: var(--color-blue);
  --accent-hover: #0071eb;
  --accent-pressed: #0062cc;
  --accent-tint: rgba(0, 122, 255, 0.12);

  /* Shadows. Apple keeps elevation subtle — mostly a tight contact shadow. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 1px rgba(0, 0, 0, 0.03);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.05);
  --shadow-xl: 0 20px 48px rgba(0, 0, 0, 0.16), 0 4px 12px rgba(0, 0, 0, 0.06);
  --shadow-nav: 0 var(--hairline) 0 var(--separator);

  /* Data-visualisation series. Ordered for maximum adjacent contrast and
     kept distinguishable for the common colour-vision deficiencies. */
  --chart-1: #007aff;
  --chart-2: #34c759;
  --chart-3: #ff9500;
  --chart-4: #af52de;
  --chart-5: #ff2d55;
  --chart-6: #30b0c7;
  --chart-7: #ffcc00;
  --chart-8: #5856d6;
  --chart-grid: rgba(60, 60, 67, 0.12);
  --chart-axis: rgba(60, 60, 67, 0.5);
  /* The exercise demonstrations. A body, the iron it is holding and the frame
     around it, each with its own weight so the picture reads at thumbnail size:
     the person darkest, the equipment mid, the furniture quietest. */
  --anat-body: #c3c8cf;
}

/* ==========================================================================
   DARK THEME
   Applied when the OS asks for dark AND the user has not forced light.
   Only the tokens that change are redefined — the ramp above still applies.
   ========================================================================== */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg-base: #000000;
    --bg-elevated: #1c1c1e;
    --bg-elevated-2: #2c2c2e;
    --bg-input: #1c1c1e;
    --bg-hover: rgba(255, 255, 255, 0.06);
    --bg-active: rgba(255, 255, 255, 0.1);
    --bg-selected: rgba(10, 132, 255, 0.2);
    --bg-scrim: rgba(0, 0, 0, 0.6);

    --material-chrome: rgba(30, 30, 32, 0.92);
    --material-sidebar: rgba(28, 28, 30, 0.8);
    --material-thick: rgba(28, 28, 30, 0.94);

    --text-primary: #ffffff;
    --text-secondary: rgba(235, 235, 245, 0.6);
    --text-tertiary: rgba(235, 235, 245, 0.3);
    --text-quaternary: rgba(235, 235, 245, 0.18);

    --separator: rgba(84, 84, 88, 0.6);
    --separator-opaque: #38383a;
    --border-subtle: rgba(235, 235, 245, 0.12);

    --fill-primary: rgba(120, 120, 128, 0.36);
    --fill-secondary: rgba(120, 120, 128, 0.32);
    --fill-tertiary: rgba(118, 118, 128, 0.24);
    --fill-quaternary: rgba(118, 118, 128, 0.18);

    /* iOS brightens accents in dark mode so they hold contrast on black. */
    --color-blue: #0a84ff;
    --color-green: #30d158;
    --color-indigo: #5e5ce6;
    --color-orange: #ff9f0a;
    --color-pink: #ff375f;
    --color-purple: #bf5af2;
    --color-red: #ff453a;
    --color-teal: #40cbe0;
    --color-yellow: #ffd60a;
    --color-mint: #63e6e2;
    --color-brown: #ac8e68;
    --color-cyan: #64d2ff;
    --color-graphite: #98989f;

    --color-positive: #30d158;
    --color-negative: #ff453a;
    --color-warning: #ff9f0a;
    --color-neutral: rgba(235, 235, 245, 0.6);
    --tint-positive: rgba(48, 209, 88, 0.18);
    --tint-negative: rgba(255, 69, 58, 0.18);
    --tint-warning: rgba(255, 159, 10, 0.18);
    --tint-accent: rgba(10, 132, 255, 0.2);

    --accent: var(--color-blue);
    --accent-hover: #3395ff;
    --accent-pressed: #0071e3;
    --accent-tint: rgba(10, 132, 255, 0.18);

    --text-link: var(--color-blue);

    /* On dark, shadows do little work — a light border carries elevation. */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
    --shadow-xl: 0 20px 48px rgba(0, 0, 0, 0.6);

    --chart-1: #0a84ff;
    --chart-2: #30d158;
    --chart-3: #ff9f0a;
    --chart-4: #bf5af2;
    --chart-5: #ff375f;
    --chart-6: #40cbe0;
    --chart-7: #ffd60a;
    --chart-8: #5e5ce6;
    --chart-grid: rgba(235, 235, 245, 0.12);
    --chart-axis: rgba(235, 235, 245, 0.5);
  }
}

/* Explicit dark selection — duplicated so the manual toggle beats the OS. */
:root[data-theme="dark"] {
  --bg-base: #000000;
  --bg-elevated: #1c1c1e;
  --bg-elevated-2: #2c2c2e;
  --bg-input: #1c1c1e;
  --bg-hover: rgba(255, 255, 255, 0.06);
  --bg-active: rgba(255, 255, 255, 0.1);
  --bg-selected: rgba(10, 132, 255, 0.2);
  --bg-scrim: rgba(0, 0, 0, 0.6);

  --material-chrome: rgba(30, 30, 32, 0.92);
  --material-sidebar: rgba(28, 28, 30, 0.8);
  --material-thick: rgba(28, 28, 30, 0.94);

  --text-primary: #ffffff;
  --text-secondary: rgba(235, 235, 245, 0.6);
  --text-tertiary: rgba(235, 235, 245, 0.3);
  --text-quaternary: rgba(235, 235, 245, 0.18);

  --separator: rgba(84, 84, 88, 0.6);
  --separator-opaque: #38383a;
  --border-subtle: rgba(235, 235, 245, 0.12);

  --fill-primary: rgba(120, 120, 128, 0.36);
  --fill-secondary: rgba(120, 120, 128, 0.32);
  --fill-tertiary: rgba(118, 118, 128, 0.24);
  --fill-quaternary: rgba(118, 118, 128, 0.18);

  --color-blue: #0a84ff;
  --color-green: #30d158;
  --color-indigo: #5e5ce6;
  --color-orange: #ff9f0a;
  --color-pink: #ff375f;
  --color-purple: #bf5af2;
  --color-red: #ff453a;
  --color-teal: #40cbe0;
  --color-yellow: #ffd60a;
  --color-mint: #63e6e2;
  --color-brown: #ac8e68;
  --color-cyan: #64d2ff;
  --color-graphite: #98989f;

  --color-positive: #30d158;
  --color-negative: #ff453a;
  --color-warning: #ff9f0a;
  --color-neutral: rgba(235, 235, 245, 0.6);
  --tint-positive: rgba(48, 209, 88, 0.18);
  --tint-negative: rgba(255, 69, 58, 0.18);
  --tint-warning: rgba(255, 159, 10, 0.18);
  --tint-accent: rgba(10, 132, 255, 0.2);

  --accent: var(--color-blue);
  --accent-hover: #3395ff;
  --accent-pressed: #0071e3;
  --accent-tint: rgba(10, 132, 255, 0.18);

  --text-link: var(--color-blue);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
  --shadow-xl: 0 20px 48px rgba(0, 0, 0, 0.6);

  --chart-1: #0a84ff;
  --chart-2: #30d158;
  --chart-3: #ff9f0a;
  --chart-4: #bf5af2;
  --chart-5: #ff375f;
  --chart-6: #40cbe0;
  --chart-7: #ffd60a;
  --chart-8: #5e5ce6;
  --chart-grid: rgba(235, 235, 245, 0.12);
  --chart-axis: rgba(235, 235, 245, 0.5);
}

/* --------------------------------------------------------------------------
   Accent overrides. The app tints itself from a single token, so an accent
   picker only has to set data-accent on the root element.
   -------------------------------------------------------------------------- */
:root[data-accent="green"]  { --accent: var(--color-green);  --accent-tint: var(--tint-positive); }
:root[data-accent="indigo"] { --accent: var(--color-indigo); --accent-tint: rgba(88, 86, 214, 0.14); }
:root[data-accent="orange"] { --accent: var(--color-orange); --accent-tint: var(--tint-warning); }
:root[data-accent="pink"]   { --accent: var(--color-pink);   --accent-tint: rgba(255, 45, 85, 0.14); }
:root[data-accent="purple"] { --accent: var(--color-purple); --accent-tint: rgba(175, 82, 222, 0.14); }
:root[data-accent="teal"]   { --accent: var(--color-teal);   --accent-tint: rgba(48, 176, 199, 0.14); }

/* More of them, all drawn from the same system palette the six above come
   from, so each one already has a light and a dark value and reads on either
   ground. Nothing here is a one-off hex. */
:root[data-accent="mint"]   { --accent: var(--color-mint);   --accent-tint: rgba(0, 199, 190, 0.14); }
:root[data-accent="cyan"]   { --accent: var(--color-cyan);   --accent-tint: rgba(50, 173, 230, 0.14); }
:root[data-accent="red"]    { --accent: var(--color-red);    --accent-tint: var(--tint-negative); }
:root[data-accent="brown"]  { --accent: var(--color-brown);  --accent-tint: rgba(162, 132, 94, 0.16); }
:root[data-accent="graphite"] { --accent: var(--color-graphite); --accent-tint: rgba(110, 110, 120, 0.16); }

/* The hover and pressed shades follow the accent rather than staying blue.
   Left alone, every non-blue accent flashed back to blue on the way down. */
:root[data-accent]:not([data-accent="blue"]) {
  --accent-hover: color-mix(in srgb, var(--accent) 88%, #000);
  --accent-pressed: color-mix(in srgb, var(--accent) 76%, #000);
}

/* --------------------------------------------------------------------------
   The fill behind a cell you can type into

   Grey is the default; the rest are the same system hues, at the same two
   weights, so a field stays a field whichever one is chosen — legible, never loud, and never mistaken for the
   focus ring, which is always the accent.
   -------------------------------------------------------------------------- */
/* The workbook's own legend colour. Dark gets a dimmer, warmer weight: the
   light-mode yellow glares on black. */
:root[data-field-color="yellow"] {
  --editable-bg: rgba(255, 204, 0, 0.14);
  --editable-bg-hover: rgba(255, 204, 0, 0.22);
  --editable-border: rgba(255, 179, 0, 0.35);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"])[data-field-color="yellow"] {
    --editable-bg: rgba(255, 214, 10, 0.13);
    --editable-bg-hover: rgba(255, 214, 10, 0.2);
    --editable-border: rgba(255, 214, 10, 0.3);
  }
}

:root[data-theme="dark"][data-field-color="yellow"] {
  --editable-bg: rgba(255, 214, 10, 0.13);
  --editable-bg-hover: rgba(255, 214, 10, 0.2);
  --editable-border: rgba(255, 214, 10, 0.3);
}

:root[data-field-color="accent"] {
  --editable-bg: color-mix(in srgb, var(--accent) 12%, transparent);
  --editable-bg-hover: color-mix(in srgb, var(--accent) 20%, transparent);
  --editable-border: color-mix(in srgb, var(--accent) 34%, transparent);
}

:root[data-field-color="green"] {
  --editable-bg: color-mix(in srgb, var(--color-green) 13%, transparent);
  --editable-bg-hover: color-mix(in srgb, var(--color-green) 21%, transparent);
  --editable-border: color-mix(in srgb, var(--color-green) 34%, transparent);
}

:root[data-field-color="teal"] {
  --editable-bg: color-mix(in srgb, var(--color-teal) 13%, transparent);
  --editable-bg-hover: color-mix(in srgb, var(--color-teal) 21%, transparent);
  --editable-border: color-mix(in srgb, var(--color-teal) 34%, transparent);
}

:root[data-field-color="purple"] {
  --editable-bg: color-mix(in srgb, var(--color-purple) 12%, transparent);
  --editable-bg-hover: color-mix(in srgb, var(--color-purple) 20%, transparent);
  --editable-border: color-mix(in srgb, var(--color-purple) 32%, transparent);
}

:root[data-field-color="pink"] {
  --editable-bg: color-mix(in srgb, var(--color-pink) 12%, transparent);
  --editable-bg-hover: color-mix(in srgb, var(--color-pink) 20%, transparent);
  --editable-border: color-mix(in srgb, var(--color-pink) 32%, transparent);
}

:root[data-field-color="grey"] {
  --editable-bg: var(--fill-quaternary);
  --editable-bg-hover: var(--fill-tertiary);
  --editable-border: var(--border-subtle);
}

/* No fill at all — the outline alone says a cell is yours to type in. */
:root[data-field-color="none"] {
  --editable-bg: transparent;
  --editable-bg-hover: var(--fill-quaternary);
  --editable-border: var(--border-subtle);
}
/* A touch screen sets the floor at 16px, because iOS Safari zooms the viewport
   whenever a focused field is smaller than that. The whole table steps up
   together rather than the fields alone — a row where the typed figures are a
   pixel larger than the printed ones is exactly the mismatch this token
   exists to prevent. */
@media (pointer: coarse) {
  :root { --cell-size: max(16px, 1rem); }
}


/* The demonstrations, on a dark card: the body lightens, the furniture stays
   quiet, and the contrast between near and far limbs is kept. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --anat-body: #565c65;
  }
}

:root[data-theme="dark"] {
  --anat-body: #565c65;
}


/* ===== src/styles/base.css ===== */
/* ==========================================================================
   Base — reset, document defaults, shared primitives
   ========================================================================== */

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

* {
  margin: 0;
  padding: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Let the browser paint form controls and scrollbars for the active theme. */
  color-scheme: light dark;
  /* The document itself never scrolls: the app is exactly one screen tall and
     the page area inside it does the scrolling (see .main). This is what stops
     a phone dragging the whole app up off the bottom of the screen and showing
     empty space under the tab bar — there is nothing below to drag to. */
  height: 100%;
  overflow: hidden;
  /* The single lever behind Settings' text-size control. A percentage is used
     rather than a pixel figure so a reader who has already raised their
     browser's default font size keeps that as the baseline and scales from
     there, instead of having it overwritten.

     TWO NUMBERS, and they answer different questions. `--text-scale-base` is
     what the app calls 100% — the density it draws at when nobody has asked
     for anything, which since v1.3.9.94 is what used to be 90%.
     `--text-scale` is the reader's own choice around that. Multiplying them
     here, in the one place the root size is set, is what lets the default
     move without touching a stored preference or the number under the slider.
     Desktop and phone alike: this is the root, and everything is rem. */
  font-size: calc(100% * var(--text-scale-base, 0.9) * var(--text-scale, 1));
}

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

body {
  font: var(--type-body);
  letter-spacing: var(--tracking-normal);
  color: var(--text-primary);
  background-color: var(--bg-base);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  height: 100%;
  overflow: hidden;
  /* iOS rubber-band scrolling should not drag the whole page around — in
     either direction. Sideways matters as much as up and down: a page that
     can be pulled a little off to the left looks like it is floating loose
     rather than sitting on the screen. */
  overscroll-behavior: none;
}

/* Theme changes animate rather than snap, but only the colour properties —
   animating `all` would drag layout and scroll position along with it. */
body,
.card,
.list,
.nav,
.tabbar,
.sidebar {
  transition: background-color var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out),
    color var(--duration-base) var(--ease-out);
}

/* Users who ask for less motion get none of it. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- Typography primitives ------------------------------------------------ */

h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}

h1 { font: var(--type-large-title); letter-spacing: var(--tracking-tight); }
h2 { font: var(--type-title2); letter-spacing: var(--tracking-tight); }
h3 { font: var(--type-title3); }
h4 { font: var(--type-headline); }

p { font: var(--type-body); }

a {
  color: var(--text-link);
  text-decoration: none;
  transition: opacity var(--duration-fast) var(--ease-out);
}
a:hover { opacity: 0.75; }

small { font: var(--type-footnote); color: var(--text-secondary); }

/* Money and any other figure that appears in a column. Tabular figures keep
   digits on a fixed advance so decimal points line up down the column. */
.num,
.money,
td.num,
.stat-value {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* --- Form controls -------------------------------------------------------- */

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

button {
  cursor: pointer;
  /* Suppress the grey flash iOS Safari paints over tapped elements. */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.4;
}

input,
textarea,
select {
  width: 100%;
}

/* iOS zooms the viewport when a focused input is under 16px. Keeping the
   font at 17px on touch avoids that jump entirely. */
input,
textarea,
select {
  font-size: 1.0625rem;
}

/* --- Focus ---------------------------------------------------------------- */
/* Keyboard users get a clear ring; pointer users do not see it. */

:focus { outline: none; }

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

/* --- Scrollbars ----------------------------------------------------------- */
/* Thin, overlay-style bars in the spirit of macOS. */

@supports selector(::-webkit-scrollbar) {
  ::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb {
    background-color: var(--fill-primary);
    border-radius: var(--radius-full);
    border: 2px solid transparent;
    background-clip: content-box;
  }
  ::-webkit-scrollbar-thumb:hover { background-color: var(--text-tertiary); }
  ::-webkit-scrollbar-corner { background: transparent; }
}

* {
  scrollbar-width: thin;
  scrollbar-color: var(--fill-primary) transparent;
}

/* --- Selection ------------------------------------------------------------ */

::selection {
  background-color: var(--accent-tint);
  color: var(--text-primary);
}

/* --- Utilities ------------------------------------------------------------ */

/* Visible to screen readers, invisible on screen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.hidden { display: none !important; }

/* Default icon size. An SVG with only a viewBox and no dimensions expands to
   fill its container, so every icon needs a floor. Declared here, in the file
   loaded before components.css, so a component rule at equal specificity
   (.list-chevron, .tab svg) always wins. */
.icon {
  width: 20px;
  height: 20px;
  flex: none;
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Any wide block (tables, charts) scrolls inside its own box so the page
   body never scrolls sideways. */
.scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
}

.stack   { display: flex; flex-direction: column; }
.row     { display: flex; flex-direction: row; align-items: center; }
.spacer  { flex: 1 1 auto; }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
.gap-6 { gap: var(--space-6); }

.text-secondary { color: var(--text-secondary); }
.text-tertiary  { color: var(--text-tertiary); }
.text-positive  { color: var(--color-positive); }
.text-negative  { color: var(--color-negative); }
.text-warning   { color: var(--color-warning); }
.text-accent    { color: var(--accent); }

.font-headline { font: var(--type-headline); }
.font-subhead  { font: var(--type-subhead); }
.font-footnote { font: var(--type-footnote); }
.font-caption  { font: var(--type-caption1); }


/* ===== src/styles/components.css ===== */
/* ==========================================================================
   Components — iOS/macOS flavoured building blocks
   ========================================================================== */

/* --------------------------------------------------------------------------
   App shell
   The shell is a CSS grid that reflows between two layouts:
   - "mobile"  : nav bar on top, tab bar pinned to the bottom
   - "desktop" : fixed sidebar on the left, toolbar on top of the content
   The active layout is driven by [data-view] on the shell, which the view
   toggle sets explicitly and the media query sets by default.
   -------------------------------------------------------------------------- */

/* Exactly one screen tall, never more. The rows that hold the page are
   minmax(0, 1fr) rather than 1fr because a grid item's automatic minimum size
   is its content: with a plain 1fr the page would push the grid taller than
   the screen and the tab bar would slide off the bottom instead of the page
   scrolling inside it. */
.shell {
  display: grid;
  height: 100vh;   /* for anything that does not know dvh */
  height: 100dvh;
  /* The measured height, when the app is on a phone and has one. `dvh` goes
     stale in Safari when its URL bar collapses without a page scroll — this
     document never scrolls — leaving a dead band under the tab bar; the
     variable is the live answer, written from visualViewport by
     ui/viewport.js and absent everywhere the unit can be trusted. */
  height: var(--shell-height, 100dvh);
  grid-template-columns: 1fr;
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "nav"
    "main"
    "tabbar";
}

.shell[data-view="desktop"] {
  grid-template-columns: var(--sidebar-width) 1fr;
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas:
    "sidebar nav"
    "sidebar main";
}

/* The app is invisible until it has a page to show, then fades up. What the
   eye gets in the meantime is the right background colour rather than a
   half-built screen that jumps — the body carries the theme from the first
   paint, so there is nothing to flash between.

   `visibility` as well as `opacity` so nothing under it can be tabbed to or
   read out while it is not really there yet. */
:root.is-booting .shell {
  opacity: 0;
  visibility: hidden;
}

.shell {
  opacity: 1;
  transition: opacity var(--duration-base) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .shell { transition: none; }
}

/* Folded away. The column collapses rather than the sidebar merely hiding, so
   the content genuinely reclaims the width instead of leaving a gap. */
.shell[data-view="desktop"].sidebar-hidden {
  grid-template-columns: 0 1fr;
}

/* --------------------------------------------------------------------------
   Auto-hiding page finder (desktop)
   --------------------------------------------------------------------------
   The finder stops being a column and becomes an overlay parked off the left
   edge; the page keeps the whole width. The ONLY thing that ever animates is
   the transform below — one composited property on one element — which is
   what makes the slide smooth however heavy the page behind it is. Animating
   the grid column instead would re-lay-out the entire spreadsheet on every
   frame. The waking, lingering and focus rules live in
   ui/sidebar-autohide.js. */
.shell[data-view="desktop"].sidebar-auto {
  grid-template-columns: 0 1fr;
}

.shell[data-view="desktop"].sidebar-auto .sidebar {
  /* Declared after (and more specifically than) `.sidebar-hidden .sidebar`:
     the fold preference has no meaning while the finder can always be
     summoned from the edge, so auto wins over hidden. */
  display: flex;
  position: fixed;
  top: 0;
  left: 0;
  height: 100%;
  width: var(--sidebar-width);
  z-index: 150; /* over the bars (100), under any sheet (200) */
  background-color: var(--bg-base);
  box-shadow: var(--shadow-xl);
  transform: translateX(calc(-100% - 24px)); /* past its own shadow too */
  transition: transform 0.26s var(--ease-out);
  will-change: transform;
}

.shell[data-view="desktop"].sidebar-auto.sidebar-open .sidebar {
  transform: translateX(0);
}

/* The width handle belongs to the column layout; an overlay has no seam to
   drag. Width is still adjustable with auto-hide switched off. */
.shell[data-view="desktop"].sidebar-auto .sidebar-resizer { display: none; }

/* The wake strip: a few pixels of the far left edge, invisible, always
   hot. Wide enough that throwing the pointer at the edge cannot miss it. */
.sidebar-peek { display: none; }

.shell[data-view="desktop"].sidebar-auto .sidebar-peek {
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  width: 14px;
  height: 100%;
  z-index: 149;
}

/* A tour must point at a finder that is actually there — open, and without
   the slide, so the spotlight measures it where it stands. */
body.is-touring .shell[data-view="desktop"].sidebar-auto .sidebar {
  transform: none;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .shell[data-view="desktop"].sidebar-auto .sidebar { transition: none; }
}

/* The divider between the page finder and the page. It is deliberately wider
   than it looks: a 1px target is a target nobody hits. */
.sidebar-resizer {
  grid-area: sidebar;
  justify-self: end;
  width: 10px;
  margin-right: -5px;
  height: 100%;
  position: sticky;
  top: 0;
  z-index: calc(var(--z-nav) + 1);
  cursor: col-resize;
  touch-action: none;
  display: none;
  background: transparent;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.shell[data-view="desktop"] .sidebar-resizer { display: block; }

.sidebar-resizer::after {
  content: "";
  position: absolute;
  inset: 0 4px;
  border-radius: var(--radius-full);
  background-color: transparent;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.sidebar-resizer:hover::after,
.sidebar-resizer:focus-visible::after,
.sidebar-resizer.is-dragging::after { background-color: var(--accent); }

/* While a divider is being dragged, nothing else on the page should select,
   or the drag paints a blue smear across every label it passes. */
body.is-resizing {
  cursor: col-resize;
  user-select: none;
  -webkit-user-select: none;
}

/* --------------------------------------------------------------------------
   Navigation bar (iOS)
   Translucent material that blurs the content scrolling beneath it.
   -------------------------------------------------------------------------- */

.nav {
  grid-area: nav;
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: flex;
  flex-direction: column;
  background-color: var(--material-chrome);
  -webkit-backdrop-filter: var(--blur-chrome);
  backdrop-filter: var(--blur-chrome);
  padding-top: var(--safe-top);
  /* A hairline, not a border-bottom: it must not add to the layout height. */
  box-shadow: inset 0 calc(var(--hairline) * -1) 0 var(--separator);
  transition: background-color var(--duration-base) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out);
}

/* At the top of a page the bar sits *on* the page rather than over it: no
   material, no separator, nothing scrolling underneath it yet to obscure. It
   picks both up the moment content starts passing beneath, which is the only
   moment they earn their weight. Height never changes between the two states,
   so the switch cannot shove the page around. */
.nav:not(.is-condensed) {
  background-color: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: none;
}

/* Where backdrop-filter is unavailable the bar must stay opaque, otherwise
   content scrolls through it and the title becomes unreadable. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav,
  .tabbar,
  .sidebar {
    background-color: var(--material-thick);
  }
}

.nav-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--nav-height);
  padding: 0 var(--space-3);
  padding-left: max(var(--space-3), var(--safe-left));
  padding-right: max(var(--space-3), var(--safe-right));
}

/* The compact title the bar shows once the large title has scrolled away.
   `min-width: 0` is what lets it give way: without it the automatic minimum
   size keeps the whole heading at its text width, and on a narrow phone the
   bar pushes the page wider than the screen rather than truncating. */
.nav-title {
  flex: 1 1 0;
  min-width: 0;
  font: var(--type-headline);
  font-size: 1.125rem;   /* a step up from the 17px body ramp, per req 3 */
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
  pointer-events: none;
}

.nav.is-condensed .nav-title {
  opacity: 1;
  transform: translateY(0);
}

/* "Year: 2026 · Month: August" — always on, at both bar states, so the figures
   on screen can never be read against the wrong period. Requirement 3. */
.nav-context {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  padding: 0 var(--gutter) var(--space-2);
  font: var(--type-footnote);
  font-size: 0.875rem;
  color: var(--text-secondary);
  letter-spacing: var(--tracking-normal);
  min-width: 0;
}

.nav-context:empty { display: none; }

/* At the top of the page the bar carries no material and no context: the page
   heading has the screen to itself, and the bar is only its buttons. Start
   scrolling and the month and year fade in beneath them.

   Height and opacity move together so the bar grows into the space rather
   than the text appearing inside a gap that was always reserved for it. The
   buttons above are untouched either way — .nav-bar keeps its own min-height,
   so nothing in the row moves as this opens and closes. */
.nav-context {
  max-height: 0;
  padding-bottom: 0;
  opacity: 0;
  overflow: hidden;
  transition: opacity var(--duration-base) var(--ease-out),
    max-height var(--duration-base) var(--ease-out),
    padding-bottom var(--duration-base) var(--ease-out);
}

.nav.is-condensed .nav-context {
  max-height: 4rem;
  padding-bottom: var(--space-2);
  opacity: 1;
}

/* Somebody who has asked for less motion gets the same behaviour without the
   fade — the text is either there or it is not. */
@media (prefers-reduced-motion: reduce) {
  .nav-context { transition: none; }
}

/* On desktop the bar has room to keep the context beside the title rather
   than under it, which reads as one line of chrome instead of two. */
.shell[data-view="desktop"] .nav-context { justify-content: flex-start; }
.shell[data-view="desktop"] .nav.is-condensed .nav-context { padding-bottom: var(--space-1); }

.nav-context b {
  font-weight: 600;
  color: var(--text-primary);
}

.nav-context .nav-context-sep { color: var(--text-quaternary); }

/* Large page title. It is ordinary content, so it scrolls away under its own
   momentum — nothing animates, nothing resizes, and the movement tracks the
   finger exactly. The compact copy in the bar fades in to replace it. */
.nav-large-title {
  padding: var(--space-1) var(--space-1) var(--space-4);
  font: var(--type-title1);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  /* Long screen names wrap rather than clip — requirement 1. */
  overflow-wrap: break-word;
}

/* On a phone the title is centred, matching the bar above it. */
.shell[data-view="mobile"] .nav-large-title { text-align: center; }

/* ...and the bar does not repeat it. On a narrow screen the compact title in
   the bar and the page heading below are both in view at once, so the name of
   the tab was simply printed twice. The page heading is the one that stays —
   it is larger, it scrolls with the content it names, and the bar still has
   the year and month to say where you are. Desktop keeps both: there the bar
   is the only title left once the heading has scrolled away. */
.shell[data-view="mobile"] .nav-title { display: none; }

.nav-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

/* A control that is present but currently useless — nothing to undo, no page
   to go back to — stays in place and dims. Removing it would shuffle its
   neighbours sideways every time the history changed. */
.nav-btn[disabled] {
  opacity: 0.3;
  pointer-events: none;
}

/* Quick Add Expense: the one bar button that is an action rather than a
   navigation, so it carries the accent as a fill. */
.nav-btn.is-prominent {
  background-color: var(--accent);
  color: var(--text-on-accent);
}

.nav-btn.is-prominent:hover { background-color: var(--accent-hover); }
.nav-btn.is-prominent:active { background-color: var(--accent-pressed); }

/* A borderless, tinted bar button — the iOS default. */
.nav-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-width: 32px;
  height: 32px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--accent);
  font: var(--type-body);
  transition: background-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

/* Six controls share a phone's bar, so each gives up a few pixels there. */
.shell[data-view="mobile"] .nav-btn {
  min-width: 30px;
  padding: 0 var(--space-1);
}

.shell[data-view="mobile"] .nav-btn svg { width: 19px; height: 19px; }

.nav-btn:hover { background-color: var(--fill-quaternary); }
.nav-btn:active { background-color: var(--fill-tertiary); transform: scale(0.94); }

/* Standing on the page this button opens. Settings is on the bar on a phone
   rather than in the tab bar, so without this it would be the one screen in
   the app you can be on with nothing lit anywhere to say so. Tinted rather
   than filled: a bar button is not a tab, and a solid accent here would
   compete with quick add sitting next to it. */
.nav-btn.is-on {
  color: var(--accent);
  background-color: var(--accent-tint);
}
.nav-btn svg { width: 20px; height: 20px; }

/* --------------------------------------------------------------------------
   Tab bar (iOS) — mobile only
   -------------------------------------------------------------------------- */

.tabbar {
  grid-area: tabbar;
  position: sticky;
  bottom: 0;
  z-index: var(--z-tabbar);
  display: flex;
  align-items: stretch;
  justify-content: space-around;
  /* A floor, not a ceiling. Pinned to exactly 49px the bar cut the labels off
     the bottom of the screen the moment the text was set any larger than the
     default — icons, and a sliced half-line of words under them. The bar is
     iOS-height when the text is iOS-sized and taller when it is not, which is
     what a tab bar with words in it has to do. */
  min-height: calc(var(--tabbar-height) + var(--safe-bottom));
  padding-bottom: var(--safe-bottom);
  background-color: var(--material-chrome);
  -webkit-backdrop-filter: var(--blur-chrome);
  backdrop-filter: var(--blur-chrome);
  box-shadow: inset 0 var(--hairline) 0 var(--separator);
}

.shell[data-view="desktop"] .tabbar { display: none; }

.tab {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: var(--space-1) var(--space-1) 0;
  color: var(--text-secondary);
  font: var(--type-caption2);
  letter-spacing: 0;
  transition: color var(--duration-fast) var(--ease-out);
  /* Apple's minimum comfortable touch target. */
  min-height: 44px;
}

.tab svg { width: 26px; height: 26px; flex: none; }
.tab[aria-selected="true"] { color: var(--accent); }
.tab:active svg { transform: scale(0.9); transition: transform var(--duration-fast) var(--ease-spring); }

.tab-label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Tight, so the label costs one line and never two however long the word
     or however large the type. */
  line-height: 1.15;
}

/* --- the tracker switch, in the middle of the bar ------------------------ */

/* Four pages and one door share this row, and they must not read as five
   pages. The door is a raised disc: it carries the accent, it is the only
   filled shape on the bar, and it sits proud of the row so the eye finds the
   middle without looking. Everything here is about telling the two kinds of
   thing apart — nothing on the flat tabs changed. */
.tab-switch {
  /* It does not stretch with the others. A fixed, slightly narrower column
     gives the four tabs their room back and keeps the disc centred on the bar
     rather than centred on a fifth of it. */
  flex: 0 0 auto;
  gap: 3px;
  color: var(--text-secondary);
}

.tab-switch-disc {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background-color: var(--accent);
  color: #fff;
  /* Proud of the row, and the row must not grow to hold it — the bar's height
     is the tab bar's own and the labels have to line up across all five.

     TWO SEPARATE JOBS, done by two separate properties, which is the part
     worth reading. The negative block margin makes the 44px disc OCCUPY the
     26px a flat tab's icon occupies, so the label below it sits on exactly
     the line the other four labels sit on. The transform then lifts it clear
     of the bar without taking part in layout at all. Done with a margin
     alone — the first cut — the sums come out the same and the label is
     pushed off the bottom of the screen, because a margin moves everything
     under it and a transform moves nothing. */
  margin-block: -9px;
  transform: translateY(-10px);
  box-shadow:
    0 4px 10px color-mix(in srgb, var(--accent) 38%, transparent),
    0 1px 2px rgba(0, 0, 0, 0.18);
  transition:
    transform var(--duration-fast) var(--ease-spring),
    box-shadow var(--duration-fast) var(--ease-out);
}

/* The disc's own symbol, which is a size of its own rather than the 26px the
   flat tabs use: inside a filled circle the same glyph reads heavier. */
.tab-switch-disc svg { width: 24px; height: 24px; }

/* The press is on the DISC, not the icon — the whole shape gives, which is
   what a raised button should do and what the flat tabs deliberately do not.
   `.tab:active svg` is overridden for the same reason: two things scaling
   inside each other reads as a wobble. */
.tab-switch:active .tab-switch-disc {
  /* The lift is part of the transform, so the press has to carry it — drop
     the translate here and the disc falls into the bar as it is pressed. */
  transform: translateY(-10px) scale(0.92);
  box-shadow:
    0 2px 6px color-mix(in srgb, var(--accent) 30%, transparent),
    0 1px 1px rgba(0, 0, 0, 0.16);
}
.tab-switch:active svg { transform: none; }

/* The switch is never "the page you are on" — it is a door, and there is no
   selected state to show. It also never takes the accent as a text colour the
   way a selected tab does, because the disc is already wearing it. */
.tab-switch[aria-selected] { color: var(--text-secondary); }

/* --- what is waiting, on a tab ------------------------------------------- */

/* Friends carries the count that used to sit on the floating bar. It rides the
   icon rather than the label, so a long word cannot push it off the tab, and
   it is positioned against the tab itself rather than the glyph — an SVG has
   no box worth anchoring to once the type scale moves. */
.tab { position: relative; }

.tab-count {
  position: absolute;
  top: 2px;
  left: 50%;
  margin-left: 4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background-color: var(--color-red, #ff3b30);
  color: #fff;
  font: var(--type-caption2);
  font-weight: 650;
  line-height: 16px;
  text-align: center;
  letter-spacing: 0;
}

/* --------------------------------------------------------------------------
   Sidebar (macOS / iPadOS) — desktop only
   -------------------------------------------------------------------------- */

.sidebar {
  grid-area: sidebar;
  display: none;
  flex-direction: column;
  gap: var(--space-1);
  background-color: var(--material-sidebar);
  -webkit-backdrop-filter: var(--blur-chrome);
  backdrop-filter: var(--blur-chrome);
  box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--separator);
  padding: var(--space-3) var(--space-2);
  padding-top: max(var(--space-3), var(--safe-top));
  padding-left: max(var(--space-2), var(--safe-left));
  position: sticky;
  top: 0;
  height: 100%;
  overflow-y: auto;
}

.shell[data-view="desktop"] .sidebar { display: flex; }

/* Folded away. Declared after the rule above because the two carry the same
   specificity, and at equal specificity the later rule is the one that wins. */
.shell.sidebar-hidden .sidebar,
.shell.sidebar-hidden .sidebar-resizer { display: none; }

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2) var(--space-4) var(--space-3);
}

/* Folds the page finder away. Sits with the brand rather than floating over
   the list, so it does not cover a section name at narrow widths. */
.sidebar-collapse {
  margin-left: auto;
  flex: none;
  color: var(--text-secondary);
}

.sidebar-collapse:hover { color: var(--accent); }

/* A screen and, for Expenses, its quick-add button. The row is the flex
   container so the button can sit flush right without the label reflowing. */
.sidebar-row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.sidebar-row .sidebar-item { flex: 1; min-width: 0; }

.sidebar-add {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.sidebar-add svg { width: 16px; height: 16px; }

/* Visible on hover, on focus, and always on a touch device — where there is
   no hover, an invisible button is no button at all. */
.sidebar-row:hover .sidebar-add,
.sidebar-add:focus-visible { opacity: 1; }

@media (pointer: coarse) {
  .sidebar-add { opacity: 1; }
}

.sidebar-add:hover {
  background-color: var(--accent-tint);
  color: var(--accent);
}

.sidebar-brand-mark {
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  background: linear-gradient(160deg, var(--accent), var(--color-indigo));
  color: #fff;
  flex: none;
  box-shadow: var(--shadow-sm);
}
.sidebar-brand-mark svg { width: 18px; height: 18px; }

/* An uploaded mark fills the tile rather than sitting inside it: it is the
   picture, not a glyph on a coloured ground. */
.sidebar-brand-mark .sidebar-mark {
  width: 30px;
  height: 30px;
}

.sidebar-brand-mark .sidebar-mark img {
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  object-fit: cover;
}

.sidebar-brand-text {
  font: var(--type-headline);
  letter-spacing: var(--tracking-tight);
  min-width: 0;
}

.sidebar-section {
  padding: var(--space-3) var(--space-3) var(--space-1);
  font: var(--type-caption1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-tertiary);
}

.sidebar-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--text-primary);
  font: var(--type-subhead);
  text-align: left;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.sidebar-item svg {
  width: 18px;
  height: 18px;
  flex: none;
  color: var(--text-secondary);
  transition: color var(--duration-fast) var(--ease-out);
}

.sidebar-item:hover { background-color: var(--bg-hover); }

.sidebar-item[aria-selected="true"] {
  background-color: var(--accent);
  color: var(--text-on-accent);
  font-weight: 600;
}
.sidebar-item[aria-selected="true"] svg { color: var(--text-on-accent); }

/* The foot: which workbook is behind all this, and nothing else. */
.sidebar-footer { margin-top: auto; padding-top: var(--space-4); }

.sidebar-workbook {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  min-width: 0;
}

.sidebar-workbook-name {
  font: var(--type-caption1);
  color: var(--text-tertiary);
  min-width: 0;
}

/* The version is the half worth reading twice, so it gets the pill. */
.sidebar-workbook-version {
  flex: none;
  padding: 1px var(--space-2);
  border-radius: var(--radius-full);
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
  font: var(--type-caption2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   Main content
   -------------------------------------------------------------------------- */

.main {
  grid-area: main;
  min-width: 0; /* lets grid children shrink instead of forcing overflow */
  /* The one thing on the page that scrolls. Because it is a scroller of its
     own rather than the document, `overscroll-behavior` is honoured here on
     iOS: a flick at the end of the page stops at the end of the page instead
     of bouncing the whole app and exposing the background beneath it. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior-y: contain;
  overscroll-behavior-x: none;
  -webkit-overflow-scrolling: touch;
  padding: var(--gutter);
  padding-left: max(var(--gutter), var(--safe-left));
  padding-right: max(var(--gutter), var(--safe-right));
  padding-bottom: var(--space-10);
}

.shell[data-view="desktop"] .main {
  padding: var(--section-gap) var(--gutter) var(--space-16);
}

.content { max-width: var(--content-max); margin: 0 auto; width: 100%; }

.page-section { margin-bottom: var(--section-gap); }

.section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-3);
  margin-bottom: var(--space-3);
  padding: 0 var(--space-1);
  /* A note beside a heading drops beneath it rather than squeezing the
     heading into a two-line column on a narrow screen. */
  flex-wrap: wrap;
}

.section-header > .section-title,
.section-header > .section-toggle { flex: 1 1 auto; }

.section-title {
  font: var(--type-title3);
  letter-spacing: var(--tracking-tight);
}

/* --------------------------------------------------------------------------
   Collapsible sections
   -------------------------------------------------------------------------- */

.section-header.is-collapsible { align-items: center; }

.section-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  flex: 1;
  padding: var(--space-1) var(--space-1) var(--space-1) 0;
  border-radius: var(--radius-sm);
  color: inherit;
  text-align: left;
  transition: color var(--duration-fast) var(--ease-out);
}

.section-toggle:hover { color: var(--accent); }
.section-toggle .section-title { min-width: 0; }

.section-chevron {
  width: 18px;
  height: 18px;
  flex: none;
  color: var(--text-secondary);
  transition: transform var(--duration-base) var(--ease-out);
}

/* Pointing down when open, right when folded — the direction the content
   will travel if you press it. */
.page-section.is-collapsed .section-chevron { transform: rotate(-90deg); }

/* Collapsed content is hidden, not removed: printing still reaches it, and so
   does the browser's find-in-page. */
.page-section.is-collapsed > .section-body {
  display: none;
}

/* A folded section leaves only its heading, so it should not also leave the
   full gap a section with content needs. */
.page-section.is-collapsed { margin-bottom: var(--space-3); }

/* Paper has no interaction, so every section prints open. */
@media print {
  .page-section.is-collapsed > .section-body { display: block; }
  .section-chevron { display: none; }
}

/* The uppercase header that sits above an iOS grouped list. */
.group-header {
  padding: 0 var(--space-4) var(--space-2);
  font: var(--type-footnote);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-secondary);
}

.group-footer {
  padding: var(--space-2) var(--space-4) 0;
  font: var(--type-footnote);
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   Cards
   -------------------------------------------------------------------------- */

.card {
  background-color: var(--bg-elevated);
  border-radius: var(--radius-xl);
  padding: var(--space-4);
  box-shadow: var(--shadow-sm);
}

.card-lg { padding: var(--space-5); border-radius: var(--radius-2xl); }

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.card-title {
  font: var(--type-headline);
  letter-spacing: var(--tracking-tight);
}

.card-subtitle { font: var(--type-footnote); color: var(--text-secondary); }

/* Auto-fitting card grid — one column on a phone, as many as fit on desktop. */
.card-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}

.shell[data-view="desktop"] .card-grid {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-4);
}

/* Stat grid. On a phone the first tile spans both columns so the headline
   figure reads as the headline, and the rest sit two-up — six full-width
   tiles would be nothing but scrolling. */
.stat-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}

.stat-grid > :first-child { grid-column: 1 / -1; }

.stat-grid > :first-child .stat-value { font: var(--type-large-title); letter-spacing: var(--tracking-tight); }
.stat-grid > :not(:first-child) .stat-value { font: var(--type-title3); letter-spacing: var(--tracking-tight); }

.shell[data-view="desktop"] .stat-grid {
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
}

.shell[data-view="desktop"] .stat-grid > :first-child { grid-column: auto; }
.shell[data-view="desktop"] .stat-grid > :first-child .stat-value { font: var(--type-title1); letter-spacing: var(--tracking-tight); }

/* --------------------------------------------------------------------------
   Stat tile — the headline number treatment
   -------------------------------------------------------------------------- */

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  background-color: var(--bg-elevated);
  border-radius: var(--radius-xl);
  padding: var(--space-4);
  box-shadow: var(--shadow-sm);
  min-width: 0;
}

.stat-label {
  font: var(--type-footnote);
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.stat-value {
  font: var(--type-title1);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  min-width: 0;
  overflow-wrap: anywhere;
}

.stat-value.is-positive { color: var(--color-positive); }
.stat-value.is-negative { color: var(--color-negative); }

.stat-meta {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font: var(--type-footnote);
  color: var(--text-secondary);
}

/* Delta pill: green for up, red for down. */
.delta {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  font: var(--type-caption1);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.delta.is-positive { background-color: var(--tint-positive); color: var(--color-positive); }
.delta.is-negative { background-color: var(--tint-negative); color: var(--color-negative); }
.delta.is-neutral  { background-color: var(--fill-tertiary); color: var(--text-secondary); }

/* --------------------------------------------------------------------------
   Grouped list (iOS inset-grouped table view)
   -------------------------------------------------------------------------- */

.list {
  background-color: var(--bg-elevated);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

.list-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  text-align: left;
  color: var(--text-primary);
  position: relative;
  transition: background-color var(--duration-fast) var(--ease-out);
}

/* Separators are inset from the left to clear the row's icon, exactly as
   iOS insets them to line up with the label. */
.list-row + .list-row::before {
  content: "";
  position: absolute;
  top: 0;
  left: var(--space-4);
  right: 0;
  height: var(--hairline);
  background-color: var(--separator);
}

.list-row.has-icon + .list-row.has-icon::before { left: calc(var(--space-4) + 28px + var(--space-3)); }

button.list-row:hover { background-color: var(--bg-hover); }
button.list-row:active { background-color: var(--bg-active); }

.list-icon {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-xs);
  display: grid;
  place-items: center;
  flex: none;
  color: #fff;
  background-color: var(--accent);
}
.list-icon svg { width: 17px; height: 17px; }

.list-content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.list-title { font: var(--type-body); letter-spacing: var(--tracking-normal); }
.list-subtitle { font: var(--type-footnote); color: var(--text-secondary); }

.list-value {
  font: var(--type-body);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  text-align: right;
  flex: none;
}
.list-value.is-strong { color: var(--text-primary); font-weight: 600; }

/* The grey chevron that marks a row as tappable. */
.list-chevron { width: 14px; height: 14px; color: var(--text-tertiary); flex: none; }

/* --------------------------------------------------------------------------
   Segmented control (iOS)
   -------------------------------------------------------------------------- */

.segmented {
  display: inline-flex;
  padding: 2px;
  background-color: var(--fill-tertiary);
  border-radius: var(--radius-sm);
  gap: 2px;
  position: relative;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
.segmented::-webkit-scrollbar { display: none; }

.segment {
  flex: 1 1 auto;
  padding: var(--space-1) var(--space-3);
  border-radius: calc(var(--radius-sm) - 2px);
  font: var(--type-subhead);
  font-weight: 500;
  color: var(--text-primary);
  white-space: nowrap;
  min-height: 30px;
  transition: background-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

.segment[aria-selected="true"] {
  background-color: var(--bg-elevated);
  /* The hairline is what keeps the chosen segment legible in a theme where
     the card colour and the track behind it are close together — which a
     custom theme is free to make them. */
  box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--separator);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-2) var(--space-5);
  border-radius: var(--radius-md);
  font: var(--type-body);
  font-weight: 600;
  letter-spacing: var(--tracking-normal);
  transition: background-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}

.btn:active { transform: scale(0.97); }
.btn svg { width: 18px; height: 18px; }

.btn-filled { background-color: var(--accent); color: var(--text-on-accent); }
.btn-filled:hover { background-color: var(--accent-hover); }
.btn-filled:active { background-color: var(--accent-pressed); }

.btn-tinted { background-color: var(--accent-tint); color: var(--accent); }
.btn-tinted:hover { filter: brightness(0.97); }

.btn-gray { background-color: var(--fill-tertiary); color: var(--text-primary); }

/* The button that goes ahead with something irreversible. Red, so it can
   never be mistaken for the safe half of a pair. */
.btn-danger {
  background-color: var(--color-negative);
  color: #fff;
}

.btn-danger:hover { filter: brightness(0.94); }
.btn-danger:active { filter: brightness(0.88); }
.btn-gray:hover { background-color: var(--fill-secondary); }

.btn-plain { color: var(--accent); padding-inline: var(--space-2); }
.btn-plain:hover { background-color: var(--fill-quaternary); }

.btn-destructive { background-color: var(--tint-negative); color: var(--color-negative); }

.btn-sm { min-height: 32px; padding: var(--space-1) var(--space-3); font: var(--type-subhead); font-weight: 600; }
.btn-block { width: 100%; }

/* --------------------------------------------------------------------------
   Switch (iOS toggle)
   -------------------------------------------------------------------------- */

.switch {
  position: relative;
  display: inline-block;
  width: 51px;
  height: 31px;
  flex: none;
}

.switch input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
  z-index: 1;
}

.switch-track {
  position: absolute;
  inset: 0;
  background-color: var(--fill-primary);
  border-radius: var(--radius-full);
  transition: background-color var(--duration-base) var(--ease-out);
  pointer-events: none;
}

.switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 27px;
  height: 27px;
  background-color: #fff;
  border-radius: 50%;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15), 0 1px 1px rgba(0, 0, 0, 0.16);
  transition: transform var(--duration-base) var(--ease-spring);
  pointer-events: none;
}

.switch input:checked ~ .switch-track { background-color: var(--color-green); }
.switch input:checked ~ .switch-thumb { transform: translateX(20px); }
.switch input:focus-visible ~ .switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --------------------------------------------------------------------------
   Progress bar — budget usage
   -------------------------------------------------------------------------- */

.progress {
  height: 6px;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  transition: width var(--duration-slow) var(--ease-out),
    background-color var(--duration-base) var(--ease-out);
}

.progress-fill.is-warning { background-color: var(--color-warning); }
.progress-fill.is-over { background-color: var(--color-negative); }

/* --------------------------------------------------------------------------
   Badges / pills
   -------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  font: var(--type-caption1);
  font-weight: 600;
  background-color: var(--fill-tertiary);
  color: var(--text-secondary);
  white-space: nowrap;
}

.badge svg { width: 13px; height: 13px; }
.delta svg { width: 12px; height: 12px; }
.stat-label svg { width: 14px; height: 14px; }

.badge.is-positive { background-color: var(--tint-positive); color: var(--color-positive); }
.badge.is-negative { background-color: var(--tint-negative); color: var(--color-negative); }
.badge.is-warning  { background-color: var(--tint-warning); color: var(--color-warning); }
.badge.is-accent   { background-color: var(--accent-tint); color: var(--accent); }

/* --------------------------------------------------------------------------
   Data table — for sheet views
   -------------------------------------------------------------------------- */

.table-wrap {
  background-color: var(--bg-elevated);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

/* Wide tables scroll sideways rather than squeezing their columns to nothing.
   `overscroll-behavior-x: contain` stops a horizontal flick at the end of a
   table from turning into a browser back-swipe on a phone. */
.table-scroll {
  overflow: auto;
  max-width: 100%;
  max-height: 70vh;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

/* A table with more to the right of it says so. Without this a wide table
   simply looks cut off at the screen edge, which is exactly what a column of
   figures disappearing under the bezel looks like. The shade sits above the
   cells and lets taps through to them. */
.table-wrap { position: relative; }

.table-wrap::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 26px;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
  background: linear-gradient(to right, transparent, var(--bg-elevated));
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
}

.table-wrap.has-overflow::after { opacity: 1; }

@media print { .table-wrap::after { display: none; } }

table.data {
  border-collapse: separate;
  border-spacing: 0;
  /* auto + min-width lets columns size to their content and the container
     scroll, instead of width:100% squeezing nowrap cells until they clip. */
  width: auto;
  min-width: 100%;
  font: var(--type-subhead);
  /* Shared with the fields inside it, so a figure you typed and a figure the
     sheet came with are the same size. See --cell-size in tokens.css. */
  font-size: var(--cell-size);
}

/* Text columns wrap; numeric ones never do.
   A truncated client name or note is lost information — the reader cannot
   recover it — whereas a figure is short by nature and only ever needs one
   line. So text cells wrap onto a second line and the row grows, while the
   money columns keep their single-line alignment.

   `overflow-wrap: break-word` rather than `anywhere`: `anywhere` lets the
   browser count a word's *narrowest* possible break when sizing the column,
   so a table of ordinary words collapses into a ribbon two letters wide and
   every entry reads vertically. `break-word` only splits a word that genuinely
   cannot fit, which is the readability requirement 6 asks for. */
table.data td.cell-text,
table.data th.cell-text {
  white-space: normal;
  overflow-wrap: break-word;
  /* A field inside the cell is `width: 100%`, so it contributes nothing to the
     column's intrinsic width — left alone the browser sizes the column from
     the input's minimum and two-word entries wrap for no reason. This floor is
     what gives a name room to sit on one line. */
  min-width: 15ch;
  /* Capped so the first column cannot eat a phone screen and push the figures
     off the edge; the table scrolls, so there is somewhere for them to go. */
  max-width: 24ch;
}

.shell[data-view="desktop"] table.data td.cell-text,
.shell[data-view="desktop"] table.data th.cell-text {
  min-width: 12ch;
  max-width: 34ch;
}

table.data td.num,
table.data th.num {
  white-space: nowrap;
  /* Fields already use tabular figures; without this the printed ones beside
     them are proportional, and a column of numbers stops lining up the moment
     one of its rows is typed. */
  font-variant-numeric: tabular-nums;
}

table.data th,
table.data td {
  padding: var(--space-2) var(--cell-gutter);
  text-align: left;
  white-space: nowrap;
  border-bottom: var(--hairline) solid var(--separator);
  /* The pinned first column and the column dividers both position against
     their cell. */
  position: relative;
}

/* A phone needs *more* room between columns, not less.

   The old reasoning — save every pixel, the screen is small — produced a
   Monthly Log whose input boxes sat edge to edge with nothing between them,
   which is unreadable however many columns it fits. The table scrolls
   sideways anyway, so a narrow screen buys nothing by crowding: what it costs
   is the ability to tell one figure from the next at a glance.

   So the gutter grows on a phone rather than shrinking. Every rule below
   reads it from this token, including the arithmetic that keeps a field's
   text on the same line as a printed cell's. */
.shell[data-view="mobile"] { --cell-gutter: var(--space-4); }

.shell[data-view="mobile"] table.data th,
.shell[data-view="mobile"] table.data td { padding: var(--space-2) var(--cell-gutter); }

/* The field keeps the same horizontal padding on a phone: it is what the
   cell above subtracts, and shrinking one without the other puts typed text
   back out of line with printed text. Only the vertical padding gives way. */
.shell[data-view="mobile"] .cell-input { padding: 2px var(--cell-pad-x); }

/* --------------------------------------------------------------------------
   The table's outer edges

   Between columns, tight is right: every pixel spent there is a pixel a
   column of figures does not have. Against the card's own edge it is the
   opposite — a table sat its first word 8 or 12 pixels from the edge while
   every list and card in the app insets its text by 16, so the tables alone
   read as though they were pushed up against the wall.

   So the gutter is put back on the outermost cells only. The interior keeps
   its tighter spacing, the columns keep their width, and the first word of a
   row now starts on the same line as every other first word on the page.
   Last-child too, so the right-hand figure is not flush against the edge
   either. This comes after the mobile rule above deliberately: on a phone the
   edges matter more, not less, and a wide table scrolls anyway.
   -------------------------------------------------------------------------- */
table.data th:first-child,
table.data td:first-child { padding-left: var(--space-4); }

table.data th:last-child,
table.data td:last-child { padding-right: var(--space-4); }

/* The row's own controls are already inset by the cell above; a second
   helping would push them off the edge of a phone. */
table.data td.row-action { padding-right: var(--space-2); }

/* --------------------------------------------------------------------------
   A cell holding a field

   A field brings its own horizontal padding, which sat on top of the cell's:
   a typed value's first character started eight pixels to the right of a
   printed value's in the row above it, and a column looked ragged the moment
   part of it was filled in. The cell gives that width back, so the field's
   *text* lands exactly where a plain cell's text would while the field keeps
   the inset box that says it can be typed into.
   -------------------------------------------------------------------------- */
table.data td.has-field {
  padding-left: calc(var(--cell-gutter) - var(--cell-pad-x));
  padding-right: calc(var(--cell-gutter) - var(--cell-pad-x));
}

table.data td.has-field:first-child { padding-left: calc(var(--space-4) - var(--cell-pad-x)); }
table.data td.has-field:last-child { padding-right: calc(var(--space-4) - var(--cell-pad-x)); }

/* A phone needs *more* room between columns, not less.

   The old reasoning — save every pixel, the screen is small — produced a
   Monthly Log whose input boxes sat edge to edge with nothing between them,
   which is unreadable however many columns it fits. The table scrolls
   sideways anyway, so a narrow screen buys nothing by crowding: what it costs
   is the ability to tell one figure from the next at a glance.

   So the gutter grows on a phone rather than shrinking. Every rule below
   reads it from this token, including the arithmetic that keeps a field's
   text on the same line as a printed cell's. */
.shell[data-view="mobile"] { --cell-gutter: var(--space-4); }

.shell[data-view="mobile"] table.data th,
.shell[data-view="mobile"] table.data td { padding: var(--space-2) var(--cell-gutter); }

table.data thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background-color: var(--bg-elevated-2);
  font: var(--type-caption1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   Per-table column resizing (requirement 10)
   --------------------------------------------------------------------------
   Widths are declared on the table's own <col> elements and only become
   authoritative once the user has actually dragged something — until then the
   browser's content-driven sizing is the better answer, and every untouched
   table keeps it. `is-sized` is what switches the table over. */

table.data.is-sized {
  table-layout: fixed;
  width: 100%;
  min-width: 0;
}

/* Under fixed layout a cell can be narrower than its text, so text has to be
   allowed to wrap or be trimmed rather than pushing the column back open. */
table.data.is-sized td,
table.data.is-sized th { overflow: hidden; }

table.data.is-sized td.cell-text,
table.data.is-sized th.cell-text { max-width: none; }

table.data.is-sized td.num { text-overflow: ellipsis; }

/* The header needs room for its own divider, and the label must not run
   underneath it. */
table.data th { position: relative; }
.th-label { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }

.col-resizer {
  position: absolute;
  top: 0;
  right: -5px;
  width: 11px;
  height: 100%;
  cursor: col-resize;
  touch-action: none;
  z-index: 3;
}

/* The line itself. It is always there, faintly — a divider nobody can see is
   a feature nobody finds — and takes the accent when you reach for it. */
.col-resizer::after {
  content: "";
  position: absolute;
  top: 18%;
  bottom: 18%;
  left: 4px;
  width: 2px;
  border-radius: var(--radius-full);
  background-color: var(--separator);
  opacity: 0.55;
  transition: background-color var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out),
    width var(--duration-fast) var(--ease-out);
}

.col-resizer:hover::after,
.col-resizer:focus-visible::after,
.col-resizer.is-dragging::after {
  background-color: var(--accent);
  opacity: 1;
  width: 3px;
}

/* --------------------------------------------------------------------------
   Sticky first column
   --------------------------------------------------------------------------
   A wide table scrolled sideways loses the one column that says what each row
   *is*. Pinning it keeps the month, the item or the client in view while the
   figures move past — requirement 4 asks for it by name on the Monthly Log,
   and every table wide enough to scroll has the same problem.

   The pinned cells need an opaque background of their own: they are painted
   over the cells sliding underneath, and a transparent one would show both. */
table.data.has-sticky-first tbody td:first-child,
table.data.has-sticky-first tbody th:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background-color: var(--bg-elevated);
}

table.data.has-sticky-first thead th:first-child {
  position: sticky;
  left: 0;
  /* Above the other header cells, which are themselves sticky to the top. */
  z-index: 3;
  background-color: var(--bg-elevated-2);
}

/* Hovering a row must repaint the pinned cell too, or it stands out as the one
   cell that did not react. */
table.data.has-sticky-first tbody tr:hover td:first-child {
  background-color: var(--bg-hover);
  /* The hover tint is translucent, so the row beneath would show through. */
  background-image: linear-gradient(var(--bg-elevated), var(--bg-elevated));
  background-blend-mode: normal;
}

/* A hairline down the pinned edge, so the seam reads as a boundary rather
   than as two tables that happen to touch. */
table.data.has-sticky-first tbody td:first-child::after,
table.data.has-sticky-first thead th:first-child::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--hairline);
  background-color: var(--separator);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}

.table-wrap.is-scrolled table.data.has-sticky-first tbody td:first-child::after,
.table-wrap.is-scrolled table.data.has-sticky-first thead th:first-child::after {
  opacity: 1;
}

table.data.has-sticky-first tbody td:first-child { position: sticky; }

/* Paper has no scrolling, so nothing needs pinning to it. */
@media print {
  table.data.has-sticky-first tbody td:first-child,
  table.data.has-sticky-first tbody th:first-child,
  table.data.has-sticky-first thead th:first-child { position: static; }
}

/* A divider is a screen affordance; on paper it is a stray mark. */
@media print { .col-resizer { display: none; } }

/* Row-header column stays visible while the table scrolls sideways. */
table.data tbody th[scope="row"] {
  position: sticky;
  left: 0;
  z-index: 1;
  background-color: var(--bg-elevated);
  font-weight: 500;
}

table.data td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

table.data tbody tr:hover td,
table.data tbody tr:hover th[scope="row"] { background-color: var(--bg-hover); }

table.data tr:last-child td,
table.data tr:last-child th { border-bottom: none; }

/* A cell whose value comes from a formula, so it can be told apart from a
   typed constant when auditing the sheet. */
td.is-formula { color: var(--text-primary); }
td.is-formula::after {
  content: "";
  display: inline-block;
  width: 4px;
  height: 4px;
  margin-left: var(--space-1);
  border-radius: 50%;
  background-color: var(--accent);
  vertical-align: middle;
  opacity: 0.55;
}

td.is-error { color: var(--color-negative); font-weight: 600; }

/* --------------------------------------------------------------------------
   Sheet / modal
   -------------------------------------------------------------------------- */

.scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  background-color: var(--bg-scrim);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  display: grid;
  place-items: end center;
  animation: fade-in var(--duration-base) var(--ease-out);
  /* A drag that starts on the backdrop moves nothing. The sheet inside is
     unaffected: it is a scroll container of its own, so a touch on it never
     consults this element — but a flick that misses the sheet used to reach
     the page behind, and this is one of the two things that stop it (the
     other is the touchmove refusal in ui/scrim-lock.js, because iOS treats
     `touch-action` as more of a suggestion than Chrome does). */
  touch-action: none;
  overscroll-behavior: contain;
  /* And a mouse wheel dies here too. A wheel is routed by the compositor to
     the first SCROLLABLE layer under the pointer, walking layers rather than
     the DOM — so a scrim that is not a scroll container simply is not
     consulted, and a wheel over it scrolled the page behind the sheet.
     `auto` makes the scrim a scroll node with nothing to scroll: the wheel
     stops on it, and `overscroll-behavior` above refuses the hand-off. The
     bar that could flicker during a sheet's entrance is hidden. (The
     keyboard-fitted scrim's curtain is `position: fixed`, so it neither
     contributes overflow here nor gets clipped by this.) */
  overflow: auto;
  scrollbar-width: none;
}

.shell[data-view="desktop"] ~ .scrim,
.scrim.is-centered { place-items: center; }

/* --- a scrim pinned to what the keyboard leaves -----------------------------
   Set by ui/keyboard-sheet.js, which writes `top`, `left`, `width` and
   `height` from `visualViewport`. `inset: 0` above would fight those, so the
   two edges it no longer needs are given back: with top, height AND bottom
   all set, one of them has to be ignored, and which one is not worth relying
   on. */
.scrim.is-fitted {
  right: auto;
  bottom: auto;
  /* Opaque, and deliberately so. With the keyboard up, iOS pans the layout
     viewport to keep the caret in view — a movement no CSS forbids and no
     listener can cancel, because no gesture asked for it. The scrim is
     re-pinned the moment the pan is reported, but for one frame the world
     behind it shifts; painted solid, there is no world behind it to see
     shift. A pinned sheet covers the whole visible strip anyway, so the
     glass showed nothing worth keeping. */
  background-color: var(--bg-base);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* And a curtain beyond the strip, for the keyboard's OPENING. While the
   keyboard animates in, the browser pans the layout viewport in steps and
   reports each one after it has already been painted — so however fast the
   scrim is re-pinned, it is a frame behind, and each frame exposed a slice
   of the app above or below it: the "background drags, then snaps into
   place" of the report. The scrim cannot be early, but it can be BIGGER
   than its lag: this sheet of the same solid colour is fixed to the layout
   viewport itself, half a screen taller in both directions, and moves as
   one with whatever the browser pans. Wherever the visible strip lands
   mid-animation, what shows around the sheet is this — one flat colour,
   whose movement is invisible because every part of it looks the same. */
.scrim.is-fitted::before {
  content: '';
  position: fixed;
  inset: -50% 0;
  background-color: var(--bg-base);
  z-index: -1;
}

/* And the page underneath is held still while any such sheet is open. Both
   scrollers, because iOS rubber-bands whichever of them it likes. */
html.is-sheet-held,
html.is-sheet-held body { overflow: hidden; }
html.is-sheet-held .main { overflow: hidden; }

.sheet {
  width: 100%;
  max-width: 560px;
  max-height: 88dvh;
  /* Both axes, and the sideways one on purpose. Naming only `overflow-y` here
     leaves the other axis computing to `auto`, which quietly makes every sheet
     in the app a horizontal scroller: one field a hair too wide for the phone —
     and a date field on iOS is exactly that — and the whole sheet can be
     dragged off to the left with its right-hand edge disappearing. It never
     scrolls sideways on purpose, so it never scrolls sideways. */
  overflow-y: auto;
  overflow-x: hidden;
  /* A flick that reaches the end of the sheet stops at the end of the sheet
     rather than handing the rest of the gesture to the page behind it. */
  overscroll-behavior: contain;
  background-color: var(--bg-base);
  border-radius: var(--radius-2xl) var(--radius-2xl) 0 0;
  padding: var(--space-3) var(--space-4) calc(var(--space-6) + var(--safe-bottom));
  box-shadow: var(--shadow-xl);
  animation: slide-up var(--duration-slow) var(--ease-out);
}

.scrim.is-centered .sheet {
  border-radius: var(--radius-2xl);
  animation: pop-in var(--duration-base) var(--ease-spring);
  padding-bottom: var(--space-6);
}

/* A title on the middle line of the sheet rather than the middle of whatever
   the close button left it. The button comes out of the flow and sits over
   the header instead, which on a phone is most of the width. */
.sheet-header.is-centred {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
}

.sheet-header.is-centred .sheet-title {
  text-align: center;
  flex: none;
}

.sheet-header.is-centred > .nav-btn {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}

/* The drag indicator ("grabber") at the top of an iOS sheet. */
.sheet-grabber {
  width: 36px;
  height: 5px;
  border-radius: var(--radius-full);
  background-color: var(--fill-primary);
  margin: 0 auto var(--space-4);
}

.sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.sheet-title { font: var(--type-title3); letter-spacing: var(--tracking-tight); }

/* A value that is only the empty-placeholder dash. It keeps the position a
   figure would occupy but stops shouting: at headline size a hyphen reads as
   a bar drawn across the card, which is the "oversized dash" requirement 2
   rules out. */
.is-blank {
  font: var(--type-body) !important;
  font-weight: 400 !important;
  color: var(--text-tertiary) !important;
  letter-spacing: 0;
}

/* --------------------------------------------------------------------------
   Archive (filed years)
   -------------------------------------------------------------------------- */

.shell[data-view="desktop"] .archive-grid {
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

.archive-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* The whole heading is the target: a year card is something you open, and
   making people find a small "Open" link first is a step for nothing. */
.archive-open {
  display: block;
  width: 100%;
  text-align: left;
  color: inherit;
  border-radius: var(--radius-md);
  transition: color var(--duration-fast) var(--ease-out);
}

.archive-open:hover { color: var(--accent); }

.archive-year {
  font: var(--type-title1);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
}

.archive-meta {
  font: var(--type-footnote);
  color: var(--text-secondary);
}

.archive-figures {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding-top: var(--space-3);
  border-top: var(--hairline) solid var(--separator);
}

.archive-figure {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  font: var(--type-subhead);
}

.archive-figure-label { color: var(--text-secondary); }

.archive-figure-value {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.archive-figure-value.is-strong {
  font-weight: 600;
  color: var(--accent);
}

.archive-actions { margin-top: auto; }
.archive-actions .btn:last-child { flex: none; }

/* --------------------------------------------------------------------------
   Tracking year (Year screen)
   -------------------------------------------------------------------------- */

.year-card { padding: var(--card-pad); }

.year-field {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.year-label {
  font: var(--type-subhead);
  color: var(--text-secondary);
  flex: none;
}

/* Wide enough for four digits and the menu arrow at any interface scale, and
   set in the title ramp because it is the heading the whole page hangs off. */
.year-select {
  font: var(--type-title3);
  font-size: max(16px, 1.25rem);
  font-variant-numeric: tabular-nums;
  min-width: 7.5rem;
  max-width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
}

.year-divider {
  height: var(--hairline);
  background-color: var(--separator);
  margin: var(--space-3) 0;
}

/* --------------------------------------------------------------------------
   Months-of-income scale (Savings)
   -------------------------------------------------------------------------- */

.months-card { margin-bottom: var(--space-3); }

.months-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}

.months-title {
  font: var(--type-subhead);
  color: var(--text-secondary);
}

.months-value {
  font: var(--type-headline);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

/* A ruler, not a list of buttons: whole months are labelled and taller, the
   half steps sit between them as marks. */
.months-scale {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  overflow-x: auto;
  padding-bottom: var(--space-1);
  scrollbar-width: none;
}

.months-scale::-webkit-scrollbar { display: none; }

.months-step {
  flex: 1 1 0;
  min-width: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 30px;
  border-radius: var(--radius-xs);
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
  font: var(--type-caption1);
  font-variant-numeric: tabular-nums;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.months-step.is-whole {
  height: 38px;
  font-weight: 600;
  color: var(--text-primary);
  background-color: var(--fill-tertiary);
}

.months-step:hover { background-color: var(--accent-tint); color: var(--accent); }

.months-step[aria-checked="true"] {
  background-color: var(--accent);
  color: var(--text-on-accent);
  font-weight: 600;
}

.months-note { margin-top: var(--space-2); }

/* On paper the whole scale is noise; only the chosen figure matters. */
@media print {
  .months-scale, .months-note { display: none; }
}

/* --------------------------------------------------------------------------
   Tracker name
   -------------------------------------------------------------------------- */

.name-field {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--separator);
  background-color: var(--bg-input);
  color: var(--text-primary);
  font: var(--type-headline);
  /* Under 16px iOS zooms the page the moment this is focused. */
  font-size: max(16px, 1.0625rem);
}

.name-field:focus { outline: none; border-color: var(--accent); }

/* --------------------------------------------------------------------------
   The name, locked until asked for (v1.3.10.7)
   --------------------------------------------------------------------------
   A name is read far more often than it is changed, so the resting state is
   the name itself: no boxes, no chrome, and one very small pencil above and
   to the right. Pressing it gives back the two fields it always was.
   -------------------------------------------------------------------------- */

.name-edit {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: var(--hairline) solid var(--separator);
}

/* Locked, the two fields are a name on two lines, so they close up. Editing,
   they are two controls, so they part again. */
.name-edit:not(.is-editing) { gap: 0; }

.name-edit-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}

.name-edit-label {
  font-size: var(--type-caption2);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.name-edit-btn {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.name-edit-btn svg { width: 13px; height: 13px; }
.name-edit-btn:hover { color: var(--accent); background: var(--fill-quaternary); }
.name-edit-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.name-edit.is-editing .name-edit-btn { color: var(--accent); }

/* Locked: the field is still a field — same text, same place, so nothing
   jumps when it unlocks — with its box taken away. */
.name-edit:not(.is-editing) .name-field {
  border-color: transparent;
  background-color: transparent;
  padding: 1px 0;
  cursor: default;
}

/* The empty case is the one that would otherwise vanish: no name, no box, and
   a blank gap where a control used to be. The placeholder stays legible. */
.name-edit:not(.is-editing) .name-field::placeholder { color: var(--text-tertiary); }

/* --------------------------------------------------------------------------
   Customize theme, in the profile
   --------------------------------------------------------------------------
   Ultimate only, and the same builder Settings opens. Thin and full width, so
   it reads as a door rather than as a fourth thing to decide.
   -------------------------------------------------------------------------- */

.profile-theme {
  width: 100%;
  min-height: 34px;
  justify-content: center;
  margin-top: var(--space-3);
  padding-block: var(--space-1);
  font-size: var(--type-footnote);
}

.profile-theme svg { width: 15px; height: 15px; }

/* --------------------------------------------------------------------------
   Text-size control
   -------------------------------------------------------------------------- */

.scale-control {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
}

.scale-slider {
  flex: 1;
  min-width: 0;
  height: 28px;
  accent-color: var(--accent);
  cursor: pointer;
}

/* The two A's flanking the slider, the smaller and larger ends of the range —
   the same shorthand Apple uses for its own text-size control. */
.scale-mark {
  font-weight: 600;
  color: var(--text-secondary);
  line-height: 1;
}

.scale-mark.is-small { font-size: 0.8125rem; }
.scale-mark.is-large { font-size: 1.25rem; }

/* --------------------------------------------------------------------------
   Quick add form
   -------------------------------------------------------------------------- */

.quick-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.quick-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.quick-label {
  font: var(--type-footnote);
  color: var(--text-secondary);
  letter-spacing: var(--tracking-normal);
}

/* --------------------------------------------------------------------------
   The quick-add sheet
   --------------------------------------------------------------------------
   Four fields, two switches and two buttons, and every one of them belongs on
   the middle line. Left-aligned labels above full-width fields left the whole
   dialog hugging one edge with a column of nothing beside it — fine in a
   settings page that goes on for a screenful, wrong for something this small.
   -------------------------------------------------------------------------- */

.sheet.is-quick {
  max-width: 400px;
  text-align: center;
}

/* See `.sheet-header.is-centred` below, which this sheet uses for its title. */

.sheet.is-quick .quick-row { align-items: stretch; }
.sheet.is-quick .quick-label { text-align: center; }

/* Every field the full width of the sheet.
   The extra selector weight is deliberate: a numeric cell in a *table* is
   capped at 160px so a column of them reads as a column, and that cap has to
   be lifted here or the four fields end up four different widths. */
.sheet.is-quick .quick-form .quick-row .quick-field {
  width: 100%;
  max-width: none;
  box-sizing: border-box;
}

/* Typed text sits on the centre line too, so a half-filled form still reads
   as one column rather than as a ragged left edge. */
.sheet.is-quick .quick-field { text-align: center; }
/* A note is a sentence, and a centred sentence is unreadable. */
.sheet.is-quick textarea.quick-field { text-align: left; }

/* A select centres its text but keeps the chevron at its own edge, so it is
   padded symmetrically rather than being pushed off-centre by it. */
.sheet.is-quick select.quick-field {
  text-align: center;
  text-align-last: center;
  padding-inline: var(--space-5);
}

/* The two switches share a line on anything but the narrowest phone: they are
   both one short label and one small control, and stacking them made the
   dialog taller for no gain. */
.quick-switches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  justify-content: center;
}

/* One per line. Side by side they fit, but "Repeats monthly" then wraps to
   two lines against its own switch, which looks like a mistake rather than a
   layout. */
.quick-switch {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  flex: 1 1 100%;
  justify-content: space-between;
}

.quick-footnote {
  font: var(--type-caption);
  color: var(--text-secondary);
  margin-top: var(--space-3);
}

/* Side by side, each half the width — one decision, two equal weights. */
.quick-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.quick-actions .btn {
  flex: 1;
  min-width: 0;
  /* "Add expense" broke across two lines on a narrow phone and made the
     button twice as tall as the one beside it. */
  white-space: nowrap;
  padding-inline: var(--space-3);
}

/* The fields here are full-width form controls rather than table cells, so
   they drop the cell's right alignment and cramped height. */
.quick-field {
  width: 100%;
  min-height: 44px;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
}

/* Date inputs paint their own calendar affordance; give it the accent so it
   reads as a control rather than a smudge. */
.quick-field[type="date"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.6;
}

.quick-field[type="date"]:hover::-webkit-calendar-picker-indicator { opacity: 1; }

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes pop-in {
  from { transform: scale(0.94); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* --------------------------------------------------------------------------
   Empty state
   -------------------------------------------------------------------------- */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-12) var(--space-6);
  text-align: center;
  color: var(--text-secondary);
}

.empty-icon {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-xl);
  display: grid;
  place-items: center;
  background-color: var(--fill-quaternary);
  color: var(--text-tertiary);
  margin-bottom: var(--space-1);
}
.empty-icon svg { width: 28px; height: 28px; }

.empty-title { font: var(--type-headline); color: var(--text-primary); }
.empty-body { font: var(--type-subhead); max-width: 34ch; }

/* --------------------------------------------------------------------------
   Drop zone — importing the workbook
   -------------------------------------------------------------------------- */

.dropzone {
  border: 2px dashed var(--separator);
  border-radius: var(--radius-2xl);
  padding: var(--space-10) var(--space-6);
  text-align: center;
  transition: border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
  cursor: pointer;
}

.dropzone:hover,
.dropzone.is-dragover {
  border-color: var(--accent);
  background-color: var(--accent-tint);
}

/* --------------------------------------------------------------------------
   Toast
   -------------------------------------------------------------------------- */

.toast-host {
  position: fixed;
  left: 50%;
  bottom: calc(var(--tabbar-height) + var(--safe-bottom) + var(--space-4));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: center;
  pointer-events: none;
  width: max-content;
  max-width: calc(100vw - var(--space-8));
}

.shell[data-view="desktop"] ~ .toast-host { bottom: var(--space-6); }

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-full);
  background-color: var(--material-thick);
  -webkit-backdrop-filter: var(--blur-chrome);
  backdrop-filter: var(--blur-chrome);
  box-shadow: var(--shadow-lg);
  font: var(--type-subhead);
  animation: pop-in var(--duration-base) var(--ease-spring);
}

.toast svg { width: 18px; height: 18px; flex: none; }
.toast.is-error svg { color: var(--color-negative); }
.toast.is-success svg { color: var(--color-positive); }

/* --------------------------------------------------------------------------
   Skeleton loading
   -------------------------------------------------------------------------- */

.skeleton {
  background: linear-gradient(90deg, var(--fill-quaternary) 25%, var(--fill-tertiary) 37%, var(--fill-quaternary) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
  border-radius: var(--radius-sm);
}

@keyframes shimmer {
  from { background-position: 100% 50%; }
  to { background-position: 0 50%; }
}

/* --------------------------------------------------------------------------
   Charts
   -------------------------------------------------------------------------- */

.chart-wrap { width: 100%; }

.chart { display: block; }

/* Category labels sit under the plot on the same fractional grid as the
   marks, so each label lands under its own bar or point. */
.chart-labels {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  margin-top: var(--space-1);
}

.chart-label {
  font: var(--type-caption2);
  color: var(--text-tertiary);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chart-empty {
  display: grid;
  place-items: center;
  min-height: 120px;
  font: var(--type-subhead);
  color: var(--text-tertiary);
}

.sparkline { flex: none; opacity: 0.9; }

/* Donut with a centred figure */
.donut-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  flex-wrap: wrap;
}

.donut-stack { position: relative; display: grid; place-items: center; flex: none; }

.donut-center {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  pointer-events: none;
  text-align: center;
}

.donut-center-value {
  font: var(--type-title3);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
}

.donut-center-label { font: var(--type-caption1); color: var(--text-secondary); }

/* Legend */
/* The legend sizes to what it holds rather than filling the card.

   Left to grow, two short rows — "Active · $6,200 · 87%" — were stretched
   across eight hundred pixels, putting the label hard against the left edge
   and the figure hard against the right with nothing but air between them.
   The chart and its legend read as one object, so they are sized as one and
   centred together; the space then falls evenly on both sides instead of
   pooling at the far right. */
.legend {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  flex: 0 1 auto;
  width: min(340px, 100%);
}

/* On a narrow screen the legend wraps under the chart, where filling the
   width is exactly right. */
@media (max-width: 560px) {
  .legend { width: 100%; }
}

.legend-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font: var(--type-subhead);
  min-width: 0;
}

.legend-swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex: none;
}

.legend-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* A little air before the figure, so a long label never reads as though it
     runs into the amount. */
  padding-right: var(--space-2);
}

.legend-value {
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  flex: none;
}

/* --------------------------------------------------------------------------
   Formula audit
   -------------------------------------------------------------------------- */

.formula-code {
  font: 400 0.8125rem/1.5 var(--font-mono);
  background-color: var(--fill-quaternary);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-xs);
  color: var(--text-primary);
  overflow-x: auto;
  white-space: pre;
  display: block;
  max-width: 100%;
}

.formula-ref {
  font: 600 0.75rem/1 var(--font-mono);
  color: var(--accent);
  background-color: var(--accent-tint);
  padding: 3px var(--space-2);
  border-radius: var(--radius-full);
  flex: none;
}

/* Accuracy meter on the audit screen */
.meter {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}

.meter-value {
  font: var(--type-large-title);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
}

.meter-value.is-good { color: var(--color-positive); }
.meter-value.is-warn { color: var(--color-warning); }
.meter-value.is-bad  { color: var(--color-negative); }

/* --------------------------------------------------------------------------
   Sheet picker
   -------------------------------------------------------------------------- */

.sheet-tabs {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  padding-bottom: var(--space-2);
  scrollbar-width: none;
  margin-bottom: var(--space-3);
}
.sheet-tabs::-webkit-scrollbar { display: none; }

.sheet-tab {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  font: var(--type-subhead);
  font-weight: 500;
  white-space: nowrap;
  flex: none;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.sheet-tab[aria-selected="true"] {
  background-color: var(--accent);
  color: var(--text-on-accent);
  font-weight: 600;
}

/* Editable cell input inside a sheet table */
.cell-input {
  width: 100%;
  min-width: 72px;
  padding: 3px var(--cell-pad-x);
  border-radius: var(--radius-xs);
  background-color: transparent;
  font: var(--type-subhead);
  /* The same token the table uses. A field that set its own size — 16px
     against the table's 15px — is what made typed values look pasted in from
     another application. The touch-screen floor that keeps iOS from zooming
     now moves the whole table up together; see tokens.css. */
  font-size: var(--cell-size);
  font-variant-numeric: tabular-nums;
  text-align: right;
  border: 1px solid transparent;
  /* Comfortably tappable without making the table rows tall. */
  min-height: 32px;
  transition: background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

/* Touch devices get a taller target; a pointer does not need one. */
@media (pointer: coarse) {
  .cell-input { min-height: 38px; }
}

/* Editable cells carry a standing tint. An input that is only visible on
   hover is invisible when empty, which is exactly when it most needs to
   advertise itself — an untouched monthly log would otherwise look like a
   blank, read-only table. */
.cell-input {
  background-color: var(--editable-bg);
  border-color: var(--editable-border);
}

.cell-input:hover { background-color: var(--editable-bg-hover); }

.cell-input:focus {
  background-color: var(--bg-elevated);
  border-color: var(--accent);
  outline: none;
}

.cell-input::placeholder { color: var(--text-tertiary); }

/* Every numeric field fills the cell it sits in, so a column of boxes reads as
   one column and not as a ragged set stepping in and out with whatever each
   row happens to hold. A field never sizes itself to its own figure: the room
   for a long one is made by the column, which is the only thing that can make
   it without pulling the rows around it out of line. See `numericColumnFit`.

   `min-width: 0` is what lets that hold. The 72px floor on `.cell-input` would
   otherwise keep a field open past a column dragged narrower than itself, and
   the figures would spill across the divider. */
table.data .cell-input:not(.cell-input-text):not(.cell-input-date) {
  width: 100%;
  min-width: 0;
  max-width: 100%;
}

/* Outside a table there is no column to line up with, and the field shares its
   line with a label. So here it does size to its contents — through the `size`
   attribute, which is how an input is asked for a width in characters — and
   gives the rest of the line back to the label. Filling the row instead left
   "Nest Egg starting total" wrapping one word per line beside a box mostly
   full of nothing. */
.list-row .cell-input:not(.cell-input-text):not(.cell-input-date) {
  width: auto;
  min-width: 6.5rem;
  max-width: 100%;
}

/* A date cell sizes to the picker's own text; left-aligned, because a date
   reads as a label rather than a figure. */
.cell-input-date {
  text-align: left;
  min-width: 8.5rem;
  max-width: 100%;
  font-variant-numeric: normal;
}

td.cell-date { white-space: nowrap; }

.cell-input-date::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.55;
}

.cell-input-date:hover::-webkit-calendar-picker-indicator { opacity: 1; }

/* A cell the user has overridden, so it can be told from imported data.

   The weight is for figures only. A column of numbers reads better with the
   entered ones standing out, but a name or a description set in semibold just
   looks like it is shouting — and every line you type is one you entered, so
   the emphasis marks nothing. Lettering stays at its normal weight. */
/* Weight only — the colour stays the one that was chosen.
   This used to paint an orange tint over the field, which on a dark screen
   reads as red and, worse, meant that typing into a box changed it away from
   the colour set under Text box colour. A setting that only holds until you
   use the thing it applies to is not a setting. */
.cell-input.is-overridden {
  font-weight: 600;
}

.cell-input-text.is-overridden { font-weight: 400; }

/* --------------------------------------------------------------------------
   Search field
   -------------------------------------------------------------------------- */

.search-field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-tertiary);
  margin-bottom: var(--space-3);
}

.search-field svg { width: 17px; height: 17px; color: var(--text-secondary); flex: none; }

.search-field input {
  background: none;
  border: none;
  outline: none;
  font: var(--type-body);
  color: var(--text-primary);
  min-width: 0;
}

.search-field input::placeholder { color: var(--text-tertiary); }

/* --------------------------------------------------------------------------
   Tailored tracker screens
   -------------------------------------------------------------------------- */

/* Marks a whole column as calculated, in the header rather than on every cell. */
.col-formula-mark {
  display: inline-block;
  margin-left: var(--space-1);
  font-family: var(--font-mono);
  font-style: italic;
  color: var(--accent);
  opacity: 0.75;
  text-transform: none;
}

/* The formula shown beneath a field label. Monospaced and dimmed so it reads
   as provenance, not as content. */
.formula-hint {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* On a phone the formula provenance is more noise than help — the value and
   its label are what fit. */
.shell[data-view="mobile"] .formula-hint { display: none; }

/* --------------------------------------------------------------------------
   Dropdowns bound to workbook cells
   -------------------------------------------------------------------------- */

.cell-select {
  /* Native select, restyled to sit in a table cell like the text inputs do,
     while keeping the platform's own picker behaviour. */
  appearance: none;
  -webkit-appearance: none;
  text-align: left;
  padding-right: var(--space-5);
  cursor: pointer;
  min-width: 120px;
  /* The chevron is drawn as a background image so it cannot be clipped by the
     cell and needs no extra element. */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 12px center, right 7px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.cell-select:focus { background-color: var(--bg-elevated); }

/* --------------------------------------------------------------------------
   Month bar
   -------------------------------------------------------------------------- */

.month-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}

.month-scroll {
  display: flex;
  gap: var(--space-1);
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: var(--space-1);
  flex: 1 1 320px;
  min-width: 0;
  scroll-snap-type: x proximity;
}
.month-scroll::-webkit-scrollbar { display: none; }

.month-chip {
  flex: none;
  min-width: 52px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-tertiary);
  font: var(--type-subhead);
  font-weight: 500;
  white-space: nowrap;
  scroll-snap-align: center;
  position: relative;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.month-chip[aria-selected="true"] {
  background-color: var(--accent);
  color: var(--text-on-accent);
  font-weight: 600;
}

/* A dot marks a month with entries; a ring marks one already closed. */
.month-chip .month-dot {
  display: block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  margin: 3px auto 0;
  background-color: var(--text-tertiary);
}
.month-chip[aria-selected="true"] .month-dot { background-color: var(--text-on-accent); }
.month-chip.is-closed .month-dot { background-color: var(--color-positive); }
.month-chip.is-empty .month-dot { background-color: transparent; }

/* --------------------------------------------------------------------------
   Guided tour
   --------------------------------------------------------------------------
   The dimming is four panels rather than one box with a hole in it: a single
   element with a vast box-shadow would swallow every click on the page, and
   the whole point of a spotlight is that the thing under it still works.
   -------------------------------------------------------------------------- */

.tour-root {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  /* The root itself catches nothing; its panels and card do. */
  pointer-events: none;
}

.tour-shade {
  position: fixed;
  background-color: rgba(0, 0, 0, 0.55);
  pointer-events: auto;
  transition: opacity var(--duration-fast) var(--ease-out);
}

/* The outline around whatever is being explained. */
.tour-ring {
  position: fixed;
  border-radius: var(--radius-md);
  box-shadow: 0 0 0 2px var(--accent), 0 0 0 6px var(--accent-tint);
  pointer-events: none;
  transition: top var(--duration-base) var(--ease-out),
    left var(--duration-base) var(--ease-out),
    width var(--duration-base) var(--ease-out),
    height var(--duration-base) var(--ease-out);
}

.tour-card {
  position: fixed;
  width: min(360px, calc(100vw - 24px));
  padding: var(--space-4);
  border-radius: var(--radius-xl);
  background-color: var(--bg-elevated);
  box-shadow: var(--shadow-xl);
  pointer-events: auto;
  transition: top var(--duration-base) var(--ease-out),
    left var(--duration-base) var(--ease-out);
}

/* A step about the app as a whole, with nothing in particular to point at. */
.tour-card.is-centred {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.tour-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.tour-badge {
  font: var(--type-caption2);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-on-accent);
  background-color: var(--accent);
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
}

/* "Step 3 / 14" — always visible, so nobody has to wonder how much is left. */
.tour-progress {
  font: var(--type-footnote);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

.tour-close {
  margin-left: auto;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  color: var(--text-secondary);
}

.tour-close svg { width: 16px; height: 16px; }
.tour-close:hover { background-color: var(--fill-quaternary); color: var(--text-primary); }

.tour-title {
  font: var(--type-title3);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-2);
}

.tour-body {
  font: var(--type-subhead);
  color: var(--text-secondary);
}

.tour-missing {
  font: var(--type-footnote);
  color: var(--text-tertiary);
  margin-top: var(--space-2);
}

.tour-tip {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--accent-tint);
  font: var(--type-footnote);
  color: var(--text-primary);
}

.tour-tip-label {
  font-weight: 700;
  color: var(--accent);
  flex: none;
}

.tour-foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.tour-dots { flex: 1; display: flex; justify-content: center; }

.tour-skip {
  font: var(--type-footnote);
  color: var(--text-secondary);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
}

.tour-skip:hover { color: var(--accent); background-color: var(--fill-quaternary); }

/* The page still scrolls during a tour — the spotlight follows it — because
   locking it would leave the tour unable to bring its own targets into view. */

@media print { .tour-root { display: none; } }

/* --------------------------------------------------------------------------
   Print
   --------------------------------------------------------------------------
   Nothing on screen is printed. print.js builds a separate document from the
   current screen's content and attaches it as #print-root; these rules hide
   the app and lay that document out for paper.

   Everything below is sized in points and millimetres, never rem. The
   interface scale moves the root font size, so a rem here would mean the
   printed page came out at whatever size the app happened to be set to —
   which is precisely the "must work regardless of text scaling" the
   requirement asks for.
   -------------------------------------------------------------------------- */

.print-only { display: none; }

/* Off-screen while the dialog is open, so nothing flashes on the page. */
#print-root {
  position: absolute;
  left: -10000px;
  top: 0;
  width: 186mm;   /* A4 width less the page margins */
}

@page {
  size: A4 portrait;
  margin: 12mm 12mm 14mm;
}

@media print {
  /* The app itself has no business on paper. */
  .shell,
  .toast-host,
  .scrim { display: none !important; }

  html, body {
    background: #fff !important;
    color: #000 !important;
    /* The document is written in points; the root scale must not reach it. */
    font-size: 12pt !important;
    margin: 0;
    padding: 0;
    /* On screen the document is one screen tall and does not scroll, which on
       paper would mean one page and the rest thrown away. Paper is as long as
       it needs to be. */
    height: auto !important;
    overflow: visible !important;
  }

  #print-root {
    position: static;
    left: auto;
    width: auto;
    display: block;
  }

  a[href]::after { content: ""; }
}

/* Several screens in one document: each starts a fresh sheet, with its own
   heading, so a printed bundle reads as a set of pages rather than as one
   long run of tables nobody can tell apart. */
.print-part { margin-bottom: 6pt; }
.print-part.is-new-page { break-before: page; page-break-before: always; }

.print-part-title {
  font-size: 15pt;
  font-weight: 700;
  margin: 0;
}

.print-part-subtitle {
  font-size: 9.5pt;
  color: #555;
  margin-top: 2pt;
}

/* --------------------------------------------------------------------------
   The printed document
   -------------------------------------------------------------------------- */

.print-doc {
  font-family: var(--font-text);
  font-size: 9.5pt;
  line-height: 1.4;
  color: #000;
  background: #fff;
  /* Long words and long account names break rather than run off the sheet. */
  overflow-wrap: break-word;
}

.print-doc-head {
  border-bottom: 1pt solid #000;
  padding-bottom: 3mm;
  margin-bottom: 6mm;
}

.print-doc-brand {
  font-size: 8pt;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #555;
  margin-bottom: 1mm;
}

.print-doc-title {
  font-size: 20pt;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.15;
  margin: 0;
}

.print-doc-subtitle {
  font-size: 11pt;
  color: #333;
  margin-top: 1mm;
}

.print-doc-meta {
  font-size: 8.5pt;
  color: #666;
  margin-top: 2mm;
}

.print-block {
  margin-bottom: 6mm;
  /* Keep a block whole where it fits on one page; let a long one flow rather
     than pushing a mostly-empty page ahead of it. */
  break-inside: auto;
}

.print-block-title {
  font-size: 11pt;
  font-weight: 700;
  margin: 0 0 2mm;
  padding-bottom: 1mm;
  border-bottom: 0.5pt solid #999;
  /* A heading stranded at the foot of a page belongs with what follows it. */
  break-after: avoid;
}

/* --- tables ------------------------------------------------------------- */

.print-doc table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 3mm;
  /* Auto layout, and no width is ever declared on a column: the printed table
     sizes itself to the paper, so no dragged-in column width can push a
     column off the edge. */
  table-layout: auto;
}

.print-doc th,
.print-doc td {
  padding: 1.2mm 2mm;
  text-align: left;
  vertical-align: top;
  border-bottom: 0.5pt solid #ddd;
}

.print-doc thead th {
  font-size: 8pt;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #444;
  border-bottom: 0.75pt solid #666;
  white-space: nowrap;
}

/* Repeat a long table's header on every page it runs onto. */
.print-doc thead { display: table-header-group; }
.print-doc tr { break-inside: avoid; }

/* Scoped to .print-doc so it outranks the `text-align: left` default above —
   at lower specificity the figures quietly stack up on the left. */
.print-doc .print-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Headline figures and label/value lists both read as two columns: the name
   on the left taking whatever it needs, the figure right-aligned. */
.print-figures th,
.print-pairs th {
  font-weight: 400;
  width: auto;
}

.print-figures td,
.print-pairs td { width: 30%; }

.print-figures td { font-weight: 700; font-size: 10.5pt; }

.print-pairs tr.is-strong th,
.print-pairs tr.is-strong td { font-weight: 700; }

.print-figure-meta,
.print-hint {
  display: block;
  font-size: 7.5pt;
  color: #666;
}

.print-note {
  font-size: 8pt;
  color: #444;
  margin: 1mm 0 3mm;
}

.print-doc-foot {
  margin-top: 6mm;
  padding-top: 2mm;
  border-top: 0.5pt solid #999;
  font-size: 7.5pt;
  color: #666;
}

/* --------------------------------------------------------------------------
   Adjustable section controls
   -------------------------------------------------------------------------- */

.row-action { padding-left: 0 !important; padding-right: var(--space-2) !important; }

/* Note, repeat and bin, in that order: context, then an instruction about
   next month, then the one that takes something away. */
.row-tools {
  display: flex;
  align-items: center;
  gap: 2px;
}

.row-tool {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 50%;
  color: var(--text-tertiary);
  background-color: transparent;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.row-tool svg { width: 15px; height: 15px; }

.row-tool:hover { background-color: var(--fill-quaternary); color: var(--text-primary); }

/* A note already written is worth seeing at a glance, without opening it. */
.row-note.has-note { color: var(--accent); }
.row-note.has-note:hover { background-color: var(--accent-tint); color: var(--accent); }

/* The repeat mark is a state, not an action, so it stays lit while it is on. */
.row-repeat.is-on {
  color: var(--accent);
  background-color: var(--accent-tint);
}

.row-repeat.is-on:hover { background-color: var(--accent-tint); }

.row-remove:hover {
  background-color: var(--tint-negative);
  color: var(--color-negative);
}

/* The question mark beside the bin. Same size and same weight as its
   neighbour, because it is the same kind of thing — a small tool on a row —
   and one of the two being louder would make the row feel lopsided. */
.row-help:hover {
  background-color: var(--accent-tint);
  color: var(--accent);
}

/* On touch there is no hover to reveal anything, so the controls stay legible
   the whole time. */
@media (pointer: coarse) {
  .row-tool { color: var(--text-secondary); }
}

@media print {
  .row-action, .row-tools { display: none !important; }
}

/* --------------------------------------------------------------------------
   Editable option lists
   -------------------------------------------------------------------------- */

/* The plus that opens a column's list. Sits in the header beside the label,
   quiet until reached for. */
.col-list-edit {
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  margin-left: var(--space-1);
  vertical-align: middle;
  border-radius: 50%;
  color: var(--text-tertiary);
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.col-list-edit svg { width: 12px; height: 12px; }

.col-list-edit:hover { background-color: var(--accent-tint); color: var(--accent); }

@media (pointer: coarse) { .col-list-edit { color: var(--text-secondary); } }
@media print { .col-list-edit { display: none; } }

.option-list {
  display: flex;
  flex-direction: column;
  margin: var(--space-3) 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  background-color: var(--bg-elevated-2);
}

.option-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--hairline) solid var(--separator);
}

.option-row:last-child { border-bottom: none; }

.option-name {
  flex: 1;
  min-width: 0;
  font: var(--type-subhead);
  overflow-wrap: break-word;
}

/* Which options came with the workbook and which the user added — the
   difference matters, because removing one of each does a different thing. */
.option-origin {
  font: var(--type-caption2);
  color: var(--text-tertiary);
  flex: none;
}

.option-origin.is-added { color: var(--accent); }

.option-remove {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: 50%;
  color: var(--text-tertiary);
}

.option-remove svg { width: 14px; height: 14px; }
.option-remove:hover { background-color: var(--tint-negative); color: var(--color-negative); }

.option-add {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.option-field {
  flex: 1;
  min-width: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--separator);
  background-color: var(--bg-input);
  color: var(--text-primary);
  font: var(--type-subhead);
  /* Under 16px iOS zooms the page the moment this is focused. */
  font-size: max(16px, 0.9375rem);
}

.option-field:focus { outline: none; border-color: var(--accent); }

/* --------------------------------------------------------------------------
   Expense note popover
   --------------------------------------------------------------------------
   Hidden by default so the Expenses table stays a table of figures. Hovering
   the marker previews the note; clicking pins it open to edit, because a box
   that closes when the pointer drifts is a box nobody can type into. */

.note-popover {
  /* Fixed, not absolute: the page scrolls inside .main rather than the
     document moving, so document coordinates no longer track the button this
     is attached to. It is placed in viewport coordinates and re-placed
     whenever anything scrolls (see positionPopover). */
  position: fixed;
  z-index: var(--z-sheet);
  width: min(300px, calc(100vw - 24px));
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  background-color: var(--bg-elevated);
  border: var(--hairline) solid var(--separator);
  box-shadow: var(--shadow-lg);
  animation: pop-in var(--duration-fast) var(--ease-out);
}

.note-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

/* Named after the expense it belongs to, so whose note this is never has to
   be guessed. */
.note-title {
  font: var(--type-footnote);
  font-weight: 600;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.note-close {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: 50%;
  color: var(--text-tertiary);
}

.note-close svg { width: 14px; height: 14px; }
.note-close:hover { background-color: var(--fill-quaternary); color: var(--text-primary); }

.note-field {
  width: 100%;
  min-height: 72px;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  border: 1px solid var(--separator);
  background-color: var(--bg-input);
  color: var(--text-primary);
  font: var(--type-subhead);
  /* Under 16px iOS zooms the page the moment this is focused. */
  font-size: max(16px, 0.9375rem);
  resize: vertical;
}

.note-field:focus { outline: none; border-color: var(--accent); }

.note-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Only the pinned state is interactive; a hover preview must not swallow the
   pointer on its way somewhere else. */
.note-popover:not(.is-pinned) .note-field { pointer-events: none; }

@media print { .note-popover { display: none; } }

/* --------------------------------------------------------------------------
   Text entry fields
   --------------------------------------------------------------------------
   Names, notes and descriptions are textareas so they wrap and grow instead
   of clipping. They must still read as one line of a table, not as a comment
   box, so the chrome is identical to the numeric inputs.
   -------------------------------------------------------------------------- */

.cell-input-text {
  text-align: left;
  resize: none;
  overflow: hidden;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.35;
  /* A textarea's baseline is its *last* line, not its first, so left on the
     default it rides two or three pixels above the plain input beside it in
     the same row. Aligning on the middle instead puts the two back on the
     same line — which is the only thing the eye actually notices. */
  vertical-align: middle;
  min-width: 12ch;
  /* Long enough for a real description, short enough that the money columns
     stay on screen beside it. */
  max-width: 34ch;
  font-family: var(--font-text);
}

/* In a settings list the field sits at the right and needs less room. */
.list-row .cell-input-text { max-width: 22ch; }

@media print {
  .cell-input-text { max-width: none; }
}

/* --------------------------------------------------------------------------
   A note that lives in a column of its own
   --------------------------------------------------------------------------
   Same marker as the one on an expense row, so a note reads the same wherever
   it is kept. The cell is only as wide as the button, which is the whole
   point: a Notes column used to be the widest thing in the table and empty
   nine rows out of ten.
   -------------------------------------------------------------------------- */
.cell-note {
  width: 1%;
  white-space: nowrap;
  text-align: center;
}

.cell-note .row-tool { margin-inline: auto; }

/* --------------------------------------------------------------------------
   A tip that can be put away
   -------------------------------------------------------------------------- */
.tip-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-lg);
  background-color: var(--accent-tint);
  color: var(--text-secondary);
  font: var(--type-footnote);
}

.tip-card .tip-body { flex: 1 1 auto; min-width: 0; }
.tip-card strong { color: var(--text-primary); font-weight: 600; }

.tip-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background-color: var(--accent);
  color: var(--text-on-accent);
  font: var(--type-caption2);
  font-weight: 700;
}

.tip-close {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  color: var(--text-tertiary);
}

.tip-close:hover { background-color: var(--fill-quaternary); color: var(--text-primary); }
.tip-close svg { width: 14px; height: 14px; }

/* Put away, the tip leaves behind the smallest possible way back: a circled T,
   sitting beside the heading it belongs to. */
.tip-recall {
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  flex: none;
  border-radius: 50%;
  border: 1px solid var(--separator);
  color: var(--text-tertiary);
  font: var(--type-caption2);
  font-weight: 700;
  line-height: 1;
}

.tip-recall:hover {
  border-color: var(--accent);
  color: var(--accent);
  background-color: var(--accent-tint);
}

/* --------------------------------------------------------------------------
   Picking the mark beside the tracker's name
   -------------------------------------------------------------------------- */
.icon-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.icon-choice {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
  border: 1.5px solid transparent;
  transition: border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.icon-choice:hover { color: var(--text-primary); }

.icon-choice[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
  background-color: var(--accent-tint);
}

.icon-choice svg { width: 20px; height: 20px; }

.icon-choice img,
.sidebar-mark img {
  width: 20px;
  height: 20px;
  object-fit: contain;
  border-radius: var(--radius-xs);
}

.sidebar-mark {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  flex: none;
}

/* --------------------------------------------------------------------------
   The quick-add sheet on a phone
   --------------------------------------------------------------------------
   Narrower and tighter than a full sheet: this dialog is four short fields and
   two buttons, and at full width on a phone it filled the screen for no
   reason. The fields keep their 44px targets — the saving is all padding.
   -------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .scrim.is-centered .sheet.is-compact {
    max-width: min(360px, calc(100vw - 2 * var(--space-5)));
    padding: var(--space-2) var(--space-4) var(--space-4);
    border-radius: var(--radius-xl);
  }

  .sheet.is-compact .quick-form { gap: var(--space-2); }
  .sheet.is-compact .sheet-title { font: var(--type-headline); }

}

/* A table row that goes somewhere. The whole row is the target, so a month is
   opened by tapping anywhere along it rather than hunting for a link. */
tr.is-clickable { cursor: pointer; }
tr.is-clickable:hover > td { background-color: var(--fill-quaternary); }

/* An element hidden with the `hidden` attribute must actually be hidden. The
   browser's own [hidden] rule is a bare attribute selector, so any class here
   that sets `display` beats it — which is how a folded-away row ends up on
   screen anyway. */
[hidden] { display: none !important; }

/* --------------------------------------------------------------------------
   The account button, hard right of the bar
   -------------------------------------------------------------------------- */
.account-btn {
  /* Pushed to the end of the row, and kept there whatever else the bar
     carries — so the one control that is about *you* is always in the same
     corner, the way it is in every other application. */
  margin-left: auto;
}

/* The bar's own gap already separates the buttons; the account button asks for
   a little more, so it reads as its own thing rather than the last of the
   tools beside it. */
.nav-actions .account-btn { margin-left: var(--space-3); }

/* The two buttons that are about you rather than about the page.
   On a phone there is no title between the two ends of the bar — it is hidden
   at this width — so without this every button packs against the left and the
   whole right-hand half of the bar is dead space. This pushes the pair that
   belong under a thumb to the edge, and leaves the rest gathered on the left
   where the page controls belong together. */
.nav-personal {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.shell[data-view="mobile"] #nav-trailing { flex: 1 1 auto; min-width: 0; }
.shell[data-view="mobile"] .nav-personal { margin-left: auto; }

.account-avatar {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  overflow: hidden;
  flex: none;
  background-color: var(--accent-tint);
  color: var(--accent);
  font: var(--type-caption1);
  font-weight: 600;
  letter-spacing: 0;
}

.account-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.account-avatar.is-initials { background-color: var(--accent); color: var(--text-on-accent); }

.account-avatar.is-large {
  width: 56px;
  height: 56px;
  font: var(--type-title3);
  font-weight: 600;
}

.account-btn.is-signed-in { padding: 0; }

/* --------------------------------------------------------------------------
   Signing in, and the profile
   -------------------------------------------------------------------------- */
.auth-sheet { max-width: 380px; }

.profile-head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-4);
  min-width: 0;
}

/* The @name, above the email. Set in the accent so it reads as the chosen
   thing rather than a second address, and truncated the same way — a long
   name should push nothing off the sheet. */
.profile-handle {
  font: var(--type-subhead);
  font-weight: 600;
  color: var(--accent);
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.profile-handle[hidden] { display: none; }

/* --------------------------------------------------------------------------
   The dashboard switch, and the master account's device check
   --------------------------------------------------------------------------
   Both live in the profile sheet and both belong to a minority of accounts, so
   both are drawn quietly: a tinted row rather than a filled button, sitting in
   the same rhythm as everything else in the sheet.
   -------------------------------------------------------------------------- */

.dash-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  margin-top: var(--space-4);
  padding: var(--space-3);
  border: 1px solid var(--separator);
  border-radius: var(--radius-md);
  background: var(--fill-quaternary);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.dash-toggle:hover { background: var(--fill-tertiary); }
.dash-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* On the dashboard already, so the row is the way back — tinted with the
   accent so the two states are told apart at a glance rather than by reading. */
.dash-toggle.is-on {
  border-color: var(--accent);
  background: var(--accent-tint);
}

.dash-toggle-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  color: var(--accent);
}

.dash-toggle-mark svg { width: 17px; height: 17px; }

.dash-toggle-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1;
}

.dash-toggle-title {
  font: var(--type-subhead);
  font-weight: 600;
  color: var(--text-primary);
}

.dash-toggle-sub {
  font: var(--type-caption1);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dash-toggle-go { flex: none; color: var(--text-tertiary); display: grid; place-items: center; }
.dash-toggle-go svg { width: 15px; height: 15px; }

.master-check {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--separator);
}

/* --------------------------------------------------------------------------
   The picture, and what you can do to it
   --------------------------------------------------------------------------
   Change and Remove used to sit beside the picture as two permanent links on
   a dialog whose subject is a name and an email address. Behind the picture
   they cost nothing until wanted, and the picture is where somebody looks for
   them anyway.
   -------------------------------------------------------------------------- */

.picture-control { position: relative; flex: none; }

.picture-trigger {
  position: relative;
  display: block;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  border-radius: var(--radius-full);
}

.picture-trigger:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* The small mark that says the picture is a control. Without it a picture is
   just a picture and nobody thinks to press it. */
.picture-badge {
  position: absolute;
  right: -2px;
  bottom: -2px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  color: #fff;
  border: 2px solid var(--bg-base);
  transition: transform var(--duration-fast) var(--ease-out);
}

.picture-badge svg { width: 13px; height: 13px; }
.picture-trigger:hover .picture-badge { transform: scale(1.08); }

.picture-menu {
  position: absolute;
  left: 0;
  top: calc(100% + var(--space-2));
  z-index: 5;
  min-width: 190px;
  padding: var(--space-1);
  border-radius: var(--radius-lg);
  background-color: var(--bg-elevated);
  border: 1px solid var(--separator);
  box-shadow: var(--shadow-lg);
  animation: pop-in var(--duration-fast) var(--ease-out);
}

.picture-choice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 44px;
  padding: 0 var(--space-3);
  border: none;
  border-radius: var(--radius-md);
  background: none;
  color: var(--text-primary);
  font: var(--type-subhead);
  text-align: left;
  cursor: pointer;
}

.picture-choice:hover { background-color: var(--fill-quaternary); }
.picture-choice.is-danger { color: var(--color-red); }
.picture-choice svg { width: 17px; height: 17px; flex: none; }

/* The quiet way between the three states of the sign-in sheet, and out of the
   profile. A link, not a button: these are changes of mind, not actions. */
.auth-links {
  display: flex;
  justify-content: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

.btn-link {
  background: none;
  color: var(--accent);
  font: var(--type-footnote);
  padding: var(--space-1) 0;
  text-align: left;
}

.btn-link:hover { text-decoration: underline; }

/* --------------------------------------------------------------------------
   The first-visit welcome
   --------------------------------------------------------------------------
   Deliberately small. It is shown once, it answers one question, and every
   line of it is either the answer or a way out. Anything longer would be a
   thing to dismiss rather than a thing to read.
   -------------------------------------------------------------------------- */

.welcome-sheet {
  text-align: center;
  padding-top: var(--space-6);
  max-width: 22rem;
}

/* The app's own mark, shown at the size it is drawn at. No plate behind it:
   the artwork already carries its own tile, and painting the accent under it
   put a blue square around the icon. The rounding is clipped rather than
   drawn, so the tile's corners follow the card's radius. */
.welcome-mark {
  width: 52px;
  height: 52px;
  margin: 0 auto var(--space-4);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.welcome-mark img { display: block; width: 100%; height: 100%; }

.welcome-title {
  font: var(--type-title2);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-2);
}

.welcome-body {
  font: var(--type-subhead);
  color: var(--text-secondary);
  margin-bottom: var(--space-5);
  text-wrap: pretty;
}

.welcome-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.welcome-actions .btn { width: 100%; }

.welcome-skip {
  margin-top: var(--space-3);
  /* A real way out, at the size of one: the honest answer to "do I need an
     account?" is no, and a link nobody can find would say otherwise. */
  font: var(--type-subhead);
}

/* A step with a long body and a tip can outgrow the room beside its
   spotlight. placeCard caps the height to whatever room that side has; this is
   what lets the overflow scroll instead of running off the screen, taking the
   buttons that move the tour on with it. */
.tour-card {
  display: flex;
  flex-direction: column;
  max-height: calc(100dvh - 24px);
  overflow: hidden;
}

/* Only the middle gives way. The step's heading stays put and so do Back,
   Skip and Next, which are the only way out of a tour. */
.tour-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.tour-head,
.tour-foot { flex: none; }

/* On a phone a 360px card beside a spotlight leaves too little for either, so
   it takes the width and stacks above or below instead. */
.tour-root.is-mobile .tour-card {
  width: calc(100vw - 24px);
}

/* Only the placed ones. A centred card — the steps that point at nothing —
   is positioned by its own transform, and forcing its left edge to the gutter
   drags it half its width off the side of the screen. */
.tour-root.is-mobile .tour-card:not(.is-centred) {
  left: 12px !important;
}

/* --------------------------------------------------------------------------
   The Ledgers hub
   --------------------------------------------------------------------------
   Only ever seen on a phone, where it is three short groups with room to
   spare. A little space above settles it under the heading; kept small on
   purpose, since the first row still has to be under a thumb.
   -------------------------------------------------------------------------- */
.ledgers-hub { margin-top: var(--space-4); }

/* --------------------------------------------------------------------------
   Settings, in columns
   --------------------------------------------------------------------------
   A desktop has two or three times the width a settings list needs, and one
   column down the middle of it means scrolling past groups that would have
   fitted on the screen together.

   CSS columns rather than a grid, for one reason: a grid needs each section
   assigned to a column, and assigning them is rearranging them. This is not —
   the sections keep their order and simply flow, down one column and on to the
   next, the way text does. `break-inside: avoid` is what keeps a group whole
   instead of splitting a list across the gap.

   Only ever on the desktop layout, so a phone is untouched at any width.
   -------------------------------------------------------------------------- */
.settings-columns {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

@media (min-width: 1100px) {
  .shell[data-view="desktop"] .settings-columns {
    display: block;
    columns: 2;
    column-gap: var(--space-6);
  }

  .shell[data-view="desktop"] .settings-columns > * {
    break-inside: avoid;
    /* Columns have no row gap of their own; this stands in for one. */
    margin-bottom: var(--space-6);
  }

  /* ABOUT SPANS, so it is genuinely last rather than merely written last.
     Balanced columns put the final section wherever the heights work out — on
     a three-column screen About landed at the top of the third column, level
     with and slightly above the sections it is supposed to sit under. Spanning
     takes it out of the balance: everything else fills the columns above it,
     and it runs across the foot of the page where reference material belongs. */
  .shell[data-view="desktop"] .settings-columns > [data-settings="about"] {
    column-span: all;
    margin-bottom: 0;
  }
}

/* Folding the page finder away is worth a column on its own — that is most of
   a column's width returned to the page. */
@media (min-width: 1500px) {
  .shell[data-view="desktop"] .settings-columns { columns: 3; }
}

@media (min-width: 1280px) {
  .shell[data-view="desktop"].sidebar-hidden .settings-columns,
  .shell[data-view="desktop"].sidebar-auto .settings-columns { columns: 3; }
}


/* --------------------------------------------------------------------------
   Swatches, and the dropper at the end of them
   --------------------------------------------------------------------------
   Every colour in the app is offered the same way: a row of choices that are
   known to work together, and then a dropper for anybody who wants something
   else. The dropper is a native colour input wearing a swatch — it cannot be
   restyled, but it can be covered.
   -------------------------------------------------------------------------- */

.swatch-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

.swatch {
  border: none;
  border-radius: 50%;
  padding: 0;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}

.swatch:hover { transform: scale(1.06); }

.swatch.is-selected {
  box-shadow: 0 0 0 3px var(--bg-elevated), 0 0 0 5px var(--accent);
}

.swatch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* The dropper wears the colour it is holding, or the spectrum when it is
   holding nothing — which is the whole of what it has to say for itself. */
.swatch-dropper {
  position: relative;
  display: inline-grid;
  place-items: center;
  border-radius: 50%;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.swatch-dropper.is-selected {
  box-shadow: 0 0 0 3px var(--bg-elevated), 0 0 0 5px var(--accent);
}

.swatch-dropper-face {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(
    #ff3b30, #ff9500, #ffcc00, #34c759, #00c7be, #007aff, #af52de, #ff2d55, #ff3b30
  );
}

.swatch-dropper {
  border: none;
  padding: 0;
  cursor: pointer;
}

.swatch-dropper:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}


/* --------------------------------------------------------------------------
   The colour picker
   --------------------------------------------------------------------------
   A spectrum, a hue slider and a hex box, opened from the dropper. Two rules
   run through all of it:

     `touch-action: none` on both surfaces. Without it a downward drag on a
     phone is read as a scroll and the gesture is taken away mid-hunt, which
     is the single thing that made the old picker feel broken.

     Nothing here closes on a choice. The panel stays until the X, a press
     outside it, or Escape — because finding a colour is thirty small
     decisions in a row, not one.
   -------------------------------------------------------------------------- */

/* Catches a press anywhere else. Invisible, and above everything except the
   panel itself, so the sheet underneath cannot be operated by accident while
   a colour is being chosen. */
.cp-catcher {
  position: fixed;
  inset: 0;
  z-index: 1200;
  background: none;
}

.cp-panel {
  position: fixed;
  z-index: 1201;
  width: 264px;
  max-width: calc(100vw - 24px);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  background-color: var(--bg-elevated);
  border: 1px solid var(--separator-opaque);
  box-shadow: var(--shadow-lg);
}

.cp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.cp-title {
  font: var(--type-footnote);
  color: var(--text-secondary);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.cp-close {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  color: var(--text-secondary);
  cursor: pointer;
}

.cp-close svg { width: 14px; height: 14px; }
.cp-close:hover { background-color: var(--fill-secondary); color: var(--text-primary); }
.cp-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Saturation across, brightness down, at whatever hue the slider is on. */
.cp-area {
  position: relative;
  height: 150px;
  border-radius: var(--radius-sm);
  cursor: crosshair;
  touch-action: none;
  background:
    linear-gradient(to top, #000, rgba(0, 0, 0, 0)),
    linear-gradient(to right, #fff, var(--cp-hue, #f00));
}

.cp-hue {
  position: relative;
  height: 22px;
  border-radius: var(--radius-full);
  cursor: pointer;
  touch-action: none;
  background: linear-gradient(to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}

.cp-area:focus-visible,
.cp-hue:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* The two thumbs. White ring on a dark halo, so one is visible against any
   colour it can be dragged over — including white, and including black. */
.cp-area-thumb,
.cp-hue-thumb {
  position: absolute;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), var(--shadow-sm);
  pointer-events: none;
}

.cp-area-thumb {
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
}

.cp-hue-thumb {
  top: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  background-color: transparent;
}

/* A little larger while it is being dragged, so the finger holding it can
   still tell where it is. */
.cp-area.is-dragging .cp-area-thumb { transform: scale(1.15); }
.cp-hue.is-dragging .cp-hue-thumb { transform: scale(1.1); }

.cp-foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.cp-preview {
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: var(--radius-full);
  box-shadow: inset 0 0 0 1px var(--separator);
}

.cp-hex {
  flex: 1;
  min-width: 0;
  padding: 7px var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--editable-border);
  background-color: var(--editable-bg);
  color: var(--text-primary);
  font: var(--type-subhead);
  /* The one place in the app a monospaced face earns itself: six characters
     that are compared against each other by eye. */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--cell-size);
  text-transform: lowercase;
}

.cp-hex:focus { outline: none; border-color: var(--accent); background-color: var(--bg-elevated); }

@media (prefers-reduced-motion: reduce) {
  .cp-area-thumb, .cp-hue-thumb { transition: none; }
}


/* --------------------------------------------------------------------------
   The theme builder
   --------------------------------------------------------------------------
   Drawn out of the same tokens as everything else, so it re-colours under the
   hand that is changing it. That is the preview: no panel imitating the app,
   the app itself.
   -------------------------------------------------------------------------- */

.sheet.is-theme { max-width: 460px; }

.theme-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-2);
}

.theme-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.theme-field-label {
  font: var(--type-footnote);
  color: var(--text-secondary);
  letter-spacing: var(--tracking-normal);
}

/* A figure, a label and the accent — the three things a theme has to keep
   readable, shown together so a choice that ruins one is obvious at once. */
.theme-sample {
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background-color: var(--bg-elevated);
  border: 1px solid var(--separator);
}

.theme-sample-label {
  font: var(--type-footnote);
  color: var(--text-secondary);
}

.theme-sample-figure {
  font: var(--type-title2);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  margin: var(--space-1) 0 var(--space-3);
}

.theme-sample-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.theme-sample-chip {
  font: var(--type-caption);
  padding: 3px var(--space-2);
  border-radius: var(--radius-full);
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
}

.theme-sample-accent {
  font: var(--type-caption);
  padding: 3px var(--space-2);
  border-radius: var(--radius-full);
  background-color: var(--accent);
  color: #fff;
}

/* Each face shown in itself, because a list of font names tells you nothing
   about what a font looks like. */
.theme-fonts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.theme-font {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border: 1.5px solid var(--separator);
  background-color: var(--bg-elevated);
  color: var(--text-primary);
  cursor: pointer;
  flex: 1 1 96px;
}

.theme-font.is-selected {
  border-color: var(--accent);
  background-color: var(--accent-tint);
}

.theme-font-name { font-size: 0.8125rem; font-weight: 600; }
.theme-font-sample {
  font-size: 0.75rem;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.theme-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* --- three rows, then get out of the way ----------------------------------
   Twenty-five faces and nineteen palettes are a good problem and a bad wall:
   opened whole, the two lists push the colours, the accent and the kept
   themes off the bottom of a phone. So each is a grid with a known row
   height — the one thing flex-wrap cannot give — capped at three rows and
   scrolling inside itself, until the button below it says otherwise. */

.theme-rows {
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Room for the scrollbar so the last column is not sliced by it. */
  padding-right: 2px;
}

.theme-rows.theme-fonts,
.theme-rows.theme-chips {
  display: grid;
  gap: var(--space-2);
}

.theme-rows.theme-fonts {
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  grid-auto-rows: 48px;
  max-height: calc(48px * 3 + var(--space-2) * 2);
}

.theme-rows.theme-chips {
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  grid-auto-rows: 38px;
  max-height: calc(38px * 3 + var(--space-2) * 2);
}

.theme-rows.is-all {
  max-height: none;
  overflow-y: visible;
}

.theme-rows .theme-font { min-height: 0; padding: var(--space-1) var(--space-2); }
.theme-rows .theme-chip { min-height: 0; justify-content: flex-start; }

.theme-rows-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: flex-start;
}

.theme-rows-wrap > .theme-rows { width: 100%; }

.theme-rows-more {
  align-self: flex-start;
  padding: 4px 0;
  border: none;
  background: none;
  color: var(--accent);
  font: var(--type-footnote);
  cursor: pointer;
}

.theme-note {
  margin-top: var(--space-2);
  color: var(--text-secondary);
  font: var(--type-caption2);
}

.theme-chip,
.theme-chip-open {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 38px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid var(--separator);
  background-color: var(--bg-elevated);
  color: var(--text-primary);
  font: var(--type-subhead);
  cursor: pointer;
}

/* A saved theme carries its own delete, so the chip becomes a pair. */
.theme-chip.is-saved {
  padding: 0;
  gap: 0;
  overflow: hidden;
}

.theme-chip.is-saved .theme-chip-open {
  border: none;
  border-radius: 0;
  background: none;
}

.theme-chip-remove {
  display: grid;
  place-items: center;
  width: 34px;
  align-self: stretch;
  border: none;
  border-left: 1px solid var(--separator);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
}

.theme-chip-remove:hover { color: var(--color-red); }
.theme-chip-remove svg { width: 14px; height: 14px; }

/* The two colours a theme is mostly made of, in one small mark. */
.theme-chip-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid;
  flex: none;
}

.theme-saved {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.theme-saved .name-field { flex: 1; min-width: 0; }


/* --------------------------------------------------------------------------
   The print menu
   --------------------------------------------------------------------------
   The floating bar's print button still means "this page, now". This is the
   other kind: a list of what the tracker holds, a tick beside each, and the
   actual pages shown before anything is sent — because a print dialog is the
   last place to be surprised, and on a phone the system print sheet gives you
   no way back to check.
   -------------------------------------------------------------------------- */

.sheet.is-print { max-width: 520px; }

.print-picks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.print-pick {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-full);
  border: 1.5px solid var(--separator);
  background-color: var(--bg-elevated);
  color: var(--text-primary);
  font: var(--type-subhead);
  cursor: pointer;
  flex: 1 1 auto;
}

.print-pick.is-on {
  border-color: var(--accent);
  background-color: var(--accent-tint);
}

.print-pick-box {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-xs);
  border: 1.5px solid var(--separator);
  color: #fff;
  flex: none;
}

.print-pick.is-on .print-pick-box {
  background-color: var(--accent);
  border-color: var(--accent);
}

.print-pick-box svg { width: 13px; height: 13px; }

/* The pages themselves, shrunk to fit. `zoom` rather than a transform,
   because a transformed element keeps its untransformed height and leaves a
   column of empty space under a preview that is already short enough. */
.print-preview-frame {
  margin-top: var(--space-2);
  max-height: 42vh;
  overflow: auto;
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  border: 1px solid var(--separator);
  background-color: #fff;
  color: #000;
  -webkit-overflow-scrolling: touch;
}

.print-preview { zoom: 0.55; }

.print-preview-empty {
  font: var(--type-footnote);
  color: #666;
  text-align: center;
  margin: var(--space-5) 0;
}

.print-summary {
  margin-top: var(--space-2);
  font: var(--type-caption);
  color: var(--text-secondary);
  text-align: center;
}

@media (max-width: 560px) {
  .print-preview { zoom: 0.4; }
  .print-preview-frame { max-height: 34vh; }
  .print-pick { flex-basis: calc(50% - var(--space-2)); }
}


/* --------------------------------------------------------------------------
   Staying signed in
   -------------------------------------------------------------------------- */

.auth-stay {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  font: var(--type-footnote);
  color: var(--text-secondary);
  cursor: pointer;
  flex-wrap: wrap;
}

.auth-stay input {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  flex: none;
  cursor: pointer;
}

/* The consequence, said quietly beside the choice rather than in a paragraph
   underneath it that nobody reads. */
.auth-stay-note {
  font: var(--type-caption);
  color: var(--text-tertiary);
  margin-left: auto;
}

/* --------------------------------------------------------------------------
   A one-time code
   -------------------------------------------------------------------------- */

.code-field {
  width: 100%;
  text-align: center;
  letter-spacing: 0.35em;
  font-variant-numeric: tabular-nums;
  font-size: 1.375rem;
  font-weight: 600;
}

/* ==========================================================================
   The fitness tracker
   ==========================================================================
   One rule governs this whole section: nothing scrolls sideways on a phone.
   The financial tracker has wide tables because a ledger genuinely is one — a
   month of expenses has eight columns and no honest way to have fewer. A
   workout does not. It is a short list of short things, and the moment it
   needs a horizontal scroll bar the design has gone wrong rather than the
   screen having got small.

   So everything here is a stack that reflows: cards on a phone, columns where
   there is room, and no table anywhere in the log.
   ========================================================================== */

/* --- the switch between trackers ---------------------------------------- */

.nav-btn.mode-switch {
  position: relative;
}

/* A hairline ring so the switch reads as a control rather than as another
   page button. It is the one thing in the bar that changes what the whole app
   is, and it should not look identical to Undo. */
.nav-btn.mode-switch::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: var(--radius-full);
  border: 1px solid var(--separator);
  pointer-events: none;
}

.nav-btn.mode-switch:hover::after { border-color: var(--accent); }

/* --- shared shapes -------------------------------------------------------- */

.fit-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Two columns where there is room, one where there is not. `auto-fit` with a
   floor means the browser decides, which is right: the breakpoint that matters
   is how wide this card is, not how wide the window is. */
.fit-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: var(--space-3);
}

.fit-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.fit-label,
.fit-mini > span,
.fit-unit-toggle {
  font: var(--type-caption);
  color: var(--text-secondary);
  letter-spacing: 0.02em;
}

.fit-hint {
  font: var(--type-caption2);
  color: var(--text-tertiary);
}

/* A hint that has nothing to say takes up no room. The boxes that fill
   themselves in keep an empty one standing by so there is somewhere for the
   explanation to appear without the field jumping about. */
.fit-hint:empty { display: none; }

/* A figure the app worked out, and the line saying where from. Tinted rather
   than boxed: it has to read as "this was filled in for you" at a glance and
   as "and you can type over it" at the same glance, which a badge or a lock
   would get exactly backwards. */
.fit-hint.is-worked-out { color: var(--accent); }

.fit-input.is-worked-out { color: var(--accent); }

/* Typing over one makes it yours, and it goes back to reading like every other
   box on the page the moment you do. */
.fit-input.is-worked-out:focus { color: var(--text-primary); }

.fit-input {
  width: 100%;
  min-width: 0;
  padding: 10px var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--editable-border);
  background-color: var(--editable-bg);
  color: var(--text-primary);
  font: var(--type-body);
  font-size: var(--cell-size);
  font-variant-numeric: tabular-nums;
  min-height: 44px;
  transition: border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

.fit-input:focus {
  outline: none;
  border-color: var(--accent);
  background-color: var(--bg-elevated);
}

.fit-input::placeholder { color: var(--text-tertiary); }

/* A date or time field left to itself is as wide as the date or time inside
   it, and a browser holds it there whatever the column says — which is how one
   field ends up wider than the sheet it is in and hangs off the edge of the
   phone. Stripping the native appearance is what lets the width apply.

   Every picker kind, not just the one that was reported: they all size
   themselves the same way, and fixing them one at a time as each is noticed is
   how the next one gets missed. */
.fit-input[type="date"],
.fit-input[type="time"],
.fit-input[type="datetime-local"],
.fit-input[type="month"],
.fit-input[type="week"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
}

textarea.fit-notes {
  resize: vertical;
  min-height: 72px;
  font-variant-numeric: normal;
}

.fit-select { appearance: none; padding-right: var(--space-6); }

/* A unit printed inside the box rather than beside it, so a number and its
   unit are one control and the grid stays on its columns. */
.fit-suffixed { position: relative; display: flex; }
.fit-suffixed .fit-input { padding-right: 3.5rem; }
.fit-suffix {
  position: absolute;
  right: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
  font: var(--type-caption);
  color: var(--text-tertiary);
  pointer-events: none;
}

.fit-duration {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.fit-time { width: 4.5rem; text-align: center; }
.fit-unit { font: var(--type-caption); color: var(--text-secondary); }

.fit-empty {
  font: var(--type-subhead);
  color: var(--text-tertiary);
  padding: var(--space-3) 0;
}

.fit-block { display: flex; flex-direction: column; gap: var(--space-2); }

.fit-block-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.fit-block-title {
  font: var(--type-subhead);
  font-weight: 600;
  color: var(--text-secondary);
  margin: 0;
}

.fit-add { align-self: flex-start; }

.fit-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.fit-section-head .section-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

.fit-section-head .section-title svg { width: 18px; height: 18px; }

/* --- the exercise type-ahead ---------------------------------------------- */

.fit-suggest-wrap { position: relative; flex: 1; min-width: 0; }

.fit-suggest {
  position: absolute;
  z-index: 40;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 15rem;
  overflow-y: auto;
  padding: var(--space-1);
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated);
  border: 1px solid var(--separator-opaque);
  box-shadow: var(--shadow-lg);
}

.fit-suggest-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: 10px var(--space-2);
  border-radius: var(--radius-xs);
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  color: var(--text-primary);
}

.fit-suggest-row:hover,
.fit-suggest-row.is-active { background-color: var(--fill-quaternary); }

.fit-suggest-name { font: var(--type-subhead); }
.fit-suggest-tag {
  font: var(--type-caption2);
  color: var(--text-tertiary);
  flex: none;
}

/* --------------------------------------------------------------------------
   Exercises somebody wrote
   --------------------------------------------------------------------------
   The plus at the end of the exercise box, the small mark their exercises
   carry wherever they are listed, and the card that stands in for a
   demonstration. All three are deliberately quiet: a custom exercise is an
   ordinary exercise that happens to be theirs, not a different kind of thing.
   -------------------------------------------------------------------------- */

/* Inside the box, at the end, the size of the icons already in fields. */
.fit-exercise-add {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background-color: var(--accent-tint);
  color: var(--accent);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.fit-exercise-add svg { width: 15px; height: 15px; }
.fit-exercise-add:hover { background-color: var(--accent); color: #fff; }
.fit-exercise-add:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Room for it, and only while it is there — an empty gutter on every exercise
   box would be a permanent cost for an occasional button. */
.fit-suggest-wrap.has-add .fit-exercise { padding-right: 36px; }

/* "Yours", after the name. Small enough to be read second. */
.fit-mine {
  font: var(--type-caption2);
  color: var(--accent);
  background-color: var(--accent-tint);
  padding: 1px 7px;
  border-radius: var(--radius-full);
  margin-left: var(--space-2);
  vertical-align: 1px;
  white-space: nowrap;
}

/* The search box and New exercise, side by side, wrapping on a narrow phone
   rather than squeezing the search to nothing. */
.fit-lib-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.fit-lib-head .search-row { flex: 1 1 12rem; min-width: 0; }
.fit-lib-head > .btn { flex: none; }

/* Says which library this is. The desktop has the sidebar and a page heading
   doing that already, so it only appears where the two cards sit one above
   the other with nothing between them to tell them apart. */
.fit-lib-title { margin-bottom: var(--space-2); }
.shell[data-view="desktop"] .fit-lib-title { display: none; }

/* The row into the exercise library, for the same reason and the same way.
   A phone reaches that reference only through this row, because the tab bar
   holds four and it is not one of them; the desktop page finder gives it a
   line of its own, and two doors to one room on the same screen is one too
   many. */

/* What is shown instead of a demonstration. */

.fit-custom-note {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font: var(--type-footnote);
  color: var(--text-secondary);
}

.fit-custom-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font: var(--type-caption1);
  color: var(--accent);
  background-color: var(--accent-tint);
  padding: 3px 9px;
  border-radius: var(--radius-full);
  flex: none;
}

.fit-custom-badge svg { width: 13px; height: 13px; }

/* Their instructions, as written — line breaks kept, because somebody who
   typed three steps on three lines meant three lines. */
.fit-custom-how {
  font: var(--type-subhead);
  color: var(--text-primary);
  white-space: pre-wrap;
}

/* Why a sheet will not save. Sits under the fields it is about. */
.fit-problem {
  font: var(--type-footnote);
  color: var(--color-red);
  margin: 0;
}

/* --- the lifting log ------------------------------------------------------ */

.fit-sets { display: flex; flex-direction: column; gap: var(--space-2); }

/* A card per exercise. This is the shape that keeps the log off a horizontal
   scroll: five columns of a table cannot fit 390px, four fields wrapped in a
   card can. */
.fit-set {
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.fit-set-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Two rows. The top is a pair — the Sets box beside the reps strip, both
   bottom-aligned, so with one row of reps they sit inline and with two the
   Sets box rides down inline with the second. The foot keeps the three equal
   columns it always had. */
.fit-set-figures {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.fit-figures-top {
  display: flex;
  gap: 10px;
  align-items: flex-end;
}

.fit-figures-foot {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
  align-items: end;
}

/* Two digits is the most Sets will ever hold, so 54px and no more — the width
   given up is exactly what lets five rep boxes breathe beside it. */
.fit-mini-sets { flex: none; width: 54px; }
.fit-mini-reps { flex: 1; }

.fit-mini {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.fit-mini .fit-input { text-align: center; padding-inline: var(--space-1); }

/* The reps/seconds switch is the label itself — tapping the word changes what
   the box means, which is smaller than a separate control and reads as one
   thing rather than two. */
.fit-unit-toggle {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-align: left;
  color: var(--accent);
  font-weight: 600;
}

.fit-side {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 38px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid var(--separator);
  background-color: var(--bg-elevated);
  color: var(--text-secondary);
  font: var(--type-caption);
  cursor: pointer;
}

.fit-side svg { width: 15px; height: 15px; }

/* Two toggles on the bottom row instead of one across it — per set, and which
   side. The row was already there; a phone has no line to spare for a second. */
/* Two words on a third of a phone, so they are allowed to wrap rather than
   pushing the row wider than the card. */
.fit-set-figures .fit-side { padding-inline: var(--space-2); text-align: center; }
.fit-set-figures .fit-side span { white-space: normal; line-height: 1.15; }

/* Reps set by set. Small boxes that wrap, with a spare one always at the end —
   the count of sets follows what is filled in, so nobody has to say up front
   how many they are about to do. */
.fit-reps { display: block; }

/* FIVE ACROSS, then wrap — a grid rather than a wrapping flex, so the boxes
   line up in columns down the second row instead of sitting wherever the
   widths happen to leave them. Ten is the ceiling and the strip never has
   more than two rows. */
.fit-reps-strip {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-2);
}

.fit-rep-box {
  width: 100%;
  min-width: 0;
  /* The Sets box's own vertical metrics — same padding, same type step — so
     the two compute to the same height instead of chasing it with a magic
     number. A strip of boxes two pixels shorter than their neighbour reads as
     a mistake, not a design. */
  min-height: 44px;
  padding: 10px 2px;
  border: 1px solid var(--separator);
  border-radius: var(--radius-sm);
  background-color: var(--fill-quaternary);
  color: var(--text-primary);
  /* NOT a smaller step. iOS Safari zooms the viewport whenever a focused
     field is under 16px and does not zoom back out afterwards, which is why
     `--cell-size` has a coarse-pointer floor in tokens.css — a box this size
     is exactly the kind that tempts a smaller number, and 15px here was that
     bug reported from a phone. */
  font: var(--type-body);
  font-size: var(--cell-size);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.fit-rep-box::placeholder { color: var(--text-quaternary); }
.fit-rep-box:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* The spinners are a desktop affordance worth nothing on a box this size. */
.fit-rep-box::-webkit-outer-spin-button,
.fit-rep-box::-webkit-inner-spin-button { appearance: none; margin: 0; }
.fit-rep-box { appearance: textfield; -moz-appearance: textfield; }

/* A logged superset, read back. The upper line of the pair carries the mark;
   the rule down the left says the two belong together. */
.fit-view-line.is-tied { position: relative; }

.fit-view-line.is-tied::before {
  content: '';
  position: absolute;
  left: calc(var(--space-1) * -1);
  top: 50%;
  bottom: calc(var(--space-2) * -1);
  width: 2px;
  border-radius: 1px;
  background-color: var(--accent);
  opacity: 0.45;
}

/* The word itself comes from the DOM rather than from `content`, because it
   is counted — two is a superset, three a tri-set, four or more a giant set —
   and a stylesheet cannot count. */
.fit-view-tie {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 1px 6px;
  border-radius: var(--radius-full);
  background-color: var(--accent-tint);
  color: var(--accent);
  font: var(--type-caption2);
  font-weight: 600;
  white-space: nowrap;
}

/* --- the superset tie ------------------------------------------------------
   In the gap between two cards, because what it records is the join between
   them rather than anything about either one. Untied it is quiet and small;
   tied, a line runs through it to say the two above and below belong
   together. */
.fit-tie {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 26px;
  margin: calc(var(--space-1) * -1) 0;
}

.fit-tie::before {
  content: '';
  position: absolute;
  inset: 0 auto;
  left: 50%;
  width: 2px;
  background-color: var(--accent);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}

.fit-tie.is-tied::before { opacity: 0.45; }

.fit-tie-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 24px;
  padding: 0 var(--space-2);
  border: 1px solid var(--separator);
  border-radius: var(--radius-full);
  background-color: var(--bg-elevated, var(--fill-quaternary));
  color: var(--text-tertiary);
  font: var(--type-caption2);
  cursor: pointer;
}

.fit-tie-btn svg { width: 13px; height: 13px; }

.fit-tie.is-tied .fit-tie-btn {
  border-color: transparent;
  background-color: var(--accent);
  color: #fff;
  font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
  .fit-tie::before { transition: none; }
}

.fit-side.is-single {
  background-color: var(--accent-tint);
  border-color: transparent;
  color: var(--accent);
  font-weight: 600;
}

/* --- the day bar ---------------------------------------------------------- */

.fit-daybar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  min-width: 0;
}

.fit-daybar > .nav-btn { flex: none; }

.fit-daybar-middle {
  flex: 1;
  min-width: 0;
  text-align: center;
  position: relative;
  /* The picker laid over this is invisible but still a thing with edges. If a
     browser insists on making it wider than the bar, it is clipped here rather
     than reaching off the side of the screen. */
  overflow: hidden;
}

/* "Wednesday, September 16" is a long thing to fit between two arrows on a
   small phone. It shortens rather than pushing the bar past the edge of the
   card, which is what took the date off the side of the screen. */
.fit-day-title,
.fit-day-sub {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fit-day-title { font: var(--type-headline); font-weight: 700; }
.fit-day-sub { font: var(--type-caption); color: var(--text-secondary); }

/* The whole middle is the date picker's hit area: the title says the day and
   pressing the day is how you change it, with no separate control to find. */
.fit-day-picker {
  position: absolute;
  inset: 0;
  opacity: 0;
  width: 100%;
  height: 100%;
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
  border: none;
  cursor: pointer;
  /* Same reason as the date field above: without this iOS gives it the width
     of the date it holds and it hangs off the side of the card, invisible but
     still there to be dragged. */
  -webkit-appearance: none;
  appearance: none;
}

/* --- the week strip -------------------------------------------------------- */

.fit-week {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
}

.fit-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--space-2) 2px;
  min-height: 76px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.fit-day:hover { background-color: var(--fill-tertiary); }

.fit-day-name { font: var(--type-caption2); text-transform: uppercase; }
.fit-day-num { font: var(--type-subhead); font-weight: 600; color: var(--text-primary); }
.fit-day-mark { display: flex; min-height: 18px; align-items: center; }
.fit-day-mark svg { width: 16px; height: 16px; }
.fit-day-count { font: var(--type-caption2); color: var(--text-tertiary); }

.fit-day.is-done { background-color: var(--accent-tint); color: var(--accent); }
.fit-day.is-done .fit-day-num { color: var(--accent); }
.fit-day.is-planned { border-color: var(--accent); }
.fit-day.is-rest { opacity: 0.72; }
.fit-day.is-today { box-shadow: inset 0 0 0 2px var(--accent); }

.fit-week-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* --- stat tiles ------------------------------------------------------------ */

.fit-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(140px, 47%), 1fr));
  gap: var(--space-2);
}

.fit-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3);
  min-width: 0;
}

.fit-stat-icon { color: var(--accent); }
.fit-stat-icon svg { width: 18px; height: 18px; }
.fit-stat-value {
  font: var(--type-title2);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  /* The one place a long figure could overflow on a phone. It shrinks rather
     than spills, because a stat tile is a glance and a wrapped number is not. */
  overflow-wrap: anywhere;
}
.fit-stat-label { font: var(--type-caption); color: var(--text-secondary); }
.fit-stat-sub { font: var(--type-caption2); color: var(--text-tertiary); }

.fit-total { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.fit-total-value { font: var(--type-headline); font-weight: 700; font-variant-numeric: tabular-nums; }
.fit-total-label { font: var(--type-caption2); color: var(--text-secondary); }

.fit-daytotal,
.fit-month-totals {
  display: flex;
  justify-content: space-around;
  gap: var(--space-2);
  padding: var(--space-3);
  flex-wrap: wrap;
}

.fit-month-totals { padding-top: var(--space-3); }

/* --- workout cards ---------------------------------------------------------- */

.fit-list { display: flex; flex-direction: column; gap: var(--space-2); }

.fit-card {
  display: flex;
  align-items: stretch;
  gap: var(--space-1);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  overflow: hidden;
}

.fit-card-main {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3);
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  color: var(--text-primary);
}

.fit-card-main:hover { background-color: var(--fill-tertiary); }

.fit-card-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  background-color: var(--accent-tint);
  color: var(--accent);
}

.fit-card-icon svg { width: 18px; height: 18px; }

.fit-card-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.fit-card-title {
  font: var(--type-subhead);
  font-weight: 600;
  /* Long names truncate rather than pushing the card wide. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fit-card-meta { font: var(--type-caption); color: var(--text-secondary); }
.fit-card-figures {
  font: var(--type-caption);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.fit-card-note {
  font: var(--type-caption2);
  color: var(--text-tertiary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.fit-card-photo,
.fit-card-share {
  align-self: center;
  flex: none;
}

.fit-card-photo { margin-left: var(--space-1); }
.fit-card-share { margin-right: var(--space-2); }

/* Tinted rather than grey: it is the one control on the card that has
   something behind it, and a card with a picture should say so. */
.fit-card-photo { color: var(--accent); }
.fit-card-photo:hover { background-color: var(--accent-tint); }

/* --- the plan card ---------------------------------------------------------- */

.fit-plan {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border-left: 3px solid var(--accent);
}

.fit-plan.is-rest { border-left-color: var(--text-tertiary); }
.fit-plan.is-done { border-left-color: var(--color-green); }

.fit-plan-mark {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-full);
  background-color: var(--accent-tint);
  color: var(--accent);
  flex: none;
}

.fit-plan.is-rest .fit-plan-mark { background-color: var(--fill-quaternary); color: var(--text-secondary); }
.fit-plan.is-done .fit-plan-mark { background-color: var(--color-green); color: #fff; }

.fit-plan-body { flex: 1; min-width: 0; }
.fit-plan-title { font: var(--type-subhead); font-weight: 600; }
.fit-plan-sub { font: var(--type-caption); color: var(--text-secondary); }

.fit-restrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  min-height: 48px;
}

.fit-plan-ex { display: flex; flex-direction: column; gap: var(--space-2); }
.fit-plan-exrow { display: flex; align-items: center; gap: var(--space-2); }
.fit-jump { align-self: center; margin-top: var(--space-2); }

/* --- weigh-ins ---------------------------------------------------------------- */

.fit-weighs { display: flex; flex-direction: column; gap: var(--space-1); }

.fit-weigh {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background-color: var(--fill-quaternary);
  border: none;
  text-align: left;
  cursor: pointer;
  color: var(--text-primary);
  min-height: 44px;
}

.fit-weigh:hover { background-color: var(--fill-tertiary); }

.fit-weigh-time {
  font: var(--type-caption);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  min-width: 3.5rem;
}

.fit-weigh-value {
  font: var(--type-headline);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.fit-weigh-note {
  font: var(--type-caption2);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- library ------------------------------------------------------------------ */

.fit-chips {
  display: flex;
  gap: var(--space-1);
  flex-wrap: wrap;
  padding: var(--space-2) var(--space-3) var(--space-3);
}

/* --- the body-part filter, folded ------------------------------------------
   The same control as `.adm-sort` on the admin accounts tab, in the fitness
   side's own names: one line closed, the chips underneath when open. Kept
   separate rather than sharing the `.adm-` classes because those belong to a
   screen this one has nothing to do with, and a shared rule is a shared
   reason to be careful — this needed neither.

   Sixteen chips, not six, so they go BELOW the line rather than beside it;
   the admin's `flex-wrap` beside the label works for a handful and would
   leave this one straddling the toggle. */
.fit-filter { display: flex; flex-direction: column; }

/* The same fold, twice, inside the New exercise sheet — prime movers and
   assisting. Stacked with a hairline between so the two lists read as two
   questions rather than one long strip of chips. */
.fit-muscle-picks {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--separator);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.fit-muscle-picks > .fit-filter + .fit-filter { border-top: 1px solid var(--separator); }
.fit-muscle-picks .fit-filter-toggle { width: 100%; justify-content: flex-start; }
.fit-muscle-picks .fit-filter-current { margin-left: auto; text-align: right; }

.fit-filter-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  align-self: flex-start;
  padding: var(--space-2) var(--space-3);
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--text-tertiary);
}

.fit-filter-label {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.fit-filter-current { font-size: var(--type-caption1); color: var(--text-secondary); }

/* Redundant once the chips are showing which one is on. */
.fit-filter.is-open .fit-filter-current { display: none; }

.fit-filter-caret {
  width: 14px;
  height: 14px;
  transition: transform var(--duration-base) var(--ease-out);
}

.fit-filter.is-open .fit-filter-caret { transform: rotate(180deg); }

/* The chips carry their own bottom padding; closed, the line should not sit
   on the card's edge. */
.fit-filter:not(.is-open) .fit-filter-toggle { padding-bottom: var(--space-3); }

@media (prefers-reduced-motion: reduce) {
  .fit-filter-caret { transition: none; }
}

.fit-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid var(--separator);
  background-color: var(--bg-elevated);
  color: var(--text-secondary);
  font: var(--type-caption);
  cursor: pointer;
  min-height: 36px;
}

.fit-chip svg { width: 14px; height: 14px; }

.fit-chip.is-on {
  background-color: var(--accent);
  border-color: transparent;
  color: #fff;
  font-weight: 600;
}

.fit-count { font: var(--type-caption); color: var(--text-secondary); }

/* --- type breakdown ------------------------------------------------------------ */

.fit-typebreak { display: flex; flex-direction: column; gap: var(--space-1); }

.fit-typerow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
}

.fit-typerow-icon { color: var(--accent); display: flex; }
.fit-typerow-icon svg { width: 17px; height: 17px; }
.fit-typerow-name { flex: 1; font: var(--type-subhead); }
.fit-typerow-count { font: var(--type-subhead); font-weight: 700; font-variant-numeric: tabular-nums; }

.fit-delta { font: var(--type-caption); color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.fit-delta.is-up { color: var(--color-orange); }
.fit-delta.is-down { color: var(--color-green); }

/* --- quick add ------------------------------------------------------------------ */

.fit-quick { display: flex; flex-direction: column; gap: var(--space-2); }

.fit-quick-choice {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--separator);
  background-color: var(--bg-elevated);
  cursor: pointer;
  text-align: left;
  color: var(--text-primary);
  min-height: 68px;
}

.fit-quick-choice:hover { border-color: var(--accent); background-color: var(--fill-quaternary); }

.fit-quick-icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-full);
  background-color: var(--accent-tint);
  color: var(--accent);
  flex: none;
}

.fit-quick-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.fit-quick-title { font: var(--type-headline); font-weight: 600; }
.fit-quick-sub { font: var(--type-caption); color: var(--text-secondary); }

/* --- pictures --------------------------------------------------------------------- */

.fit-photo-slot { display: flex; }

.fit-photo {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
}

.fit-photo img {
  width: 84px;
  height: 84px;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

.fit-photo-kept {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font: var(--type-caption);
  color: var(--text-secondary);
}

/* --- sharing ------------------------------------------------------------------------ */

.fit-share-preview {
  display: grid;
  place-items: center;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  max-height: 42vh;
  overflow: auto;
}

.fit-share-preview img {
  max-width: 100%;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}

/* --- settings: the order of the types --------------------------------------------- */

/* The workout types, folded away behind their own row in the Workouts group.
   The rows are the page's own list rows, so padding, separators and hover all
   come from the list they sit in; only the compact icon and the reorder
   buttons are new. */
.fit-order { display: flex; flex-direction: column; }

/* Pointing down at what it opened, right when it is folded — the same turn
   the collapsible page sections make. */
.fit-types-chevron { transition: transform var(--duration-base) var(--ease-out); }
.fit-types-toggle[aria-expanded="true"] .fit-types-chevron { transform: rotate(90deg); }

/* The first type row draws its own separator: the toggle above it is an
   icon row, and the list's inset rule would clear an icon this row does not
   have. */
.fit-order > .fit-order-row:first-child::before { left: 0; }

.fit-order-row {
  gap: var(--space-3);
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
  min-height: 44px;
  background-color: var(--fill-quaternary);
}

.fit-order-icon { color: var(--accent); display: flex; flex: none; }
.fit-order-icon svg { width: 17px; height: 17px; }
.fit-order-name { flex: 1; min-width: 0; font: var(--type-subhead); }

.fit-order-badge {
  font: var(--type-caption2);
  color: var(--accent);
  background-color: var(--accent-tint);
  padding: 3px 8px;
  border-radius: var(--radius-full);
}

.fit-order-moves { display: flex; gap: 2px; }

/* --- sheets that hold a whole form ------------------------------------------------- */

/* The editor is a page, not a prompt. On a phone it takes the screen; on a
   desktop it stays a dialog, because there the window already is the page. */
.sheet.is-tall {
  max-height: min(88vh, 900px);
  overflow-y: auto;
  overflow-x: hidden;
}

.sheet.is-fit .fit-form { margin-top: var(--space-2); }

.fit-daynote {
  font: var(--type-caption);
  color: var(--text-secondary);
  text-align: center;
}

@media (max-width: 480px) {
  .sheet.is-tall {
    max-height: 100dvh;
    height: 100dvh;
    border-radius: 0;
    max-width: 100%;
    width: 100%;
  }

  /* Three figure boxes across 390px, with the side switch on its own line
     underneath. Any narrower and the numbers stop being tappable. */
  .fit-set-figures { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  .fit-day { min-height: 68px; padding-inline: 0; }
}


/* The search box on the Library and the exercise library.

   Neither of these classes had a rule until now — the magnifier sat on its own
   line above a borderless input, which is what an unstyled `div` and an
   unstyled `input` look like. It is the field styling every other box on this
   side uses, with the icon inside it. */
.search-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--editable-border);
  background-color: var(--editable-bg);
  min-height: 44px;
}

.search-row:focus-within { border-color: var(--accent); background-color: var(--bg-elevated); }
.search-row > svg { width: 18px; height: 18px; flex: none; color: var(--text-tertiary); }

.search-input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  color: var(--text-primary);
  font: var(--type-body);
  /* The touch-screen floor, for the same reason every other field has one:
     under 16px iOS zooms the page the moment this is focused. */
  font-size: var(--cell-size);
  padding: 10px 0;
}

.search-input:focus { outline: none; }
.search-input::placeholder { color: var(--text-tertiary); }
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

/* --------------------------------------------------------------------------
   The exercise sheet: anatomy and technique

   The drawn figures are retired. What is there instead is the anatomy plate
   with the muscles the movement trains picked out, and the technique written
   out beneath it — see demo.js. Both have to stay legible in a sheet somebody
   opened to answer one question, on a phone, in a gym.
   -------------------------------------------------------------------------- */

.demo-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-2);
}

.demo-card.is-custom { gap: var(--space-3); }

/* --- what kind of movement it is ----------------------------------------- */

.demo-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.demo-tag {
  font: var(--type-caption1);
  color: var(--accent);
  background-color: var(--accent-tint);
  padding: 4px 10px;
  border-radius: var(--radius-full);
}

.demo-tag.is-quiet {
  color: var(--text-secondary);
  background-color: var(--fill-tertiary);
}

/* A whole-body lift is not a body part, and the badge says so before anything
   else on the card does. */
.demo-tag.is-total {
  color: var(--bg-elevated);
  background-color: var(--accent);
  font-weight: 600;
}

.tech-total {
  font: var(--type-footnote);
  color: var(--text-secondary);
  margin: calc(var(--space-2) * -1) 0 0;
}

/* --- the muscle chart ---------------------------------------------------- */

.anat-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.anat-figure-row {
  display: flex;
  justify-content: center;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  /* The plate's own paper, in both themes. The figure is not cut out of its
     background — a cut edge on a dark ground is a ragged halo — so the panel
     is the paper, and the drawing sits on it exactly as drawn. */
  background-color: #fdfdfe;
}

/* The two bodies. They are drawn at the plate's own size and let down to fit,
   so a phone at three device pixels to the point still gets every fibre. */
.anat-chart {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: var(--space-3);
  width: 100%;
  max-width: 420px;
}

.anat-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin: 0;
  min-width: 0;
  flex: 1 1 0;
}

.anat-figure {
  display: block;
  width: 100%;
  height: auto;
  max-height: 300px;
  object-fit: contain;
}

/* Nothing to draw: the plate did not load. The muscles are named in words
   directly underneath either way, so the card still says what it works. */
.anat-chart.is-missing { display: none; }

.anat-caption {
  /* On the paper panel, so it cannot follow the theme's text colours. */
  color: #86868b;
  font: var(--type-caption2);
  text-align: center;
}

.anat-legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  font: var(--type-caption2);
  color: var(--text-secondary);
}

.anat-key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.anat-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background-color: var(--accent);
  flex: none;
}

.anat-dot.is-also { opacity: 0.36; }

/* The muscles named in words as well as drawn — a chart nobody can read the
   labels off is half an answer. */
.anat-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px var(--space-2);
  margin: 0;
}

.anat-list dt {
  font: var(--type-caption1);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  padding-top: 1px;
}

.anat-list dd {
  font: var(--type-subhead);
  color: var(--text-primary);
  margin: 0;
}

.anat-list dd.is-quiet { color: var(--text-secondary); }

/* --- the technique ------------------------------------------------------- */

.tech-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.tech-step {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.tech-heading {
  font: var(--type-caption1);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  margin: 0;
}

.tech-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding-left: 1.35em;
}

.tech-list li {
  font: var(--type-subhead);
  color: var(--text-primary);
  line-height: 1.45;
}

.tech-list li::marker {
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

/* The faults are a warning, not another step, so they are bullets in the
   warning colour rather than a numbered continuation of the rep. */
.tech-step.is-faults .tech-list {
  list-style: disc;
}

.tech-step.is-faults .tech-list li::marker { color: var(--color-orange); }
.tech-step.is-faults .tech-list li { color: var(--text-secondary); }

/* --- a custom exercise's own words --------------------------------------- */

@media (max-width: 380px) {
  .anat-figure { max-height: 240px; }
}


/* --------------------------------------------------------------------------
   Looking at a workout's picture
   -------------------------------------------------------------------------- */

/* The sheet gives the picture the room it can, and no more: a photograph that
   fills a phone from edge to edge has pushed the buttons off the bottom. */
.sheet.is-photo { max-width: 520px; }

.fit-photo-view {
  display: grid;
  place-items: center;
  min-height: 180px;
  max-height: 56dvh;
  margin-top: var(--space-2);
  border-radius: var(--radius-lg);
  background-color: var(--fill-quaternary);
  overflow: hidden;
}

.fit-photo-view img {
  display: block;
  max-width: 100%;
  max-height: 56dvh;
  object-fit: contain;
}

/* A profile picture fills the frame even when the stored copy is small —
   soft beats a postage stamp that reads as broken. */
.fit-photo-view img.is-fill {
  width: min(100%, 420px);
  height: auto;
}

/* While the bytes come out of IndexedDB, and for the rare case where they are
   not on this device at all — a workout synced from a phone whose pictures
   have not arrived yet. */
.fit-photo-wait {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-6);
  color: var(--text-tertiary);
  font: var(--type-footnote);
  text-align: center;
}

.fit-photo-wait svg { width: 26px; height: 26px; }

/* --------------------------------------------------------------------------
   Several workouts planned in one day
   -------------------------------------------------------------------------- */

/* Each planned workout is its own block, so three of them read as three
   things rather than one long form. */
.fit-plan-block {
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.fit-plan-block .fit-block-head { margin-bottom: var(--space-2); }

/* The type's mark beside the block's name, so the morning run and the evening
   yoga are told apart at a glance rather than by reading. */
.fit-plan-block .fit-plan-mark {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: var(--radius-xs);
  display: grid;
  place-items: center;
  color: var(--accent);
  background-color: var(--accent-tint);
}

.fit-plan-block .fit-plan-mark svg { width: 16px; height: 16px; }
.fit-plan-block .fit-block-title { flex: 1; min-width: 0; }

.fit-plan-exwrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Morning, midday, evening, any. Four short words, so they fit across a phone
   without the last one wrapping onto its own line. */
.fit-slots { width: 100%; }
.fit-slots .segment { flex: 1; min-width: 0; padding-inline: var(--space-1); }

/* =========================================================================
   The admin dashboard
   =========================================================================
   Built out of the same cards, badges and sheets as the rest of the app, for
   a reason beyond tidiness: an admin tool that looks like a different program
   is one nobody trusts to be looking at the same data. Nothing here is a new
   idiom — a tile is the fitness tracker's stat tile, a row is a list row, the
   detail sheet is the sheet everything else uses.
   ========================================================================= */

/* --- the bar across the top of every tab --------------------------------- */

.adm-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.adm-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.adm-head-tools { display: flex; align-items: center; gap: var(--space-2); }

/* The time the figures are from. Small, because it is only interesting when
   you are wondering whether to press Refresh — and then it is exactly what
   you want. */
.adm-stamp {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

.adm-refresh svg { width: 15px; height: 15px; }

/* Turning while it fetches, so pressing it visibly did something even when
   the answer comes back in eighty milliseconds. */
.adm-refresh.is-busy svg { animation: adm-spin 900ms linear infinite; }

@keyframes adm-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .adm-refresh.is-busy svg { animation: none; opacity: 0.55; }
}

/* --- the headline numbers ------------------------------------------------ */

.adm-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-2);
}

.adm-tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3);
  min-width: 0;
}

.adm-tile-icon {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  color: var(--accent);
  background-color: var(--accent-tint);
  margin-bottom: var(--space-1);
}

.adm-tile-icon svg { width: 16px; height: 16px; }

.adm-tile-value {
  font-size: var(--type-title2);
  font-weight: 600;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.adm-tile-label { font-size: var(--type-footnote); color: var(--text-secondary); }
.adm-tile-sub { font-size: var(--type-caption1); color: var(--text-tertiary); }

/* A number that wants doing something about wears the warning colour rather
   than a red badge: it is a nudge, not an alarm. */
.adm-tile.is-warn .adm-tile-icon {
  color: var(--color-warning);
  background-color: var(--tint-warning);
}

/* --- the one banner ------------------------------------------------------ */

.adm-alert {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  text-align: left;
  cursor: pointer;
  border: none;
  font: inherit;
  color: inherit;
  padding: var(--space-3) var(--space-4);
  background-color: var(--tint-warning);
}

.adm-alert:hover { background-color: var(--tint-warning); filter: brightness(0.98); }

.adm-alert-icon {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: var(--radius-full);
  display: grid;
  place-items: center;
  color: var(--color-warning);
  background-color: var(--bg-elevated);
}

.adm-alert-icon svg { width: 17px; height: 17px; }
.adm-alert-text { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.adm-alert-text strong { font-size: var(--type-subhead); font-weight: 600; }
.adm-alert-text span { font-size: var(--type-caption1); color: var(--text-secondary); }
.adm-alert > svg:last-child { width: 16px; height: 16px; color: var(--text-tertiary); flex: none; }

/* --- charts side by side, then stacked ----------------------------------- */

.adm-split {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}

@media (min-width: 780px) {
  .adm-split { grid-template-columns: 1fr 1fr; align-items: start; }
}

.adm-donut { display: grid; place-items: center; padding-block: var(--space-2); }

.adm-legend { display: flex; flex-direction: column; gap: var(--space-1); }

.adm-legend-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--type-footnote);
}

.adm-legend-value { font-weight: 600; font-variant-numeric: tabular-nums; }
.adm-legend-share { color: var(--text-tertiary); margin-inline-start: auto; }

.adm-note {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  margin-top: var(--space-2);
  line-height: 1.45;
}

.adm-note.is-pending { color: var(--color-warning); }

/* --- the plan badges ----------------------------------------------------- */

.adm-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: var(--radius-full);
  font-size: var(--type-caption2);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
  color: var(--text-secondary);
  background-color: var(--fill-tertiary);
}

.adm-badge svg { width: 11px; height: 11px; }

/* Three plans, three weights of the same idea rather than three unrelated
   colours: grey for the one you start on, the accent for paying, and the
   accent filled in for the top one. */
.adm-badge.is-premium { color: var(--accent); background-color: var(--accent-tint); }

.adm-badge.is-ultimate {
  color: var(--text-on-accent);
  background-color: var(--accent);
}

.adm-badge.is-admin { color: var(--color-purple); background-color: var(--fill-tertiary); }
.adm-badge.is-pending,
.adm-badge.is-warn { color: var(--color-warning); background-color: var(--tint-warning); }

/* --- filters ------------------------------------------------------------- */

.adm-filters { display: flex; flex-direction: column; gap: var(--space-3); }

.adm-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.adm-chip {
  padding: 5px 11px;
  border-radius: var(--radius-full);
  border: var(--hairline) solid var(--separator);
  background-color: transparent;
  color: var(--text-secondary);
  font-size: var(--type-caption1);
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.adm-chip:hover { background-color: var(--bg-hover); }

.adm-chip.is-on {
  color: var(--text-on-accent);
  background-color: var(--accent);
  border-color: var(--accent);
}

.adm-sort { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

.adm-sort-label {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

/* The fold. One line closed, the chips beside it open. */
.adm-sort-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--text-tertiary);
}

.adm-sort-current {
  font-size: var(--type-caption1);
  color: var(--text-secondary);
}

/* Redundant once the chips are showing which one is on. */
.adm-sort.is-open .adm-sort-current { display: none; }

.adm-sort-caret {
  width: 14px;
  height: 14px;
  transition: transform var(--duration-base) var(--ease-out);
}

.adm-sort.is-open .adm-sort-caret { transform: rotate(180deg); }

.adm-count {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

/* --- the account list ---------------------------------------------------- */

.adm-list { display: flex; flex-direction: column; gap: var(--space-1); }

.adm-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  text-align: left;
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  border: var(--hairline) solid var(--separator);
  background-color: var(--bg-elevated);
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.adm-row:hover { background-color: var(--bg-hover); }

.adm-row-main { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }

.adm-row-top {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-wrap: wrap;
}

.adm-row-name {
  font-size: var(--type-subhead);
  font-weight: 600;
  color: var(--text-primary);
}

.adm-row-email,
.adm-row-meta {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The @name in front of the address. Coloured so the eye can pick out the
   half it is scanning for without reading the whole line. */
.adm-row-handle {
  color: var(--accent);
  font-weight: 600;
}

.adm-row-handle::after {
  content: " · ";
  color: var(--text-tertiary);
  font-weight: 400;
}

.adm-row-side { flex: none; }
.adm-row-chev { width: 16px; height: 16px; color: var(--text-quaternary); flex: none; }

/* On a phone the usage bar goes under the name rather than beside it: there
   is not room for both, and the name is what you are scanning for. */
@media (max-width: 560px) {
  .adm-row { flex-wrap: wrap; }
  .adm-row-side { width: 100%; }
  .adm-row-chev { display: none; }
}

/* --- how much room is in use --------------------------------------------- */

.adm-usage {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 116px;
}

.adm-usage-text {
  font-size: var(--type-footnote);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.adm-usage-note { font-size: var(--type-caption2); color: var(--text-tertiary); }

/* The figure and its share, on one line with the bar under both. */
.adm-usage-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}

.adm-usage-pct {
  font-size: var(--type-caption2);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

.adm-usage-pct.is-warm { color: var(--color-warning); }
.adm-usage-pct.is-hot { color: var(--color-negative); }

.adm-usage-track {
  height: 5px;
  border-radius: var(--radius-full);
  background-color: var(--fill-quaternary);
  overflow: hidden;
}

.adm-usage-fill {
  height: 100%;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  transition: width var(--duration-base) var(--ease-out);
}

.adm-usage-fill.is-warm { background-color: var(--color-warning); }
.adm-usage-fill.is-hot { background-color: var(--color-negative); }

.adm-usage-big .adm-usage { min-width: 0; }
.adm-usage-big .adm-usage-track { height: 8px; }

/* --- the detail sheet ---------------------------------------------------- */

.sheet.is-admin .fit-block { margin-top: var(--space-3); }

.adm-detail-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: var(--hairline) solid var(--separator);
}

.adm-detail-email {
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* Above the address, for the same reason it sits above it in your own
   profile: it is the name they chose, and the one they will use. */
.adm-detail-handle {
  font: var(--type-subhead);
  font-weight: 600;
  color: var(--accent);
  overflow-wrap: anywhere;
}

/* --- their limit --------------------------------------------------------- */

.adm-limit {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: var(--hairline) solid var(--separator);
}

.adm-limit-steps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.adm-limit-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.adm-detail-badges { display: flex; gap: var(--space-1); flex-wrap: wrap; }

.adm-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.adm-fact {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  min-width: 0;
}

/* Secondary rather than tertiary: these sit on a tinted panel, and tertiary
   grey on that ground is a label you have to lean in to read. */
.adm-fact-label {
  font-size: var(--type-caption2);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  font-weight: 600;
}

.adm-fact-value {
  font-size: var(--type-footnote);
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

/* --- picking a plan for somebody ----------------------------------------- */

.adm-tier-picker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-2);
}

.adm-tier-option {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  border: var(--hairline) solid var(--separator);
  background-color: var(--bg-elevated);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.adm-tier-option:hover:not(:disabled) { border-color: var(--accent); }

/* The plan they are already on is not a button. Showing it as one invites the
   press that does nothing, which is the worst kind. */
.adm-tier-option.is-on {
  border-color: var(--accent);
  background-color: var(--accent-tint);
  cursor: default;
}

.adm-tier-name { font-size: var(--type-subhead); font-weight: 600; }
.adm-tier-what { font-size: var(--type-caption1); color: var(--text-secondary); }

/* --- the request queue --------------------------------------------------- */

.adm-request {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  border: var(--hairline) solid var(--separator);
  background-color: var(--bg-elevated);
}

.adm-request-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.adm-request-who { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.adm-request-move { display: flex; align-items: center; gap: var(--space-1); flex: none; }
.adm-request-arrow { width: 13px; height: 13px; color: var(--text-tertiary); }

/* Somebody else's words. Set apart, so it is clear it was typed by the person
   being decided about rather than written by the app. */
.adm-request-reason {
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  line-height: 1.5;
  padding: var(--space-2) var(--space-3);
  border-inline-start: 2px solid var(--separator);
  background-color: var(--fill-quaternary);
  border-radius: var(--radius-sm);
  overflow-wrap: anywhere;
}

.adm-request-reason.is-quiet { color: var(--text-quaternary); font-style: italic; }

.adm-request-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.adm-request-actions .btn { flex: 1; min-width: 120px; }
.adm-deny { color: var(--color-negative); }

/* --- short lists --------------------------------------------------------- */

.adm-row-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.adm-mini-list { display: flex; flex-direction: column; }

.adm-mini {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-block: var(--space-2);
  border-bottom: var(--hairline) solid var(--separator);
  font-size: var(--type-footnote);
}

.adm-mini:last-child { border-bottom: none; }
.adm-mini-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.adm-mini-when { color: var(--text-tertiary); font-size: var(--type-caption1); flex: none; }

/* --- nothing to show ----------------------------------------------------- */

.adm-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-4);
  text-align: center;
}

.adm-empty-icon {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-full);
  display: grid;
  place-items: center;
  color: var(--text-tertiary);
  background-color: var(--fill-quaternary);
}

.adm-empty-icon svg { width: 19px; height: 19px; }
.adm-empty-title { font-size: var(--type-subhead); font-weight: 600; color: var(--text-primary); }
.adm-empty-detail { font-size: var(--type-footnote); color: var(--text-tertiary); max-width: 42ch; line-height: 1.5; }

.adm-refused { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); text-align: center; }
.adm-refused .adm-empty-icon { color: var(--color-negative); background-color: var(--tint-negative); }

/* =========================================================================
   Your plan, in the profile
   ========================================================================= */

.plan-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: var(--hairline) solid var(--separator);
}

/* The membership row in the profile, since v1.3.10.7 — a door rather than a
   block with buttons under it. Shaped like the Administration row above it:
   a tinted panel, a chevron, and nothing to decide until you are through it. */
.plan-open {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  margin-top: var(--space-4);
  padding: var(--space-3);
  border: 1px solid var(--separator);
  border-radius: var(--radius-md);
  background: var(--fill-quaternary);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.plan-open:hover { background: var(--fill-tertiary); }
.plan-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.plan-open-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  flex: 1;
}

.plan-open-go { flex: none; color: var(--text-tertiary); display: grid; place-items: center; }
.plan-open-go svg { width: 15px; height: 15px; }

/* --------------------------------------------------------------------------
   The account page
   --------------------------------------------------------------------------
   Everything that can be done about a membership, on one sheet, reached from
   the profile row above and from Settings → Account.
   -------------------------------------------------------------------------- */

.acct-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

/* The dates somebody asks for when they are checking a charge. A definition
   list in spirit: label at one end, answer at the other, one per line, so it
   reads down rather than being hunted through. */
.acct-facts {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-2);
  border-top: var(--hairline) solid var(--separator);
}

.acct-fact {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--separator);
}

.acct-fact-label {
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  flex: none;
}

.acct-fact-value {
  font-size: var(--type-footnote);
  font-weight: 600;
  color: var(--text-primary);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.plan-now { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.plan-now-text { display: flex; flex-direction: column; gap: 1px; }

.plan-now-label {
  font-size: var(--type-caption2);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.plan-now-name { font-size: var(--type-subhead); font-weight: 600; }
.plan-blurb { font-size: var(--type-caption1); color: var(--text-secondary); line-height: 1.5; }

.plan-pending {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--tint-warning);
  font-size: var(--type-caption1);
  color: var(--text-secondary);
}

.plan-intro {
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  line-height: 1.5;
  margin-bottom: var(--space-3);
}

.plan-options { display: flex; flex-direction: column; gap: var(--space-2); }

/* Monthly or yearly, above the cards so every price answers to it. The
   segment borrows the app's segmented-control look; the note beside it says
   the saving so nobody has to divide. */
.plan-interval {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}
.plan-interval-seg {
  display: inline-flex;
  padding: 2px;
  border-radius: var(--radius-full);
  background-color: var(--bg-secondary, var(--bg-elevated-2));
}
.plan-interval-btn {
  border: none;
  background: none;
  font: inherit;
  font-size: var(--type-footnote);
  font-weight: 600;
  color: var(--text-secondary);
  padding: 6px 14px;
  border-radius: var(--radius-full);
  cursor: pointer;
}
.plan-interval-btn.is-on {
  background-color: var(--bg-elevated);
  color: var(--text-primary);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
.plan-interval-note {
  font-size: var(--type-caption1);
  color: var(--accent);
  font-weight: 500;
}

/* The sheet's two actions are not equals: Continue carries a price and needs
   the width for it ("Continue — $3.99/month" clipped on a 390px phone when
   the row was split evenly). Weighted rather than hugged — a bare `auto` on
   the grey button shrank Cancel to 71px, under the 100px readable-tap floor
   account-check holds this sheet to. */
.plan-sheet-body .quick-actions .btn-gray { flex: 1; min-width: 104px; }
.plan-sheet-body .quick-actions .btn-filled { flex: 1.9; }

.plan-option {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  border: var(--hairline) solid var(--separator);
  background-color: var(--bg-elevated);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.plan-option.is-on { border-color: var(--accent); background-color: var(--accent-tint); }
.plan-option:disabled { opacity: 0.55; cursor: default; }

/* The suggested plan's mark. A capsule beside the name, in the accent so it
   reads as the sheet's own recommendation rather than a fourth plan fact.
   The card it sits on is usually also .is-on, whose border says the rest —
   the badge is what survives when somebody selects a different card. */
.plan-reco {
  font-size: var(--type-caption2);
  font-weight: 600;
  color: var(--accent);
  background-color: var(--accent-tint);
  border-radius: var(--radius-full);
  padding: 2px 8px;
  white-space: nowrap;
}

.plan-option-head { display: flex; align-items: center; gap: var(--space-2); }
.plan-option-name { font-size: var(--type-subhead); font-weight: 600; }

/* The price sits at the far end of the head row, which puts all three in a
   column somebody can read down. `margin-inline-start: auto` rather than
   `justify-content: space-between`, because the head also holds the "Current
   plan" badge and that must stay beside the name. */
.plan-option-price {
  margin-inline-start: auto;
  font-size: var(--type-subhead);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.plan-option-price.is-quiet {
  margin-inline-start: 0;
  font-size: var(--type-caption1);
  font-weight: 500;
  color: var(--text-secondary);
}

/* The plan somebody is already on. Not `:disabled`'s flat 0.55 — that reads
   as broken rather than as "this is yours", and it is the one card on the
   screen that should look settled rather than dimmed. */
.plan-option.is-mine {
  opacity: 1;
  border-style: dashed;
  background-color: var(--bg-secondary);
}

/* Two ranks, both centred: the decision on top with the width to match, the
   two errands under it evenly spaced. `:empty` matters — the lower rank is
   always in the markup and is nothing at all on a plan that has neither
   button, and an empty flex row would still add its gap. */
.plan-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.plan-action-buy { min-width: min(260px, 100%); justify-content: center; }

.plan-actions-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
}

.plan-actions-row:empty { display: none; }

/* What renews, and when — or what was cancelled and how long is left. */
.plan-sub-line {
  margin: 0;
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  line-height: 1.5;
}

.plan-sub-line.is-warn { color: var(--color-warning); font-weight: 500; }

.plan-sheet-body { display: flex; flex-direction: column; gap: var(--space-3); }
.adm-note.is-centred { text-align: center; }

.plan-points {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin: 0;
  padding-inline-start: var(--space-4);
  font-size: var(--type-caption1);
  color: var(--text-secondary);
}

/* =========================================================================
   Looking at a workout, before editing one
   =========================================================================
   The read-only face of a logged workout. Built out of the same blocks the
   editor uses so the two do not read as different applications — what changes
   is that nothing here is a control.
   ========================================================================= */

.fit-view-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: var(--hairline) solid var(--separator);
}

.fit-view-mark {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-md);
  display: grid;
  place-items: center;
  color: var(--accent);
  background-color: var(--accent-tint);
}

.fit-view-mark svg { width: 21px; height: 21px; }

.fit-view-when { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.fit-view-date { font-size: var(--type-subhead); font-weight: 600; color: var(--text-primary); }
.fit-view-meta { font-size: var(--type-caption1); color: var(--text-tertiary); }

/* --- the picture --------------------------------------------------------- */

.fit-view-photo {
  position: relative;
  display: block;
  width: 100%;
  margin-top: var(--space-3);
  padding: 0;
  border: none;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background-color: var(--fill-quaternary);
  cursor: pointer;
  line-height: 0;
  transition: box-shadow var(--duration-fast) var(--ease-out);
}

.fit-view-photo.is-ready:hover { box-shadow: var(--shadow-md); }

/* Deliberately not full bleed. The picture is part of the record, not the
   record — a workout opened to check a pace should not have to be scrolled
   past a photograph to get to it. Tall enough to recognise the day, short
   enough that the figures stay on the first screen. */
.fit-view-photo img {
  display: block;
  width: 100%;
  max-height: 210px;
  object-fit: cover;
  transition: transform var(--duration-base) var(--ease-out);
}

/* A slow push in on hover. Enough to say the picture is live, far too little
   to move anything about underneath it — the frame clips it. */
.fit-view-photo.is-ready:hover img { transform: scale(1.03); }

/* The corner mark: the affordance itself. Dark enough to sit on a bright
   photograph and light enough to sit on a dark one, which is what a blurred
   panel buys over a flat colour. */
.fit-view-photo-badge {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  width: 30px;
  height: 30px;
  border-radius: var(--radius-full);
  display: grid;
  place-items: center;
  color: #fff;
  background-color: rgb(0 0 0 / 0.45);
  backdrop-filter: saturate(180%) blur(8px);
  -webkit-backdrop-filter: saturate(180%) blur(8px);
  transition: background-color var(--duration-fast) var(--ease-out);
}

.fit-view-photo-badge svg { width: 15px; height: 15px; }
.fit-view-photo.is-ready:hover .fit-view-photo-badge { background-color: var(--accent); }

/* And the words, along the bottom, over a gradient so they stay legible on a
   photograph of anything at all. */
.fit-view-photo-hint {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: var(--space-4) var(--space-3) var(--space-2);
  color: #fff;
  font-size: var(--type-caption1);
  font-weight: 500;
  line-height: 1.3;
  text-align: start;
  background: linear-gradient(to top, rgb(0 0 0 / 0.62), transparent);
}

.fit-view-photo-hint svg { width: 13px; height: 13px; flex: none; }

@media (prefers-reduced-motion: reduce) {
  .fit-view-photo.is-ready:hover img { transform: none; }
}

.fit-view-photo-wait {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 120px;
  color: var(--text-tertiary);
  font-size: var(--type-caption1);
  line-height: 1.4;
}

.fit-view-photo-wait svg { width: 18px; height: 18px; }

/* --- the headline figures ------------------------------------------------ */

.fit-view-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.fit-view-figure {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  min-width: 0;
}

/* A mark per figure. Five grey boxes of numbers are five things to read;
   five with a glyph on each are five things to recognise. */
.fit-view-figure-icon {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: var(--radius-xs);
  display: grid;
  place-items: center;
  color: var(--accent);
  background-color: var(--accent-tint);
}

.fit-view-figure-icon svg { width: 15px; height: 15px; }

.fit-view-figure-text {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
}

.fit-view-figure-value {
  font-size: var(--type-headline);
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

/* Sentence case, ordinary letter-spacing. Small caps with tracking is a label
   style for one or two words; "AVERAGE HEART RATE" set that way reads as
   shouting and takes two lines to do it. */
.fit-view-figure-label {
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  line-height: 1.25;
}

/* --- the lists ----------------------------------------------------------- */

.fit-view-list { display: flex; flex-direction: column; }

.fit-view-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding-block: var(--space-2);
  border-bottom: var(--hairline) solid var(--separator);
  font-size: var(--type-footnote);
}

.fit-view-line:last-child { border-bottom: none; }

.fit-view-line-label { color: var(--text-secondary); min-width: 0; overflow-wrap: anywhere; }

.fit-view-line-value {
  font-weight: 600;
  color: var(--text-primary);
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The same accent the editor tints a worked-out box with, so "the app filled
   this in" means one thing in both places. */
.fit-view-line-value.is-derived { color: var(--accent); }

.fit-view-notes {
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.fit-view-empty {
  font-size: var(--type-footnote);
  color: var(--text-tertiary);
  text-align: center;
  padding-block: var(--space-4);
}

.sheet.is-view .fit-block { margin-top: var(--space-3); }



/* =========================================================================
   Friends
   =========================================================================
   The fitness side only. Everything here is built out of the same pieces the
   admin dashboard uses — the same cards, the same empty states, the same
   request rows — because the two screens do the same kind of thing: show a
   list somebody has to act on. What is new below is the parts that have no
   equivalent anywhere else: a face, a chip, and two figures side by side with
   a bar between them.
   ========================================================================= */

/* Your own @name, at the top, as a link rather than a button — it is a fact
   about you that happens to be changeable. */
.friend-me {
  font-size: var(--type-title3);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  padding: 0;
}

.friend-me:hover { color: var(--accent); }

/* --- a face -------------------------------------------------------------- */

.friend-avatar {
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--radius-full);
  overflow: hidden;
  display: grid;
  place-items: center;
  background-color: var(--fill-tertiary);
  color: var(--text-secondary);
  font-size: var(--type-subhead);
  font-weight: 600;
}

.friend-avatar img { width: 100%; height: 100%; object-fit: cover; }
.friend-avatar.is-initials { text-transform: uppercase; }

.friend-avatar.is-big {
  width: 56px;
  height: 56px;
  font-size: var(--type-title2);
}

/* Small enough that two or three overlap in a chat row without the row
   growing to fit them. */
.friend-avatar.is-small {
  width: 30px;
  height: 30px;
  font-size: var(--type-caption1);
  box-shadow: 0 0 0 2px var(--bg-elevated);
}

/* --- the grid of people --------------------------------------------------- */

.friend-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--space-2);
}

.friend-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.friend-card:hover { background-color: var(--bg-hover); }
.friend-card:active { background-color: var(--bg-active); }

.friend-card-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }

.friend-card-name {
  font-size: var(--type-subhead);
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.friend-card-at,
.friend-card-stats,
.friend-card-quiet,
.friend-card-together {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.friend-card-stats { color: var(--text-secondary); }
.friend-card-quiet { font-style: italic; }
.friend-card-together { color: var(--accent); font-weight: 600; }

/* --- something waiting on you --------------------------------------------- */

.friend-alert { border-color: var(--accent); }

.friend-request {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  background-color: var(--bg-secondary);
}

.friend-request-top { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.friend-request-who { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.friend-found:empty { display: none; }
.friend-found { margin-top: var(--space-3); }

/* --- the profile ---------------------------------------------------------- */

.friend-sheet { gap: var(--space-3); }

.friend-hero { display: flex; align-items: center; gap: var(--space-3); }
.friend-hero-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.friend-hero-name {
  font-size: var(--type-title3);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}

.friend-hero-at { font-size: var(--type-footnote); color: var(--text-tertiary); }

/* --- the month, side by side ---------------------------------------------- */

.friend-score {
  font-size: var(--type-caption1);
  font-weight: 600;
  color: var(--text-tertiary);
}

.friend-score.is-up { color: var(--color-positive); }
.friend-score.is-down { color: var(--text-secondary); }

.friend-vs-key {
  display: flex;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.friend-vs-key-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
}

.friend-vs-key-item::before {
  content: '';
  width: 10px;
  height: 10px;
  border-radius: 3px;
  background-color: var(--accent);
}

.friend-vs-key-item.is-theirs::before { background-color: var(--text-quaternary); }

.friend-vs { display: flex; flex-direction: column; gap: var(--space-2); }

/* Label in the middle, one side each. On a phone the label goes above so the
   two bars keep their width — a contest squeezed into 60px reads as a tie. */
.friend-vs-row {
  display: grid;
  grid-template-columns: 1fr minmax(96px, auto) 1fr;
  align-items: center;
  gap: var(--space-2);
}

.friend-vs-label {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  text-align: center;
  white-space: nowrap;
}

.friend-vs-side { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.friend-vs-side.is-mine { flex-direction: row-reverse; }

.friend-vs-value {
  font-size: var(--type-footnote);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  white-space: nowrap;
  flex: none;
}

.friend-vs-value.is-lead { color: var(--text-primary); font-weight: 700; }

.friend-vs-track {
  flex: 1;
  min-width: 0;
  height: 8px;
  border-radius: var(--radius-full);
  background-color: var(--fill-quaternary);
  overflow: hidden;
  display: flex;
}

.friend-vs-side.is-mine .friend-vs-track { justify-content: flex-end; }

.friend-vs-fill {
  height: 100%;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  transition: width var(--duration-fast) var(--ease-out);
}

.friend-vs-fill.is-theirs { background-color: var(--text-quaternary); }

/* --- six months ----------------------------------------------------------- */

.friend-run {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--space-1);
}

.friend-run-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--space-2) 2px;
  border-radius: var(--radius-sm);
  background-color: var(--fill-quaternary);
}

.friend-run-cell.is-up { background-color: var(--tint-positive); }
.friend-run-cell.is-down { background-color: var(--fill-tertiary); }
.friend-run-cell.is-level { background-color: var(--accent-tint); }

.friend-run-month {
  font-size: var(--type-caption2);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.friend-run-score {
  font-size: var(--type-caption1);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

/* --- what passed between you ---------------------------------------------- */

.friend-log { display: flex; flex-direction: column; }

.friend-log-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--separator);
  text-align: left;
  background-color: transparent;
  border-left: none;
  border-right: none;
  border-top: none;
}

.friend-log-row:last-child { border-bottom: none; }

/* The row is the target when it leads somewhere, and plain text when it does
   not. Both share a layout so a list of the two kinds does not step about. */
.friend-log-main {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
  padding: 0;
  border: none;
  background: transparent;
  color: inherit;
  text-align: left;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.friend-log-main.is-flat { cursor: default; }
.friend-log-main:not(.is-flat):hover { background-color: var(--bg-hover); }
.friend-log-main:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.friend-log-acts { display: flex; align-items: center; gap: 2px; flex: none; }
.friend-log-acts .nav-btn { width: 30px; height: 30px; }
.friend-log-acts .nav-btn svg { width: 15px; height: 15px; }

/* --- the three tabs of a profile ------------------------------------------
   Segmented rather than a row of links: they are three views of one person,
   exactly one is showing, and that is what a segmented control means. */

.friend-tabs {
  display: flex;
  gap: 2px;
  margin: var(--space-3) 0 var(--space-2);
  padding: 3px;
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.friend-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
  padding: 7px var(--space-2);
  border: none;
  border-radius: calc(var(--radius-md) - 3px);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--type-footnote);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.friend-tab svg { width: 15px; height: 15px; flex: none; }
.friend-tab-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.friend-tab:hover { color: var(--text-primary); }

.friend-tab.is-on {
  background-color: var(--bg-elevated);
  color: var(--text-primary);
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}

/* The four panes live side by side and switching only changes which one is
   shown — nothing is rebuilt, so nothing can flicker. */
.friend-panes { display: flex; flex-direction: column; }
.friend-pane { display: flex; flex-direction: column; gap: var(--space-3); }
.friend-pane[hidden] { display: none; }

.friend-tab-count {
  flex: none;
  min-width: 18px;
  padding: 0 5px;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  color: #fff;
  font-size: var(--type-caption2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* --------------------------------------------------------------------------
   Chats
   -------------------------------------------------------------------------- */

/* The tabs on the Friends SCREEN, as opposed to the ones inside a friend's
   sheet. Same control, wider setting: it is the top of a page rather than a
   row inside a card, so it stands clear of the header above it. */
.friend-tabs.is-screen { margin: 0 0 var(--space-1); }

/* A friend's sheet carries four tabs now, and four labels with an icon and a
   count apiece do not fit across a phone — "Together" was arriving as "T…".
   The icons go first: they are decoration beside a word that says the same
   thing, and the word is the half worth keeping. */
@media (max-width: 480px) {
  .friend-sheet .friend-tab svg { display: none; }
  .friend-sheet .friend-tab {
    padding: 7px 4px;
    gap: 3px;
    font-size: var(--type-caption1);
  }
  .friend-sheet .friend-tab-count {
    min-width: 15px;
    padding: 0 3px;
    font-size: 10px;
  }
}

/* --- a chat row, and the way to be rid of it -------------------------------

   The lane holds two things side by side: the row, and a delete pane just
   past the right edge. On a phone the lane is a real horizontal scroller, so
   swiping it is a scroll — which is the entire trick. `ui/scrim-lock.js`
   cancels every touch drag that is not aimed at something genuinely
   scrollable, and a scroller is the one thing it never refuses. A swipe built
   out of pointer events and a transform would be arguing with that listener;
   this one is simply allowed.

   `scroll-snap-type: x mandatory` keeps the pane open or shut and never
   stranded half way, and `overscroll-behavior-x: contain` stops a flick at
   the end of the lane turning into the browser's back gesture — the same
   guard, for the same reason, as `.table-scroll`.                          */

.chat-lane {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
  border-radius: var(--radius-md);
}

.shell[data-view="mobile"] .chat-lane {
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.shell[data-view="mobile"] .chat-lane::-webkit-scrollbar { display: none; }

/* The row is the whole lane, so the pane is off-screen until it is asked for
   — and the pane is a fixed width rather than a share of one, so the distance
   of the swipe is the same on a small phone and a large one. */
.shell[data-view="mobile"] .chat-row {
  flex: 0 0 100%;
  scroll-snap-align: start;
}

.shell[data-view="mobile"] .chat-del-pane {
  flex: 0 0 72px;
  scroll-snap-align: end;
}

/* On a pointer there is nothing to swipe, so the lane stops scrolling and the
   pane comes back inside: the row gives up the width and the same button sits
   at the end of it. */
.shell[data-view="desktop"] .chat-row { flex: 1 1 auto; min-width: 0; }
.shell[data-view="desktop"] .chat-del-pane { flex: none; }

.chat-del-pane { display: flex; }

.chat-del {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  border: none;
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  color: var(--text-tertiary);
  cursor: pointer;
}

.chat-del:hover { background-color: var(--color-negative); color: #fff; }

/* Swiped open, the pane is not a suggestion — it is the only thing there and
   it says what it does. On the desktop side it stays quiet until pointed at,
   because it sits in the row rather than behind it. */
.shell[data-view="mobile"] .chat-del {
  background-color: var(--color-negative);
  color: #fff;
}

.shell[data-view="mobile"] .chat-del-point,
.shell[data-view="desktop"] .chat-del-swipe { display: none; }

.shell[data-view="desktop"] .chat-del {
  width: 32px;
  height: 32px;
  background-color: transparent;
}

.chat-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  text-align: left;
  cursor: pointer;
}

.chat-row:hover { background-color: var(--fill-tertiary); }

.chat-row-faces { display: flex; flex: none; }
/* Two or three faces, overlapped, so a group reads as a group at a glance. */
.chat-row-faces > * + * { margin-left: -10px; }

.chat-row-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1;
}

.chat-row-name {
  font: var(--type-subhead);
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-row-last,
.chat-row-quiet {
  font: var(--type-caption1);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-row-quiet { color: var(--text-tertiary); font-style: italic; }
.chat-row-when { flex: none; font: var(--type-caption2); color: var(--text-tertiary); }

.chat-row-unread {
  flex: none;
  min-width: 20px;
  padding: 2px 6px;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  color: #fff;
  font-size: var(--type-caption2);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* --- picking who a new chat is with ---------------------------------------- */

.chat-pick-list { max-height: 320px; overflow-y: auto; }

.chat-pick {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2);
  border: 1.5px solid transparent;
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  color: var(--text-primary);
  text-align: left;
  cursor: pointer;
}

.chat-pick.is-on { border-color: var(--accent); background-color: var(--accent-tint); }
.chat-pick:disabled { opacity: 0.4; cursor: default; }
.chat-pick-name { flex: 1; min-width: 0; font: var(--type-subhead); }
.chat-pick-tick { flex: none; color: var(--accent); }

/* --- the conversation ------------------------------------------------------ */

/* The sheet is a column: header, then the conversation, and the box to type
   in pinned under it. Only the thread scrolls — the sheet itself must not,
   or focusing the box scrolls the name off the top of the screen. */
.chat-sheet {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  max-height: 88dvh;
}

/* On a phone the scrim is pinned to the strip the keyboard leaves (see
   ui/keyboard-sheet.js), so the sheet has nothing to work out: it fills what
   it has been given. */
.scrim.is-fitted .chat-sheet { height: 100%; max-height: 100%; }

.chat-sheet .sheet-header { flex: none; }

.chat-pane {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

.chat-thread {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* Takes whatever the sheet has left after the header, the box and the
     note. On a desktop, where the sheet is sized by its content, it still
     needs a shape of its own. */
  flex: 1;
  min-height: 0;
  height: min(52dvh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

/* With the keyboard up there is no room to spare: the thread takes what is
   left rather than asking for a height of its own — and it is the one thing
   inside a pinned scrim that may still be dragged, the scrim itself having
   given up panning so a flick cannot reach the page behind it. */
.scrim.is-fitted .chat-thread {
  height: auto;
  touch-action: pan-y;
}

.scrim.is-fitted .chat-input { touch-action: pan-y; }

.chat-sheet .chat-composer,
.chat-sheet .chat-attached,
.chat-sheet .chat-note { flex: none; }

.chat-day {
  align-self: center;
  margin: var(--space-2) 0 var(--space-1);
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  background-color: var(--bg-elevated);
  font: var(--type-caption2);
  color: var(--text-tertiary);
}

.chat-bubble {
  align-self: flex-start;
  max-width: 78%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-lg);
  background-color: var(--bg-elevated);
  box-shadow: var(--shadow-sm);
}

.chat-bubble.is-mine {
  align-self: flex-end;
  background-color: var(--accent);
}

.chat-text {
  margin: 0;
  font: var(--type-subhead);
  color: var(--text-primary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.chat-bubble.is-mine .chat-text,
.chat-bubble.is-mine .chat-when,
.chat-bubble.is-mine .chat-gone { color: var(--text-on-accent); }

.chat-when {
  display: block;
  margin-top: 2px;
  font-size: 10px;
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

.chat-gone {
  margin: 0 0 2px;
  font: var(--type-caption2);
  font-style: italic;
  color: var(--text-tertiary);
}

.chat-picture {
  display: block;
  margin: 0 0 var(--space-1);
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

.chat-picture img {
  display: block;
  max-width: 100%;
  max-height: 220px;
  border-radius: var(--radius-md);
}

/* --- saying something ------------------------------------------------------ */

.chat-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.chat-input {
  flex: 1;
  min-width: 0;
  min-height: 38px;
  max-height: 120px;
  resize: none;
  line-height: 1.35;
}

.chat-attach,
.chat-send { flex: none; padding: 0 var(--space-3); min-height: 38px; }

.chat-attached {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.chat-attached-thumb {
  width: 52px;
  height: 52px;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

.chat-note {
  margin: var(--space-2) 0 0;
  font: var(--type-caption2);
  color: var(--text-tertiary);
  text-align: center;
}

/* --------------------------------------------------------------------------
   What happened lately
   -------------------------------------------------------------------------- */

.notice-row {
  display: flex;
  align-items: stretch;
  gap: var(--space-1);
}

.notice-main {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
  padding: var(--space-2);
  border: none;
  border-radius: var(--radius-md);
  background: none;
  text-align: left;
  cursor: pointer;
}

.notice-main:hover { background-color: var(--fill-quaternary); }

.notice-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  color: #fff;
}

.notice-mark svg { width: 16px; height: 16px; }

.notice-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }

.notice-what { font: var(--type-subhead); color: var(--text-primary); }
.notice-what strong { font-weight: 600; }

.notice-detail {
  font: var(--type-caption1);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.notice-when { flex: none; font: var(--type-caption2); color: var(--text-tertiary); }

.notice-dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 32px;
  border: none;
  border-radius: var(--radius-md);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
}

.notice-dismiss:hover { background-color: var(--fill-quaternary); color: var(--text-primary); }
.notice-dismiss svg { width: 15px; height: 15px; }

/* --------------------------------------------------------------------------
   A friend's own page
   -------------------------------------------------------------------------- */

/* The profile picture is a button now — it opens at the size the screen
   allows. Nothing about how it looks changes; it simply says it can be
   pressed. */
.friend-hero-face {
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
  border-radius: var(--radius-full);
}

.friend-hero-face:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* How long you have been friends: the quietest line on the sheet, under
   everything else, which is the weight it carries. */
.friend-since {
  margin: var(--space-3) 0 0;
  font: var(--type-caption2);
  color: var(--text-tertiary);
  text-align: center;
}

/* Remove sits BENEATH the two things you do with a friend rather than beside
   them — a full-width row of its own, so "stop being friends" is not a
   thumb's width from the most common action on the sheet. */
.quick-actions.is-under { margin-top: var(--space-2); }
.quick-actions .btn.is-wide { flex: 1; }

/* --- your own profile ------------------------------------------------------ */

.friend-hero-at.btn-link { align-self: flex-start; padding: 0; }

/* The thing somebody trains most — one line of character beside a block of
   quantity. */
.me-favourite {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px var(--space-2);
  border-radius: var(--radius-full);
  background-color: var(--accent-tint);
  color: var(--accent);
  font-size: var(--type-caption1);
  font-weight: 600;
}

.me-favourite svg { width: 13px; height: 13px; }

/* Bests of things that belong to nothing in particular — longest session,
   furthest, best week. Rows rather than the record grid, because each has a
   date on it and the grid has two columns already. */
.me-bests {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-3);
}

.me-best {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--separator);
}

.me-best:last-child { border-bottom: none; }
.me-best-label { flex: 1; min-width: 0; font-size: var(--type-footnote); color: var(--text-secondary); }
.me-best-value { font-weight: 600; font-variant-numeric: tabular-nums; }
.me-best-when {
  flex: none;
  min-width: 5.5rem;
  text-align: right;
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
}

/* --- personal records ------------------------------------------------------ */

.friend-prs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 1px;
  margin-top: var(--space-2);
  background-color: var(--separator);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.friend-pr {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background-color: var(--bg-elevated);
}

.friend-pr-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--type-footnote);
  color: var(--text-secondary);
}

.friend-pr-value {
  flex: none;
  font-size: var(--type-subhead);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.friend-log-row.is-pick {
  padding-inline: var(--space-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.friend-log-row.is-pick:hover { background-color: var(--bg-hover); }
.friend-log-row.is-pick.is-on { background-color: var(--accent-tint); }

.friend-log-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }

.friend-log-name {
  font-size: var(--type-subhead);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.friend-log-meta,
.friend-log-note {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.friend-log-note { font-style: italic; }

.friend-log-tag {
  flex: none;
  font-size: var(--type-caption2);
  font-weight: 600;
  color: var(--text-secondary);
  background-color: var(--fill-quaternary);
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
}

.friend-pick-tick { width: 16px; height: 16px; color: var(--accent); flex: none; }

/* --- choosing who ---------------------------------------------------------- */

/* A column now, because the search row and the chips are two things stacked
   rather than one wrapping row. The chips keep the wrapping. */
.friend-pick { display: flex; flex-direction: column; }
.friend-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.friend-pick-workouts { max-height: 300px; overflow-y: auto; }

/* The line above each list: what it is showing, and the icon that searches it.
   Sticky so scrolling forty workouts does not take the search away with it. */
.friend-pick-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 2px 0;
  background-color: var(--bg-elevated);
}

.friend-pick-label {
  flex: 1;
  min-width: 0;
  font-size: var(--type-caption1);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
}

/* The one about to be sent, when the filter no longer shows it. */
.friend-pick-held {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: var(--hairline) solid var(--separator);
}

.friend-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px var(--space-3);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-full);
  background-color: var(--bg-elevated);
  font-size: var(--type-footnote);
  color: var(--text-primary);
  cursor: pointer;
}

.friend-chip.is-on {
  border-color: var(--accent);
  background-color: var(--accent-tint);
  color: var(--accent);
  font-weight: 600;
}

.friend-chip-tick { width: 14px; height: 14px; opacity: 0; }
.friend-chip.is-on .friend-chip-tick { opacity: 1; }

.friend-warn:empty { display: none; }

/* --- choosing a name ------------------------------------------------------- */

.friend-name-row { display: flex; align-items: center; gap: var(--space-1); }

.friend-name-at {
  font-size: var(--type-title3);
  color: var(--text-tertiary);
  font-weight: 600;
}

.friend-name-field { flex: 1; min-width: 0; }

.friend-name-status {
  min-height: 1.4em;
  font-size: var(--type-footnote);
  color: var(--text-tertiary);
}

.friend-name-status.is-good { color: var(--color-positive); }
.friend-name-status.is-bad { color: var(--color-negative); }
.friend-name-status.is-quiet { color: var(--text-tertiary); }

/* --- a partner, on the log -------------------------------------------------- */

.fit-partner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  background-color: var(--bg-secondary);
}

.fit-partner-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.fit-partner-label {
  font-size: var(--type-caption2);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-tertiary);
}

.fit-partner-who {
  font-size: var(--type-subhead);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fit-partner-add { flex: none; }

/* --- the dot on the bar ------------------------------------------------------
   Something is waiting. Drawn on the button rather than as a number, because
   the count is not the point — "there is something there" is. */
.nav-friends { position: relative; }

/* The count on the Friends button. It replaced a plain dot: with messages in
   the app, "something is waiting" stopped being enough to act on. Small
   enough not to unbalance a 28px button, and it grows sideways rather than
   outwards so a two-digit number does not shove the icon off centre. */
.nav-count {
  position: absolute;
  top: 2px;
  right: 0;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  border-radius: var(--radius-full);
  background-color: var(--color-negative);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 2px var(--bg-elevated);
  pointer-events: none;
}

@media (max-width: 560px) {
  .friend-vs-row { grid-template-columns: 1fr 1fr; grid-template-areas: 'label label' 'mine theirs'; }
  .friend-vs-label { grid-area: label; text-align: left; }
  .friend-vs-side.is-mine { grid-area: mine; }
  .friend-vs-side.is-theirs { grid-area: theirs; }
  .friend-grid { grid-template-columns: 1fr; }
}

/* =========================================================================
   The personal tracker
   =========================================================================
   Built out of the same cards, lists and sheets as the other two. What is
   new below is the handful of things neither of them has: a stepper that
   walks time, a grid of habits, a checkable list, a month calendar, and an
   editor that takes the screen on a phone.
   ========================================================================= */

/* --- walking through time ------------------------------------------------ */

.me-stepper {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
}

.me-stepper-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  text-align: center;
}

.me-stepper-label {
  font-size: var(--type-headline);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.me-stepper-sub {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.me-stepper-now { flex: none; }

/* --- headings and counts -------------------------------------------------- */

.me-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.me-head-tools { display: flex; align-items: center; gap: var(--space-2); }

/* --- goals -----------------------------------------------------------------
   Five rows to a period, always drawn — the empty ones are the invitation, so
   there is no "add" button anywhere in here. Written to read the same in the
   Goals tab and in the sheet the journal opens, because they are the same
   view; nothing below knows which of the two it is inside. */

.me-goals-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}

.me-goals-title {
  font-size: var(--type-headline);
  font-weight: 650;
  color: var(--text-primary);
  margin: 0;
}

/* "3 of 5". Tinted only when it is all five — a count that is always coloured
   says nothing, and the one state worth catching the eye is the finished one. */
.me-goals-count {
  font-size: var(--type-subhead);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  flex: none;
}
.me-goals-count.is-all { color: var(--accent); font-weight: 600; }

.me-goals-blurb { margin: 2px 0 var(--space-3); }

.me-goals-list { display: flex; flex-direction: column; gap: var(--space-1); }

/* Top-aligned, not centred: a finished goal carries a second line saying when,
   and centring would drop the tick into the gap between the words and the
   date rather than leaving it beside the words it belongs to. */
.me-goal-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  border-radius: var(--radius-sm);
}

/* The tick, and the whole of what "done" looks like. The text goes quiet and
   is struck through; it does not disappear, because what you set is as much
   the record as what you finished. */
.me-goal-tick {
  flex: none;
  border: none;
  background: transparent;
  padding: 4px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--text-tertiary);
  transition: color var(--duration-fast) var(--ease-out);
}
.me-goal-tick svg { width: 20px; height: 20px; }
/* Sits on the field's own text rather than on the top of its box. */
.me-goal-tick { margin-top: 5px; }
.me-goal-tick:hover { color: var(--text-secondary); }
.me-goal-tick.is-on, .me-goal-tick.is-on:hover { color: var(--accent); }

/* The placeholder circle on an empty row is not a control and must not look
   like one — no pointer, no hover, and out of the tab order entirely. */
.me-goal-tick.is-blank {
  color: var(--text-quaternary);
  cursor: default;
  pointer-events: none;
}

.me-goal-text { flex: 1; min-width: 0; }

.me-goal-field {
  width: 100%;
  border-color: transparent;
  background: transparent;
  padding-left: var(--space-2);
  padding-right: var(--space-2);
}
.me-goal-field:hover { border-color: var(--separator); }
.me-goal-field:focus { border-color: var(--accent); background-color: var(--bg-elevated); }

.me-goal-row.is-done .me-goal-field {
  color: var(--text-tertiary);
  text-decoration: line-through;
}

/* When it was finished — the log, and the only reason a month from last year
   is worth opening. */
.me-goal-when {
  display: block;
  padding-left: var(--space-2);
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
}

/* Removal stays out of the way until the row is wanted: a row of five delete
   crosses is a page that looks like it is about deleting. */
.me-goal-drop {
  flex: none;
  opacity: 0;
  /* Quiet by default. `.nav-btn` is the accent, and five accent-coloured
     crosses down the side of a list make the page look like it is about
     deleting things rather than about setting them. */
  color: var(--text-tertiary);
  transition: opacity var(--duration-fast) var(--ease-out);
}
.me-goal-row:hover .me-goal-drop,
.me-goal-drop:focus-visible { opacity: 1; }
.me-goal-drop:hover { color: var(--color-red); }

/* A touch screen has no hover to reveal it with, so there it is simply there. */
@media (hover: none) {
  .me-goal-drop { opacity: 1; }
}

/* --- the months you have set ---------------------------------------------- */

.me-goal-history { display: flex; flex-direction: column; gap: 2px; }

.me-goal-past {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2);
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-primary);
  text-align: left;
  font-size: var(--type-subhead);
}
.me-goal-past:hover { background-color: var(--bg-hover); }

.me-goal-past-count {
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  flex: none;
}
.me-goal-past-count.is-all { color: var(--accent); font-weight: 600; }

/* --- the month, read back ---------------------------------------------------
   Two bars and a sentence. The bars share one scale so the eye can do the
   comparison the page is making: how much month is gone against how much goal
   is done. The sentence is chosen in goals-lines.js and is behaviour, not
   decoration — see the tests that hold it. */

.me-goalglance-line {
  margin: 0 0 var(--space-3);
  font-size: var(--type-headline);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.35;
}

.me-goalglance-bars { display: flex; flex-direction: column; gap: var(--space-2); }

.me-goalglance-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.me-goalglance-label {
  flex: none;
  width: 52px;
  font-size: var(--type-caption1);
  color: var(--text-secondary);
}

.me-goalglance-track {
  flex: 1;
  height: 6px;
  border-radius: 3px;
  background-color: var(--fill-quaternary);
  overflow: hidden;
}

/* Time is neutral; done is the accent. The month bar filling is not an
   achievement, and colouring it like one would make falling behind look like
   progress. */
.me-goalglance-fill {
  height: 100%;
  border-radius: 3px;
  background-color: var(--text-quaternary);
  transition: width var(--duration-base) var(--ease-out);
}
.me-goalglance-fill.is-done { background-color: var(--accent); }

.me-goalglance-share {
  flex: none;
  width: 40px;
  text-align: right;
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* --- the quiet week --------------------------------------------------------- */

.me-goalquiet {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.me-goalquiet-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
}
.me-goalquiet-mark svg { width: 18px; height: 18px; }

.me-goalquiet-text { flex: 1; min-width: 0; }
.me-goalquiet-title {
  margin: 0 0 2px;
  font-size: var(--type-subhead);
  font-weight: 650;
  color: var(--text-primary);
}
.me-goalquiet .me-note { margin: 0; }

/* --- what sits under a goal's words ---------------------------------------- */

.me-goal-meta {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding-left: var(--space-2);
  padding-bottom: 2px;
}

.me-goal-bar { display: flex; align-items: center; gap: var(--space-2); }

.me-goal-bar-track {
  flex: 1;
  max-width: 260px;
  height: 5px;
  border-radius: 2.5px;
  background-color: var(--fill-quaternary);
  overflow: hidden;
}

.me-goal-bar-fill {
  height: 100%;
  border-radius: 2.5px;
  background-color: var(--accent);
  transition: width var(--duration-base) var(--ease-out);
}

/* "38 km to go" — the remaining distance, which is where the motivation
   lives, not the share already banked. */
.me-goal-bar-label {
  flex: none;
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.me-goal-plan {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
}
.me-goal-plan svg { width: 12px; height: 12px; flex: none; }

.me-goal-open { flex: none; color: var(--text-tertiary); }
.me-goal-open:hover { color: var(--accent); }
.me-goal-again { flex: none; }

/* --- the goal sheet --------------------------------------------------------- */

.me-goalsheet-why {
  margin: calc(-1 * var(--space-2)) 0 var(--space-3);
  text-align: center;
  font-size: var(--type-subhead);
  color: var(--text-secondary);
  font-style: italic;
}

.me-goalsheet-checkin {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.me-goalsheet-checkin .fit-input { flex: 1; min-width: 0; }
.me-goalsheet-amount { flex: none !important; width: 132px; }

.me-goalsheet-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
}

.me-goalsheet-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: var(--space-2);
}

.me-goalsheet-label {
  font-size: var(--type-caption1);
  color: var(--text-secondary);
}

.me-goalsheet-log { display: flex; flex-direction: column; gap: 2px; margin-top: var(--space-2); }

.me-goalsheet-entry {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 5px var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--type-subhead);
}
.me-goalsheet-entry:nth-child(odd) { background-color: var(--fill-quaternary); }

.me-goalsheet-date { flex: none; width: 92px; color: var(--text-secondary); font-size: var(--type-caption1); }
.me-goalsheet-gain { flex: none; color: var(--accent); font-weight: 600; font-variant-numeric: tabular-nums; }
.me-goalsheet-note { color: var(--text-primary); min-width: 0; overflow-wrap: anywhere; }

.me-goalsheet-foot { justify-content: space-between; }

.me-count {
  font-size: var(--type-caption1);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-tertiary);
  background-color: var(--fill-quaternary);
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
}

.me-count.is-full { color: var(--color-positive); background-color: var(--tint-positive); }

.me-note {
  font-size: var(--type-footnote);
  color: var(--text-tertiary);
  margin-top: var(--space-2);
  line-height: 1.5;
}

/* --- habits ---------------------------------------------------------------
   A grid rather than a list. Six to nine things crossed off with a thumb want
   to be targets, not rows — and the whole set should be readable in one look,
   which a column of nine rows is not. */

.me-habits {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-2);
}

.me-habit {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  min-height: 52px;
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.me-habit:hover { background-color: var(--bg-hover); }
.me-habit:active { transform: scale(0.985); }

.me-habit.is-done {
  border-color: var(--color-positive);
  background-color: var(--tint-positive);
}

.me-habit.is-done .me-habit-name { color: var(--color-positive); font-weight: 600; }
.me-habit.is-done .me-habit-mark { color: var(--color-positive); }

.me-habit.is-ahead { opacity: 0.55; cursor: default; }
.me-habit.is-ahead:active { transform: none; }

.me-habit-mark { flex: none; color: var(--text-tertiary); display: grid; place-items: center; }
.me-habit-mark svg { width: 22px; height: 22px; }

.me-habit-name {
  font-size: var(--type-subhead);
  color: var(--text-primary);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.me-streak {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-3);
  font-size: var(--type-footnote);
  font-weight: 600;
  color: var(--color-orange);
}

.me-streak svg { width: 15px; height: 15px; }

/* --- setting them --------------------------------------------------------- */

.me-habit-list { display: flex; flex-direction: column; }

.me-habit-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--separator);
}

.me-habit-row:last-child { border-bottom: none; }
.me-habit-row.is-stopped { opacity: 0.7; }

.me-habit-row-mark {
  flex: none;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
  border: none;
}

.me-habit-row-mark.is-pick { cursor: pointer; }
.me-habit-row-mark.is-pick:hover { background-color: var(--accent-tint); color: var(--accent); }
.me-habit-row-mark svg { width: 19px; height: 19px; }

.me-habit-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.me-habit-row-name { font-size: var(--type-subhead); color: var(--text-primary); }
.me-habit-row-sub { font-size: var(--type-caption1); color: var(--text-tertiary); }
.me-habit-name-field { padding: 4px 6px; height: auto; }

.me-habit-row-tools { display: flex; align-items: center; gap: 2px; flex: none; }

.me-habit-new { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
.me-habit-new input { flex: 1; min-width: 0; }

.me-icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.me-icon-choice {
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated);
  color: var(--text-secondary);
  cursor: pointer;
}

.me-icon-choice:hover { background-color: var(--bg-hover); }
.me-icon-choice.is-on { border-color: var(--accent); background-color: var(--accent-tint); color: var(--accent); }
.me-icon-choice svg { width: 21px; height: 21px; }

/* --- tasks ---------------------------------------------------------------- */

.me-tasks { display: flex; flex-direction: column; }

.me-task {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--separator);
}

.me-task:last-child { border-bottom: none; }

.me-task-box {
  flex: none;
  border: none;
  background: transparent;
  color: var(--text-quaternary);
  padding: 4px;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.me-task-box svg { width: 21px; height: 21px; }
.me-task.is-done .me-task-box { color: var(--color-positive); }

.me-task-text {
  flex: 1;
  min-width: 0;
  font-size: var(--type-body);
  color: var(--text-primary);
  cursor: pointer;
  overflow-wrap: anywhere;
}

.me-task.is-done .me-task-text {
  color: var(--text-tertiary);
  text-decoration: line-through;
  text-decoration-color: var(--text-quaternary);
}

/* The row tools stay out of the way until the row is under the pointer. On a
   touch screen there is no hover, so they are simply always there. */
.me-task-tool { flex: none; opacity: 0; transition: opacity var(--duration-fast) var(--ease-out); }
.me-task:hover .me-task-tool,
.me-task-tool:focus-visible { opacity: 1; }

@media (hover: none) {
  .me-task-tool { opacity: 1; }
}

.me-task-add {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: var(--hairline) solid var(--separator);
}

.me-task-add-icon { width: 18px; height: 18px; color: var(--text-quaternary); flex: none; }
.me-task-field { flex: 1; min-width: 0; border: none; background: transparent; padding-inline: 0; }
.me-task-field:focus { outline: none; }

/* --- the month grid -------------------------------------------------------
   Cells that stay square-ish and legible from a phone to a wide screen. The
   marks are the point: small, coloured by tracker, filled for what happened
   and hollow for what is only planned. */

.me-cal { padding: var(--space-3); }

.me-cal-heads {
  display: grid;
  grid-template-columns: 22px repeat(7, 1fr);
  gap: 2px;
  margin-bottom: var(--space-1);
}

.me-cal-head {
  grid-column: auto;
  text-align: center;
  font-size: var(--type-caption2);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  padding-bottom: 4px;
}

.me-cal-heads .me-cal-head:first-child { grid-column: 2; }

.me-cal-weeks { display: flex; flex-direction: column; gap: 2px; }

.me-cal-week {
  display: grid;
  grid-template-columns: 22px repeat(7, 1fr);
  gap: 2px;
}

/* The handle that opens a whole week. Quiet until you go near it. */
/* Open this week.
   IT WAS ALL BUT INVISIBLE, and the reason was the two faint things multiplied
   together rather than either one of them: `--text-quaternary` is 0.18 alpha
   by design, and 0.35 opacity on top of it left the icon drawn at about six
   per cent — a hairline of light grey on light grey, on any theme, which is
   what it was reported as. Visibility is one decision now, and it is the
   colour: the token is defined per theme, so the icon reads on a light one, a
   dark one and a custom one without anything having to be picked twice. */
.me-cal-weekno {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out);
}

.me-cal-weekno svg { width: 13px; height: 13px; }
/* Still answers the pointer, just by getting darker rather than by arriving. */
.me-cal-week:hover .me-cal-weekno { color: var(--text-primary); }
.me-cal-weekno:hover { background-color: var(--bg-hover); color: var(--accent); }
/* A row with no days in it has no week to open. */
.me-cal-weekno:disabled { opacity: 0; cursor: default; }

.me-cal-cell {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-height: 58px;
  padding: 5px 2px 4px;
  border: var(--hairline) solid transparent;
  border-radius: var(--radius-sm);
  background-color: transparent;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.me-cal-cell.is-blank { cursor: default; }
.me-cal-cell:hover:not(.is-blank) { background-color: var(--bg-hover); }
.me-cal-cell.has-marks { background-color: var(--fill-quaternary); }
.me-cal-cell.is-ahead { opacity: 0.72; }

.me-cal-cell.is-today {
  border-color: var(--accent);
  background-color: var(--accent-tint);
}

.me-cal-num {
  font-size: var(--type-footnote);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  line-height: 1.1;
}

.me-cal-cell.is-today .me-cal-num { color: var(--accent); font-weight: 700; }

.me-cal-marks {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 3px;
  margin-top: auto;
}

/* A shade bigger than they were, with a rim mixed from the mark's own colour.
   The rim is what makes a yellow dot readable on a pale cell without giving
   every other one a black outline. */
.me-cal-mark {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-full);
  background-color: var(--mark, var(--accent));
  box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--mark, var(--accent)) 65%, black);
  flex: none;
}

/* Hollow: intention, not fact. */
.me-cal-mark.is-planned {
  background-color: transparent;
  box-shadow: inset 0 0 0 1.5px var(--mark, var(--accent));
}

.me-cal-habits {
  width: 70%;
  max-width: 34px;
  height: 3px;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  overflow: hidden;
}

.me-cal-habits-fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-full);
  background-color: var(--color-orange);
  transition: width var(--duration-fast) var(--ease-out);
}

.me-cal-habits.is-full .me-cal-habits-fill { background-color: var(--color-positive); }

@media (max-width: 480px) {
  .me-cal { padding: var(--space-2); }
  .me-cal-cell { min-height: 48px; }
  .me-cal-mark { width: 5px; height: 5px; }
}

/* --- what money can and cannot say --------------------------------------- */

.me-filed,
.me-undated {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  width: 100%;
  text-align: left;
}

.me-undated { cursor: pointer; }
.me-undated:hover { background-color: var(--bg-hover); }

.me-filed > svg,
.me-undated > svg { width: 20px; height: 20px; color: var(--text-tertiary); flex: none; }

.me-filed-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.me-filed-title { font-size: var(--type-subhead); font-weight: 600; color: var(--text-primary); }
.me-filed-sub { font-size: var(--type-caption1); color: var(--text-tertiary); line-height: 1.45; }

.me-filed-figures { display: flex; gap: var(--space-4); flex: none; }
.me-filed-figure { display: flex; flex-direction: column; align-items: flex-end; }
.me-filed-figure-value { font-size: var(--type-subhead); font-weight: 600; font-variant-numeric: tabular-nums; }
.me-filed-figure-label { font-size: var(--type-caption2); color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; }

@media (max-width: 560px) {
  .me-filed { flex-wrap: wrap; }
  .me-filed-figures { width: 100%; justify-content: space-between; }
  .me-filed-figure { align-items: flex-start; }
}

/* --- the cards ------------------------------------------------------------ */

.me-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--space-2);
}

.me-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.me-card:hover { background-color: var(--bg-hover); }

.me-card-icon {
  flex: none;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
}

.me-card-icon svg { width: 18px; height: 18px; }

.me-card.is-accent .me-card-icon { background-color: var(--accent-tint); color: var(--accent); }
.me-card.is-good .me-card-icon { background-color: var(--tint-positive); color: var(--color-positive); }
.me-card.is-warn .me-card-icon { background-color: var(--tint-warning); color: var(--color-warning); }
.me-card.is-bad .me-card-icon { background-color: var(--tint-negative); color: var(--color-negative); }

.me-card-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.me-card-value {
  font-size: var(--type-title3);
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  line-height: 1.15;
}

.me-card-title { font-size: var(--type-footnote); font-weight: 600; color: var(--text-secondary); }
.me-card-detail {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- one day, read ------------------------------------------------------- */

.me-day-sub {
  font-size: var(--type-footnote);
  color: var(--text-tertiary);
  margin: calc(var(--space-2) * -1) 0 var(--space-2);
  text-align: center;
}

.me-day-list { display: flex; flex-direction: column; }

.me-day-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) 0;
  border: none;
  border-bottom: var(--hairline) solid var(--separator);
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.me-day-row:last-child { border-bottom: none; }
.me-day-row:hover { background-color: var(--bg-hover); }
.me-day-row.is-reminder { cursor: default; }
.me-day-row.is-planned .me-day-title { color: var(--text-secondary); }

.me-day-mark {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background-color: color-mix(in srgb, var(--mark, var(--accent)) 14%, transparent);
  color: var(--mark, var(--accent));
  border: none;
  cursor: inherit;
}

.me-day-row.is-reminder .me-day-mark { cursor: pointer; }
.me-day-mark svg { width: 17px; height: 17px; }

.me-day-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }

.me-day-title {
  font-size: var(--type-subhead);
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.me-day-title.is-done {
  color: var(--text-tertiary);
  text-decoration: line-through;
  text-decoration-color: var(--text-quaternary);
}

.me-day-detail {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.me-day-tag {
  flex: none;
  font-size: var(--type-caption2);
  font-weight: 600;
  color: var(--text-secondary);
  background-color: var(--fill-quaternary);
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
}

/* --- a week -------------------------------------------------------------- */

.me-week-tiles { display: flex; gap: var(--space-2); margin-bottom: var(--space-3); }

.me-week-tile {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--space-3) var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.me-week-tile-icon { color: var(--text-tertiary); }
.me-week-tile-icon svg { width: 17px; height: 17px; }
.me-week-tile-value { font-size: var(--type-headline); font-weight: 700; font-variant-numeric: tabular-nums; }
.me-week-tile-label { font-size: var(--type-caption2); color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; }

.me-week-list { display: flex; flex-direction: column; }

.me-week-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) 0;
  border: none;
  border-bottom: var(--hairline) solid var(--separator);
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.me-week-row:last-child { border-bottom: none; }
.me-week-row:hover { background-color: var(--bg-hover); }

.me-week-when {
  flex: none;
  width: 40px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
}

.me-week-dow { font-size: var(--type-caption2); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-tertiary); }
.me-week-num { font-size: var(--type-headline); font-weight: 600; font-variant-numeric: tabular-nums; }
.me-week-row.is-today .me-week-num,
.me-week-row.is-today .me-week-dow { color: var(--accent); }

.me-week-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }

.me-week-said {
  font-size: var(--type-subhead);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.me-week-said.is-quiet { color: var(--text-quaternary); font-style: italic; }
.me-week-marks { display: flex; gap: 3px; }

/* --- the journal ----------------------------------------------------------
   A library on a phone; a workspace on a desktop. */

/* --------------------------------------------------------------------------
   The journal is a workspace, not a page
   --------------------------------------------------------------------------
   Every other screen is content: it flows, and the whole page scrolls. The
   journal is the opposite — it should fill the window exactly, and the only
   things that scroll are the library on the left and whatever is being written
   on the right.

   THE FIRST ATTEMPT GUESSED, and guessing is why the page still scrolled: the
   workspace was given `min-height: calc(100dvh - a few tokens)`, and the tokens
   did not add up to the real chrome. Anything left over became page scroll, and
   the foot of the editor went under the fold.

   So nothing is calculated here. `.main` is already a scroller with a definite
   height — the shell grid gives it one — so turning off its scrolling and
   handing that height down the chain is exact by construction, at every window
   size, with no number to keep in step.

   Scoped to this one screen with `[data-screen]`, so no other page changes.
   -------------------------------------------------------------------------- */

.shell[data-view="desktop"][data-screen="me-journal"] .main {
  /* The page itself does not scroll. Its two children do. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Tighter than the rest of the app, on purpose: on a screen whose whole
     point is room to write, a section-gap above the title is a strip of
     nothing across the top of the window. */
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
}

/* The big title, kept but made to cost less. */
.shell[data-view="desktop"][data-screen="me-journal"] .main > .content:first-child {
  flex: none;
}

.shell[data-view="desktop"][data-screen="me-journal"] .nav-large-title {
  margin: 0 0 var(--space-3);
  font-size: var(--type-title2);
}

/* The screen itself takes everything left. */
.shell[data-view="desktop"][data-screen="me-journal"] #content {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* THE FULL WIDTH OF THE PAGE. `--content-max` is right for a column of text
     and wrong for a two-pane workspace — it was leaving a third of a wide
     screen empty on the one screen where room matters most, and worse with the
     page finder folded away. */
  max-width: none;
}

.shell[data-view="desktop"][data-screen="me-journal"] #content > * {
  flex: 1;
  min-height: 0;
}

.me-work {
  display: grid;
  grid-template-columns: minmax(260px, 340px) 1fr;
  gap: var(--space-3);
  align-items: stretch;
  min-height: 0;
}

/* The library keeps its own scroll, so a long list of notes cannot push the
   editor off the bottom of a screen it is supposed to fill. */
.me-work-lib {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  padding-right: 2px;
}

.me-work-main { min-height: 0; }
.me-work-panes { min-height: 0; }
.me-pane { min-height: 0; }
.me-editor.is-pane { height: 100%; }

/* The dialog form, which is the one a note opens in from the library. Given a
   height rather than only a ceiling, for the same reason: `max-height` alone
   leaves a short note in a short box. */
.me-editor:not(.is-pane):not(.is-phone) { height: 84dvh; }

.me-work-lib { min-width: 0; }
.me-work-main { min-width: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.me-work-panes { flex: 1; }

.me-work-blank {
  border: var(--hairline) dashed var(--separator);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-4);
  background-color: var(--bg-elevated);
}

.me-work-panes { display: grid; grid-template-columns: 1fr; gap: var(--space-2); }
.me-work-panes.is-split { grid-template-columns: 1fr 1fr; }

.me-pane {
  min-width: 0;
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-lg);
  background-color: var(--bg-elevated);
  overflow: hidden;
  display: flex;
}

@media (max-width: 1100px) {
  .me-work { grid-template-columns: 1fr; }
  .me-work-panes.is-split { grid-template-columns: 1fr; }
}

/* --- the shelf of journals -------------------------------------------------
   The daily journal everybody has, then up to five of somebody's own. Chips
   rather than a list: five names take one line, and the open one has to be
   obvious at a glance because everything under it belongs to whichever one
   that is.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   The search that is an icon until it is wanted
   --------------------------------------------------------------------------
   Three lists share it: friends, the workouts you could send, and the people
   you could send one to. Each is short enough that a permanent box would be
   clutter and long enough to be a chore once the app has been used a while.
   -------------------------------------------------------------------------- */

.fit-search-btn {
  flex: none;
  padding: 6px 9px;
}

.fit-search-btn svg { width: 16px; height: 16px; }
.fit-search-btn[aria-expanded="true"] { color: var(--accent); background-color: var(--accent-tint); }

.fit-search-row { margin: var(--space-2) 0 var(--space-3); }
.fit-search-row[hidden] { display: none; }

/* Pushes the tools on a head row to the right, so the search icon and the
   count are not jammed against the title. */
.adm-head-gap { flex: 1; }

/* --------------------------------------------------------------------------
   Who a workout was done with
   --------------------------------------------------------------------------
   Names rather than a count, on the card, in the editor and on the sheet. A
   workout done with somebody is a different thing from one done alone, and a
   log that recorded the exercises but not the company recorded the smaller
   half of it.
   -------------------------------------------------------------------------- */

/* Provenance: a workout kept from a friend. Quiet, because it is a fact about
   where the workout came from rather than anything about the training. */
.fit-card-from {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  align-self: flex-start;
  padding: 1px var(--space-2) 1px 5px;
  border-radius: var(--radius-full);
  background-color: var(--fill-quaternary);
  color: var(--text-tertiary);
  font-size: var(--type-caption2);
  max-width: 100%;
}

.fit-card-from > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fit-card-from-mark { width: 11px !important; height: 11px !important; flex: none; }

.fit-card-with {
  font-size: var(--type-caption1);
  color: var(--accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fit-partner-who {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.fit-partner-none { font-size: var(--type-footnote); color: var(--text-tertiary); }

.fit-partner-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px 2px 2px var(--space-2);
  border-radius: var(--radius-full);
  background-color: var(--accent-tint);
  color: var(--accent);
  font-size: var(--type-caption1);
  font-weight: 600;
  max-width: 14rem;
}

.fit-partner-chip > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fit-partner-off {
  display: grid;
  place-items: center;
  flex: none;
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  opacity: 0.55;
  cursor: pointer;
}

.fit-partner-off:hover { opacity: 1; background-color: color-mix(in srgb, var(--accent) 20%, transparent); }
.fit-partner-off svg { width: 11px; height: 11px; }

.fit-view-with { display: flex; flex-wrap: wrap; gap: var(--space-1); }

.fit-view-partner {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px var(--space-3);
  border-radius: var(--radius-full);
  background-color: var(--fill-quaternary);
  font-size: var(--type-footnote);
}

.fit-view-partner svg { width: 14px; height: 14px; color: var(--accent); }

.me-shelf {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.me-shelf-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px var(--space-3) 6px var(--space-2);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-full);
  background-color: var(--bg-elevated);
  color: var(--text-secondary);
  font-size: var(--type-footnote);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.me-shelf-chip svg { width: 15px; height: 15px; flex: none; }
.me-shelf-chip:hover { background-color: var(--bg-hover); }

.me-shelf-chip.is-on {
  border-color: var(--accent);
  background-color: var(--accent-tint);
  color: var(--accent);
  font-weight: 600;
}

.me-shelf-name { max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The plus at the end of the tab strip. Pushed to the right so it sits at the
   end of the tabs rather than jammed against the last one. */
.me-tab-add { margin-left: auto; }

/* The open journal's own line: rename it in place, or delete it. Only ever
   drawn for a journal of somebody's own — the daily one has neither. */
.me-shelf-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: var(--hairline) solid var(--separator);
}

.me-shelf-bar .me-crumb-name { flex: 1; min-width: 0; }

/* Today's row, at the head of a journal. Tinted so the day you are on is
   found without reading, and pressable when there is nothing there yet. */
.me-note-row.is-today {
  border: var(--hairline) solid var(--accent);
  background-color: var(--accent-tint);
  border-radius: var(--radius-md);
}

.me-note-row.is-today:hover { background-color: color-mix(in srgb, var(--accent) 18%, transparent); }
.me-note-row.is-today .me-note-mark { color: var(--accent); }

/* --- tabs ------------------------------------------------------------------ */

.me-tabs {
  display: flex;
  align-items: center;
  gap: 2px;
  overflow-x: auto;
  padding-bottom: 2px;
  scrollbar-width: thin;
}

.me-tab {
  display: flex;
  align-items: center;
  flex: none;
  max-width: 200px;
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background-color: var(--bg-secondary);
}

.me-tab.is-on { background-color: var(--bg-elevated); border-bottom-color: transparent; }
.me-tab.is-split { box-shadow: inset 2px 0 0 var(--accent); }

.me-tab-name {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  border: none;
  background: transparent;
  padding: 7px var(--space-2);
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  cursor: pointer;
}

.me-tab.is-on .me-tab-name { color: var(--text-primary); font-weight: 600; }
.me-tab-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-tab-icon { width: 14px; height: 14px; flex: none; }

.me-tab-x {
  border: none;
  background: transparent;
  color: var(--text-quaternary);
  padding: 6px 6px 6px 0;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.me-tab-x svg { width: 13px; height: 13px; }
.me-tab-x:hover { color: var(--color-negative); }

.me-tab-tool {
  flex: none;
  margin-left: var(--space-2);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-sm);
  background-color: var(--bg-secondary);
  color: var(--text-tertiary);
  padding: 6px;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.me-tab-tool svg { width: 15px; height: 15px; }
.me-tab-tool.is-on { color: var(--accent); border-color: var(--accent); background-color: var(--accent-tint); }

/* --- the library ----------------------------------------------------------- */

.me-search-row { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.me-search-icon { width: 17px; height: 17px; color: var(--text-quaternary); flex: none; }
.me-search { flex: 1; min-width: 0; }

.me-crumbs { display: flex; align-items: center; gap: 4px; margin-bottom: var(--space-2); }
.me-crumb-gap { flex: 1; }
.me-crumb-sep { width: 14px; height: 14px; color: var(--text-quaternary); flex: none; }

.me-crumb {
  display: flex;
  align-items: center;
  gap: 5px;
  border: none;
  background: transparent;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
  font-size: var(--type-footnote);
  color: var(--text-tertiary);
  cursor: pointer;
  min-width: 0;
}

.me-crumb svg { width: 15px; height: 15px; flex: none; }
.me-crumb:hover { background-color: var(--bg-hover); }
.me-crumb.is-here { color: var(--text-primary); font-weight: 600; cursor: default; }

.me-crumb-name {
  border: none;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: 600;
  padding: 0;
  min-width: 0;
  width: 12ch;
}

.me-crumb-name:focus { outline: none; border-bottom: 1px solid var(--accent); }

.me-folders {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.me-folder {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated);
  cursor: pointer;
  text-align: left;
}

.me-folder:hover { background-color: var(--bg-hover); }
.me-folder-icon { width: 17px; height: 17px; color: var(--color-yellow, var(--accent)); flex: none; }
.me-folder-name { flex: 1; min-width: 0; font-size: var(--type-subhead); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-folder-count { font-size: var(--type-caption1); color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

.me-note-list { display: flex; flex-direction: column; }

.me-note-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) 0;
  border: none;
  border-bottom: var(--hairline) solid var(--separator);
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.me-note-row:last-child { border-bottom: none; }
.me-note-row:hover { background-color: var(--bg-hover); }
.me-note-row.is-row { cursor: default; }

.me-note-open {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
  border: none;
  background: transparent;
  padding: 0;
  text-align: left;
  cursor: pointer;
}

.me-note-mark {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background-color: var(--fill-quaternary);
  color: var(--text-secondary);
}

.me-note-mark svg { width: 16px; height: 16px; }
.me-note-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.me-note-title { font-size: var(--type-subhead); color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-note-sub { font-size: var(--type-caption1); color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-note-when { flex: none; font-size: var(--type-caption1); color: var(--text-quaternary); font-variant-numeric: tabular-nums; }

.me-note-tool { flex: none; opacity: 0; transition: opacity var(--duration-fast) var(--ease-out); }
.me-note-row:hover .me-note-tool,
.me-note-tool.is-on,
.me-note-tool:focus-visible { opacity: 1; }
.me-note-tool.is-on { color: var(--accent); }

@media (hover: none) { .me-note-tool { opacity: 1; } }

/* --- the editor -------------------------------------------------------------
   On a phone this is the screen. Height comes from visualViewport in JS —
   see the note at the top of editor.js — because 100vh lies once a keyboard
   is up. */

.me-editor {
  display: flex;
  flex-direction: column;
  min-height: 0;
  width: 100%;
  background-color: var(--bg-elevated);
}

.me-editor.is-pane { border-radius: var(--radius-lg); }
.me-editor:not(.is-pane) { max-height: 88dvh; }

.me-editor.is-phone {
  position: fixed;
  inset: 0 0 auto 0;
  max-height: none;
  border-radius: 0;
  padding: 0;
  max-width: none;
  width: 100vw;
}

/* While writing on a phone the page behind must not scroll under the sheet. */
/* THE PAGE BEHIND MUST NOT MOVE.
   `overflow: hidden` on html and body was not enough, and the reason is this
   app's own layout: the document does not scroll, `.main` does — it is the one
   scroller in the shell. So while somebody was typing, a drag anywhere over the
   sheet ran the page underneath it, and letters went in while the library slid
   about behind them.

   Locking the scroller itself is the fix. The position it was at is put back on
   the way out, because a browser that has had `overflow: hidden` applied to a
   scroller may hand it back at the top. */
html.is-writing, html.is-writing body { overflow: hidden; }
html.is-writing .main { overflow: hidden; }

/* And the scrim keeps a drag to itself rather than passing it up to the
   document, which is what rubber-bands the whole page on iOS. */
.scrim.is-writing {
  overflow: hidden;
  overscroll-behavior: contain;
  touch-action: none;
}

/* …but the sheet inside it still has to be usable: `touch-action: none` on an
   ancestor stops a descendant panning too, and the thing being panned is the
   text somebody is writing. */
.scrim.is-writing .me-editor,
.scrim.is-writing .me-editor * { touch-action: auto; }

.me-editor-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--hairline) solid var(--separator);
  flex: none;
}

.me-editor-where {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.me-editor-kind {
  font-size: var(--type-caption2);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
}

/* The day, on every dated entry, whatever it has been titled. Not uppercased
   — it is a date being read, not a label being scanned. */
.me-editor-date {
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.me-editor-title {
  flex: none;
  border: none;
  background: transparent;
  padding: var(--space-3) var(--space-4) var(--space-1);
  font-family: inherit;
  font-size: var(--type-title3);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}

.me-editor-title:focus { outline: none; }
.me-editor-title[readonly] { color: var(--text-secondary); }
/* The one field in the app that had no placeholder colour, so it fell to the
   browser's default and drew the hint in near-black at 700 weight — which read
   as the entry having been NAMED "Title" rather than as the field waiting for
   a name. Every other input here is `--text-tertiary`. */
.me-editor-title::placeholder { color: var(--text-tertiary); }

.me-editor-body {
  flex: 1;
  min-height: 0;
  width: 100%;
  border: none;
  resize: none;
  background: transparent;
  color: var(--text-primary);
  font-family: inherit;
  font-size: calc(var(--type-body) * 1.02);
  line-height: 1.65;
  padding: var(--space-2) var(--space-4) var(--space-4);
}

.me-editor-body:focus { outline: none; }

/* --- the desktop writing surface -------------------------------------------
   A `contenteditable` rather than a textarea, so that Bold looks like bold
   while it is being written rather than only in the reader. It shares every
   rule above with `.me-editor-read`, which is the point: the page you write on
   and the page you read are the same page.

   It is its own scroller — a textarea scrolls itself and a div does not, and
   without this a long note grows the pane instead of moving under the caret. */
.me-editor-write {
  overflow-y: auto;
  outline: none;
}

/* A `contenteditable` has no placeholder of its own, and `:empty` will not do
   the job: a browser keeps a `<br>` inside an empty one, so it is never empty.
   The class is set from the text instead — see `markEmpty` in richtext.js. */
.me-editor-write.is-empty::before {
  content: attr(data-placeholder);
  color: var(--text-quaternary);
  pointer-events: none;
  display: block;
  position: absolute;
}

.me-editor-write > *:first-child { margin-top: 0; }
.me-editor-write > *:last-child { margin-bottom: 0; }

/* The first paragraph carries the placeholder above it, so the surface needs a
   corner for it to be positioned against. */
.me-editor-body { position: relative; }

/* A link to another note is not clickable while it is being written — clicking
   it there means putting the caret in it to change the name. It still looks
   like what it is, so that a linked word reads as linked on the page you wrote
   it on. */
.me-editor-write .me-md-link { cursor: text; }

.me-editor-read {
  display: none;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-2) var(--space-4) var(--space-4);
  line-height: 1.65;
}

.me-editor.is-reading .me-editor-body { display: none; }
.me-editor.is-reading .me-editor-read { display: block; }
.me-editor.is-reading .me-toolbar { display: none; }

.me-editor-read h2, .me-editor-write h2 { font-size: var(--type-title3); margin: var(--space-4) 0 var(--space-2); }
.me-editor-read h3, .me-editor-write h3 { font-size: var(--type-headline); margin: var(--space-3) 0 var(--space-1); }
.me-editor-read h4, .me-editor-write h4 { font-size: var(--type-subhead); margin: var(--space-3) 0 var(--space-1); }
.me-editor-read p, .me-editor-write p { margin: 0 0 var(--space-3); }
.me-editor-read ul, .me-editor-read ol,
.me-editor-write ul, .me-editor-write ol { margin: 0 0 var(--space-3); padding-left: var(--space-5); }
.me-editor-read li, .me-editor-write li { margin-bottom: 4px; }
.me-editor-read blockquote, .me-editor-write blockquote {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-3);
  border-left: 3px solid var(--separator);
  color: var(--text-secondary);
}
.me-editor-read code, .me-editor-write code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  background-color: var(--fill-quaternary);
  border-radius: 4px;
  padding: 1px 4px;
}

/* Struck through and highlighted. The highlight is the accent at low opacity
   rather than a fixed yellow: yellow marker on a dark theme is a stripe of
   daylight across the page, and the accent is already the colour this app
   uses for "the writer meant this". */
.me-editor-read s, .me-editor-write s { color: var(--text-tertiary); }
.me-editor-read mark, .me-editor-write mark {
  background-color: color-mix(in srgb, var(--accent) 22%, transparent);
  color: inherit;
  border-radius: 3px;
  padding: 0 2px;
}

.me-md-link { color: var(--accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); }
.me-md-link:hover { border-bottom-color: var(--accent); }
.me-md-link.is-missing { color: var(--text-tertiary); border-bottom-style: dashed; }

/* The bar that rides above the keyboard. */
.me-toolbar {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 6px var(--space-2);
  border-top: var(--hairline) solid var(--separator);
  background-color: var(--bg-secondary);
  flex: none;
}

.me-toolbar-gap { flex: 1; }

/* A hairline between groups, so the bar reads as "how it looks / what the line
   is / where it points" rather than as nine buttons in a row. */
.me-toolbar-rule {
  width: 1px;
  height: 18px;
  margin: 0 var(--space-2);
  background-color: var(--separator);
  flex: none;
}

.me-tool {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  padding: 7px 9px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 2px;
}

.me-tool svg { width: 17px; height: 17px; }
.me-tool-caret { width: 12px !important; height: 12px !important; opacity: 0.5; }

/* --- text size -------------------------------------------------------------
   A menu, because size is a set where exactly one answer is true at a time.
   It opens upward: the bar is at the foot of the editor, and a menu that
   dropped from it would fall off the bottom of the sheet. */

.me-tool-group { position: relative; display: flex; }

.me-size-menu {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  z-index: 20;
  min-width: 190px;
  padding: var(--space-1);
  display: flex;
  flex-direction: column;
  gap: 1px;
  background-color: var(--bg-elevated);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg, 0 12px 32px -8px rgba(0, 0, 0, 0.28));
}

.me-size-menu[hidden] { display: none; }

.me-size-option {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  width: 100%;
  padding: 7px var(--space-2);
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  text-align: left;
  cursor: pointer;
}

.me-size-option:hover { background-color: var(--bg-hover); }
.me-size-option:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The sample shows the size it sets, which is the only honest label for a
   size control — a word saying "Heading" in body type explains nothing. */
.me-size-sample {
  flex: none;
  width: 30px;
  color: var(--text-secondary);
  font-weight: 600;
  line-height: 1;
}

.me-size-1 { font-size: var(--type-title3); }
.me-size-2 { font-size: var(--type-headline); }
.me-size-3 { font-size: var(--type-subhead); }
.me-size-body { font-size: var(--type-footnote); font-weight: 400; }

.me-size-label { font-size: var(--type-subhead); }
.me-tool:hover { background-color: var(--bg-hover); color: var(--accent); }
.me-tool:active { background-color: var(--bg-active); }

.me-tool.is-on { background-color: var(--bg-active); color: var(--accent); }
.me-size-option.is-on { color: var(--accent); }
.me-size-option.is-on .me-size-sample { color: var(--accent); }

.me-editor-count {
  font-size: var(--type-caption1);
  color: var(--text-quaternary);
  font-variant-numeric: tabular-nums;
  padding-right: var(--space-2);
}

/* --- an addition, on the account detail ------------------------------------
   Laid out so it can be checked: labels left, figures right in a tabular
   column, and a rule above the only line that is a total. */

.adm-sum { display: flex; flex-direction: column; margin: var(--space-3) 0 0; }

.adm-sum-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 5px 0;
}

.adm-sum-label { font-size: var(--type-subhead); color: var(--text-secondary); }
.adm-sum-note { font-size: var(--type-caption1); color: var(--text-tertiary); }

.adm-sum-value {
  margin-left: auto;
  font-size: var(--type-subhead);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.adm-sum-rule {
  height: 0;
  border-top: var(--hairline) solid var(--separator);
  margin: 2px 0;
}

.adm-sum-row.is-total .adm-sum-label,
.adm-sum-row.is-total .adm-sum-value { font-weight: 700; color: var(--text-primary); }

.adm-subtotal {
  font-size: var(--type-footnote);
  color: var(--text-tertiary);
  margin: var(--space-2) 0 0;
  padding-left: var(--space-2);
  border-left: 2px solid var(--separator);
  line-height: 1.5;
}

/* The word count, on a phone, where there is no toolbar to carry it. */
.me-editor-foot {
  flex: none;
  display: flex;
  justify-content: flex-end;
  padding: 4px var(--space-4) calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
  border-top: var(--hairline) solid var(--separator);
}

.me-editor.is-reading .me-editor-foot { display: none; }

/* What the marks on the calendar mean. */
.me-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-2) var(--space-3);
}

.me-legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-caption1);
  color: var(--text-secondary);
}

.me-legend-item.is-note { color: var(--text-tertiary); font-style: italic; }

/* --- a task's time -------------------------------------------------------
   The chip on the row IS the field. A time input styled down to the size of
   a label: no spinner, no border, no box until it is hovered — so an empty
   one is a faint dash and a set one is just the time, and either can be
   changed by tapping it. */

.me-task-time {
  flex: none;
  width: 5.5em;
  min-width: 0;
  border: none;
  background: transparent;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: var(--type-caption1);
  font-variant-numeric: tabular-nums;
  color: var(--text-quaternary);
  text-align: right;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.me-task-time.is-set { opacity: 1; color: var(--accent); font-weight: 600; }
.me-task:hover .me-task-time,
.me-task-time:focus { opacity: 1; }
.me-task-time:hover { background-color: var(--bg-hover); }

.me-task.is-done .me-task-time.is-set { color: var(--text-tertiary); font-weight: 400; }

/* Chrome draws a clock button inside a time input; at this size it is noise. */
.me-task-time::-webkit-calendar-picker-indicator { display: none; }

@media (hover: none) {
  .me-task-time { opacity: 1; }
}

/* --- giving one on the way in --------------------------------------------- */

.me-task-time-field {
  flex: none;
  width: 0;
  /* A flex item's default `min-width: auto` is its CONTENT minimum, and a
     time input's content minimum is wide. Without this the closed field
     refused to shrink and squeezed the task field beside it to nothing —
     invisible on a desktop, where there is room for both, and fatal on a
     phone, where there is not. */
  min-width: 0;
  padding: 0;
  border: none;
  background: transparent;
  opacity: 0;
  overflow: hidden;
  font-family: inherit;
  font-size: var(--type-footnote);
  font-variant-numeric: tabular-nums;
  transition: width var(--duration-fast) var(--ease-out),
              opacity var(--duration-fast) var(--ease-out);
}

.me-task-time-field.is-open {
  width: 6.5em;
  opacity: 1;
  padding: 2px 4px;
}

.me-task-clock { flex: none; color: var(--text-quaternary); }
.me-task-clock.is-on { color: var(--accent); }

.me-task-sheet-row { display: flex; gap: var(--space-2); align-items: flex-start; }
.me-task-time-sheet { flex: none; width: 8em; }

/* --- the legal popups (Terms of Service / Privacy Policy) -----------------
   One sheet shape for both documents. The sheet is a column and the document
   is the one scroller inside it, so the title and the Done button hold still
   while a long text moves — and on a phone the drag guard leaves the body its
   drag because it genuinely scrolls. */

.legal-sheet {
  display: flex;
  flex-direction: column;
  max-width: 640px;
}

.legal-body {
  overflow-y: auto;
  min-height: 0;
  overscroll-behavior: contain;
  padding-right: var(--space-2);
}

.legal-foot {
  display: flex;
  justify-content: flex-end;
  padding-top: var(--space-3);
}

.legal-effective {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  margin-bottom: var(--space-3);
}

.legal-lead {
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  background-color: var(--fill-quaternary);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin-bottom: var(--space-3);
}

.legal-h {
  font-size: var(--type-subhead);
  font-weight: 600;
  margin: var(--space-4) 0 var(--space-2);
}

.legal-p,
.legal-list {
  font-size: var(--type-footnote);
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
  line-height: 1.5;
}

.legal-list { padding-left: 1.2em; }
.legal-list li { margin-bottom: var(--space-1); }

/* The one-line agreement under a welcome or sign-in sheet's buttons. */
.legal-line {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  text-align: center;
  margin-top: var(--space-3);
  line-height: 1.5;
}

.legal-line .legal-link {
  font-size: inherit;
  padding: 0;
}

/* --- the sign-in sheet's v1.3.9.4 furniture ------------------------------- */

/* On a phone the stay-signed-in row sits centred under the fields. The sheet
   lives outside .shell, so the shell's data-view cannot reach it — the
   app's own 900px line (DESKTOP_BREAKPOINT) is used directly instead. */
@media (max-width: 899px) {
  .auth-stay {
    justify-content: center;
    text-align: center;
  }

  /* The note takes its own line here, and `margin-left: auto` — which parks it
     at the right-hand end beside the label on a wide sheet — beats the row's
     `justify-content` on that line and pinned it to the right edge. An auto
     margin always wins over `justify-content`, so the margin is what has to
     go. Given its own full-width line, it centres under the tick. */
  .auth-stay-note {
    margin-left: 0;
    flex-basis: 100%;
    text-align: center;
  }
}

/* The agreement tick above Create account. */
.auth-agree {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-3);
  /* Deliberately below the caption scale. This is a consent line, not
     something to read for pleasure: it has to be legible and it must not
     compete with the buttons and fields it sits between, and at 12px on a
     phone it was the loudest block on the sheet. The colour is one step up
     from tertiary, though: the tick is required, and the faintest ink on the
     sheet was hiding the one box the button needs. */
  font-size: 0.65rem;
  line-height: 1.45;
  color: var(--text-secondary);
  cursor: pointer;
  text-align: left;
}

.auth-agree input {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  flex: none;
  cursor: pointer;
  margin-top: 1px;
}

.auth-agree .legal-link {
  font-size: inherit;
  padding: 0;
}

/* -------------------------------------------------------------------------
   Locked, everywhere it appears
   -------------------------------------------------------------------------
   `.is-locked` was carried by three controls and styled by nothing — it did
   exactly as much as a comment. A door somebody's plan does not open has to
   LOOK different from one it does, or the press is a surprise both ways
   round: the padlock is the only signal, and a padlock in the accent colour
   reads as an ordinary control that happens to be drawn oddly.

   So: locked things go quiet. They keep their full size and their place —
   this is the opposite of hiding them — and they lose the accent, which in
   this app is what says "this does something for you". They stay pressable
   at full opacity on purpose; `:disabled`'s 0.55 says "not yet", and the
   whole point of a gate is that it IS ready, for a price.

   SECONDARY, NOT TERTIARY. The first cut used the faintest ink in the set
   and the Customize theme button read as disabled — which is the one thing a
   gate must not do, since the whole point is that it IS pressable. The same
   correction the Terms tick needed in v1.3.10.9: legible, clearly not the
   accent, and obviously still a control.

   `.nav-btn.is-locked` was here for one release and is gone with the button
   it dressed (v1.3.11.2) — Friends is the fourth tab and one door is the
   whole rule. A selector nothing wears is the kind of thing the next reader
   trusts and the one after that copies.
   ------------------------------------------------------------------------- */

.btn.is-locked {
  color: var(--text-secondary);
}
.btn.is-locked .icon {
  opacity: 0.8;
}
/* A grey button on a grey sheet needs its edge back. */
.btn.btn-gray.is-locked {
  border: var(--hairline) solid var(--separator);
}

/* The locked tab's padlock, riding where the waiting count rides on the
   Friends tab a paying account has — the same corner, so the two never
   collide and the eye already knows to look there. A tab is `--text-
   secondary` at rest anyway, so the lock is the whole signal and it is the
   only thing that needs sizing down: `.tab svg` is 26px, which is an icon
   rather than a badge. Specificity, not `!important`. */
.tab svg.tab-lock {
  position: absolute;
  top: 3px;
  left: 50%;
  margin-left: 5px;
  width: 13px;
  height: 13px;
  color: var(--text-tertiary);
}

/* The gate a locked feature opens: one sentence and a door to the plans.
   The sentence gets body colour and room to breathe — it is the whole sheet,
   and a gate whose one line whispers reads as an apology. */
.gate-line {
  font-size: var(--type-subhead);
  color: var(--text-secondary);
  line-height: 1.5;
  margin-top: var(--space-3);
}

/* Appears the moment the two password boxes agree. */
.auth-match {
  font-size: var(--type-caption2);
  color: var(--text-tertiary);
  line-height: 1.45;
  margin: 0;
}

/* The agreement as a footnote, where it is a reminder rather than the act. */
.legal-line.is-small {
  font-size: 0.65rem;
  line-height: 1.45;
}

/* -------------------------------------------------------------------------
   Two-step sign-in
   -------------------------------------------------------------------------
   Three small pieces: the QR code Supabase draws, the secret for somebody who
   cannot scan it, and the six-digit box. The last one is the only one with a
   real constraint behind it — digits in a row want to be monospaced and spaced
   out, or a transcription error looks like a rejection.
   ------------------------------------------------------------------------- */

.qr-frame {
  display: flex;
  justify-content: center;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  /* White regardless of theme: a QR code is read by a camera, and inverting it
     on a dark background is how you get one that will not scan. */
  background-color: #fff;
}

.qr-code { width: 180px; height: 180px; display: block; }

.secret-block {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--bg-input);
  border: 1px solid var(--separator);
  color: var(--text-primary);
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.95rem;
  /* A base32 secret is long and has no spaces of its own. */
  word-break: break-all;
  user-select: all;
}

.code-input {
  text-align: center;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  letter-spacing: 0.35em;
  /* The letter-spacing adds a trailing gap after the last digit, which throws
     the centring off by half a space without this. */
  text-indent: 0.35em;
}

.btn.is-danger { background-color: var(--color-red); color: #fff; }
.btn.is-danger:hover { filter: brightness(0.95); }

/* --- the human check ---------------------------------------------------------
   Cloudflare Turnstile draws itself inside a cross-origin iframe, so nothing
   here can reach into it: these rules place the box and nothing more. Its
   colours come from the `theme` option in cloud/turnstile.js, which is read
   off `data-theme` so the widget follows the app rather than the system.
   -------------------------------------------------------------------------- */

.captcha-slot {
  display: flex;
  justify-content: center;
  margin-top: var(--space-3);
  /* A managed widget is 65px tall and arrives a beat after the sheet does.
     Holding the space stops the buttons below jumping down under a thumb
     that is already on its way to them. */
  min-height: 65px;
}

/* Interaction-only: no height held, because the whole point is that in the
   ordinary case there is nothing to see and nothing should move. */
.captcha-slot.is-quiet {
  margin-top: 0;
  min-height: 0;
}

/* Nothing inside means nothing rendered — the script failed, was blocked, or
   the check is switched off — and then no room is held either. The 65px above
   is a promise about a widget that is arriving, not a hole to keep for one
   that is not: the first live morning showed exactly that hole, a blank strip
   in the sign-in sheet on a phone whose widget never came. Turnstile's first
   act on render is to put its iframe in the container, which un-empties it
   and opens the space at the same moment there is something to fill it. */
.captcha-slot:empty {
  margin-top: 0;
  min-height: 0;
}

/* Where a widget goes when the call that needs it has no sheet of its own —
   a reset link asked for from Settings, the code sent to a device that has
   not been seen in a month. Invisible until Cloudflare actually wants
   something, because until then the widget inside has no height at all.
   Above the sheets, since one may well be open behind it. */
.captcha-float {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  z-index: calc(var(--z-toast) - 1);
  /* The strip spans the window so the widget can centre in it; only the
     widget itself should ever swallow a tap. */
  pointer-events: none;
}

.captcha-float > * { pointer-events: auto; }

/* --- a chat picture that has not arrived yet --------------------------------
   The message list carries a flag rather than the bytes (v1.3.9.82), so a
   picture lands a moment after the bubble it belongs to. Holding its space
   is what stops the thread jumping under somebody who is reading it — the
   same reason the captcha slot holds 65px, and the same rule: room is held
   only while something is actually on its way.
   -------------------------------------------------------------------------- */

.chat-picture.is-loading {
  width: 180px;
  height: 120px;
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated-2);
  animation: chat-picture-wait 1.4s ease-in-out infinite;
}

@keyframes chat-picture-wait {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
  .chat-picture.is-loading { animation: none; }
}

/* --- the two-step sign-in sheets ------------------------------------------
   Both of them — turning it on, and answering the challenge at sign-in — are
   a centred title over a code box, one button and one way out. Everything in
   them is centred to match that title.

   The button needed saying out loud: `.btn` is `inline-flex`, so inside a
   plain block-level <form> it sat at its natural width against the left edge
   while the field above it spanned the sheet. The form is a column now, so
   the button stretches like every other primary button in the app. */

.twofa-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.twofa-sheet p { text-align: center; }

/* `.btn-link` is left-aligned by default, which is right where it sits in a
   row of links and wrong as the lone way out of a centred sheet. */
.twofa-sheet .btn-link { text-align: center; }

/* The version at the foot of Settings. Centred and in the pill's own quiet
   grey: it is a fact to look up, not a sentence to read. */
.group-footer.settings-version {
  text-align: center;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-normal);
}

/* =========================================================================
   The operations dashboard (v1.3.9.97)
   =========================================================================
   Everything below belongs to the eight admin screens and the ten-tab account
   sheet. It sits after the original `.adm-` block rather than inside it so a
   reader can see which half is which — the earlier half is the three-screen
   dashboard's furniture, still used, and this half is what the revamp added.

   Two rules run through all of it and are worth keeping:

     · A COLOUR IS NEVER THE MESSAGE. Every state that is drawn in a colour is
       also drawn in a word, because a red dot means nothing in a screenshot
       pasted into a message, and nothing at all to somebody who cannot pick it
       out from the green one.
     · THE PHONE IS THE HARD CASE. These are stacked layouts that become
       columns on a desktop, not desktop tables squeezed down.
   ========================================================================= */

/* --- status dots --------------------------------------------------------- */

.adm-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-caption1);
  font-weight: 500;
  color: var(--text-secondary);
  white-space: nowrap;
}

.adm-status-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-full);
  background-color: var(--text-tertiary);
  flex: none;
}

.adm-status.is-ok .adm-status-dot { background-color: var(--color-green); }
.adm-status.is-warn .adm-status-dot { background-color: var(--color-warning); }
.adm-status.is-bad .adm-status-dot { background-color: var(--color-red); }
.adm-status.is-ok .adm-status-text { color: var(--text-secondary); }
.adm-status.is-bad .adm-status-text { color: var(--color-red); }

.adm-badge.is-ok { color: var(--color-green); background-color: color-mix(in srgb, var(--color-green) 14%, transparent); }
.adm-badge.is-bad { color: var(--color-red); background-color: color-mix(in srgb, var(--color-red) 14%, transparent); }
.adm-badge.is-support { color: var(--accent); background-color: var(--accent-tint); }
.adm-badge.is-billing,
.adm-badge.is-technical,
.adm-badge.is-account,
.adm-badge.is-retention,
.adm-badge.is-internal { color: var(--text-secondary); background-color: var(--fill-tertiary); }

.adm-alert.is-bad { background-color: color-mix(in srgb, var(--color-red) 10%, transparent); }
.adm-alert.is-bad .adm-alert-icon { color: var(--color-red); }

/* --- Home: the sections list -------------------------------------------- */

.adm-sections { display: flex; flex-direction: column; gap: var(--space-1); }

.adm-section {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-2);
  border: none;
  border-radius: var(--radius-md);
  background-color: transparent;
  text-align: left;
  cursor: pointer;
}

.adm-section:hover { background-color: var(--bg-hover); }

.adm-section-icon {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  background-color: var(--fill-tertiary);
  color: var(--text-secondary);
  flex: none;
}

.adm-section-icon svg { width: 15px; height: 15px; }
.adm-section-text { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.adm-section-text strong { font-size: var(--type-subhead); font-weight: 600; }
.adm-section-text span { font-size: var(--type-caption1); color: var(--text-secondary); }
.adm-section-chev { width: 15px; height: 15px; color: var(--text-tertiary); flex: none; }

/* --- Home: storage bars -------------------------------------------------- */

.adm-bars { display: flex; flex-direction: column; gap: var(--space-2); }
.adm-bar-row { display: flex; flex-direction: column; gap: 5px; }
.adm-bar-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.adm-bar-value { font-size: var(--type-caption1); color: var(--text-secondary); font-variant-numeric: tabular-nums; }

.adm-bar-track {
  height: 7px;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  overflow: hidden;
}

.adm-bar-fill { height: 100%; border-radius: var(--radius-full); background-color: var(--accent); }
.adm-bar-fill.is-basic { background-color: var(--text-tertiary); }
.adm-bar-fill.is-premium { background-color: var(--accent); }
.adm-bar-fill.is-ultimate { background-color: var(--color-purple); }

/* --- the plan grid ------------------------------------------------------- */

/* 172px rather than 140: the widest plan label is "No membership", and a
   `.adm-badge` is `white-space: nowrap`, so at 140 the pill was drawn 31px
   wider than the card it sits in and spilled over the next one. Measured, not
   guessed — `admin-check` walks every box on all eight screens at both widths
   and compares `scrollWidth` against `clientWidth`. */
.adm-plan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(172px, 1fr));
  gap: var(--space-2);
}

/* And the safety net, so a longer label in a narrower place can never do this
   again: inside a plan card the pill wraps rather than overflowing. Nothing is
   truncated — a plan's name is the one thing on this card that may not be
   shortened to fit. */
.adm-plan-card .adm-badge,
.adm-model-row .adm-badge {
  max-width: 100%;
  white-space: normal;
  text-align: center;
}

.adm-plan-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-plan-count { font-size: var(--type-title2); font-weight: 650; font-variant-numeric: tabular-nums; }
.adm-plan-sub { font-size: var(--type-caption2); color: var(--text-tertiary); }
.adm-plan-now { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.adm-plan-now .adm-note { margin-top: 0; }

/* --- the entitlement matrix ---------------------------------------------
   A grid rather than a <table>, because it has to become readable on a phone
   and a table cannot reflow. The first column is the label and the rest are
   the plans; on a narrow screen the whole thing scrolls sideways rather than
   shrinking its type. */

.adm-matrix {
  display: flex;
  flex-direction: column;
  gap: 1px;
  overflow-x: auto;
  scrollbar-width: thin;
}

.adm-matrix-row {
  display: grid;
  grid-template-columns: minmax(120px, 1.4fr) repeat(3, minmax(84px, 1fr));
  gap: var(--space-1);
  align-items: center;
  padding: 7px 0;
  border-bottom: var(--hairline) solid var(--separator);
  min-width: 380px;
}

.adm-matrix.is-roles .adm-matrix-row {
  grid-template-columns: minmax(150px, 1.6fr) repeat(4, minmax(74px, 1fr));
  min-width: 460px;
}

.adm-matrix-row.is-head {
  border-bottom-color: var(--separator-opaque);
}

.adm-matrix-row.is-head .adm-matrix-cell {
  font-weight: 600;
  color: var(--text-primary);
  font-size: var(--type-caption1);
}

.adm-matrix-row:last-child { border-bottom: none; }

.adm-matrix-cell {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: var(--type-caption1);
  color: var(--text-secondary);
}

.adm-matrix-cell svg { width: 13px; height: 13px; }
.adm-matrix-cell.is-label { color: var(--text-primary); font-weight: 500; }
.adm-matrix-cell.is-yes { color: var(--color-green); }
.adm-matrix-cell.is-no { color: var(--text-tertiary); }

/* --- the users list, which becomes columns on a desktop ------------------ */

.adm-user-head { display: none; }

.adm-row-plan,
.adm-row-state,
.adm-row-seen { display: none; }

.adm-row-flag {
  display: inline-flex;
  color: var(--color-warning);
}

.adm-row-flag.is-problem { color: var(--color-red); }
.adm-row-flag svg { width: 14px; height: 14px; }

@media (min-width: 900px) {
  .adm-user-head {
    display: grid;
    grid-template-columns: minmax(0, 2.2fr) 96px 132px minmax(0, 1.3fr) 132px 20px;
    gap: var(--space-2);
    align-items: center;
    padding: 0 var(--space-3) var(--space-1);
    font-size: var(--type-caption2);
    font-weight: 600;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-tertiary);
  }

  .adm-user-row {
    display: grid;
    grid-template-columns: minmax(0, 2.2fr) 96px 132px minmax(0, 1.3fr) 132px 20px;
    gap: var(--space-2);
    align-items: center;
  }

  .adm-user-row .adm-row-plan,
  .adm-user-row .adm-row-state,
  .adm-user-row .adm-row-seen { display: flex; align-items: center; min-width: 0; }

  /* Said once per row, on the desktop layout, in its own column — so the
     stacked phone copy of the same fact is hidden rather than repeated. */
  .adm-user-row .adm-row-meta.is-phone-only { display: none; }
}

/* --- the account sheet --------------------------------------------------- */

.adm-account-sheet {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.adm-sheet-head { flex: none; }
.adm-sheet-tabs { flex: none; }

.adm-sheet-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-top: var(--space-2);
  min-height: 0;
}

.adm-head-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
}

.adm-head-who { display: flex; flex-direction: column; gap: 3px; align-items: center; text-align: center; }
.adm-head-meta { font-size: var(--type-caption1); color: var(--text-tertiary); }

.adm-head-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  justify-content: center;
}

.adm-action.is-warn { color: var(--color-warning); }

/* --- the tab strip -------------------------------------------------------
   Ten tabs will not fit on a phone and a wrapped second row pushes the body
   off the screen, so the strip scrolls sideways. A strip that genuinely
   overflows keeps its drag under the scrim's refusal rule (doc 04, Rule D),
   which is why this is a real scroller rather than a squeezed row. */

.adm-tabs {
  display: flex;
  gap: 2px;
  overflow-x: auto;
  scrollbar-width: none;
  border-bottom: var(--hairline) solid var(--separator);
  padding-bottom: 1px;
}

.adm-tabs::-webkit-scrollbar { display: none; }

.adm-tab {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 8px 11px;
  border: none;
  border-bottom: 2px solid transparent;
  background-color: transparent;
  color: var(--text-secondary);
  font-size: var(--type-footnote);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}

.adm-tab:hover { color: var(--text-primary); }
.adm-tab.is-on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.adm-tab-icon { width: 14px; height: 14px; }

.adm-tab-count {
  padding: 0 5px;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  font-size: var(--type-caption2);
  font-weight: 600;
}

.adm-tab.is-on .adm-tab-count { background-color: var(--accent-tint); color: var(--accent); }

/* --- health -------------------------------------------------------------- */

.adm-health-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
  border-left: 3px solid var(--color-green);
}

.adm-health-card.is-attention { border-left-color: var(--color-warning); background-color: var(--tint-warning); }
.adm-health-card.is-problem { border-left-color: var(--color-red); background-color: color-mix(in srgb, var(--color-red) 9%, transparent); }
.adm-health-card .adm-note { margin-top: 0; }

.adm-health-reasons {
  margin: 0;
  padding-left: var(--space-3);
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  line-height: 1.5;
}

.adm-health-grid { display: flex; flex-direction: column; gap: var(--space-2); }

.adm-health-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.adm-health-label { font-size: var(--type-subhead); font-weight: 600; min-width: 140px; }
.adm-health-row .adm-note { margin-top: 0; }

/* --- the "not connected" note -------------------------------------------
   NOT AN ERROR, and drawn so it cannot be read as one: no red, no warning
   glyph, and the detail says what would make it show something. */

.adm-notyet {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-4) var(--space-3);
  text-align: center;
}

.adm-notyet-icon {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  color: var(--text-tertiary);
}

.adm-notyet-icon svg { width: 16px; height: 16px; }
.adm-notyet-title { font-size: var(--type-subhead); font-weight: 600; }
.adm-notyet-detail { font-size: var(--type-caption1); color: var(--text-secondary); max-width: 46ch; line-height: 1.5; }

.adm-notyet-steps {
  margin: var(--space-1) 0 0;
  padding-left: var(--space-3);
  text-align: left;
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
  line-height: 1.55;
  max-width: 52ch;
}

.adm-notyet-steps li { margin-bottom: 3px; }

.adm-list-plain {
  margin: var(--space-1) 0 0;
  padding-left: var(--space-3);
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  line-height: 1.55;
}

.adm-list-plain li { margin-bottom: 5px; }

/* --- the timeline -------------------------------------------------------- */

.adm-timeline { display: flex; flex-direction: column; gap: 2px; }

.adm-event {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: 7px 0;
  border-bottom: var(--hairline) solid var(--separator);
}

.adm-event:last-child { border-bottom: none; }

.adm-event-icon {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  color: var(--text-secondary);
  flex: none;
  margin-top: 1px;
}

.adm-event-icon svg { width: 12px; height: 12px; }
.adm-event-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.adm-event-what { font-size: var(--type-footnote); }
.adm-event-when { font-size: var(--type-caption2); color: var(--text-tertiary); }

.adm-tab-filters { display: flex; flex-wrap: wrap; gap: var(--space-1); }

/* --- notes --------------------------------------------------------------- */

.adm-notes { display: flex; flex-direction: column; gap: var(--space-2); }

.adm-note-card {
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-note-card.is-pinned {
  background-color: var(--accent-tint);
  border-left: 3px solid var(--accent);
}

.adm-note-head { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
.adm-note-by { font-size: var(--type-caption2); color: var(--text-tertiary); flex: 1; min-width: 0; }
.adm-note-tools { display: flex; gap: 2px; }
.adm-note-tools .nav-btn { width: 26px; height: 26px; }
.adm-note-tools svg { width: 14px; height: 14px; }

.adm-note-body {
  margin-top: 6px;
  font-size: var(--type-footnote);
  line-height: 1.5;
  white-space: pre-wrap;
}

.adm-note-box { min-height: 68px; resize: vertical; }

/* --- sessions ------------------------------------------------------------ */

.adm-session {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-session-what { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.adm-session-device { font-size: var(--type-footnote); font-weight: 500; }
.adm-session-when { font-size: var(--type-caption2); color: var(--text-tertiary); }

/* --- entitlements on the account sheet ----------------------------------- */

.adm-ent-list { display: flex; flex-direction: column; }

.adm-ent-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 7px 0;
  border-bottom: var(--hairline) solid var(--separator);
  font-size: var(--type-footnote);
}

.adm-ent-row:last-child { border-bottom: none; }
.adm-ent-label { color: var(--text-secondary); }
.adm-ent-yes { display: inline-flex; align-items: center; gap: 4px; color: var(--color-green); font-weight: 500; }
.adm-ent-yes svg { width: 13px; height: 13px; }
.adm-ent-no { color: var(--text-tertiary); }

/* --- an override, said loudly -------------------------------------------- */

.adm-override {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--tint-warning);
  color: var(--color-warning);
  font-size: var(--type-caption1);
  line-height: 1.45;
  margin-bottom: var(--space-2);
}

.adm-override svg { width: 15px; height: 15px; flex: none; margin-top: 1px; }

/* --- the danger zone ----------------------------------------------------- */

.adm-danger {
  border: var(--hairline) solid color-mix(in srgb, var(--color-red) 40%, transparent);
  border-radius: var(--radius-md);
  padding: var(--space-2);
}

.adm-danger .fit-block-title { color: var(--color-red); }

/* --- the More menu ------------------------------------------------------- */

.adm-menu-wrap { position: relative; }

.adm-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: var(--z-sheet);
  min-width: 210px;
  padding: 4px;
  border-radius: var(--radius-md);
  background-color: var(--bg-elevated);
  border: var(--hairline) solid var(--separator);
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
}

.adm-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 8px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background-color: transparent;
  color: var(--text-primary);
  font-size: var(--type-footnote);
  text-align: left;
  cursor: pointer;
}

.adm-menu-item:hover { background-color: var(--bg-hover); }
.adm-menu-item svg { width: 15px; height: 15px; color: var(--text-secondary); flex: none; }
.adm-menu-item.is-danger { color: var(--color-red); }
.adm-menu-item.is-danger svg { color: var(--color-red); }
.adm-menu-item.is-disabled { color: var(--text-tertiary); cursor: default; }
.adm-menu-item.is-disabled:hover { background-color: transparent; }

/* --- scheduled jobs and table counts ------------------------------------- */

.adm-job {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-job-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.adm-job-name { font-size: var(--type-footnote); font-weight: 600; }
.adm-job .adm-note { margin-top: 0; }

.adm-table-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-1);
}

.adm-table-cell {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1);
  padding: 6px 9px;
  border-radius: var(--radius-sm);
  background-color: var(--fill-quaternary);
}

.adm-table-name { font-size: var(--type-caption1); color: var(--text-secondary); }
.adm-table-rows { font-size: var(--type-footnote); font-weight: 600; font-variant-numeric: tabular-nums; }

.adm-sub-title {
  font-size: var(--type-caption1);
  font-weight: 600;
  color: var(--text-secondary);
  margin-top: var(--space-2);
}

/* --- the audit ----------------------------------------------------------- */

.adm-audit { display: flex; flex-direction: column; }

.adm-audit-day {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 6px 0;
  background-color: var(--bg-elevated);
  font-size: var(--type-caption2);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.adm-audit-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: 6px 0;
  border-bottom: var(--hairline) solid var(--separator);
}

.adm-audit-row:last-child { border-bottom: none; }

.adm-audit-time {
  font-size: var(--type-caption2);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
  min-width: 46px;
  flex: none;
  margin-top: 2px;
}

.adm-audit-icon {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  background-color: var(--fill-tertiary);
  color: var(--text-secondary);
  flex: none;
}

.adm-audit-icon svg { width: 11px; height: 11px; }
.adm-audit-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.adm-audit-what { font-size: var(--type-footnote); }
.adm-audit-who { font-size: var(--type-caption2); color: var(--text-tertiary); }

.adm-audit-row.is-bad .adm-audit-icon { color: var(--color-red); background-color: color-mix(in srgb, var(--color-red) 12%, transparent); }
.adm-audit-row.is-warn .adm-audit-icon { color: var(--color-warning); background-color: var(--tint-warning); }
.adm-audit-row.is-quiet .adm-audit-what { color: var(--text-secondary); }

/* --- administrators ------------------------------------------------------ */

.adm-admin-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-admin-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.adm-admin-role { display: flex; flex-wrap: wrap; gap: var(--space-1); align-items: center; }
.adm-admin-role .adm-note { margin-top: 0; }

.adm-role-blurbs { display: flex; flex-direction: column; gap: 6px; margin-top: var(--space-2); }
.adm-role-blurb { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.adm-role-blurb .adm-note { margin-top: 0; flex: 1; min-width: 180px; }

@media (min-width: 900px) {
  .adm-admin-row { flex-direction: row; align-items: center; justify-content: space-between; }
  .adm-admin-who { flex: 1; }
}

/* --- a tappable mini row ------------------------------------------------- */

.adm-mini.is-tappable {
  width: 100%;
  border: none;
  background-color: transparent;
  cursor: pointer;
  text-align: left;
  border-radius: var(--radius-sm);
}

.adm-mini.is-tappable:hover { background-color: var(--bg-hover); }
.adm-mini-state { font-size: var(--type-caption2); color: var(--text-tertiary); }

/* =========================================================================
   The dashboard uses the whole screen
   =========================================================================
   `--content-max` is 1200px, which is right for a column of text and wrong
   for a wall of figures: on a wide monitor it left the dashboard as a narrow
   strip down the middle with a third of the screen empty on either side,
   while the Users table squeezed five columns into the same 1200px it would
   have had on a laptop.

   The same opt-out the journal takes (`me-journal`, above), for the same
   reason — a workspace is not a document. `data-screen` carries the screen's
   id, so one prefix selector covers all eight and any ninth.

   BOTH `.content` DIVS. `main` holds two: the large page heading and the
   screen body. Widening only the second leaves the heading centred at 1200px
   with the content starting to the left of it, which reads as a mistake
   rather than as a wide layout.

   Everything inside is already `repeat(auto-fit, minmax(…, 1fr))`, so the
   tiles, the facts and the split panels take the extra room without any of
   them being told about it separately. Nothing here changes the phone: the
   cap is only reached above 1200px, and this is scoped to the desktop view. */

.shell[data-view="desktop"][data-screen^="admin-"] .content {
  max-width: none;
}

/* A little more air at the edges once the width is unbounded — at 1200px the
   page's own padding was most of the margin, and without a cap the first
   column would otherwise start hard against the page finder. */
@media (min-width: 1400px) {
  .shell[data-view="desktop"][data-screen^="admin-"] .content {
    padding-inline: var(--space-4);
  }

}

/* A tile has a natural width, and neither stretching past it nor packing
   below it is using the room. Both were tried here and both were wrong: four
   tiles across 2500px put the figure in one corner of six hundred empty
   pixels, and a plain `auto-fit` floor then packed eight across a 1440px
   laptop at 172px each — tighter than the layout this replaced, which is the
   opposite of what was asked for.

   So the count is stated rather than inferred, and both steps divide the
   eight tiles evenly — four across up to a wide desktop, eight across beyond
   it — so a row is never left with two tiles trailing under six. Measured:
   324px each at 1440, 218px at 1920, 298px at 2560. The 1920 case is the
   tightest and was checked rather than assumed; the two longest captions wrap
   to a second line there and still read. */
@media (min-width: 1200px) {
  .shell[data-view="desktop"][data-screen^="admin-"] .adm-tiles {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (min-width: 1800px) {
  .shell[data-view="desktop"][data-screen^="admin-"] .adm-tiles {
    grid-template-columns: repeat(8, minmax(0, 1fr));
  }
}

/* The donut beside its legend rather than above it. Stacked, the chart sat in
   the middle of a wide panel with its key stretched across the bottom and the
   percentages drifting away from the plans they belonged to. Side by side the
   key is next to what it explains, and the panel is no taller than it needs
   to be.

   A wrapper rather than `:has()` on the card, because the key and the chart
   being one thing is a fact about the markup and belongs in it. */
.adm-donut-row { display: grid; gap: var(--space-3); }
.adm-donut-row .adm-donut { min-width: 0; }

@media (min-width: 700px) {
  .adm-donut-row {
    grid-template-columns: minmax(160px, 240px) 1fr;
    align-items: center;
  }

  /* Held to the width the key actually needs. Given the whole column, the
     share drifted to the far right of the panel and stopped reading as
     belonging to the plan on its left. */
  .adm-donut-row .adm-legend { margin-top: 0; max-width: 340px; }
}

/* The dot and the detail beside it are two things, and read as one word when
   only a margin separates them — "Everything running0 in the deletion queue". */
.adm-sys-line { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

.adm-sys-detail {
  font-size: var(--type-caption1);
  color: var(--text-tertiary);
}

.adm-sys-detail::before {
  content: "·";
  margin-right: var(--space-2);
  color: var(--text-tertiary);
}

/* The not-connected note is an explanation, not an empty state to be framed
   by whitespace. Given a full-width card it grew a band of white above and
   below the one paragraph that mattered. */
.adm-notyet { padding: var(--space-3) var(--space-2); }

/* The three-panel row on Home and Analytics, once there is room for three
   rather than two. Below this it stays the two-column split it was. */
@media (min-width: 1600px) {
  .shell[data-view="desktop"][data-screen^="admin-"] .adm-split {
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  }
}

/* --- the account sheet ---------------------------------------------------
   560px is the width of every other sheet in the app, and every other sheet
   holds a form. This one holds ten tabs, a facts grid and a storage sum, and
   at 560px the tab strip scrolls sideways on a desktop that has room for all
   ten at once — which is the cramped feeling, on the screen where it is least
   necessary.

   Capped rather than unbounded: a sheet is a thing over a page and has to
   read as one, so it stops at 1080px and never takes more than 92% of the
   viewport. The phone is untouched — under 480px the sheet is already
   full-screen. */
@media (min-width: 900px) {
  .sheet.adm-account-sheet {
    max-width: min(1080px, 92vw);
  }

  /* With the room, the facts read as columns rather than as pairs. */
  .adm-account-sheet .adm-facts {
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  }

  /* The header stops being a stacked column and becomes what it is: who on
     the left, what to do about them on the right. */
  .adm-account-sheet .adm-head-card {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
  }

  .adm-account-sheet .adm-head-who { align-items: flex-start; text-align: left; }
  .adm-account-sheet .adm-head-actions { justify-content: flex-end; flex: none; }
}

/* --- the account sheet's quick actions -----------------------------------
   `.quick-actions` is a no-wrap flex whose buttons carry `white-space:
   nowrap`, which is right for the two short labels it was built for and
   wrong for four long ones: on a 390px phone each was squeezed to about
   ninety pixels while its text refused to wrap, so "Send a password reset",
   "Add a note", "End every session" and "Email them" overlapped into one
   run-on line. A grid that wraps instead, left as its own class so the rest
   of the app's two-button rows keep the behaviour they were tuned for. */

.adm-quick {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.adm-quick .btn {
  width: 100%;
  min-width: 0;
  /* Wrapping is the point here — a label that needs two lines gets two. */
  white-space: normal;
  justify-content: center;
  text-align: center;
}

/* --- the sort fold, grouped ----------------------------------------------
   Thirteen orders is a wall as one flat chip row. Grouped under the question
   each answers — Joined, Seen, Storage, Name, Plan, Attention — the fold
   reads as six questions rather than thirteen answers, and the pairs sit
   beside each other so it is obvious every order can be reversed. */

.adm-sort-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
}

.adm-sort-group { display: flex; flex-direction: column; gap: 5px; min-width: 0; }

.adm-sort-group-label {
  font-size: var(--type-caption2);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

/* --- billing -------------------------------------------------------------- */

.adm-plan-line,
.adm-invoice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-plan-line-who,
.adm-invoice-who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 140px;
}

.adm-plan-line-who .adm-note,
.adm-invoice-who .adm-note { margin-top: 0; }

.adm-plan-line-side {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.adm-plan-subs,
.adm-invoice-amount {
  font-size: var(--type-footnote);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.adm-invoice-id {
  font-size: var(--type-footnote);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* --- how a plan is got ---------------------------------------------------- */

.adm-model { display: flex; flex-direction: column; gap: var(--space-2); }

.adm-model-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-model-row .adm-note { margin-top: 0; }
.adm-model-how { font-size: var(--type-subhead); font-weight: 600; }

@media (min-width: 700px) {
  .adm-model-row {
    /* 132px rather than 96: the first column holds a plan badge, the widest
       label is "No membership", and `.adm-badge` is `white-space: nowrap` —
       so at 96 the pill was drawn 31px wider than its column and lay across
       the plan name beside it. Caught by the measured sweep in admin-check,
       not by looking. */
    display: grid;
    grid-template-columns: 132px minmax(160px, 240px) 1fr;
    align-items: center;
    gap: var(--space-2);
  }
}

/* =========================================================================
   Slow queries, on the System screen
   =========================================================================
   A SQL statement is the one thing on the admin screens that must not be
   truncated to one line: the interesting part of a slow query is usually its
   join or its where clause, and both live past the fold. So this is the one
   list that wraps rather than ellipsises, and it breaks on any character —
   normalised SQL has long unbroken runs (`$1,$2,$3,$4`) that would otherwise
   push the card wider than the screen on a phone.
   ========================================================================= */

/* One line per statement, and nothing here scrolls inside itself — see the
   note above `slowTable()` in system.js. Twenty boxes with their own
   scrollbars is what this replaced, and a wheel that begins over one of them
   used to eat the page scroll entirely. */

.adm-slow-wrap { margin-top: var(--space-3); }

.adm-slow-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}

.adm-slow-table th {
  padding: 0 var(--space-2) var(--space-2);
  border-bottom: var(--hairline) solid var(--separator);
  font-size: var(--type-caption2);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  text-align: left;
  white-space: nowrap;
}

/* `width: 100%` on the first column is what makes it take everything the
   sized columns leave. Relying on `table-layout: fixed` to distribute the
   remainder is not enough here: the two widest columns are dropped on a phone
   with `display: none`, and Chrome keeps their share of the table reserved —
   the row measured 361px while its cells added to 207, with 154px of empty
   table beyond the last number. Claiming the remainder explicitly leaves
   nothing to reserve. */
.adm-slow-table th:first-child { width: 100%; }

.adm-slow-table th:first-child,
.adm-slow-stmt { padding-left: 0; }

.adm-slow-num { text-align: right; width: 5.5rem; }
.adm-slow-table th.adm-slow-num:last-child { padding-right: 0; }

.adm-slow-tr {
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.adm-slow-tr > td {
  padding: var(--space-2);
  border-bottom: var(--hairline) solid var(--separator);
  font-size: var(--type-caption1);
  color: var(--text-secondary);
  vertical-align: middle;
}

.adm-slow-tr > td.adm-slow-num {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.adm-slow-tr > td.adm-slow-num:last-child { padding-right: 0; }

.adm-slow-tr:hover > td { background-color: var(--fill-quaternary); }
.adm-slow-tr:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.adm-slow-tr.is-open > td { background-color: var(--fill-quaternary); border-bottom-color: transparent; }

/* The cell stays a cell — see the note in system.js — and states NO width of
   its own. `table-layout: fixed` gives every column without a width an equal
   share of what the sized ones leave, so with one unsized column that is all
   the remainder. (`max-width: 0`, the usual ellipsis trick for an auto-layout
   table, collapses the column here instead: it was drawn 70px wide inside a
   361px table, leaving half the table empty.) `overflow: hidden` is what lets
   the line inside it ellipsise rather than widen the row. */
.adm-slow-stmt {
  overflow: hidden;
  /* Room between the ellipsis and the first number, so the two do not read as
     one run of text. */
  padding-right: var(--space-3);
}

.adm-slow-stmt-in {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.adm-slow-caret {
  flex: none;
  display: grid;
  place-items: center;
  color: var(--text-tertiary);
  transition: transform var(--duration-fast) var(--ease-out);
}

.adm-slow-caret svg { width: 13px; height: 13px; }
.adm-slow-tr.is-open .adm-slow-caret { transform: rotate(90deg); color: var(--accent); }

/* The resting state of a statement: one line, ellipsised. The whole of it is
   one press away, which is what keeps this panel a fixed height per row
   however long the SQL is. */
.adm-slow-line {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--type-caption1);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1;
}

.adm-slow-detail > td {
  padding: 0 0 var(--space-3);
  border-bottom: var(--hairline) solid var(--separator);
}

.adm-slow-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background-color: var(--fill-quaternary);
}

.adm-slow-sqlhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.adm-slow-sqllabel {
  font-size: var(--type-caption2);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

/* NO `max-height`, NO `overflow`. The full statement, laid out in full, so the
   page is the only thing that scrolls. `overflow-wrap: anywhere` because
   normalised SQL has long unbroken runs (`$1,$2,$3,$4`) that would otherwise
   push the card wider than a phone. */
.adm-slow-sql {
  margin: 0;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--type-caption1);
  line-height: 1.45;
  color: var(--text-secondary);
  background-color: var(--bg-secondary);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.adm-slow-copy {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
  padding: 3px var(--space-2);
  border: var(--hairline) solid var(--separator);
  border-radius: var(--radius-full);
  background-color: var(--bg-elevated);
  color: var(--text-secondary);
  font-size: var(--type-caption2);
  font-weight: 600;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.adm-slow-copy svg { width: 12px; height: 12px; }
.adm-slow-copy:hover { color: var(--text-primary); border-color: var(--text-tertiary); }
.adm-slow-copy.is-done { color: var(--color-positive); border-color: var(--color-positive); }
.adm-slow-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A phone has no room for five columns. Worst and Total leave the row — they
   are both in the detail, so nothing is lost by pressing. */
@media (max-width: 700px) {
  .adm-slow-wide { display: none; }
  .adm-slow-num { width: 4.5rem; }
}
