/* FollowStack AI — design tokens (native redesign: qa/work-integrator/ui/SPEC.md §1)
   No web font. The app renders in the platform's own face everywhere (SF on
   Apple, Roboto on Android, Segoe UI Variable on Windows): a remote font arrived
   after first paint, was absent offline and had no optical sizes. */

:root {
  color-scheme: light;

  /* ---- font (§1.1) ---- */
  --font-ui: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-round: ui-rounded, var(--font-ui);          /* hero figure + count tiles only */
  --font-code: ui-monospace, "SF Mono", Menlo, Consolas, monospace; /* API keys, code, raw payloads only */
  --font: var(--font-ui);                            /* legacy alias: every existing call site keeps working */
  --mono: var(--font-ui);                            /* legacy alias: numbers are SF + tabular-nums (.mono below) */

  /* ---- type scale (§1.2 / §1.3), desktop column; the phone column is below ---- */
  --fs-type-scale: 1;
  --t-display: calc(36px * var(--fs-type-scale)); --lh-display: calc(42px * var(--fs-type-scale));
  --t-large:   calc(28px * var(--fs-type-scale)); --lh-large:   calc(34px * var(--fs-type-scale));
  --t-title1:  calc(25px * var(--fs-type-scale)); --lh-title1:  calc(30px * var(--fs-type-scale));
  --t-title2:  calc(21px * var(--fs-type-scale)); --lh-title2:  calc(26px * var(--fs-type-scale));
  --t-title3:  calc(18px * var(--fs-type-scale)); --lh-title3:  calc(24px * var(--fs-type-scale));
  --t-headline:calc(15px * var(--fs-type-scale)); --lh-headline:calc(20px * var(--fs-type-scale));
  --t-body:    calc(14px * var(--fs-type-scale)); --lh-body:    calc(20px * var(--fs-type-scale));
  --t-callout: calc(15px * var(--fs-type-scale)); --lh-callout: calc(20px * var(--fs-type-scale));
  --t-bubble:  calc(15px * var(--fs-type-scale));
  --t-subhead: calc(13px * var(--fs-type-scale)); --lh-subhead: calc(18px * var(--fs-type-scale));
  --t-footnote:calc(12px * var(--fs-type-scale)); --lh-footnote:calc(16px * var(--fs-type-scale));
  --t-caption: calc(11px * var(--fs-type-scale)); --lh-caption: calc(14px * var(--fs-type-scale));
  --t-tab:     calc(11px * var(--fs-type-scale));
  --t-count:   calc(24px * var(--fs-type-scale)); --lh-count: calc(30px * var(--fs-type-scale));
  --t-input: 16px;
  --lh-body-r: 1.4286;   /* 20 / 14 */

  /* ---- labels, surfaces, fills (§1.4). Palette: Harbor (deep navy + sea blue),
     owner's pick 2026-09-29 — ui/feedback/color-research.md §4 / §7.A. Every
     neutral is a cool blue-grey; nothing in the chrome leans green any more. ---- */
  --label: #0C1524;
  --label-2: #515D6E;       /* 6.69 on white, 6.07 on --bg, 5.66 on the accent tint; 13px minimum */
  --label-3: #667183;       /* placeholders + disabled text, on --cell only (4.94) */
  --label-4: #A1A9B5;       /* chevrons and decorative glyphs, never text */
  --bg: #F2F4F7;
  --bg-plain: #FFFFFF;
  --cell: #FFFFFF;
  --cell-2: #F5F7FA;
  --cell-press: #E8ECF1;
  --sep: rgba(12, 21, 36, 0.14);
  --sep-opaque: #D4D9E1;
  --fill: rgba(116, 124, 140, 0.20);
  --fill-2: rgba(116, 124, 140, 0.14);
  --fill-3: rgba(116, 124, 140, 0.10);

  /* legacy names, re-pointed so 1,500+ existing references change meaning without edits */
  --text: var(--label);
  --text-2: var(--label-2);
  --text-3: #515D6E; /* A11Y-6: literal kept (hardening-r4 asserts it); equals --label-2 in light */
  --surface: var(--cell);
  --surface-2: var(--cell-2);
  --border: var(--sep);
  --border-strong: var(--sep-opaque);

  /* ---- accent, semantic, modules, AI (§1.5) ---- */
  --accent: #0A6FA0;
  --accent-text: #075E8A;   /* accent-coloured TEXT: 7.05 on white, 5.97 on its tint */
  --accent-pressed: #075E8A;
  --accent-tint: rgba(10, 111, 160, 0.12);
  --on-accent: #FFFFFF;     /* 5.53 on --accent */
  --accent-glow: #5FBCF4;
  --accent-hover: var(--accent-pressed);
  --accent-soft: #E2EEF4;   /* opaque tint (== --accent-tint over white); A11Y-6 contrast is computed on it */
  --brand-ink: #0E1A30;

  --ink: #0E1A30;          /* sidebar background */
  --ink-2: #182742;        /* sidebar hover */
  --ink-line: #24344F;
  --ink-text: #AFBCCF;
  --ink-text-dim: #8E9AAE; /* A11Y-6: >=4.5:1 on #182742 (5.25) and #0E1A30 (6.11) */
  --ink-strong: #FFFFFF;

  --red: #C42B21;          --red-tint: rgba(196, 43, 33, 0.12);
  --orange: #A4520A;       --orange-tint: rgba(196, 98, 10, 0.13);
  --orange-fill: #C4620A;
  --blue: #0A6FA0;         --blue-tint: rgba(10, 111, 160, 0.12);   /* Estimate / Scheduled = the accent (Harbor merges them) */
  --indigo: #4A48C9;       --indigo-tint: rgba(74, 72, 201, 0.12);
  --purple: #7450C8;       --purple-tint: rgba(116, 80, 200, 0.12);   /* AI only */
  /* status "good / done / on site / paid / install" (color-research §3.1). The
     brand accent no longer doubles as success, so a primary button never reads
     as a status. Status only — never chrome. 5.38 white, 4.56 on its tint. */
  --green: #1F7A3A;        --green-tint: rgba(31, 122, 58, 0.12);
  --green-soft: #E4EFE7;   /* opaque --green-tint over white */
  --teal: #0E8A7E; --brown: #8C6440; --pink: #C23A6B; --gray: #6B7280; /* module squircles only */

  /* legacy semantic names. The -soft values stay opaque (the tint composited on
     white) so a sticky or overlapping element painted with one never turns
     see-through; dark mode has its own opaque set. */
  --warn: #9A4507; /* A11Y-6: the warn chip (on --warn-soft) >= 4.5:1; literal kept (hardening-r4 asserts it) */
  --warn-soft: #FCEFD9;
  --danger: var(--red);
  --danger-soft: #FBE9E7;
  --info: var(--blue);
  --info-soft: #E2EEF4;
  --good: var(--green);
  --good-soft: var(--green-soft);
  --violet: var(--purple);
  --violet-soft: #EEE9F7;
  --ai: var(--purple);
  --ai-soft: #F4F0FB;

  /* ---- spacing, layout, hit targets (§1.7) ---- */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-7: 28px; --sp-8: 32px;
  --margin: clamp(16px, 4.6vw, 20px);
  --safe-top: max(env(safe-area-inset-top, 0px), var(--fs-sat, 0px));
  --safe-bottom: max(env(safe-area-inset-bottom, 0px), var(--fs-sab, 0px));
  --hair: 0.5px;

  /* ---- radii (§1.8), desktop column; phone below ---- */
  --r-xs: 6px;
  --r-sm: 8px;
  --r-md: 10px;
  --r-lg: 14px;
  --r-xl: 16px;
  --r-sheet: 16px;
  --r-pill: 999px;

  /* ---- elevation (§1.8) ---- */
  --shadow-sm: 0 1px 2px rgba(14, 26, 48, 0.06);
  --shadow-md: 0 2px 8px rgba(14, 26, 48, 0.08), 0 1px 2px rgba(14, 26, 48, 0.05);
  --shadow-lg: 0 12px 32px rgba(14, 26, 48, 0.14), 0 2px 8px rgba(14, 26, 48, 0.08);
  --e0: none;
  --e1: 0 1px 2px rgba(14, 26, 48, 0.05);
  --e2: 0 1px 2px rgba(14, 26, 48, 0.06), 0 12px 28px -10px rgba(14, 26, 48, 0.22);
  --float: 0 12px 40px rgba(0, 0, 0, 0.18);

  /* ---- materials (§1.8). Write -webkit-backdrop-filter beside backdrop-filter. ---- */
  --glass-bg: rgba(255, 255, 255, 0.72);
  --glass-blur: blur(24px) saturate(190%);
  --glass-shadow: inset 0 0 0 0.5px rgba(12, 21, 36, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.85), 0 10px 30px rgba(14, 26, 48, 0.12), 0 1px 3px rgba(14, 26, 48, 0.08);
  --bar-bg: rgba(242, 244, 247, 0.78);
  --bar-blur: blur(24px) saturate(180%);
  --media-glass-bg: rgba(8, 14, 26, 0.52);
  --media-glass-blur: blur(16px);
  --scrim: rgba(6, 12, 24, 0.34);
  /* hero (money card), dark in both themes. Glow alpha .22 (was .30) so the
     delta pill and the white .72 text pass AA at the glow centre, the worst
     composite point (color-research §3.5): delta 4.67, white .72 5.27. */
  --hero-surface: radial-gradient(120% 80% at 100% 0%, rgba(95, 188, 244, 0.22), transparent 55%), radial-gradient(90% 70% at 0% 110%, rgba(10, 111, 160, 0.50), transparent 60%), linear-gradient(165deg, #17305A, #0E1A30 55%, #0A1324);
  --hero-bg: var(--hero-surface);
  --hero-delta: #B8DEFF;
  --hero-delta-tint: rgba(95, 188, 244, 0.16);
  --hero-bar: #5FBCF4;                        /* today's bar ends in it */
  --hero-bar-rest: rgba(184, 222, 255, 0.42); /* past days with data */

  /* ---- motion (§7) ---- */
  --ease-ios: cubic-bezier(0.2, 0.8, 0.2, 1);
  --spring-snappy: cubic-bezier(0.2, 0.8, 0.2, 1);
  --spring-smooth: cubic-bezier(0.25, 0.8, 0.25, 1);
  --dur-press: 100ms; --dur-release: 250ms; --dur-control: 250ms; --dur-nav: 350ms; --dur-sheet: 420ms;

  /* Outdoor mode (§1.6) reads these per theme */
  --label-2-outdoor: #3C4656;   /* 9.53 on white (outdoor secondary >= 9.5:1) */
  --label-3-outdoor: #515D6E;
  --fill-3-outdoor: rgba(116, 124, 140, 0.20);
}
/* the spring curves need linear(), which older WebKit (iOS < 17.2) does not parse */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --spring-snappy: linear(0, 0.2 6%, 0.6 14%, 0.9 24%, 1.02 34%, 1.01 44%, 1);
    --spring-smooth: linear(0, 0.25 8%, 0.63 20%, 0.87 34%, 0.97 50%, 1);
  }
}

/* ---- phone column (§1.2): keyed off the viewport query, not data-fs-layout,
   so the right sizes are there before any script runs. 760 = FS_DRAWER_MAX. ---- */
@media (max-width: 760px) {
  :root {
    --t-display: calc(48px * var(--fs-type-scale)); --lh-display: calc(54px * var(--fs-type-scale));
    --t-large:   calc(34px * var(--fs-type-scale)); --lh-large:   calc(41px * var(--fs-type-scale));
    --t-title1:  calc(28px * var(--fs-type-scale)); --lh-title1:  calc(34px * var(--fs-type-scale));
    --t-title2:  calc(22px * var(--fs-type-scale)); --lh-title2:  calc(28px * var(--fs-type-scale));
    --t-title3:  calc(20px * var(--fs-type-scale)); --lh-title3:  calc(25px * var(--fs-type-scale));
    --t-headline:calc(17px * var(--fs-type-scale)); --lh-headline:calc(22px * var(--fs-type-scale));
    --t-body:    calc(17px * var(--fs-type-scale)); --lh-body:    calc(22px * var(--fs-type-scale));
    --t-callout: calc(17px * var(--fs-type-scale)); --lh-callout: calc(22px * var(--fs-type-scale));
    --t-bubble:  calc(16px * var(--fs-type-scale));
    --t-subhead: calc(15px * var(--fs-type-scale)); --lh-subhead: calc(20px * var(--fs-type-scale));
    --t-footnote:calc(13px * var(--fs-type-scale)); --lh-footnote:calc(18px * var(--fs-type-scale));
    --t-caption: calc(12px * var(--fs-type-scale)); --lh-caption: calc(16px * var(--fs-type-scale));
    --t-tab:     calc(12px * var(--fs-type-scale));
    --t-count:   calc(30px * var(--fs-type-scale)); --lh-count: calc(34px * var(--fs-type-scale));
    --t-input:   calc(17px * var(--fs-type-scale));
    --lh-body-r: 1.2941;   /* 22 / 17 */
    /* desktop leaves these undefined, so letterSpacing: var(--fs-track, 0.08em)
       keeps its old value there (the codemod's desktop-preserving fallback) */
    --fs-track: 0px; --fs-case: none;

    --r-xs: 8px; --r-sm: 12px; --r-md: 16px; --r-lg: 22px; --r-xl: 28px; --r-sheet: 38px;
    --hair: 0.5px;
  }
}

/* ---- accent choices from the Tweaks panel (app.js stamps html[data-fs-accent]).
   Inline custom properties would beat the dark palette, so they live here. ---- */
:root[data-fs-accent="blue"] {
  --accent: #2A5FB0; --accent-text: #24539B; --accent-pressed: #234F94;
  --accent-tint: rgba(42, 95, 176, 0.12); --accent-soft: #E6EDF8;
}
:root[data-fs-accent="orange"] {
  --accent: #A04E1F; --accent-text: #94481D; --accent-pressed: #8A431A;
  --accent-tint: rgba(160, 78, 31, 0.12); --accent-soft: #F7EBE2;
}

/* ---- dark (§1.4–1.6). html[data-theme] = light | dark | absent (system).
   The two blocks are the same declarations — KEEP THEM IN SYNC. ---- */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --label: #EEF2F8; --label-2: #A7B0BE; --label-3: #7E8898; --label-4: #475061;
    --bg: #05070B; --bg-plain: #05070B; --cell: #121722; --cell-2: #1A212E; --cell-press: #222A38;
    --sep: rgba(230, 238, 250, 0.13); --sep-opaque: #2C3342;
    --fill: rgba(128, 138, 154, 0.34); --fill-2: rgba(128, 138, 154, 0.26); --fill-3: rgba(128, 138, 154, 0.20);
    --text-3: var(--label-2);
    --accent: #55B6EC; --accent-text: #55B6EC; --accent-pressed: #34A3DE;
    --accent-tint: rgba(85, 182, 236, 0.16); --on-accent: #031222; --accent-soft: #1D3042;
    --brand-ink: #0A1224;
    --ink: #0A1224; --ink-2: #131D33; --ink-line: #1E2A42;
    --red: #FF6A5F;    --red-tint: rgba(255, 106, 95, 0.18);
    --orange: #FFA940; --orange-tint: rgba(255, 159, 46, 0.18); --orange-fill: #C4620A;
    --blue: #55B6EC;   --blue-tint: rgba(85, 182, 236, 0.16);
    --indigo: #8E8CFF; --indigo-tint: rgba(142, 140, 255, 0.18);
    --purple: #B39AFF; --purple-tint: rgba(179, 154, 255, 0.18);
    --green: #5CD27E;  --green-tint: rgba(92, 210, 126, 0.16); --green-soft: #1E3531;
    --teal: #26A89B; --brown: #A87C56; --pink: #E0588A; --gray: #878E9B;
    --warn: var(--orange); --warn-soft: #3D311A; --danger-soft: #3D2823; --info-soft: #1D3042;
    --violet-soft: #303040; --ai-soft: #303040;
    --shadow-sm: none; --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4); --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
    --e1: none; --e2: inset 0 0 0 0.5px rgba(255, 255, 255, 0.08); --float: 0 12px 40px rgba(0, 0, 0, 0.5);
    --glass-bg: rgba(34, 40, 52, 0.58);
    --glass-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.12), 0 10px 30px rgba(0, 0, 0, 0.45);
    --bar-bg: rgba(5, 7, 11, 0.72);
    --scrim: rgba(0, 0, 0, 0.5);
    --label-2-outdoor: #C8D0DC; --label-3-outdoor: #A7B0BE; --fill-3-outdoor: rgba(128, 138, 154, 0.40);
  }
  :root[data-fs-accent="blue"]:not([data-theme="light"]) {
    --accent: #6B9BFF; --accent-text: #6B9BFF; --accent-pressed: #5585F0; --accent-tint: rgba(107, 155, 255, 0.16); --accent-soft: #233040;
  }
  :root[data-fs-accent="orange"]:not([data-theme="light"]) {
    --accent: #FFA066; --accent-text: #FFA066; --accent-pressed: #F08A4C; --accent-tint: rgba(255, 160, 102, 0.16); --accent-soft: #3D3124;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --label: #EEF2F8; --label-2: #A7B0BE; --label-3: #7E8898; --label-4: #475061;
  --bg: #05070B; --bg-plain: #05070B; --cell: #121722; --cell-2: #1A212E; --cell-press: #222A38;
  --sep: rgba(230, 238, 250, 0.13); --sep-opaque: #2C3342;
  --fill: rgba(128, 138, 154, 0.34); --fill-2: rgba(128, 138, 154, 0.26); --fill-3: rgba(128, 138, 154, 0.20);
  --text-3: var(--label-2);
  --accent: #55B6EC; --accent-text: #55B6EC; --accent-pressed: #34A3DE;
  --accent-tint: rgba(85, 182, 236, 0.16); --on-accent: #031222; --accent-soft: #1D3042;
  --brand-ink: #0A1224;
  --ink: #0A1224; --ink-2: #131D33; --ink-line: #1E2A42;
  --red: #FF6A5F;    --red-tint: rgba(255, 106, 95, 0.18);
  --orange: #FFA940; --orange-tint: rgba(255, 159, 46, 0.18); --orange-fill: #C4620A;
  --blue: #55B6EC;   --blue-tint: rgba(85, 182, 236, 0.16);
  --indigo: #8E8CFF; --indigo-tint: rgba(142, 140, 255, 0.18);
  --purple: #B39AFF; --purple-tint: rgba(179, 154, 255, 0.18);
  --green: #5CD27E;  --green-tint: rgba(92, 210, 126, 0.16); --green-soft: #1E3531;
  --teal: #26A89B; --brown: #A87C56; --pink: #E0588A; --gray: #878E9B;
  --warn: var(--orange); --warn-soft: #3D311A; --danger-soft: #3D2823; --info-soft: #1D3042;
  --violet-soft: #303040; --ai-soft: #303040;
  --shadow-sm: none; --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4); --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
  --e1: none; --e2: inset 0 0 0 0.5px rgba(255, 255, 255, 0.08); --float: 0 12px 40px rgba(0, 0, 0, 0.5);
  --glass-bg: rgba(34, 40, 52, 0.58);
  --glass-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.12), 0 10px 30px rgba(0, 0, 0, 0.45);
  --bar-bg: rgba(5, 7, 11, 0.72);
  --scrim: rgba(0, 0, 0, 0.5);
  --label-2-outdoor: #C8D0DC; --label-3-outdoor: #A7B0BE; --fill-3-outdoor: rgba(128, 138, 154, 0.40);
}
:root[data-theme="dark"][data-fs-accent="blue"] {
  --accent: #6B9BFF; --accent-text: #6B9BFF; --accent-pressed: #5585F0; --accent-tint: rgba(107, 155, 255, 0.16); --accent-soft: #233040;
}
:root[data-theme="dark"][data-fs-accent="orange"] {
  --accent: #FFA066; --accent-text: #FFA066; --accent-pressed: #F08A4C; --accent-tint: rgba(255, 160, 102, 0.16); --accent-soft: #3D3124;
}

/* ---- Outdoor mode (§1.6): html[data-fs-contrast="outdoor"] = on, "off" = off,
   absent / "auto" = follow the system's Increase Contrast. The values come
   from the *-outdoor tokens each theme defines, so one rule serves both. ---- */
:root[data-fs-contrast="outdoor"] {
  --label-2: var(--label-2-outdoor);
  --label-3: var(--label-3-outdoor);
  --text-3: var(--label-2-outdoor);
  --sep: var(--sep-opaque);
  --fill-3: var(--fill-3-outdoor);
}
@media (prefers-contrast: more) {
  :root:not([data-fs-contrast="off"]):not([data-fs-contrast="outdoor"]) {
    --label-2: var(--label-2-outdoor);
    --label-3: var(--label-3-outdoor);
    --text-3: var(--label-2-outdoor);
    --sep: var(--sep-opaque);
    --fill-3: var(--fill-3-outdoor);
  }
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
/* §1.2: body text at --t-body on an integer line (14/20 desktop, 17/22 phone;
   was 14px x 1.45 = 20.3px), no -webkit-font-smoothing (no effect on iOS, thins
   text on macOS), and a real background so the dark palette paints the page.
   The line-height is the same value written as a RATIO (--lh-body-r): a px
   line-height inherits as a fixed length, so every 12-13px inline-sized label
   would have sat on a 20-22px line and loosened the whole desktop. A ratio
   gives exactly 20px / 22px at body size and scales with the text below it. */
body {
  font: 400 var(--t-body)/var(--lh-body-r) var(--font-ui);
  background: var(--bg);
  color: var(--label);
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}
#root { height: 100vh; }

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

button { font-family: var(--font-ui); cursor: pointer; }
input, textarea, select { font-family: var(--font-ui); }

/* scrollbars */
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: var(--fill); border-radius: 8px; border: 3px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-track { background: transparent; }

/* shared utility classes */
.fs-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}
/* §1.1: .mono (228 uses) and var(--mono) (85 inline uses) keep their names but
   render in the UI face with tabular figures, weight capped at 600 — that one
   change ends the faux-bold 700/800 monospace numbers. Real code (API keys,
   template placeholders) uses var(--font-code) explicitly. */
.mono, [style*="var(--mono)"] { font-family: var(--font-ui) !important; font-variant-numeric: tabular-nums; }
.mono[style*="font-weight: 7"], .mono[style*="font-weight: 8"], .mono[style*="font-weight: 9"],
[style*="var(--mono)"][style*="font-weight: 7"], [style*="var(--mono)"][style*="font-weight: 8"], [style*="var(--mono)"][style*="font-weight: 9"] { font-weight: 600 !important; }
.num { font-variant-numeric: tabular-nums; }
.num-round { font-family: var(--font-round); font-variant-numeric: tabular-nums; }
code, .fs-code { font-family: var(--font-code); }

/* ── RC1: the content pane is NOT the viewport ────────────────────────────
   A persistent 218px sidebar runs from 761px up, so the pane every screen
   actually renders into is `viewport - 218`. An iPad Pro 11" in portrait is
   834px wide but its pane is 616px — narrower than an iPhone 15's whole
   screen — and a viewport-keyed `@media (max-width: …)` happily gave it the
   desktop layout. Anything that describes PANE CONTENT (tables, the calendar
   grid, card grids, the pipeline) is therefore keyed off the pane itself with
   `@container`; anything that is genuinely about the window (the sidebar, the
   drawer, safe areas, fixed overlays, the topbar) stays on `@media`.

   Thresholds: a rule that was tuned on desktop (its number is > 760) converts
   to `N - 218`, which keeps desktop behaviour bit-identical while tablets stop
   pretending to be desktops. A rule tuned on phones (N <= 760, where the
   sidebar is already a drawer and pane == viewport) keeps its number. */
.fs-main { container-type: inline-size; }
/* …and `.fs-main` is created only by the AppShell (ui.js), so a SHELL-LESS screen
   — one app.js renders on its own, outside the shell — had no size container at
   all and every `@container` rule below silently evaluated false on it. The
   new-company wizard is the one that hurts: on a 375px iPhone SE its `.fs-grid-5`
   kept the desktop `repeat(5, 1fr)` and rendered five 59x196 slivers (labels
   clipped mid-word inside a 31px content box), `.fs-cols` kept `1.2fr 1fr` so the
   Company name field was 137px wide, and `.fs-stepper` never wrapped — 24px of the
   row sat outside a viewport that `overflow-x: clip` refuses to scroll. Making the
   wizard's own root a container activates the rules that were written for exactly
   this markup (`.fs-grid-5` -> 2-up at ≤682 / ≤560, `.fs-cols` -> 1 column, the
   `[style*="grid-template-columns: 1.2fr 1fr"]` rule at :311, `.fs-stepper` wrap).
   Scoped to this screen's root, not `[data-screen-label]` at large: container-type
   also makes the element a containing block for fixed descendants, and the wizard
   is verified to have none. */
[data-screen-label="Tenant Onboarding"] { container-type: inline-size; }

/* unified inbox 3-column grid — context rail collapses in a narrow PANE */
.fs-inbox-grid {
  display: grid;
  grid-template-columns: minmax(220px, 300px) minmax(0, 1fr) minmax(240px, 312px);
  height: 100%;
}
.fs-inbox-context { display: flex; flex-direction: column; }
@container (max-width: 962px) {
  .fs-inbox-grid { grid-template-columns: minmax(200px, 256px) minmax(0, 1fr); }
  .fs-inbox-context { display: none !important; }
  .fs-inbox-thread { display: none !important; }
}

/* tables: hover via CSS (replaces per-row JS handlers) */
.fs-table tbody tr { transition: background 0.1s; }
.fs-table tbody tr:hover { background: var(--surface-2); }
/* Notifications-matrix per-cell channel labels only appear when the table
   stacks on phones (≤640px block below) — desktop has real column headers */
.fs-notif-clabel { display: none; }

  /* Live Call Assistant — transcript must never collapse */
  .fs-call-feed { min-height: 170px; }
  @media (max-height: 760px) {
    .fs-call-checklist .fs-chips { flex-wrap: nowrap !important; overflow-x: auto; padding-bottom: 4px; }
    .fs-call-checklist { padding-top: 8px !important; padding-bottom: 8px !important; }
  }

@container (max-width: 682px) {
  /* Calendar: agenda list replaces the week grid whenever the PANE can't fit
     the grid's 680px min-width — phones, and iPad portrait (616px of pane)
     which a viewport query used to mis-classify either way. */
  .fs-cal-inner { display: none !important; }
  .fs-cal-agenda { display: flex !important; }
}

@keyframes fsFadeUp {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: no-preference) {
  .fs-fade { animation: fsFadeUp 0.3s ease; }
}
/* L5: the scroller itself never fades. `fs-fade` on <main> put 95% of the
   screen's text in a translucent, translated layer on every tab tap (the soft,
   blurry look). A tab switch is an instant swap. */
.fs-main.fs-fade, main.fs-fade { animation: none !important; }
/* L6: a centred fixed banner is positioned with translateX(-50%); fsFadeUp's
   transform replaced it for 300ms, so the banner jumped sideways. It drops in
   with its centring kept. */
@keyframes fsDropIn {
  from { opacity: 0; transform: translate(-50%, -8px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes fsToastIn {
  from { opacity: 0; transform: translateY(-12px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: no-preference) {
  .fs-fade[style*="translateX(-50%)"] { animation: fsDropIn 0.3s var(--ease-ios); }
}

/* ============ responsive layer ============ */
/* app shell fills the dynamic viewport (iOS Safari safe) */
/* NAT-1: the offline / session / update bar is fixed to the bottom; the shell
   ends above it (boot.js sets --fs-shell-bar to the bar's height, 0 without one) */
.fs-app { height: calc(100vh - var(--fs-shell-bar, 0px)); height: calc(100dvh - var(--fs-shell-bar, 0px)); }

/* iOS auto-zooms into any focused form field whose font-size is < 16px, which
   shrinks the visual viewport below the layout viewport and makes the whole page
   scroll sideways (the real cause of the "inbox/login is wider than the screen"
   reports — only reproduces in WebKit, never Chromium). Keep every form field at
   >=16px so iOS never zooms. !important because many fields set an inline
   font-size (e.g. the inbox reply <textarea> is inline 13.5px) and inline beats a
   plain rule — but inline-without-!important still loses to a rule with !important. */
input, textarea, select { font-size: max(16px, var(--t-input)) !important; }
/* Belt-and-suspenders: clip any stray horizontal overflow at the root
   (overflow:clip, not hidden, so it never becomes a scroll container that could
   trap the inbox). NB: on iOS WebKit this does NOT clip position:fixed children —
   the off-canvas sidebar is handled directly via visibility (see @media 760px). */
html, body { overflow-x: clip; }
/* WebKit won't break long unbreakable tokens (E.164 phone numbers like
   +12109424936, emails, bare URLs) on its own, so a call-summary / message bubble
   grows to the token's width and pans the thread sideways. Let everything inside
   the conversation log wrap. */
[role="log"] { overflow-wrap: anywhere; }

/* tables: scroll sideways instead of crushing */
.fs-card:has(> table) { overflow-x: auto !important; }
.fs-card > table { min-width: 620px; }
.fs-scroll-x { overflow-x: auto !important; }
.fs-cal-inner { min-width: 680px; }

/* sidebar drawer below 760px (phones). iPad portrait (768px+) keeps the
   persistent sidebar so the tablet isn't stuck with the cramped phone layout. */
.fs-burger { display: none; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 10px; border: 1px solid var(--border); background: var(--surface); color: var(--text-2); flex-shrink: 0; }
/* The scrim exists ONLY where the drawer exists. It used to be an unconditional
   rule, so rotating a phone to landscape (852px wide → persistent sidebar) left a
   full-screen transparent-to-taps backdrop over the whole app: elementFromPoint
   at the centre, at the topbar and over the sidebar all returned .fs-nav-backdrop
   and the first tap / first scroll went nowhere. AppShell clears navOpen on the
   same 760px matchMedia so CSS and JS can't drift. */
@media (max-width: 760px) {
  .fs-nav-backdrop { position: fixed; inset: 0; background: rgba(6, 12, 24, 0.45); z-index: 140; }
  /* Parked off-screen-left when closed. CRITICAL: also visibility:hidden +
     pointer-events:none so the fixed box leaves the scrollable area — on iOS
     WKWebView, html/body overflow-x:clip does NOT clip a position:fixed
     descendant, so a parked translateX(-105%) drawer otherwise makes the whole
     page pan sideways (scrollWidth still reports 440 — invisible to it). */
  aside.fs-sidebar { position: fixed; left: 0; top: 0; bottom: 0; z-index: 150; transform: translateX(-105%); visibility: hidden; pointer-events: none; transition: transform 0.22s ease, visibility 0.22s ease; box-shadow: var(--shadow-lg); }
  aside.fs-sidebar.open { transform: translateX(0); visibility: visible; pointer-events: auto; }
  .fs-burger { display: inline-flex; }
}

/* topbar search slims down */
@media (max-width: 1100px) {
  .fs-topbar-search { width: auto !important; flex: 0 0 auto; }
  .fs-search-hint { display: none !important; }
}

/* grid utilities — !important beats inline styles. Pane-keyed (RC1): 1100 and
   900 were tuned on desktop, so they become 882 / 682 of pane. */
@container (max-width: 882px) {
  .fs-grid-5 { grid-template-columns: repeat(3, 1fr) !important; }
  .fs-grid-4 { grid-template-columns: repeat(2, 1fr) !important; }
  .fs-tpl-grid { grid-template-columns: 1fr !important; }
}
@container (max-width: 682px) {
  .fs-cols, .fs-grid-3, .fs-auto-grid { grid-template-columns: 1fr !important; }
  /* Live Call Assistant — tabbed on mobile so the transcript is never buried */
  .fs-call-grid { grid-template-columns: 1fr !important; grid-template-rows: auto minmax(0, 1fr); }
  .fs-call-tabs { display: flex !important; }
  .fs-call-grid[data-mtab="transcript"] .fs-call-panel { display: none !important; }
  .fs-call-grid[data-mtab="transcript"] .fs-call-checklist { display: none !important; }
  .fs-call-grid[data-mtab="hints"] .fs-call-left > *:not(:first-child) { display: none !important; }
  .fs-call-grid[data-mtab="hints"] .fs-call-left { flex: none; }
  .fs-call-grid[data-mtab="checklist"] .fs-call-panel { display: none !important; }
  .fs-call-grid[data-mtab="checklist"] .fs-call-left > *:not(.fs-call-checklist):not(:first-child) { display: none !important; }
  .fs-call-grid[data-mtab="checklist"] .fs-call-checklist { border-top: none; }
  .fs-call-panel { border-left: none !important; border-top: 1px solid var(--border); }
  .fs-grid-2 { grid-template-columns: 1fr !important; }
  .fs-grid-5 { grid-template-columns: repeat(2, 1fr) !important; }
  .fs-stepper { flex-wrap: wrap; row-gap: 8px; }
}
@media (max-width: 760px) {
  .fs-login-split { flex-direction: column !important; }
}
/* Mobile pane-switcher contract (shared by inbox, automations, live call). On a
   phone, two side-by-side panes used to collapse into two cramped stacked scrollers
   (250px list over a scrolling thread, etc.). Instead, the host grid (.fs-mgrid)
   becomes ONE full-screen pane + a tab bar (.fs-mtabs) switches which pane shows,
   keyed off data-mtab="a"|"b" on the host with .fs-mpane-a / .fs-mpane-b on the panes.
   Generalised from the Live Call Assistant .fs-call-grid/.fs-call-tabs mechanism. */
.fs-mtabs { display: none; }
/* Panes must be allowed to shrink below their content's min-content, or a single long
   token (e.g. a long lead name in the inbox list) forces the whole grid wider than the
   screen. A grid/flex item defaults to min-width:auto (= min-content), so set it to 0. */
.fs-mpane-a, .fs-mpane-b { min-width: 0; }
@media (max-width: 900px) {
  .fs-mgrid { grid-template-columns: 1fr !important; grid-template-rows: auto minmax(0, 1fr) !important; }
  .fs-mtabs { display: flex !important; gap: 4px; padding: 8px 12px; border-bottom: 1px solid var(--border); background: var(--surface); grid-column: 1 / -1; }
  .fs-mgrid[data-mtab="a"] .fs-mpane-b { display: none !important; }
  .fs-mgrid[data-mtab="b"] .fs-mpane-a { display: none !important; }
  .fs-mpane-b { border-left: none !important; }
}

/* ---- Inbox thread on a phone: the MESSAGES are the screen ----
   At 390x844 the wrapped lead header (name + stage + score + channel chips + five
   action buttons) and the composer's channel chips + consent line together ate
   roughly three quarters of the pane — with the keyboard up the message log was
   about 28px tall and the composer sat below the fold. Below 900px the header
   collapses to one row (name + a single ⋯ menu), the channel picker hides behind a
   disclosure, the composer is pinned to the bottom of the pane, and the log takes
   everything that's left. Above 900px none of this applies: the desktop two-pane
   layout is untouched. */
.fs-ithread-more { display: none; }
.fs-ithread-chan-btn { display: none; }
/* TAB10-03: the compact thread is for SHORT panes too — an iPad in landscape
   with the keyboard up (1024x370) left the conversation 28px tall */
@media (max-width: 900px), (max-height: 560px) {
  .fs-inbox-grid { height: 100%; min-height: 0; }

  .fs-ithread-head {
    padding: 7px 12px !important;
    gap: 8px !important;
    flex-wrap: nowrap !important;
    min-height: 50px;
  }
  .fs-ithread-head > div:first-child { max-width: none !important; flex: 1; font-size: var(--t-callout) !important; }
  .fs-ithread-meta, .fs-ithread-acts, .fs-ithread-gap { display: none !important; }
  .fs-ithread-more { display: inline-flex !important; align-items: center; justify-content: center; }

  .fs-ithread-spam { padding: 8px 12px !important; }

  /* log: every pixel the header and composer don't need */
  .fs-inbox-grid [role="log"] { padding: 12px !important; }

  .fs-ithread-composer { padding: 8px 12px calc(10px + env(safe-area-inset-bottom)) !important; }
  .fs-ithread-chan-btn { display: flex !important; }
  .fs-ithread-chan { display: none !important; }
  .fs-ithread-chan.is-open { display: flex !important; }
  /* channel chips are tap targets here, not desktop micro-pills */
  .fs-ithread-chan > button { min-height: 34px; padding: 6px 14px !important; font-size: var(--t-subhead) !important; }
  .fs-ithread-chan > span { flex-basis: 100%; margin-left: 0 !important; }
  .fs-ithread-btns { justify-content: flex-end; }
}
/* The grid owns the pane's height so the composer can never fall below the fold
   — but ONLY where there is enough height for that budget to work out (RC2).
   On a landscape phone (393px tall, ~209px of pane against 280px of content)
   the same clamp turned a 71px overrun into permanently unreachable content:
   Send sat at y 416 and swipe, wheel and a forced scrollTop all moved 0. Short
   viewports scroll the pane instead of clamping it. */
@media (max-width: 900px) and (min-height: 600px) {
  .fs-main:has(> .fs-inbox-grid) { overflow: hidden !important; }
}
@media (max-width: 900px) and (max-height: 599px) {
  .fs-main:has(> .fs-inbox-grid) { overflow-y: auto !important; }
}
@container (max-width: 560px) {
  .fs-grid-5, .fs-grid-4 { grid-template-columns: 1fr 1fr !important; }
  .fs-grid-3 { grid-template-columns: 1fr !important; }
}
/* Inline 2-column content grids that lack a responsive class (chatbot settings +
   live-preview, website/forms + other "form | preview" layouts) stack on phones +
   narrow tablets so the second column doesn't run off-screen. Targets the specific
   content column patterns by their rendered inline value — deliberately NOT a blanket
   grid rule, so intentional inline grids (the dialer keypad `repeat(3,1fr)`, icon
   `auto 1fr` rows, `52px repeat(...)` table headers) are left untouched. */
@container (max-width: 682px) {
  .fs-stack,
  [style*="grid-template-columns: 1fr 1fr"],
  [style*="grid-template-columns: 1fr 320px"],
  [style*="grid-template-columns: 1fr 360px"],
  [style*="grid-template-columns: 1fr 1.2fr"],
  [style*="grid-template-columns: 1.2fr 1fr"],
  [style*="grid-template-columns: 1.3fr 1fr"],
  [style*="grid-template-columns: 1.5fr 1fr"],
  [style*="grid-template-columns: 1.6fr 1fr"],
  [style*="grid-template-columns: 1.7fr 1fr"] {
    grid-template-columns: 1fr !important;
  }
  /* Let collapsed grid columns shrink to the viewport instead of being held wide by
     a child's min-content (e.g. the chatbot live-preview mockup) — the classic grid
     overflow fix. min-width:0 is harmless for intentional grids (keypad etc.). */
  main [style*="grid-template-columns"] > * { min-width: 0; }
}
@keyframes fsPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
@keyframes fsBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

/* ============ Dynamic Island / notch safe-area + phone polish ============ */
/* The topbar drops below the Dynamic Island / notch and respects side insets
   (landscape). The white header background fills the inset strip so the status
   bar reads cleanly over it. */
.fs-topbar {
  padding-top: env(safe-area-inset-top) !important;
  min-height: calc(58px + env(safe-area-inset-top));
  padding-left: max(16px, env(safe-area-inset-left)) !important;
  padding-right: max(16px, env(safe-area-inset-right)) !important;
}
.fs-topbar > h1 { min-width: 0; }

/* The banner strip below the topbar sits in the same inset band as the topbar.
   In landscape both dismiss (×) buttons were 100% inside the right-hand cutout
   (x 800-844 against a safe edge of 793) on every route, and the page hint
   re-appears per screen — so they were permanently un-dismissable. */
.fs-pushbanner, .fs-pagehint {
  padding-left: max(16px, env(safe-area-inset-left)) !important;
  padding-right: max(16px, env(safe-area-inset-right)) !important;
}

/* Main scroll area honors side insets in landscape (notch on the side), and the
   home indicator at the end of the scroll. calc() rather than max() so Android
   (which has a real gesture bar but small insets) gains the room too; the 0px
   fallback keeps the declaration valid on desktop. */
.fs-main-pad {
  padding: 20px;
  padding-left: max(20px, env(safe-area-inset-left));
  padding-right: max(20px, env(safe-area-inset-right));
  padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
}

/* The sidebar — drawer on a phone, persistent rail on a tablet — clears the
   island, the side cutout and the home indicator in BOTH orientations. These
   were drawer-scoped, so in landscape the brand, the company switcher and all
   six nav rows sat inside the 59px cutout and "Sign out" ran 9px into the home
   indicator. The width has to grow with the inset (and needs !important to beat
   the inline width:218) or the labels get squeezed instead of the rail shifting.
   NB: box-sizing stays border-box — content-box re-breaks the footer. */
aside.fs-sidebar {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-bottom: env(safe-area-inset-bottom);
  width: calc(218px + env(safe-area-inset-left)) !important;
}

/* Fixed overlays ride above the home indicator — and above the software keyboard
   (see --fs-kb on .fs-toast-wrap below; it is 0px whenever no keyboard is open). */
.fs-incoming-toast { width: min(330px, calc(100vw - 24px)) !important; right: 12px !important; bottom: calc(var(--fs-kb, 0px) + max(12px, env(safe-area-inset-bottom))) !important; }
.fs-tour-card { bottom: calc(var(--fs-kb, 0px) + max(18px, calc(env(safe-area-inset-bottom) + 8px))) !important; }

/* ---- topbar fits any phone without clipping ---- */
/* .fs-hide-phone / .fs-show-phone are a pair: the desktop affordance steps out
   and a phone-sized one steps in. Nothing may be hidden on a phone without a
   replacement — the global search box and the Demo switch used to just vanish. */
.fs-show-phone { display: none !important; }
@media (max-width: 680px) {
  .fs-topbar { gap: 9px !important; }
  .fs-hide-phone { display: none !important; }   /* wide search box + demo pill */
  .fs-show-phone { display: inline-flex !important; }  /* …their phone stand-ins */
  .fs-topbar-label { display: none; }            /* "New" collapses to its icon */
  .fs-new-btn { padding-left: 10px !important; padding-right: 10px !important; }
}

/* ---- command palette: sized by the viewport it has, not by a desktop guess ----
   The 90px top offset + 440px max-height used to be inline styles, so a landscape
   phone (393px tall) got a shell 530px tall: 17 of 23 results off-screen and the
   last 3 unreachable even after scrolling the inner list to its end. One base rule
   covers desktop, portrait, landscape and rotate-while-open. */
.fs-cmd-wrap { padding-top: min(90px, 8dvh); }
.fs-cmd {
  max-height: min(440px, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 8dvh - 24px));
}

/* ---- popover menus become full-width sheets under the topbar (never clipped) ---- */
@media (max-width: 560px) {
  .fs-pop {
    position: fixed !important;
    top: calc(env(safe-area-inset-top) + 62px) !important;
    left: 10px !important;
    right: 10px !important;
    width: auto !important;
    max-height: calc(100dvh - env(safe-area-inset-top) - 86px) !important;
    overflow-y: auto;
  }
  .fs-cmd-wrap { padding-top: calc(env(safe-area-inset-top) + 64px) !important; align-items: stretch !important; }
  .fs-cmd { width: 100% !important; max-height: 72dvh !important; }
  .fs-main-pad { padding-top: 14px; padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); padding-left: max(13px, env(safe-area-inset-left)); padding-right: max(13px, env(safe-area-inset-right)); }
}

/* Smallest phones (iPhone SE / mini): tighten the topbar title and gaps. */
@media (max-width: 380px) {
  .fs-topbar { gap: 7px !important; }
  .fs-topbar > h1 { font-size: var(--t-callout) !important; }
}

/* ============ accessibility + design-system layer ============ */
/* The type, spacing and radius tokens live at the top of this file (SPEC §1).
   The old --fs-xs…--fs-3xl and --space-* sets had no users and are gone. */

/* Visible keyboard focus everywhere (the app removed native outlines without a
   replacement). Pointer users don't see it; keyboard users do. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
*:focus:not(:focus-visible) { outline: none; }
/* many inputs set inline `outline:none` which beats the rule above — force a
   visible focus ring on form fields with !important so keyboard users see it */
input:focus-visible, textarea:focus-visible, select:focus-visible, [contenteditable]:focus-visible {
  outline: 2px solid var(--accent) !important;
  outline-offset: 1px;
}

/* clickable cards/rows get a real hover + focus affordance */
.fs-card-btn { transition: box-shadow 0.12s, border-color 0.12s, background 0.12s; }
.fs-card-btn:hover { box-shadow: var(--shadow-md); border-color: var(--border-strong); }

/* respect reduced-motion: stop infinite pulses + damp transitions */
@media (prefers-reduced-motion: reduce) {
  .fs-fade, [style*="fsPulse"], [style*="fsBlink"] { animation: none !important; }
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}

/* toast stack (shared FS_TOAST) — viewport-anchored so it never clips inside a pane.
   `--fs-kb` is the software keyboard's height, published by ui.js from
   visualViewport (0 when no keyboard, and 0 on every desktop). A fixed element is
   anchored to the LAYOUT viewport, which iOS does NOT shrink for the keyboard, so
   without this term the toast is painted *behind* the keyboard: measured on a
   393x852 iPhone profile the wrap sat at 770–832 with the keyboard line at 516,
   i.e. 254–316px out of sight, and toasts auto-dismiss in ~3.2s. The validation
   toasts that fire on Return ("A field with that name already exists.", "Give the
   lead a name first.") are raised with the keyboard up by definition, and they are
   the only feedback those flows give. Same formula app/widget.js already uses. */
.fs-toast-wrap { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--fs-kb, 0px) + max(20px, env(safe-area-inset-bottom))); z-index: 240; display: flex; flex-direction: column; gap: 10px; align-items: center; width: min(440px, calc(100vw - 24px)); pointer-events: none; }
.fs-toast { pointer-events: auto; width: 100%; background: var(--ink); color: var(--ink-strong); border: 1px solid rgba(255,255,255,0.08); border-radius: 15px; box-shadow: 0 14px 40px rgba(0,0,0,0.36); padding: 12px 14px; display: flex; align-items: center; gap: 12px; font-size: var(--t-subhead); position: relative; overflow: hidden; }
/* accent spine — colour-codes the notification and gives it presence */
.fs-toast::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--accent); }
.fs-toast.tone-success::before { background: #5CD27E; }
.fs-toast.tone-info::before { background: #3b9dff; }
.fs-toast.tone-warn::before { background: #f5a623; }
.fs-toast.tone-danger::before { background: #ff453a; }
.fs-toast.tone-call::before { background: #5CD27E; }
.fs-toast-ico { flex-shrink: 0; width: 36px; height: 36px; border-radius: 11px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.11); color: #fff; }
.fs-toast.tone-success .fs-toast-ico { background: rgba(92,210,126,0.22); color: #9BE7B2; }
.fs-toast.tone-info .fs-toast-ico { background: rgba(59,157,255,0.22); color: #9cc9ff; }
.fs-toast.tone-warn .fs-toast-ico { background: rgba(245,166,35,0.22); color: #ffd28a; }
.fs-toast.tone-danger .fs-toast-ico { background: rgba(255,69,58,0.22); color: #ff9d95; }
.fs-toast.tone-call .fs-toast-ico { background: rgba(92,210,126,0.22); color: #9BE7B2; }
.fs-toast-title { font-weight: 600; font-size: var(--t-subhead); line-height: var(--lh-subhead); }
.fs-toast-msg { font-size: var(--t-footnote); color: rgba(255,255,255,0.78); line-height: var(--lh-footnote); }
.fs-toast button { background: rgba(255,255,255,0.12); border: none; color: #B8DEFF; font-weight: 600; font-size: var(--t-footnote); cursor: pointer; white-space: nowrap; padding: 6px 12px; border-radius: 9px; }
.fs-toast button.fs-toast-x { background: none; color: var(--ink-text-dim); padding: 4px 2px; font-weight: 600; }
/* Phone (§3.2 ToastHost): a glass banner at the TOP, clear of the tab bar and
   the keyboard, one line of 600 text with a round tone glyph. No spine, and it
   enters with fsToastIn on the toast (never a translate on the text layer that
   outlives the entry). !important on top/bottom: nav-mobile.js still anchors the
   wrap above the old tab bar. */
@media (max-width: 760px) {
  .fs-toast-wrap { top: calc(var(--safe-top) + 8px); bottom: auto !important; width: calc(100vw - 2 * var(--margin)); }
  .fs-toast { min-height: 52px; padding: 8px 10px 8px 12px; border: 0; border-radius: 26px; color: var(--label); background: var(--glass-bg); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); box-shadow: var(--glass-shadow); }
  .fs-toast.fs-fade { animation: none; }
  .fs-toast::before { display: none; }
  .fs-toast-ico, .fs-toast.tone-success .fs-toast-ico, .fs-toast.tone-info .fs-toast-ico, .fs-toast.tone-warn .fs-toast-ico, .fs-toast.tone-danger .fs-toast-ico, .fs-toast.tone-call .fs-toast-ico { width: 28px; height: 28px; border-radius: 14px; color: #fff; background: var(--accent); }
  .fs-toast.tone-success .fs-toast-ico { background: #1F7A3A; } /* status green as a fill (white glyph 5.38) in both themes; the accent no longer means "done" */
  .fs-toast.tone-info .fs-toast-ico { background: var(--blue); }
  .fs-toast.tone-warn .fs-toast-ico { background: var(--orange-fill); }
  .fs-toast.tone-danger .fs-toast-ico { background: var(--red); }
  .fs-toast-title { color: var(--label); }
  .fs-toast-msg { color: var(--label-2); }
  .fs-toast > div:not(.fs-toast-ico) > div:only-child { font-weight: 600; }
  .fs-toast button { min-height: 44px; background: none; color: var(--accent-text); font-size: var(--t-subhead); padding: 0 8px; }
  .fs-toast button.fs-toast-x { min-width: 44px; color: var(--label-2); background: none; }
}
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  .fs-toast { animation: fsToastIn 0.35s var(--ease-ios); }
}

/* role=switch toggle */
.fs-switch { width: 38px; height: 22px; border-radius: 999px; border: none; padding: 0; position: relative; cursor: pointer; flex-shrink: 0; transition: background 0.15s; background: var(--border-strong); }
.fs-switch[aria-checked="true"] { background: var(--accent); }
.fs-switch > span { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left 0.15s; box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
.fs-switch[aria-checked="true"] > span { left: 18px; }
/* §3.2 Toggle, touch: the iOS geometry. The <button> is a transparent 51x44 box
   (R8-A11Y-02: 44px tall), its ::before is the 51x31 track and the <span> is the
   27px knob that travels 20px. It used to be squashed to 38x36 by the touch
   button floor, which drew an oval. Decorative <span class="fs-switch"> copies
   (inside a labelled row that is the real target) take the track size only. */
@media (pointer: coarse), (max-width: 760px) {
  .fs-switch { width: 51px; height: 31px; min-width: 51px; border-radius: 16px; background: var(--fill); transition: background var(--dur-control) var(--ease-ios); }
  span.fs-switch { display: inline-block; vertical-align: middle; }
  .fs-switch > span { top: 2px; left: 2px; width: 27px; height: 27px; box-shadow: 0 3px 8px rgba(0,0,0,0.15), 0 3px 1px rgba(0,0,0,0.06); transition: left var(--dur-control) var(--spring-snappy); }
  .fs-switch[aria-checked="true"] > span, .fs-switch > span[style*="left: 18px"] { left: 22px !important; }
  button.fs-switch, [role="switch"].fs-switch { height: 44px; min-height: 44px; background: transparent; border-radius: 0; }
  button.fs-switch::before, [role="switch"].fs-switch::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 31px; margin-top: -15.5px; border-radius: 16px; background: var(--fill); transition: background var(--dur-control) var(--ease-ios); }
  button.fs-switch[aria-checked="true"], [role="switch"].fs-switch[aria-checked="true"] { background: transparent; }
  button.fs-switch[aria-checked="true"]::before, [role="switch"].fs-switch[aria-checked="true"]::before { background: var(--accent); }
  button.fs-switch > span, [role="switch"].fs-switch > span { top: 8.5px; }
  button.fs-switch:disabled { opacity: 0.4; }
}

/* Pipeline board on a narrow PANE: one stage nearly fills it. 84cqw, not 84vw —
   on an iPad the pane is 218px narrower than the window, so vw columns ran off
   the board. Snap is `proximity`: `mandatory` turned a 120px flick into a whole
   stage jump and a 320px flick into two, with no way to nudge. */
@container (max-width: 700px) {
  .fs-pipe-col { width: 84cqw !important; }
  .fs-pipe-row { scroll-snap-type: x proximity; }
  .fs-pipe-col { scroll-snap-align: start; }
}
.fs-tbl-wrap { overflow-x: auto; }
.fs-tbl-wrap > table { min-width: 560px; }

/* ===== reflow wide tables into stacked cards (no sideways scroll) =====
   Keyed off the PANE (RC1) so an iPad portrait — 616px of pane, 46px short of
   the 620px table min-width — reflows instead of clipping, and repeated for a
   landscape phone (RC2), where 9 columns at 38-60px rendered as vertical
   letter-columns: header 194px of a 335px pane, 0 rows fully visible.
   The two at-rules below are one rule set in two conditions (CSS can't share a
   block across @container and @media) — KEEP THEM IN SYNC. */
@container (max-width: 640px) {
  /* kill the desktop min-width + the horizontal-scroll container */
  .fs-table { min-width: 0 !important; width: 100% !important; }
  .fs-card > table { min-width: 0 !important; }
  .fs-card:has(> table.fs-table) { overflow-x: hidden !important; }
  .fs-table thead, .fs-table colgroup { display: none !important; }
  .fs-table, .fs-table tbody, .fs-table td { display: block !important; width: 100% !important; box-sizing: border-box; }
  /* §4.5: a table becomes a list of ROWS, not label/value cards. The name is the
     title (headline), the second cell the subtitle, and the rest one quiet meta
     line that wraps — each figure keeps its column name as a plain prefix (RC8:
     the <thead> is hidden, so a bare "$9,800" would be unlabelled). ui.js stamps
     td[data-label] from the matching <th>. */
  .fs-table tr { display: flex !important; flex-wrap: wrap; align-items: baseline; column-gap: 14px; row-gap: 2px; width: 100% !important; box-sizing: border-box; min-height: 52px; padding: 11px 16px !important; border-bottom: var(--hair) solid var(--sep) !important; }
  .fs-table tr:last-child { border-bottom: 0 !important; }
  .fs-table tr:hover { background: transparent !important; }
  .fs-table td { flex: 0 1 auto; width: auto !important; padding: 0 !important; border: none !important; text-align: left !important; white-space: normal !important; max-width: 100% !important; min-width: 0; font-size: var(--t-footnote); line-height: var(--lh-footnote); color: var(--label-2); }
  .fs-table td:first-child { flex-basis: 100%; font-size: var(--t-headline); line-height: var(--lh-headline); font-weight: 600; color: var(--label); }
  .fs-table td:nth-child(2) { flex-basis: 100%; font-size: var(--t-subhead); line-height: var(--lh-subhead); }
  /* a few last-cell action columns shouldn't force a tall empty row */
  .fs-table td:empty { display: none !important; }
  .fs-table td[data-label] { display: inline-flex !important; flex-wrap: wrap; align-items: baseline; column-gap: 4px; }
  .fs-table td[data-label]::before { content: attr(data-label); font: inherit; font-weight: 400; color: var(--label-2); }
  .fs-table td:nth-child(2)[data-label]::before { content: none; }
  /* A screen that tags its columns (<th data-role="sub|amount">, copied onto the
     cells by ui.js) gets the full native row: title + subtitle on the left, the
     amount trailing, every other cell left to the detail screen. */
  .fs-table:has(td[data-role]) tr { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; column-gap: 12px; align-items: center; }
  .fs-table:has(td[data-role]) td { display: none !important; }
  .fs-table:has(td[data-role]) td:first-child { display: block !important; grid-column: 1; grid-row: 1; }
  .fs-table:has(td[data-role]) td[data-role="sub"] { display: block !important; grid-column: 1; grid-row: 2; font-size: var(--t-subhead); line-height: var(--lh-subhead); color: var(--label-2); }
  .fs-table:has(td[data-role]) td[data-role="amount"] { display: block !important; grid-column: 2; grid-row: 1 / span 2; font-size: var(--t-headline); line-height: var(--lh-headline); font-weight: 600; color: var(--label); font-variant-numeric: tabular-nums; white-space: nowrap !important; text-align: right !important; }
  .fs-table:has(td[data-role]) td::before { content: none !important; }

  /* Settings → Notifications matrix: hand-labelled already, and its toggles want
     the full width — opt it out of the row layout. */
  .fs-table.fs-notif-matrix tr { display: block !important; }
  .fs-table.fs-notif-matrix td { display: flex !important; align-items: center; justify-content: space-between; gap: 10px; font-size: var(--t-subhead); color: var(--label); }
  .fs-table.fs-notif-matrix td:first-child { display: block !important; }
  .fs-notif-matrix .fs-notif-clabel { display: inline; font-size: var(--t-footnote); font-weight: 600; color: var(--label-2); }
}
/* KEEP IN SYNC with the @container block above — landscape phone (RC2). */
@media (max-height: 500px) and (pointer: coarse) {
  /* kill the desktop min-width + the horizontal-scroll container */
  .fs-table { min-width: 0 !important; width: 100% !important; }
  .fs-card > table { min-width: 0 !important; }
  .fs-card:has(> table.fs-table) { overflow-x: hidden !important; }
  .fs-table thead, .fs-table colgroup { display: none !important; }
  .fs-table, .fs-table tbody, .fs-table td { display: block !important; width: 100% !important; box-sizing: border-box; }
  /* §4.5: a table becomes a list of ROWS, not label/value cards. The name is the
     title (headline), the second cell the subtitle, and the rest one quiet meta
     line that wraps — each figure keeps its column name as a plain prefix (RC8:
     the <thead> is hidden, so a bare "$9,800" would be unlabelled). ui.js stamps
     td[data-label] from the matching <th>. */
  .fs-table tr { display: flex !important; flex-wrap: wrap; align-items: baseline; column-gap: 14px; row-gap: 2px; width: 100% !important; box-sizing: border-box; min-height: 52px; padding: 11px 16px !important; border-bottom: var(--hair) solid var(--sep) !important; }
  .fs-table tr:last-child { border-bottom: 0 !important; }
  .fs-table tr:hover { background: transparent !important; }
  .fs-table td { flex: 0 1 auto; width: auto !important; padding: 0 !important; border: none !important; text-align: left !important; white-space: normal !important; max-width: 100% !important; min-width: 0; font-size: var(--t-footnote); line-height: var(--lh-footnote); color: var(--label-2); }
  .fs-table td:first-child { flex-basis: 100%; font-size: var(--t-headline); line-height: var(--lh-headline); font-weight: 600; color: var(--label); }
  .fs-table td:nth-child(2) { flex-basis: 100%; font-size: var(--t-subhead); line-height: var(--lh-subhead); }
  /* a few last-cell action columns shouldn't force a tall empty row */
  .fs-table td:empty { display: none !important; }
  .fs-table td[data-label] { display: inline-flex !important; flex-wrap: wrap; align-items: baseline; column-gap: 4px; }
  .fs-table td[data-label]::before { content: attr(data-label); font: inherit; font-weight: 400; color: var(--label-2); }
  .fs-table td:nth-child(2)[data-label]::before { content: none; }
  /* A screen that tags its columns (<th data-role="sub|amount">, copied onto the
     cells by ui.js) gets the full native row: title + subtitle on the left, the
     amount trailing, every other cell left to the detail screen. */
  .fs-table:has(td[data-role]) tr { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; column-gap: 12px; align-items: center; }
  .fs-table:has(td[data-role]) td { display: none !important; }
  .fs-table:has(td[data-role]) td:first-child { display: block !important; grid-column: 1; grid-row: 1; }
  .fs-table:has(td[data-role]) td[data-role="sub"] { display: block !important; grid-column: 1; grid-row: 2; font-size: var(--t-subhead); line-height: var(--lh-subhead); color: var(--label-2); }
  .fs-table:has(td[data-role]) td[data-role="amount"] { display: block !important; grid-column: 2; grid-row: 1 / span 2; font-size: var(--t-headline); line-height: var(--lh-headline); font-weight: 600; color: var(--label); font-variant-numeric: tabular-nums; white-space: nowrap !important; text-align: right !important; }
  .fs-table:has(td[data-role]) td::before { content: none !important; }

  /* Settings → Notifications matrix: hand-labelled already, and its toggles want
     the full width — opt it out of the row layout. */
  .fs-table.fs-notif-matrix tr { display: block !important; }
  .fs-table.fs-notif-matrix td { display: flex !important; align-items: center; justify-content: space-between; gap: 10px; font-size: var(--t-subhead); color: var(--label); }
  .fs-table.fs-notif-matrix td:first-child { display: block !important; }
  .fs-notif-matrix .fs-notif-clabel { display: inline; font-size: var(--t-footnote); font-weight: 600; color: var(--label-2); }
  /* a landscape phone has 393px of height: the two banners cost 126px of it */
  .fs-pagehint { display: none !important; }
}

/* Filter / chip rows scroll sideways instead of crushing. This lived in the
   640px block, so between 641 and 900px — every phone in landscape — the row
   silently went back to shrinking its children: "Filter" rendered as "Filte" at
   36px and spilled past a viewport the document clips. Every touch device gets
   it now, in both orientations. */
@media (pointer: coarse), (max-width: 900px) {
  .fs-chip-row, .fs-filters { overflow-x: auto; flex-wrap: nowrap !important; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .fs-chip-row::-webkit-scrollbar, .fs-filters::-webkit-scrollbar { display: none; }
  /* …and the items must NOT shrink, or the labels clip/overlap instead of the
     row scrolling (the 2026-07 "Import CSV crushed to 34px" bug) */
  .fs-chip-row > *, .fs-filters > * { flex-shrink: 0; }
  /* …except the search field, which is the one elastic member: it gives width
     back so the fixed chips don't push the row off a 360px screen. */
  .fs-chip-row > input[type="search"], .fs-chip-row > input[type="text"],
  .fs-filters > input[type="search"], .fs-filters > input[type="text"] {
    flex: 1 1 clamp(110px, 34%, 240px) !important;
    min-width: 110px;
  }
}

/* page hint + banners: compact so they don't eat the first screen. These sit
   ABOVE .fs-main (they are not pane content), so they stay viewport-keyed. */
@media (max-width: 640px) {
  .fs-pagehint { padding-top: 7px !important; padding-bottom: 7px !important; font-size: var(--t-footnote) !important; }
  .fs-pagehint .fs-hint-try { display: none !important; }
  .fs-pushbanner { padding-top: 8px !important; padding-bottom: 8px !important; font-size: var(--t-footnote) !important; }
  .fs-pushbanner-txt { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
}

/* Tap targets: the push-banner dismiss (X) button is rendered without a class —
   give it a 36px minimum hit area (glyph stays small, banner height unchanged). */
.fs-pushbanner button:last-child {
  min-width: 36px;
  min-height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Touch / phone tap targets: a 36px floor on every real button, found in the
   2026-07 mobile sweep ("← All leads" was 15px tall, tag-remove × was 13×8).
   The glyph keeps its size — we grow the hit area. The CAD workspace is
   excluded: its toolbar is dense by design and has its own touch handling. */
@media (pointer: coarse), (max-width: 700px) {
  button {
    min-height: 36px;
    min-width: 36px;
    align-items: center; /* inline link-buttons sit in text flow — center the taller box */
  }
  /* The rule only ever covered `button`, so the pipeline's per-card MOVE <select>
     — the control that actually advances a deal — stayed 30px. */
  select { min-height: 36px; }
  /* …and that floor was still thrown away on the ONE engine the iPhone app
     actually ships. A <select> with the default `menulist` appearance is laid
     out by the UA's native render theme, which in WebKit REPLACES the author's
     padding and clamps min-height: same build, same 393x852 profile, only the
     engine changed — Chromium honoured the declarations (Pipeline stage move
     271x34, padding 3px, min-height 34px; Leads sort 285x36; the seven New
     proposal selects 40px each) while WebKit rendered every one of them at
     25px with padding 0 and a computed min-height of 18px. Inputs and
     textareas on the same screens matched within 2px across engines, so this
     is specific to <select>, and no per-call-site `minHeight` can fix it while
     the native appearance is in force.
     `appearance: none` opts the control out of that native metric box, after
     which WebKit honours author padding/min-height exactly as Chromium does
     (measured: 25px -> 34px on Pipeline, 25px -> 40px on New proposal). The
     chevron is the one thing the native appearance was still buying us, so it
     is drawn back as a background image — `!important` because nearly every
     select in the app carries an inline `background:` shorthand (which resets
     background-image) and an inline `padding` that would otherwise let the
     text run under the glyph. Touch/narrow only: a desktop pointer keeps the
     platform-native control it has always had. */
  select:not([multiple]):not([size]) {
    -webkit-appearance: none;
    appearance: none;
    min-height: 36px;
    padding-right: 24px !important;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2363726A' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
    background-repeat: no-repeat !important;
    background-position: right 8px center !important;
    background-size: 9px 5.4px !important;
  }
  /* CAD workspace opts out: its toolbars are dense by design with their own touch handling */
  .cw-root button, .cw-host button { min-height: 0; min-width: 0; }
  .cw-root select, .cw-host select { min-height: 0; }
}

/* month view: the mobile agenda must stay hidden even when the phone media
   query would normally show it */
.fs-force-hide { display: none !important; }


/* Touch devices: small buttons ("Open", "View", "Enable", settings tabs…) were
   24–27px tall — under any thumb-target guideline. Give them a usable height
   without changing the desktop look. */
@media (pointer: coarse) {
  .fs-btn-sm { min-height: 34px; }
  .fs-btn { min-height: 38px; }
}

/* long unbroken names/emails must wrap inside cells and cards instead of clipping every column.
   TAB10-02: `break-word`, not `anywhere` — `anywhere` made every column's
   min-content ONE character, so table auto-layout squeezed text columns to 39px
   and printed "$9,800" one digit per line on iPad landscape. Money, numbers,
   dates and ids never break; a name column keeps room for a name. */
.fs-card td, .fs-card th { overflow-wrap: break-word; }
.fs-card .fs-wrap-any { overflow-wrap: anywhere; word-break: break-word; }
.fs-card td.mono, .fs-card th.mono, .fs-card td > .mono, .fs-card td.n, .fs-card th.n, .fs-card td.num, .fs-card td.nowrap { white-space: nowrap; overflow-wrap: normal; }
.fs-card th { white-space: nowrap; }
.fs-card td:first-child:not(.mono) { min-width: 120px; }

/* ── Touch-platform behaviour ─────────────────────────────────────────────
   On a phone the app IS the window: there is no browser chrome to absorb a
   stray gesture, so scrolling has to stay where the finger started. */

/* The app scrolls an inner <main>, not the window. Keeping that gesture
   contained stops a flick at the end of a list from triggering the browser's
   pull-to-refresh or bouncing the shell. */
.fs-main { overscroll-behavior: contain; }

/* Any scrollable pane keeps its gesture to itself (no chaining into the page,
   no accidental pull-to-refresh) and scrolls with momentum on iOS.
   ⚠ PER-AXIS ON PURPOSE. `overscroll-behavior: contain` applies to BOTH axes,
   and an element that cannot scroll vertically then SWALLOWS a vertical swipe
   instead of passing it up to <main>. That shipped, and it cost the Analytics
   screen: below 640px the wide tables reflow to blocks, so their
   `overflow-x: auto` wrappers grow 672–1016px tall with nothing to scroll in
   either direction — 2,527px of a 5,512px page became a gesture dead zone, and
   a flick that landed in one trapped the reader in both directions. A horizontal
   chip row did the same on Leads and Inbox. So: contain sideways everywhere,
   and contain vertically only on panes that genuinely scroll vertically. */
.fs-card [style*="overflow"], .fs-scroll, .fs-chip-row, .fs-filters,
.fs-ithread-log, .fs-cal-grid, .fs-cal-agenda, .fs-board {
  overscroll-behavior-x: contain;
  overscroll-behavior-y: auto;
  -webkit-overflow-scrolling: touch;
}
.fs-ithread-log, .fs-cal-grid, .fs-cal-agenda, .fs-scroll {
  overscroll-behavior-y: contain;
}

/* A grey flash on every tap reads as a rendering glitch; keep the app's own
   pressed states instead. Text stays selectable — a phone number or an address
   is something staff copy out constantly. */
@media (pointer: coarse) {
  button, a, [role="button"], .fs-btn { -webkit-tap-highlight-color: transparent; }
  button, [role="button"], .fs-btn { touch-action: manipulation; }
  /* L7: pressed states. They used to be `opacity: .6` on every button, link and
     row — whole cards and rows went see-through under the finger. Now (§7):
     buttons and tiles press in (scale .97 over 100ms, back on release), rows
     and clickable cards take the pressed-cell fill, icon-only buttons dim.
     An element with its own inline transform (a centred banner) keeps it:
     inline beats this rule, so it simply gets no press effect. */
  button, .fs-btn, .fs-more-tile { transition: transform var(--dur-release) var(--ease-ios); }
  button:active:not(:disabled):not([role="switch"]):not([role="tab"]):not([aria-label="Back"]),
  .fs-btn:active:not(:disabled),
  .fs-more-tile:active {
    transform: scale(0.97);
    transition-duration: var(--dur-press);
  }
  [role="button"]:not(button):active,
  .fs-card-btn:active,
  .fs-table tbody tr[role="button"]:active,
  .fs-table tbody tr[tabindex]:active {
    background-color: var(--cell-press) !important;
  }
  button[aria-label]:empty:active, button[aria-label="Back"]:active, .fs-topbar button[aria-label]:active { opacity: 0.5; }
}

/* ── Dynamic Type (iOS "Larger Text") ─────────────────────────────────────
   Every size in this app is a fixed px, so the accessibility text setting used
   to change literally nothing. `font: -apple-system-body` reports the user's
   chosen body size (17px at the default), and ui.js turns that into
   --fs-type-scale. It is clamped: the layout is built from fixed px boxes, so
   scaling past ~1.35 would push text out of its container instead of helping.
   Anything sized from the token scale or inheriting the body size follows it. */
/* body is set from --t-body (which already carries --fs-type-scale) at the top */
/* ── Book sheet on a short, wide screen (landscape phone) ──────────────────
   The sheet is one flex column: title, address, service type, day chips, slot
   grid, Cancel. On a 852x393 landscape phone that stack left the slot grid at
   its 160px floor with the card itself scrolling 89px underneath — two nested
   scrollers to reach a later time, on the screen where a booking is actually
   taken. Short AND wide is the condition (a portrait phone is narrow, an iPad
   is tall), so the width test keeps portrait and tablet layouts untouched.
   The three wrappers are `display: contents` by default, which means the
   portrait/desktop sheet renders exactly as it did before they existed. */
.fs-booksheet-body,
.fs-booksheet-form,
.fs-booksheet-slots { display: contents; }

@media (max-height: 520px) and (min-width: 680px) {
  /* use the width the screen actually has, and nearly all of its height */
  .fs-booksheet-card { width: min(760px, 100%) !important; max-height: 94dvh !important; }
  .fs-booksheet-body {
    display: grid !important;
    grid-template-columns: minmax(0, 260px) minmax(0, 1fr);
    gap: 14px;
    align-items: start;
    min-height: 0;
  }
  .fs-booksheet-form { display: flex !important; flex-direction: column; gap: 8px; min-width: 0; }
  .fs-booksheet-slots { display: block !important; min-width: 0; }
  /* the form no longer competes for this height, so give the slots the room */
  .fs-booksheet-slotbox { max-height: min(420px, 62dvh) !important; }
  /* The touch rule above pins every chip row to nowrap + sideways scroll, which
     in a 260px column shows two of the four days and hides Wed/Thu behind a
     horizontal swipe. Here the column has spare height and no horizontal room,
     so wrapping is strictly better — hence the !important, aimed at this sheet
     only. */
  .fs-booksheet-form .fs-chip-row {
    flex-wrap: wrap !important;
    overflow-x: visible;
  }
}

/* phone-1: a dialog that takes focus itself (so a screen reader announces it)
   shows no focus ring for that; its controls keep theirs */
[role="dialog"][tabindex="-1"]:focus { outline: none; }
main[tabindex="-1"]:focus { outline: none; }

/* ── Hardening round 4 ─────────────────────────────────────────────────── */
/* screen-reader-only text (the palette's live result count) */
.fs-sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* A11Y-4: an error tied to its field (FS_fieldError) */
.fs-field-err { color: var(--danger); font-size: var(--t-footnote); font-weight: 600; margin-top: 4px; }
[aria-invalid="true"] { border-color: var(--danger) !important; box-shadow: 0 0 0 1px var(--danger); }
/* A11Y-5: the toast live region is always in the page; empty, it takes no clicks */
.fs-toast-wrap.is-empty { pointer-events: none; min-height: 0; padding: 0; }
/* A11Y-3: <main> takes focus on a new screen — no ring for a pointer user */
main[tabindex="-1"]:focus:not(:focus-visible) { outline: none; }
/* A11Y-12 / R4-DAY-16: 44px targets on touch — the app's own standard */
@media (pointer: coarse) {
  .fs-btn, .fs-btn-sm { min-height: 44px; }
  .fs-chip-row > button, .fs-chip-row [role="button"], [role="tab"], .fsinv-chip, .cal-chip { min-height: 44px; }
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select { min-height: 44px; }
  .fs-sidebar nav button, .fs-nav-row, .fs-sidebar button[aria-label="Sign out"] { min-height: 44px; }
  .fs-sidebar button[aria-label="Sign out"] { min-width: 44px; justify-content: center; align-items: center; }
  .fs-topbar button[aria-label="Back"], .fs-ithread-more { min-width: 44px; min-height: 44px; }
  .fs-row-open { min-height: 44px; }
}

/* R8-A11Y-04: a sticky Save bar never hides the focused control (WCAG 2.4.11) —
   the scroll container keeps the bar's height clear when it scrolls focus into view */
.fs-main:has(.fs-sticky-actions), .fsh-body:has(.fs-sticky-actions) { scroll-padding-bottom: 96px; }

/* ══ Native layer (SPEC §1.8 materials, §2 platform rules, §4 lists/forms) ═══
   Everything below is additive: classes nothing uses yet (materials, nav-bar
   collapse), or phone-only restyles of existing markup. Desktop (>= 761px) is
   untouched unless a rule says so. */

/* ---- materials (§1.8): later packages put these on the nav bar, tab capsule,
   back circle, toasts and photo pills. Always both backdrop-filter spellings. */
.fs-glass {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
}
.fs-bar-material {
  background: var(--bar-bg);
  -webkit-backdrop-filter: var(--bar-blur);
  backdrop-filter: var(--bar-blur);
  box-shadow: 0 var(--hair) 0 var(--sep);
}
.fs-media-glass {
  background: var(--media-glass-bg);
  -webkit-backdrop-filter: var(--media-glass-blur);
  backdrop-filter: var(--media-glass-blur);
  box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.22);
  color: #fff;
}
.fs-scrim { background: var(--scrim); }
.fs-hero-surface { background: var(--hero-surface); box-shadow: var(--e2), inset 0 1px 0 rgba(255, 255, 255, 0.10); color: #fff; }
/* §4.1 scroll-linked collapse: AppShell writes --fs-nav-p (0..1) on .fs-app.
   The material fades in behind the bar; no text is ever transformed. */
.fs-nav-mat { position: relative; }
.fs-nav-mat::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: var(--bar-bg);
  -webkit-backdrop-filter: var(--bar-blur);
  backdrop-filter: var(--bar-blur);
  box-shadow: 0 var(--hair) 0 var(--sep);
  opacity: var(--fs-nav-p, 0);
}
.fs-nav-inline { opacity: var(--fs-nav-p, 0); }
/* Reduce Transparency: every glass surface goes opaque with a hairline */
@media (prefers-reduced-transparency: reduce) {
  .fs-glass, .fs-toast { background: var(--cell) !important; -webkit-backdrop-filter: none !important; backdrop-filter: none !important; box-shadow: inset 0 0 0 0.5px var(--sep) !important; }
  .fs-bar-material, .fs-nav-mat::before { background: var(--bg) !important; -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
}
/* Outdoor mode / Increase Contrast: opaque glass with a real 1px outline */
:root[data-fs-contrast="outdoor"] .fs-glass { background: var(--cell); -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: inset 0 0 0 1px var(--sep-opaque); }
:root[data-fs-contrast="outdoor"] .fs-bar-material, :root[data-fs-contrast="outdoor"] .fs-nav-mat::before { background: var(--bg); -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: 0 1px 0 var(--sep-opaque); }
@media (prefers-contrast: more) {
  :root:not([data-fs-contrast="off"]) .fs-glass { background: var(--cell); -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: inset 0 0 0 1px var(--sep-opaque); }
  :root:not([data-fs-contrast="off"]) .fs-bar-material, :root:not([data-fs-contrast="off"]) .fs-nav-mat::before { background: var(--bg); -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: 0 1px 0 var(--sep-opaque); }
}

/* ---- desktop focus (§2.2): rings only for the keyboard, never on a programmatic
   focus() (the daily-log close button ring, audit-screens §3.25) — the
   :focus-visible rules above already do this; nothing to add. */

/* ---- phone (§2.2 "phone" column). Same 760 constant as FS_DRAWER_MAX. ---- */
@media (max-width: 760px) {
  /* L8: borderless cells on the grouped background. 2,528 1px borders and no
     hairlines made every screen a stack of outlined boxes (the web-embed look). */
  .fs-card { border: 0; border-radius: var(--r-lg); box-shadow: var(--e1); }
  .fs-card[style*="border: 1px solid var(--border"] { border-style: none !important; }
  /* a card inside a card is a recessed well, not a second outline */
  .fs-card .fs-card { background: var(--cell-2); box-shadow: none; }
  .fs-card [style*="border: 1px solid var(--border"] { border-style: none !important; }
  .fs-card [style*="border: 1px solid var(--border"]:is([style*="background: var(--surface)"], [style*="background: rgb(255, 255, 255)"], [style*="background: white"], [style*="background: var(--cell)"], input, textarea, select, button):not([type="checkbox"]):not([type="radio"]) {
    background-color: var(--cell-2) !important;
  }
  /* separators inside a cell are hairlines (1 device px at 2x/3x) */
  .fs-main [style*="border-bottom: 1px solid var(--border"] { border-bottom-width: var(--hair) !important; }
  .fs-main [style*="border-top: 1px solid var(--border"] { border-top-width: var(--hair) !important; }

  /* §4.6 selects: the native picker stays (a real <select>), but the painted box
     goes — value text, an up-down glyph, no border. FSMenu (fs-ios.js) is the
     row form of the same thing. */
  select:not([multiple]):not([size]) {
    border-style: none !important;
    background-color: transparent !important;
    color: var(--label-2);
    padding-right: 26px !important;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 16'%3E%3Cpath d='M1.5 6l3.5-3.5L8.5 6M1.5 10l3.5 3.5L8.5 10' fill='none' stroke='%2356655D' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
    background-position: right 8px center !important;
    background-size: 10px 16px !important;
  }
  /* date / time: an accent capsule over the native picker (§3.3 FSFormRow) */
  input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"] {
    -webkit-appearance: none;
    appearance: none;
    border-style: none !important;
    border-radius: var(--r-pill) !important;
    background: var(--fill-3) !important;
    color: var(--accent-text) !important;
    padding: 0 14px !important;
    min-height: 44px;
    font-variant-numeric: tabular-nums;
  }
  input[type="date"]::-webkit-date-and-time-value, input[type="time"]::-webkit-date-and-time-value,
  input[type="datetime-local"]::-webkit-date-and-time-value { text-align: left; }
  /* browser-default links ("set up your price book", "Add custom fields") */
  .fs-main a[href]:not([class]) { color: var(--accent-text); text-decoration: none; }
  .fs-main div > a[href]:not([class]) { display: inline-flex; align-items: center; min-height: 44px; }

  /* chrome is not text: no callout / selection on the bars; content stays selectable */
  .fs-topbar, .fs-tabbar, .fs-more-head { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
}
@media (max-width: 760px) and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select:not([multiple]):not([size]) {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 16'%3E%3Cpath d='M1.5 6l3.5-3.5L8.5 6M1.5 10l3.5 3.5L8.5 10' fill='none' stroke='%23A9B5AF' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
  }
}
@media (max-width: 760px) {
  :root[data-theme="dark"] select:not([multiple]):not([size]) {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 16'%3E%3Cpath d='M1.5 6l3.5-3.5L8.5 6M1.5 10l3.5 3.5L8.5 10' fill='none' stroke='%23A9B5AF' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
  }
}
/* tablet (touch, > 760): borderless cards keep a 0.5px outline (§2.2) */
@media (min-width: 761px) and (pointer: coarse) {
  .fs-card { border-width: var(--hair); }
}

/* phone: bare <b>/<strong> inside the app is 600 (SPEC §1.2: no weight of 700 or
   more below the title sizes); a heading that wants 700 sets it on itself */
@media (max-width: 760px) {
  .fs-app b, .fs-app strong { font-weight: 600; }
}

/* ---- dark safety net. A few dozen screens still paint a literal white box
   (inline background #fff) around text coloured with the tokens, which in dark
   would be light text on white. Containers only: the switch knobs are spans,
   and signatures, QR codes and customer-page previews (img / iframe / canvas)
   must stay white. The screen packages replace the literals; this keeps dark
   readable until they land. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .fs-app :is(div, section, article, li, button, label, textarea, input, table, td, tr)[style*="background: rgb(255, 255, 255)"],
  :root:not([data-theme="light"]) .fs-app :is(div, section, article, li, button, label, textarea, input, table, td, tr)[style*="background: white"],
  :root:not([data-theme="light"]) .fs-app :is(div, section, article, li, button, label, textarea, input, table, td, tr)[style*="background-color: rgb(255, 255, 255)"] { background-color: var(--cell) !important; }
}
:root[data-theme="dark"] .fs-app :is(div, section, article, li, button, label, textarea, input, table, td, tr)[style*="background: rgb(255, 255, 255)"],
:root[data-theme="dark"] .fs-app :is(div, section, article, li, button, label, textarea, input, table, td, tr)[style*="background: white"],
:root[data-theme="dark"] .fs-app :is(div, section, article, li, button, label, textarea, input, table, td, tr)[style*="background-color: rgb(255, 255, 255)"] { background-color: var(--cell) !important; }
/* …and white text on the accent: the dark accent is a light mint, so a literal
   white label on it (the primary button, the selected segment) drops to ~2:1.
   It takes --on-accent (9.2:1) instead. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) :is([style*="background: var(--accent)"], [style*="background-color: var(--accent)"]):is([style*="color: rgb(255, 255, 255)"], [style*="color: white"], [style*="color: var(--ink-strong)"]):not([style*="background: var(--accent-"]) { color: var(--on-accent) !important; }
}
:root[data-theme="dark"] :is([style*="background: var(--accent)"], [style*="background-color: var(--accent)"]):is([style*="color: rgb(255, 255, 255)"], [style*="color: white"], [style*="color: var(--ink-strong)"]):not([style*="background: var(--accent-"]) { color: var(--on-accent) !important; }

/* ── TAB10-01: printing any app screen ─────────────────────────────────────
   The shell is 100dvh tall and the page scrolls INSIDE .fs-main, so a print
   stopped after page 1 (a work order lost its pier log and sign-off; AR aging
   printed 25 of 92 rows and no total) and carried the sidebar, top bar and
   banners. On paper the content flows at full height and the chrome is gone. */
@media print {
  html, body, #root { height: auto !important; min-height: 0 !important; overflow: visible !important; background: #fff !important; }
  .fs-app { height: auto !important; overflow: visible !important; display: block !important; contain: none !important; position: static !important; }
  .fs-app > div { display: block !important; overflow: visible !important; height: auto !important; }
  .fs-main { height: auto !important; overflow: visible !important; display: block !important; container-type: normal !important; flex: none !important; padding-bottom: 0 !important; }
  aside.fs-sidebar, .fs-sidebar, .fs-topbar, .fs-tabbar, .fs-notices, .fs-pushbanner, .fs-pagehint, .fs-toast-wrap, #fs-update-bar,
  .fs-sticky-actions, .fs-nav-backdrop, .fs-cmd-wrap, .fs-noprint, .fs-mtabs { display: none !important; }
  .fs-card { box-shadow: none !important; }
  .fs-card table tr { break-inside: avoid; }
  /* a work order: its sections stay whole where they fit; the buttons stay off paper */
  .fc-act, .fc-acts, .fc-actions, .fc-primary, .wo-acts { display: none !important; }
  .fc-sec, .wo-sec { break-inside: avoid; }
}

/* ══ W5-B: the desktop + iPad shell (qa/work-integrator/wave5/desktop-design.md §2.2, §2.4) ══
   The rail and the top bar share one quiet frame (--shell); the page sits on a
   rounded canvas (--canvas); cards are --card with a 1px --card-edge. Light values
   hold everywhere (the phone never paints a shell surface); from 761px up dark
   mode moves from Harbor's OLED black to Harbor Night navy — the phone keeps its
   true black (the dark blocks below are all min-width: 761px). */
:root {
  --shell: #E9EEF5;
  --canvas: #F6F8FB;
  --canvas-edge: rgba(12, 21, 36, 0.08);
  --card: #FFFFFF;
  --card-edge: rgba(12, 21, 36, 0.09);
  --card-hover: #F7F9FC;
  --well: #F3F6FA;
  --rail-hover: rgba(116, 124, 140, 0.14);
  --rail-active: rgba(10, 111, 160, 0.12);
  --search-bg: #FFFFFF;
  --search-edge: rgba(12, 21, 36, 0.12);
  --kbd: rgba(116, 124, 140, 0.14);
  --shell-float: 0 12px 40px rgba(14, 26, 48, 0.18), 0 2px 6px rgba(14, 26, 48, 0.06);
  --illo-a: #D6E8F3; --illo-b: var(--accent); --illo-c: #FFFFFF;
  /* text on the red count badge, and the white glyph on a module squircle */
  --on-red: #FFFFFF;
  --on-mod: #FFFFFF;
  /* module squircles keep their LIGHT colour in both themes (color-research §3.3) */
  --mod-accent: #0A6FA0; --mod-blue: #0A6FA0; --mod-orange-fill: #C4620A; --mod-orange: #A4520A; --mod-indigo: #4A48C9;
  --mod-purple: #7450C8; --mod-green: #1F7A3A; --mod-red: #C42B21; --mod-teal: #0E8A7E; --mod-brown: #8C6440;
  --mod-pink: #C23A6B; --mod-gray: #6B7280;
  /* the FollowStack mark (white-label companies show their own logo on the same squircle) */
  --logo-bg: linear-gradient(150deg, #1D4E80, #0E1A30 70%);
  --logo-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 1px 2px rgba(14, 26, 48, 0.25);
  --logo-a: #FFFFFF; --logo-b: #B8DEFF; --logo-c: #5FBCF4;
  --avatar-me: linear-gradient(135deg, #177AAE, #0A6FA0 45%, #0E1A30);
  /* secondary text on the dark hero (5.27:1 at the glow centre, color-research §3.5) */
  --hero-soft: rgba(255, 255, 255, 0.72);
  /* sizes: desktop (pointer) */
  --rail-w: 80px; --side-w: 264px; --top-h: 56px;
  --rail-item-w: 64px; --rail-item-h: 54px; --rail-pin-h: 50px; --rail-label: 11px;
  --ibtn: 36px; --btn-h: 32px; --btn-r: 8px; --btn-t: 13px; --av-me: 32px;
  --fly-row-h: 38px; --menu-row-h: 38px; --tab-h: 34px; --tab-t: 14px;
  --home-col: 1152px; --home-gap: 16px; --card-r: 14px; --row-h: 56px;
}
/* iPad (touch): every target ≥ 44 (SPEC §1.7) */
:root[data-fs-layout="tablet"] {
  --rail-w: 88px; --top-h: 60px;
  --rail-item-w: 72px; --rail-item-h: 60px; --rail-pin-h: 56px; --rail-label: 12px;
  --ibtn: 44px; --btn-h: 44px; --btn-r: 10px; --btn-t: 15px; --av-me: 36px;
  --fly-row-h: 48px; --menu-row-h: 48px; --tab-h: 44px; --tab-t: 15px;
  --card-r: 16px; --row-h: 64px;
}
/* Harbor Night, desktop and iPad only. KEEP THE TWO BLOCKS IN SYNC. */
@media (min-width: 761px) and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0B1222; --bg-plain: #0B1222; --cell: #111A2C; --cell-2: #18233A; --cell-press: #1E2A42; --sep-opaque: #26324A;
    --bar-bg: rgba(7, 12, 24, 0.78);
    --shell: #070C18; --canvas: #0B1222; --canvas-edge: rgba(200, 215, 240, 0.08);
    --card: #111A2C; --card-edge: rgba(200, 215, 240, 0.10); --card-hover: #152036; --well: #18233A;
    --rail-hover: rgba(200, 215, 240, 0.08); --rail-active: rgba(85, 182, 236, 0.16);
    --search-bg: #111A2C; --search-edge: rgba(200, 215, 240, 0.12); --kbd: rgba(200, 215, 240, 0.10);
    --float: 0 16px 48px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(200, 215, 240, 0.10);
    --illo-a: #1B3553; --illo-c: #111A2C; --on-red: #1A0503;
  }
}
@media (min-width: 761px) {
  :root[data-theme="dark"] {
    --bg: #0B1222; --bg-plain: #0B1222; --cell: #111A2C; --cell-2: #18233A; --cell-press: #1E2A42; --sep-opaque: #26324A;
    --bar-bg: rgba(7, 12, 24, 0.78);
    --shell: #070C18; --canvas: #0B1222; --canvas-edge: rgba(200, 215, 240, 0.08);
    --card: #111A2C; --card-edge: rgba(200, 215, 240, 0.10); --card-hover: #152036; --well: #18233A;
    --rail-hover: rgba(200, 215, 240, 0.08); --rail-active: rgba(85, 182, 236, 0.16);
    --search-bg: #111A2C; --search-edge: rgba(200, 215, 240, 0.12); --kbd: rgba(200, 215, 240, 0.10);
    --float: 0 16px 48px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(200, 215, 240, 0.10);
    --illo-a: #1B3553; --illo-c: #111A2C; --on-red: #1A0503;
  }
  /* light: floating things get the shell's softer, two-layer float */
  :root:not([data-theme="dark"]) { --float: var(--shell-float); }
}
@media (min-width: 761px) and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="dark"]) { --float: 0 16px 48px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(200, 215, 240, 0.10); }
}

/* VIS11-03: on a phone the page never pans sideways. Any stray overflow (a long
   label, an invisible <select>) used to become a horizontal drag of the whole
   screen, because .fs-main scrolls. hidden (not clip — the other axis scrolls)
   keeps position: sticky working inside the scroller. */
@media (max-width: 759px) {
  .fs-main { overflow-x: hidden; }
}

/* VIS11-04 / VIS11-08: a full-bleed screen (no content padding) that shows a
   large title keeps the title and its docked switch on the page margin */
@media (max-width: 759px) {
  .fs-main:not(.fs-main-pad) > .fs-lt, .fs-main:not(.fs-main-pad) > .fs-acc-slot { padding-left: var(--margin, 16px); padding-right: var(--margin, 16px); }
}
