/* ==========================================================================
   RunSheets — theme
   Rounded, fluid UI system with a light + dark mode, both built from the same
   CSS custom properties. Component classes are referenced directly by
   app.js — keep class names stable even if visual values change.

   Theme switching: app.js sets data-theme="light"|"dark" on <html> (and
   persists the choice to localStorage). Light is the default look (no
   attribute needed) — flipped from RunSheets' original dark-only default
   2026-08-24; [data-theme="dark"] below overrides only the tokens that need
   to change for a dark surface. Everything else (buttons, gradients, brand
   marks) references the same --accent/--blue/etc. tokens in both modes, so
   it never needs to be touched.
   ========================================================================== */

:root {
  --bg: #f3f5fb;
  --bg-soft: #ffffff;
  --surface: #ffffffb3;
  --surface-solid: #ffffff;
  --surface-2: #eef1f8;
  --surface-hover: #e6eaf5;
  --border: #e1e5f0;
  --border-soft: #ebeef5;
  --text: #171a24;
  --text-dim: #5b6377;
  --text-faint: #8991a8;

  --accent: #6d6bf7;
  --accent-2: #8b7bf6;
  --accent-hover: #7d7bff;
  --accent-soft: rgba(109, 107, 247, .11);
  --accent-ring: rgba(109, 107, 247, .28);

  --blue: #4b9bff;
  --blue-soft: rgba(75, 155, 255, .15);
  --amber: #f5a524;
  --amber-soft: rgba(245, 165, 36, .16);
  --red: #f3596b;
  --red-soft: rgba(243, 89, 107, .16);
  --green: #35d391;
  --green-soft: rgba(53, 211, 145, .16);
  --purple: #c084fc;
  --purple-soft: rgba(192, 132, 252, .16);
  --slate: #94a3b8;
  --slate-soft: rgba(148, 163, 184, .14);
  --cyan: #22d3ee;
  --cyan-soft: rgba(34, 211, 238, .16);

  /* Readable text-on-soft-background pairs (badges, banners, active nav).
     The "-soft" backgrounds above are alpha-based and look fine in either
     mode, but a pastel-on-dark text color goes nearly invisible on a pale
     light-mode chip — these give each pairing a mode-appropriate ink.
     Light-mode values here match what was previously hardcoded inline. */
  --on-accent-soft: #4f3fc4;
  --on-accent-icon: #6d28d9;
  --on-blue-soft: #1d5fa8;
  --on-cyan-soft: #0e7490;
  --on-amber-soft: #92400e;
  --on-purple-soft: #7e22ce;
  --on-green-soft: #047857;
  --on-red-soft: #b91c1c;
  --on-slate-soft: #475569;

  /* Fixed status scale (SLA health) — same hex in both modes by design,
     never themed, always paired with an icon + label. */
  --status-good: #0ca30c;
  --status-warning: #fab219;
  --status-serious: #ec835a;
  --status-critical: #d03b3b;

  /* Dashboard chart series — a validated 9-hue categorical palette (see
     design:dataviz skill), stepped for the light card surface below.
     Assign in this fixed order; never cycle or re-hue per chart. Kept at
     parity with STATUS_ORDER in app.js's renderDashboardCharts_() (9
     ticket statuses) — added --chart-9 2026-09-07 after a real bug where
     a 9-label donut (Tickets by Status) ran out of colors at index 8 and
     rendered the last slice (Closed) as an undefined/black swatch. */
  --chart-1: #2a78d6;
  --chart-2: #eb6834;
  --chart-3: #1baf7a;
  --chart-4: #eda100;
  --chart-5: #e87ba4;
  --chart-6: #008300;
  --chart-7: #4a3aa7;
  --chart-8: #e34948;
  --chart-9: #0c8599;
  --chart-grid: var(--border-soft);
  --chart-axis: var(--text-faint);
  --chart-tooltip-bg: #ffffff;
  --chart-tooltip-border: var(--border);

  --radius-xl: 22px;
  --radius-lg: 16px;
  --radius-md: 12px;
  --radius-sm: 9px;
  --radius-pill: 999px;

  --shadow-sm: 0 1px 2px rgba(23,26,36,.06);
  --shadow-md: 0 8px 24px -8px rgba(23,26,36,.16), 0 2px 6px rgba(23,26,36,.07);
  --shadow-lg: 0 24px 60px -16px rgba(23,26,36,.22), 0 4px 16px rgba(23,26,36,.10);

  --ease: cubic-bezier(.4, 0, .2, 1);
}

/* ---- Dark mode ----
   Toggled via <html data-theme="dark">. Base surfaces/text/borders switch;
   solid brand colors (--accent/--blue/--amber/...) stay the same hex in
   both modes (they're already saturated enough to read on either surface),
   so only the tokens that actually need a different value for a dark
   background are redeclared here. */
:root[data-theme="dark"] {
  --bg: #0a0d15;
  --bg-soft: #0d1119;
  --surface: #12172380;
  --surface-solid: #131826;
  --surface-2: #1a2032;
  --surface-hover: #1e2538;
  --border: #232a3d;
  --border-soft: #1b2233;
  --text: #eef1f8;
  --text-dim: #9aa4bc;
  --text-faint: #6b7590;

  --accent-soft: rgba(109, 107, 247, .16);
  --accent-ring: rgba(109, 107, 247, .35);

  --on-accent-soft: #b7b5ff;
  --on-accent-icon: #a5a3ff;
  --on-blue-soft: #8fc3ff;
  --on-cyan-soft: #67e8f9;
  --on-amber-soft: #ffc266;
  --on-purple-soft: #d9adff;
  --on-green-soft: #77e8b5;
  --on-red-soft: #ff97a4;
  --on-slate-soft: #b7c0d4;

  --chart-1: #3987e5; /* blue */
  --chart-2: #d95926; /* orange */
  --chart-3: #199e70; /* aqua */
  --chart-4: #c98500; /* gold */
  --chart-5: #d55181; /* magenta */
  --chart-6: #008300; /* green */
  --chart-7: #9085e9; /* violet */
  --chart-8: #e66767; /* red */
  --chart-9: #22b8cf; /* cyan */
  --chart-tooltip-bg: var(--surface-2);

  --shadow-sm: 0 1px 2px rgba(0,0,0,.35);
  --shadow-md: 0 8px 24px -8px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.3);
  --shadow-lg: 0 24px 60px -16px rgba(0,0,0,.6), 0 4px 16px rgba(0,0,0,.35);
}

/* ==========================================================================
   Color Theme Selector -- curated palettes (2026-09-07)
   Layers on top of the light/dark tokens above via a second attribute,
   [data-palette="..."] on <html>, set alongside the existing [data-theme].
   A palette ONLY re-declares hue tokens (accent/blue/amber/red/green/
   purple/slate/cyan, their -soft/on-*-soft pairs, --status-*, --chart-*)
   plus --border/--border-soft -- it never touches --bg/--surface/--text/
   --radius/--shadow, so a palette layers over any Industry Template
   (General Field Service/HVAC/IT Helpdesk/etc.) without moving or
   resizing anything, only recoloring it. High Contrast is the one
   exception: by definition it also strengthens --bg/--surface/--text/
   --border for maximum legibility. See site/app.js's COLOR_THEMES_ /
   applyColorTheme_() and the Settings > Color Theme section in
   login.html. Selecting "Default" clears data-palette entirely, so a
   fresh install with no attribute at all renders exactly as it always
   has -- these blocks are pure additions, nothing above was changed
   except the new --accent-strong token both button classes below now
   use (was `#fff` on bare --accent, which measures under WCAG AA for
   white button text -- see docs/00_ARCHITECTURE_AND_SCHEMA.md Sec 8). */

/* ---- Default palette additions (accent-strong, see the Color Theme
   Selector feature doc) -- mode-independent like --accent/--blue/etc. ---- */
:root {
  --accent-strong: #4b3fc0;
}

/* ---- High Contrast palette ---- */
:root[data-palette="high-contrast"] {
  --bg: #ffffff;
  --bg-soft: #ffffff;
  --surface-2: #eeeeee;
  --border: #000000;
  --border-soft: #000000;
  --text: #000000;
  --text-dim: #1a1a1a;

  --accent: #3626a7;
  --accent-2: #3626a7;
  --accent-strong: #2e2280;
  --accent-soft: rgba(54, 38, 167, 0.18);
  --on-accent-soft: #000000;
  --on-accent-icon: #000000;

  --blue: #0a4a8f;
  --blue-soft: rgba(10, 74, 143, 0.18);
  --on-blue-soft: #000000;
  --amber: #7a4a00;
  --amber-soft: rgba(122, 74, 0, 0.2);
  --on-amber-soft: #000000;
  --red: #a3001e;
  --red-soft: rgba(163, 0, 30, 0.18);
  --on-red-soft: #000000;
  --green: #0a6b2e;
  --green-soft: rgba(10, 107, 46, 0.18);
  --on-green-soft: #000000;
  --purple: #6c1f9e;
  --purple-soft: rgba(108, 31, 158, 0.18);
  --on-purple-soft: #000000;
  --slate: #33414f;
  --slate-soft: rgba(51, 65, 79, 0.18);
  --on-slate-soft: #000000;
  --cyan: #00636e;
  --cyan-soft: rgba(0, 99, 110, 0.18);
  --on-cyan-soft: #000000;

  --status-good: #0a6b2e;
  --status-warning: #7a4a00;
  --status-serious: #a34500;
  --status-critical: #a3001e;

  --chart-1: #0a4a8f;
  --chart-2: #a34500;
  --chart-3: #0a6b2e;
  --chart-4: #7a4a00;
  --chart-5: #a3001e;
  --chart-6: #3626a7;
  --chart-7: #6c1f9e;
  --chart-8: #00636e;
  --chart-9: #33414f;
}
:root[data-theme="dark"][data-palette="high-contrast"] {
  --bg: #000000;
  --bg-soft: #0a0a0a;
  --surface-2: #161616;
  --border: #ffffff;
  --border-soft: #ffffff;
  --text: #ffffff;
  --text-dim: #e6e6e6;

  --accent: #b7b0ff;
  --accent-2: #b7b0ff;
  --accent-strong: #2e2280;
  --accent-soft: rgba(183, 176, 255, 0.3);
  --on-accent-soft: #ffffff;
  --on-accent-icon: #ffffff;

  --blue: #7fc4ff;
  --blue-soft: rgba(127, 196, 255, 0.3);
  --on-blue-soft: #ffffff;
  --amber: #ffc94d;
  --amber-soft: rgba(255, 201, 77, 0.3);
  --on-amber-soft: #ffffff;
  --red: #ff8a94;
  --red-soft: rgba(255, 138, 148, 0.3);
  --on-red-soft: #ffffff;
  --green: #5fe08a;
  --green-soft: rgba(95, 224, 138, 0.3);
  --on-green-soft: #ffffff;
  --purple: #e2a6ff;
  --purple-soft: rgba(226, 166, 255, 0.3);
  --on-purple-soft: #ffffff;
  --slate: #c7d1db;
  --slate-soft: rgba(199, 209, 219, 0.3);
  --on-slate-soft: #ffffff;
  --cyan: #7fe6ef;
  --cyan-soft: rgba(127, 230, 239, 0.3);
  --on-cyan-soft: #ffffff;

  --status-good: #5fe08a;
  --status-warning: #ffc94d;
  --status-serious: #ffab6b;
  --status-critical: #ff8a94;

  --chart-1: #7fc4ff;
  --chart-2: #ffab6b;
  --chart-3: #5fe08a;
  --chart-4: #ffc94d;
  --chart-5: #ff8a94;
  --chart-6: #b7b0ff;
  --chart-7: #e2a6ff;
  --chart-8: #7fe6ef;
  --chart-9: #c7d1db;
}

/* ---- Professional Corporate palette ---- */
:root[data-palette="corporate"] {
  --border: #c3cbd6;
  --border-soft: #c3cbd6;

  --accent: #2f4d7a;
  --accent-2: #2f4d7a;
  --accent-strong: #22385c;
  --accent-soft: rgba(47, 77, 122, 0.12);
  --on-accent-soft: #22385c;
  --on-accent-icon: #22385c;

  --blue: #3b6ea5;
  --blue-soft: rgba(59, 110, 165, 0.15);
  --on-blue-soft: #1f4463;
  --amber: #9c6b0c;
  --amber-soft: rgba(156, 107, 12, 0.16);
  --on-amber-soft: #7a5209;
  --red: #a5453f;
  --red-soft: rgba(165, 69, 63, 0.16);
  --on-red-soft: #7f2f2a;
  --green: #2f7d63;
  --green-soft: rgba(47, 125, 99, 0.16);
  --on-green-soft: #215c48;
  --purple: #5c5185;
  --purple-soft: rgba(92, 81, 133, 0.16);
  --on-purple-soft: #453b68;
  --slate: #64748b;
  --slate-soft: rgba(100, 116, 139, 0.16);
  --on-slate-soft: #42505f;
  --cyan: #1f7a8c;
  --cyan-soft: rgba(31, 122, 140, 0.16);
  --on-cyan-soft: #155a68;

  --status-good: #2f7d63;
  --status-warning: #9c6b0c;
  --status-serious: #b5602a;
  --status-critical: #a5453f;

  --chart-1: #2f4d7a;
  --chart-2: #b5602a;
  --chart-3: #2f7d63;
  --chart-4: #9c6b0c;
  --chart-5: #8a4a68;
  --chart-6: #3d6b3f;
  --chart-7: #5c5185;
  --chart-8: #a5453f;
  --chart-9: #1f7a8c;
}
:root[data-theme="dark"][data-palette="corporate"] {
  --border: #3d4c60;
  --border-soft: #3d4c60;

  --accent: #7fa3d9;
  --accent-2: #7fa3d9;
  --accent-strong: #22385c;
  --accent-soft: rgba(127, 163, 217, 0.18);
  --on-accent-soft: #dce8fb;
  --on-accent-icon: #dce8fb;

  --blue: #7db8e8;
  --blue-soft: rgba(125, 184, 232, 0.18);
  --on-blue-soft: #d6ecfb;
  --amber: #d9a441;
  --amber-soft: rgba(217, 164, 65, 0.18);
  --on-amber-soft: #ffdf9e;
  --red: #d97c76;
  --red-soft: rgba(217, 124, 118, 0.18);
  --on-red-soft: #ffcac6;
  --green: #6cbfa3;
  --green-soft: rgba(108, 191, 163, 0.18);
  --on-green-soft: #bdeee0;
  --purple: #a79bd1;
  --purple-soft: rgba(167, 155, 209, 0.18);
  --on-purple-soft: #e3dcf7;
  --slate: #9fb0c4;
  --slate-soft: rgba(159, 176, 196, 0.18);
  --on-slate-soft: #dbe4ee;
  --cyan: #6fb6c4;
  --cyan-soft: rgba(111, 182, 196, 0.18);
  --on-cyan-soft: #c1e9ef;

  --status-good: #6cbfa3;
  --status-warning: #d9a441;
  --status-serious: #dd9257;
  --status-critical: #d97c76;

  --chart-1: #7fa3d9;
  --chart-2: #dd9257;
  --chart-3: #6cbfa3;
  --chart-4: #d9a441;
  --chart-5: #c992ac;
  --chart-6: #7fbf8f;
  --chart-7: #a79bd1;
  --chart-8: #d97c76;
  --chart-9: #6fb6c4;
}

/* ---- Field Service palette ---- */
:root[data-palette="field-service"] {
  --border: #c9bf9f;
  --border-soft: #c9bf9f;

  --accent: #c14a12;
  --accent-2: #c14a12;
  --accent-strong: #8a3308;
  --accent-soft: rgba(193, 74, 18, 0.16);
  --on-accent-soft: #8f3609;
  --on-accent-icon: #8f3609;

  --blue: #1e6fbf;
  --blue-soft: rgba(30, 111, 191, 0.16);
  --on-blue-soft: #144d87;
  --amber: #a06a00;
  --amber-soft: rgba(160, 106, 0, 0.2);
  --on-amber-soft: #7a5100;
  --red: #c62828;
  --red-soft: rgba(198, 40, 40, 0.16);
  --on-red-soft: #961d1d;
  --green: #2e7d32;
  --green-soft: rgba(46, 125, 50, 0.16);
  --on-green-soft: #215c24;
  --purple: #6a4c93;
  --purple-soft: rgba(106, 76, 147, 0.16);
  --on-purple-soft: #4f3970;
  --slate: #6b6455;
  --slate-soft: rgba(107, 100, 85, 0.16);
  --on-slate-soft: #4c4738;
  --cyan: #0f7d8c;
  --cyan-soft: rgba(15, 125, 140, 0.16);
  --on-cyan-soft: #0b5c67;

  --status-good: #2e7d32;
  --status-warning: #a06a00;
  --status-serious: #c1560f;
  --status-critical: #c62828;

  --chart-1: #1e6fbf;
  --chart-2: #c1560f;
  --chart-3: #2e7d32;
  --chart-4: #a06a00;
  --chart-5: #a3486c;
  --chart-6: #5a7d1e;
  --chart-7: #6a4c93;
  --chart-8: #c62828;
  --chart-9: #0f7d8c;
}
:root[data-theme="dark"][data-palette="field-service"] {
  --border: #5c4f38;
  --border-soft: #5c4f38;

  --accent: #ff8c52;
  --accent-2: #ff8c52;
  --accent-strong: #8a3308;
  --accent-soft: rgba(255, 140, 82, 0.22);
  --on-accent-soft: #ffd2b0;
  --on-accent-icon: #ffd2b0;

  --blue: #5fa8ea;
  --blue-soft: rgba(95, 168, 234, 0.22);
  --on-blue-soft: #c8e2fb;
  --amber: #ffc94d;
  --amber-soft: rgba(255, 201, 77, 0.22);
  --on-amber-soft: #ffe6a6;
  --red: #ff6b5e;
  --red-soft: rgba(255, 107, 94, 0.22);
  --on-red-soft: #ffc2ba;
  --green: #6fce74;
  --green-soft: rgba(111, 206, 116, 0.22);
  --on-green-soft: #c4f0c6;
  --purple: #b49bdc;
  --purple-soft: rgba(180, 155, 220, 0.22);
  --on-purple-soft: #e6dbf6;
  --slate: #c9bfa6;
  --slate-soft: rgba(201, 191, 166, 0.22);
  --on-slate-soft: #ece6d7;
  --cyan: #5fcfe0;
  --cyan-soft: rgba(95, 207, 224, 0.22);
  --on-cyan-soft: #c0eef4;

  --status-good: #6fce74;
  --status-warning: #ffc94d;
  --status-serious: #ffa25c;
  --status-critical: #ff6b5e;

  --chart-1: #5fa8ea;
  --chart-2: #ffa25c;
  --chart-3: #6fce74;
  --chart-4: #ffc94d;
  --chart-5: #e08fb0;
  --chart-6: #a4d15c;
  --chart-7: #b49bdc;
  --chart-8: #ff6b5e;
  --chart-9: #5fcfe0;
}

/* ---- Deuteranopia-Optimized palette ---- */
:root[data-palette="deuteranopia"] {
  --border: #b9bac2;
  --border-soft: #b9bac2;

  --accent: #5b4ea6;
  --accent-2: #5b4ea6;
  --accent-strong: #3d3376;
  --accent-soft: rgba(91, 78, 166, 0.14);
  --on-accent-soft: #40357f;
  --on-accent-icon: #40357f;

  --blue: #0072b2;
  --blue-soft: rgba(0, 114, 178, 0.16);
  --on-blue-soft: #00507d;
  --amber: #a6650c;
  --amber-soft: rgba(166, 101, 12, 0.18);
  --on-amber-soft: #7d4c09;
  --red: #c1470c;
  --red-soft: rgba(193, 71, 12, 0.18);
  --on-red-soft: #8f3509;
  --green: #00795e;
  --green-soft: rgba(0, 121, 94, 0.16);
  --on-green-soft: #00584a;
  --purple: #a5457f;
  --purple-soft: rgba(165, 69, 127, 0.16);
  --on-purple-soft: #7c335f;
  --slate: #6b7280;
  --slate-soft: rgba(107, 114, 128, 0.16);
  --on-slate-soft: #454b54;
  --cyan: #2e93b5;
  --cyan-soft: rgba(46, 147, 181, 0.16);
  --on-cyan-soft: #1f6c85;

  --status-good: #00795e;
  --status-warning: #a6650c;
  --status-serious: #c1470c;
  --status-critical: #8f3509;

  --chart-1: #0072b2;
  --chart-2: #c1470c;
  --chart-3: #00795e;
  --chart-4: #a6650c;
  --chart-5: #a5457f;
  --chart-6: #5b4ea6;
  --chart-7: #2e93b5;
  --chart-8: #8f3509;
  --chart-9: #6b7280;
}
:root[data-theme="dark"][data-palette="deuteranopia"] {
  --border: #454e5f;
  --border-soft: #454e5f;

  --accent: #a99cf0;
  --accent-2: #a99cf0;
  --accent-strong: #3d3376;
  --accent-soft: rgba(169, 156, 240, 0.22);
  --on-accent-soft: #e2dcfb;
  --on-accent-icon: #e2dcfb;

  --blue: #5cb6f0;
  --blue-soft: rgba(92, 182, 240, 0.22);
  --on-blue-soft: #cfe9fc;
  --amber: #f0a83e;
  --amber-soft: rgba(240, 168, 62, 0.22);
  --on-amber-soft: #ffdca0;
  --red: #f0844a;
  --red-soft: rgba(240, 132, 74, 0.22);
  --on-red-soft: #ffcfad;
  --green: #3fbd9c;
  --green-soft: rgba(63, 189, 156, 0.22);
  --on-green-soft: #bdf0e2;
  --purple: #e08cc0;
  --purple-soft: rgba(224, 140, 192, 0.22);
  --on-purple-soft: #f7d9ec;
  --slate: #aab2c0;
  --slate-soft: rgba(170, 178, 192, 0.22);
  --on-slate-soft: #dfe3ea;
  --cyan: #5fc3dd;
  --cyan-soft: rgba(95, 195, 221, 0.22);
  --on-cyan-soft: #c4edf6;

  --status-good: #3fbd9c;
  --status-warning: #f0a83e;
  --status-serious: #f0844a;
  --status-critical: #ff9d78;

  --chart-1: #5cb6f0;
  --chart-2: #f0844a;
  --chart-3: #3fbd9c;
  --chart-4: #f0a83e;
  --chart-5: #e08cc0;
  --chart-6: #a99cf0;
  --chart-7: #5fc3dd;
  --chart-8: #ff9d78;
  --chart-9: #aab2c0;
}

/* ---- Protanopia-Optimized palette ---- */
:root[data-palette="protanopia"] {
  --border: #b9bac2;
  --border-soft: #b9bac2;

  --accent: #3f5aa6;
  --accent-2: #3f5aa6;
  --accent-strong: #2b3d73;
  --accent-soft: rgba(63, 90, 166, 0.14);
  --on-accent-soft: #2c3f78;
  --on-accent-icon: #2c3f78;

  --blue: #0067a3;
  --blue-soft: rgba(0, 103, 163, 0.16);
  --on-blue-soft: #004a75;
  --amber: #a06d00;
  --amber-soft: rgba(160, 109, 0, 0.18);
  --on-amber-soft: #7a5400;
  --red: #c9600c;
  --red-soft: rgba(201, 96, 12, 0.18);
  --on-red-soft: #96470a;
  --green: #00806a;
  --green-soft: rgba(0, 128, 106, 0.16);
  --on-green-soft: #005c4c;
  --purple: #96528f;
  --purple-soft: rgba(150, 82, 143, 0.16);
  --on-purple-soft: #703d6b;
  --slate: #6b7280;
  --slate-soft: rgba(107, 114, 128, 0.16);
  --on-slate-soft: #454b54;
  --cyan: #1f8fae;
  --cyan-soft: rgba(31, 143, 174, 0.16);
  --on-cyan-soft: #166a82;

  --status-good: #00806a;
  --status-warning: #a06d00;
  --status-serious: #c9600c;
  --status-critical: #96470a;

  --chart-1: #0067a3;
  --chart-2: #c9600c;
  --chart-3: #00806a;
  --chart-4: #a06d00;
  --chart-5: #96528f;
  --chart-6: #3f5aa6;
  --chart-7: #1f8fae;
  --chart-8: #96470a;
  --chart-9: #6b7280;
}
:root[data-theme="dark"][data-palette="protanopia"] {
  --border: #454e5f;
  --border-soft: #454e5f;

  --accent: #8fa3ec;
  --accent-2: #8fa3ec;
  --accent-strong: #2b3d73;
  --accent-soft: rgba(143, 163, 236, 0.22);
  --on-accent-soft: #dbe3fb;
  --on-accent-icon: #dbe3fb;

  --blue: #5cb0e0;
  --blue-soft: rgba(92, 176, 224, 0.22);
  --on-blue-soft: #cee8f8;
  --amber: #eab04c;
  --amber-soft: rgba(234, 176, 76, 0.22);
  --on-amber-soft: #ffdfa8;
  --red: #f2924f;
  --red-soft: rgba(242, 146, 79, 0.22);
  --on-red-soft: #ffd4b0;
  --green: #43c2a8;
  --green-soft: rgba(67, 194, 168, 0.22);
  --on-green-soft: #bff2e6;
  --purple: #cf94c8;
  --purple-soft: rgba(207, 148, 200, 0.22);
  --on-purple-soft: #f3ddf0;
  --slate: #aab2c0;
  --slate-soft: rgba(170, 178, 192, 0.22);
  --on-slate-soft: #dfe3ea;
  --cyan: #5cc6df;
  --cyan-soft: rgba(92, 198, 223, 0.22);
  --on-cyan-soft: #c4eef6;

  --status-good: #43c2a8;
  --status-warning: #eab04c;
  --status-serious: #f2924f;
  --status-critical: #ffa572;

  --chart-1: #5cb0e0;
  --chart-2: #f2924f;
  --chart-3: #43c2a8;
  --chart-4: #eab04c;
  --chart-5: #cf94c8;
  --chart-6: #8fa3ec;
  --chart-7: #5cc6df;
  --chart-8: #ffa572;
  --chart-9: #aab2c0;
}

/* ---- Tritanopia-Optimized palette ---- */
:root[data-palette="tritanopia"] {
  --border: #bdb8b8;
  --border-soft: #bdb8b8;

  --accent: #7a3a9e;
  --accent-2: #7a3a9e;
  --accent-strong: #54276e;
  --accent-soft: rgba(122, 58, 158, 0.14);
  --on-accent-soft: #5b2b77;
  --on-accent-icon: #5b2b77;

  --blue: #5230b5;
  --blue-soft: rgba(82, 48, 181, 0.16);
  --on-blue-soft: #3c2286;
  --amber: #c8480c;
  --amber-soft: rgba(200, 72, 12, 0.18);
  --on-amber-soft: #963509;
  --red: #c0334f;
  --red-soft: rgba(192, 51, 79, 0.16);
  --on-red-soft: #8e2539;
  --green: #1f8a4c;
  --green-soft: rgba(31, 138, 76, 0.16);
  --on-green-soft: #166536;
  --purple: #9c3f8f;
  --purple-soft: rgba(156, 63, 143, 0.16);
  --on-purple-soft: #742e6a;
  --slate: #6f6a6a;
  --slate-soft: rgba(111, 106, 106, 0.16);
  --on-slate-soft: #4a4646;
  --cyan: #0a8f7d;
  --cyan-soft: rgba(10, 143, 125, 0.16);
  --on-cyan-soft: #07695c;

  --status-good: #1f8a4c;
  --status-warning: #c8480c;
  --status-serious: #c0334f;
  --status-critical: #96253d;

  --chart-1: #5230b5;
  --chart-2: #c8480c;
  --chart-3: #1f8a4c;
  --chart-4: #a8620c;
  --chart-5: #c0334f;
  --chart-6: #0a8f7d;
  --chart-7: #9c3f8f;
  --chart-8: #96253d;
  --chart-9: #6f6a6a;
}
:root[data-theme="dark"][data-palette="tritanopia"] {
  --border: #544963;
  --border-soft: #544963;

  --accent: #c69bec;
  --accent-2: #c69bec;
  --accent-strong: #54276e;
  --accent-soft: rgba(198, 155, 236, 0.22);
  --on-accent-soft: #efe0fb;
  --on-accent-icon: #efe0fb;

  --blue: #a48cf0;
  --blue-soft: rgba(164, 140, 240, 0.22);
  --on-blue-soft: #e5dcfc;
  --amber: #ff8a52;
  --amber-soft: rgba(255, 138, 82, 0.22);
  --on-amber-soft: #ffd2b0;
  --red: #ea7d94;
  --red-soft: rgba(234, 125, 148, 0.22);
  --on-red-soft: #f9ccd5;
  --green: #5ccb85;
  --green-soft: rgba(92, 203, 133, 0.22);
  --on-green-soft: #c8f0d7;
  --purple: #d98fcb;
  --purple-soft: rgba(217, 143, 203, 0.22);
  --on-purple-soft: #f5dcef;
  --slate: #bcb4bd;
  --slate-soft: rgba(188, 180, 189, 0.22);
  --on-slate-soft: #e6e1e6;
  --cyan: #4fc8b3;
  --cyan-soft: rgba(79, 200, 179, 0.22);
  --on-cyan-soft: #c0f0e6;

  --status-good: #5ccb85;
  --status-warning: #ff8a52;
  --status-serious: #ea7d94;
  --status-critical: #f0637e;

  --chart-1: #a48cf0;
  --chart-2: #ff8a52;
  --chart-3: #5ccb85;
  --chart-4: #e0a24c;
  --chart-5: #ea7d94;
  --chart-6: #4fc8b3;
  --chart-7: #d98fcb;
  --chart-8: #f0637e;
  --chart-9: #bcb4bd;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
  /* Tells the browser to draw native form chrome (date/time picker icon,
     the little calendar/clock popup itself, spin buttons, etc.) in a
     palette matching the current theme. Without this, a dark icon shows up
     on a dark field (or vice versa) — functionally invisible even though
     it's still clickable. app.js keeps this in sync when the theme toggles
     (color-scheme isn't itself a custom property, so it needs its own rule). */
  color-scheme: light;
  transition: background-color .2s var(--ease), color .2s var(--ease);
}
:root[data-theme="dark"] html, :root[data-theme="dark"] body { color-scheme: dark; }

body { margin: 0; }

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

/* scrollbars */
* { scrollbar-width: thin; scrollbar-color: #cdd3e0 transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: #d3d9e6; border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: #c0c8db; background-clip: content-box; }
:root[data-theme="dark"] * { scrollbar-color: #2a3247 transparent; }
:root[data-theme="dark"] *::-webkit-scrollbar-thumb { background: #262e44; }
:root[data-theme="dark"] *::-webkit-scrollbar-thumb:hover { background: #323c56; }

code { font-family: 'SFMono-Regular', Menlo, Consolas, monospace; }

.hidden { display: none !important; }

/* Successful-login handoff (2026-09-12) -- #app goes straight from
   display:none to visible the instant showApp_() swaps the 'hidden' class
   for 'flex' (see app.js), which would otherwise read as an abrupt jump
   cut right as renderSkeleton_()'s skeleton/spinner appears underneath.
   Reuses the existing fadeIn keyframe (already used elsewhere in this
   file) so the dashboard shell eases in instead. Scoped to the .flex
   class specifically, which app.js only ever adds once per page load, so
   this doesn't replay on every unrelated re-render. */
#app.flex { animation: fadeIn .25s var(--ease); }

/* ==========================================================================
   Login screen
   ========================================================================== */
.login-screen {
  /* Now a full scrollable landing page (nav + hero + footer), not just a
     centered card — see about.html's about.css for the .marketing-*
     classes this reuses. Stays position:fixed so it still fully covers
     #app underneath while visible, but scrolls internally instead of
     centering a single fixed-size card. */
  position: fixed; inset: 0; z-index: 50; overflow-y: auto;
  background:
    radial-gradient(60% 40% at 15% 0%, rgba(109,107,247,.16), transparent 60%),
    radial-gradient(50% 35% at 85% 10%, rgba(75,155,255,.10), transparent 60%),
    var(--bg);
  /* Same standalone-PWA safe-area reasoning as .app-shell above -- the
     login/marketing page is its own full-bleed fixed layer, so it needs
     its own safe-area padding rather than inheriting .app-shell's. */
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
.login-card-wrap { animation: rise .5s var(--ease); margin-top: 1.6rem; }
.login-card {
  width: 360px; max-width: 100%;
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--radius-xl); box-shadow: var(--shadow-lg);
  padding: 1.8rem 1.7rem;
}
.login-hint { font-size: .78rem; color: var(--text-dim); margin: 0 0 1.15rem; line-height: 1.5; }
.field-label { display: block; font-size: .72rem; font-weight: 600; color: var(--text-dim); margin: 0 0 .3rem; }
.field-input {
  width: 100%; background: var(--bg-soft); border: 1px solid var(--border);
  color: var(--text); border-radius: var(--radius-md); padding: .62rem .8rem;
  font-size: .85rem; font-family: inherit; margin-bottom: .9rem; transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.field-input::placeholder { color: var(--text-faint); }
.field-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
.login-error { font-size: .75rem; color: var(--red); margin-top: .85rem; animation: fadeIn .2s var(--ease); }

/* Login submit button loading state (2026-09-12) -- spinner + "Logging
   in…" label swap while the /login request is in flight, so credential
   submission never looks like a dead click. Reuses the same spinning-ring
   technique and view-refresh-spin keyframe as .first-load-spinner /
   .pull-refresh-spinner elsewhere in the app, just sized for inline use
   inside a button (.btn-primary already sets display:inline-flex +
   align-items:center + gap, so the spinner and label lay out for free). */
.btn-primary:disabled { cursor: not-allowed; opacity: .75; }
.btn-primary:disabled:hover { filter: none; transform: none; box-shadow: 0 6px 18px -6px rgba(109,107,247,.55); }
.btn-spinner {
  width: 15px; height: 15px; border-radius: 999px; flex-shrink: 0;
  border: 2px solid rgba(255,255,255,.45); border-top-color: #fff;
  animation: view-refresh-spin .7s linear infinite;
}
.login-about-link {
  display: inline-block; margin-top: 1.1rem; font-size: .82rem;
  font-weight: 600; color: var(--text-faint); text-decoration: none; transition: color .15s var(--ease);
}
.login-about-link:hover { color: var(--on-accent-soft); }

/* login.html's .hero has only ONE child (.hero-copy) -- about.html/index.html's
   matching .hero always has a second flex item (.hero-shot, the product
   screenshot) to balance against, but the login page never did. Without an
   explicit width cap, .hero-copy's `flex: 1 1 420px` (see about.css) just
   grows to fill the full centered .hero row, and its content (left-aligned
   by default) ends up hugging the left edge with a big empty gap on the
   right. Scoped to #loginScreen so about.html's real two-column hero is
   untouched, and to min-width:861px so it never fights about.css's own
   860px-and-below stacking rule (`.hero-copy { flex: none; }`) -- a
   `flex-basis` meant as a *width* here would get reinterpreted as a
   *height* once .hero switches to flex-direction:column below that
   breakpoint. The matching <860px rule (below, near .login-card's other
   mobile overrides) had the identical wrapper-vs-card mistake and is fixed
   the same way. */
@media (min-width: 861px) {
  #loginScreen .hero { justify-content: center; }
  #loginScreen .hero-copy { flex: 0 1 560px; text-align: center; }
  #loginScreen .hero-sub { margin-left: auto; margin-right: auto; }
  #loginScreen .login-card-wrap { text-align: left; }
  /* .login-card-wrap itself has no width rule of its own, so as a normal
     block box it simply fills 100% of hero-copy's 560px -- margin:auto on
     THAT box has nothing to do, since there's no leftover space to split.
     .login-card is the one with an actual fixed width (360px, see its base
     rule above), so centering has to go on it directly -- same pattern
     about.css's own .section-cta .login-card already uses successfully. */
  #loginScreen .login-card { margin-left: auto; margin-right: auto; }
}

/* ==========================================================================
   Buttons / inputs (shared)
   ========================================================================== */
.btn-primary {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  /* --accent-strong (Color Theme Selector, 2026-09-07): a solid-fill-safe
     shade of accent, kept mode-independent like --accent itself. The bare
     --accent/--accent-2 gradient measured under WCAG AA (4.5:1) for white
     button text on several palettes (incl. 4.12:1 on the original default
     purple) -- see docs/00_ARCHITECTURE_AND_SCHEMA.md Sec 8. */
  background: linear-gradient(135deg, var(--accent-strong), var(--accent-strong));
  color: #fff; font-size: .82rem; font-weight: 600; border: none;
  padding: .55rem 1.05rem; border-radius: var(--radius-pill); cursor: pointer;
  box-shadow: 0 6px 18px -6px rgba(109,107,247,.55);
  transition: transform .15s var(--ease), box-shadow .15s var(--ease), filter .15s var(--ease);
  white-space: nowrap;
}
.btn-primary:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 10px 24px -6px rgba(109,107,247,.6); }
.btn-primary:active { transform: translateY(0); }
.btn-block { width: 100%; margin-top: .3rem; }

.icon-btn {
  position: relative; width: 38px; height: 38px; border-radius: var(--radius-pill);
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim);
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  transition: all .15s var(--ease); flex-shrink: 0;
}
.icon-btn svg { width: 18px; height: 18px; }
.icon-btn:hover { background: var(--surface-hover); color: var(--text); border-color: var(--border-soft); }
.icon-btn-dot {
  position: absolute; top: 8px; right: 9px; width: 7px; height: 7px; border-radius: 999px;
  background: var(--red); border: 1.5px solid var(--surface-solid); display: none;
}
.icon-btn-dot.show { display: block; }
/* Internal Team Chat (2026-09-11) -- the topbar toggle's numbered unread
   badge, same positioning idea as .icon-btn-dot above but carrying a count
   instead of a plain dot (mirrors .nav-badge's pill shape/colors). */
.icon-btn-badge {
  position: absolute; top: -4px; right: -4px; min-width: 16px; height: 16px; padding: 0 .3rem;
  border-radius: var(--radius-pill); background: var(--accent); color: #fff;
  font-size: .62rem; font-weight: 700; display: none; align-items: center; justify-content: center;
  border: 1.5px solid var(--surface-solid);
}
.icon-btn-badge.show { display: flex; }

/* ---------- Assignments notification bell + dropdown (feature: in-app
   pop-up notifications for newly assigned calls) ---------- */
.notif-wrap { position: relative; display: inline-flex; flex-shrink: 0; }
.assignments-panel {
  position: absolute; top: calc(100% + 8px); right: 0; width: 320px; max-width: calc(100vw - 2rem);
  max-height: 420px; overflow-y: auto; background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: 0 18px 40px -12px rgba(0,0,0,.35); z-index: 60;
  padding: .4rem;
}
.assignments-panel.hidden { display: none; }
.assignments-panel-header {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-dim); padding: .5rem .6rem .4rem;
}
.assignments-panel-empty { padding: 1rem .6rem; font-size: .8rem; color: var(--text-dim); }
.assignment-row {
  display: flex; flex-direction: column; gap: .15rem; width: 100%; text-align: left;
  background: transparent; border: none; border-radius: var(--radius-sm, 8px); padding: .55rem .6rem;
  cursor: pointer; font-family: inherit; color: var(--text);
}
.assignment-row:hover { background: var(--surface-hover); }
.assignment-row.is-new { background: var(--accent-soft); }
.assignment-row.is-new:hover { background: var(--accent-soft); filter: brightness(0.97); }
.assignment-row-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.assignment-row-id { font-weight: 700; font-size: .82rem; display: inline-flex; align-items: center; gap: .35rem; }
.assignment-new-dot { width: 7px; height: 7px; border-radius: 999px; background: var(--red); display: inline-block; }
.assignment-row-cust { font-size: .8rem; color: var(--text); }
.assignment-row-meta { font-size: .72rem; color: var(--text-dim); }

/* Theme toggle: both icons live in the DOM, CSS shows the one matching the
   current mode (sun while light is active, moon while dark is active). */
.theme-toggle-btn .icon-sun { display: flex; }
.theme-toggle-btn .icon-moon { display: none; }
:root[data-theme="dark"] .theme-toggle-btn .icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle-btn .icon-moon { display: flex; }

.select-input {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--radius-md); padding: .5rem .75rem; font-size: .8rem; font-family: inherit;
  cursor: pointer; transition: border-color .15s var(--ease);
}
.select-input:focus { outline: none; border-color: var(--accent); }

.modal-input, .modal-notes-textarea {
  width: 100%; background: var(--bg-soft); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--radius-sm); padding: .5rem .65rem; font-size: .82rem; font-family: inherit;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.modal-input::placeholder, .modal-notes-textarea::placeholder { color: var(--text-faint); }
.modal-input:focus, .modal-notes-textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
.modal-notes-textarea { min-height: 90px; resize: vertical; }

/* Type-ahead suggestion list under .modal-input text fields (currently just
   the New Ticket modal's Customer field — see openNewTicketModal() in
   app.js). Sits in normal flow (not position:absolute) so it grows the
   surrounding .modal-field-grid row instead of needing to be clipped-aware
   of the modal's own overflow-y: auto. */
.autocomplete-results {
  margin-top: .35rem; max-height: 220px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-soft);
}
.autocomplete-item {
  padding: .5rem .65rem; font-size: .82rem; color: var(--text); cursor: pointer;
}
.autocomplete-item:hover, .autocomplete-item.active { background: var(--accent-soft); }
.autocomplete-empty { padding: .5rem .65rem; font-size: .8rem; color: var(--text-faint); }

/* Give the native date/time picker icon a bigger, obviously-clickable
   target and a hover state — the default is a tiny icon flush with the
   input's edge. Applies to .modal-input (ticket scheduling) and
   .select-input (the Dispatch timeline date picker). */
.modal-input::-webkit-calendar-picker-indicator,
.select-input::-webkit-calendar-picker-indicator {
  cursor: pointer; border-radius: 5px; padding: 3px; margin-right: -2px;
  transition: background .12s var(--ease);
}
.modal-input::-webkit-calendar-picker-indicator:hover,
.select-input::-webkit-calendar-picker-indicator:hover { background: var(--surface-hover); }

.modal-save-btn {
  /* --accent-strong: see .btn-primary's comment above -- same WCAG fix. */
  background: var(--accent-strong); color: #fff; font-size: .78rem; font-weight: 600; padding: .5rem 1rem;
  border-radius: var(--radius-pill); margin-top: .6rem; border: none; cursor: pointer;
  transition: filter .15s var(--ease), transform .15s var(--ease);
}
.modal-save-btn:hover:not(:disabled) { filter: brightness(1.1); transform: translateY(-1px); }
/* Was previously unstyled -- sendDispatchBtn (no tech assigned) and now
   generateInvoiceBtn (no time/material logged yet) both render `disabled`,
   but with no rule here they only got each browser's default disabled
   look, easy to miss on a button with an explicit inline background. */
.modal-save-btn:disabled { opacity: .4; cursor: not-allowed; }

.modal-danger-btn {
  background: transparent; color: var(--red); font-size: .78rem; font-weight: 600; padding: .5rem 1rem;
  border-radius: var(--radius-pill); margin-top: .6rem; border: 1px solid rgba(243,89,107,.4); cursor: pointer;
  transition: background .15s var(--ease), transform .15s var(--ease);
}
.modal-danger-btn:hover { background: var(--red-soft); transform: translateY(-1px); }

.tech-modal-photo-row { display: flex; align-items: flex-start; gap: 1rem; }
/* .checkbox-row is the same layout under a name that isn't tech-specific —
   used by the Settings page toggle(s) as well as the Technicians modal. */
.tech-active-check, .checkbox-row {
  display: flex; align-items: center; gap: .5rem; font-size: .82rem; color: var(--text-dim);
  margin-top: .8rem; cursor: pointer;
}
.tech-active-check input, .checkbox-row input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
.tech-modal-actions { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.tech-bio-cell {
  max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ==========================================================================
   App shell
   ========================================================================== */
/* Safe-area padding (added 2026-08-27): manifest.webmanifest declares
   display:standalone + login.html's viewport meta has viewport-fit=cover,
   which together tell iOS the web content is allowed to draw under the
   status bar / home indicator / rounded corners when launched from the
   Home Screen (a regular Safari tab never has this problem -- Safari's own
   chrome reserves that space for you; standalone mode hands you the FULL
   screen instead and expects env(safe-area-inset-*) to claim back the
   unsafe edges yourself). Nothing here used env() at all before this, so
   the topbar/sidebar could render partly or fully underneath the
   translucent status bar (apple-mobile-web-app-status-bar-style is
   black-translucent, i.e. content shows THROUGH it, not below it) on an
   actual home-screen launch -- invisible on a real iPhone even though it
   renders fine in a desktop/emulated mobile-width browser (see
   perf/responsive testing notes -- Chromium's iPhone emulation doesn't
   model safe-area-inset-* the same way real iOS Safari/WKWebView does).
   env() resolves to 0 on any browser/device without a safe area (desktop,
   older iPhones, Android), so this is a no-op everywhere except where it's
   actually needed. */
.app-shell {
  display: flex; height: 100vh; overflow: hidden;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* Sidebar */
.sidebar {
  width: 236px; flex-shrink: 0; background: var(--bg-soft);
  border-right: 1px solid var(--border-soft);
  display: flex; flex-direction: column;
}
.sidebar-brand {
  display: flex; align-items: center; gap: .65rem;
  padding: 1.25rem 1.1rem 1.1rem;
}
.brand-mark {
  width: 34px; height: 34px; border-radius: var(--radius-md); flex-shrink: 0;
  background: linear-gradient(135deg, var(--accent), var(--blue));
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; color: #fff; font-size: .95rem; overflow: hidden;
  box-shadow: 0 6px 14px -4px rgba(109,107,247,.5);
}
/* Two-letter BRAND_INITIAL (e.g. "HC") needs a smaller size + tighter
   tracking to fit the same 34px circle a single letter uses -- see the
   brand-mark-2up toggle in applyBranding_() in app.js. */
.brand-mark.brand-mark-2up { font-size: .62rem; letter-spacing: -.02em; }
.brand-name { font-size: .86rem; font-weight: 700; color: var(--text); line-height: 1.3; }
.brand-name span { font-size: .68rem; font-weight: 500; color: var(--text-faint); }

.sidebar-nav { flex: 1; padding: .5rem .7rem; display: flex; flex-direction: column; gap: .15rem; overflow-y: auto; }
.nav-item {
  display: flex; align-items: center; gap: .7rem; width: 100%; text-align: left;
  padding: .58rem .75rem; font-size: .82rem; font-weight: 500; color: var(--text-dim);
  background: transparent; border: none; border-radius: var(--radius-md); cursor: pointer;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.nav-icon { display: flex; flex-shrink: 0; width: 18px; height: 18px; opacity: .85; }
.nav-icon svg { width: 100%; height: 100%; }
.nav-item:hover { background: var(--surface-2); color: var(--text); }
.nav-item.active {
  background: var(--accent-soft); color: var(--on-accent-soft);
  box-shadow: inset 0 0 0 1px rgba(109,107,247,.35);
}
.nav-item.active .nav-icon { opacity: 1; color: var(--on-accent-icon); }

.powered-by-runsheet {
  display: flex; align-items: center; gap: .5rem; margin: .3rem .7rem 0;
  padding: .55rem .65rem; border-radius: var(--radius-md); text-decoration: none;
  font-size: .72rem; color: var(--text-faint); transition: background .15s var(--ease), color .15s var(--ease);
}
.powered-by-runsheet:hover { background: var(--surface-2); color: var(--text-dim); }
.powered-by-runsheet:hover .powered-by-mark { color: var(--accent); }
.powered-by-mark { width: 17px; height: 17px; flex-shrink: 0; color: var(--text-faint); transition: color .15s var(--ease); }
.powered-by-runsheet strong { color: var(--text-dim); font-weight: 700; }

/* Sidebar status widget (2026-09-24 fix) -- dot color is driven ONLY by
   the .status-* class that renderConnStatus_() in app.js derives; no inline
   colors. High-contrast palettes map to their own accessible greens/reds. */
:root {
  --status-online: #10b981;
  --status-syncing: #f59e0b;
  --status-offline: #ef4444;
}
:root[data-palette="high-contrast"] {
  --status-online: var(--green);
  --status-syncing: var(--amber);
  --status-offline: var(--red);
}
.sidebar-footer {
  display: flex; flex-direction: column; gap: .3rem;
  padding: .9rem 1.1rem calc(.9rem + 2px); border-top: 1px solid var(--border-soft);
  font-size: .72rem; line-height: 1.4; color: var(--text-faint);
  min-width: 0; overflow: visible;
}
.sidebar-footer-row { display: flex; align-items: flex-start; gap: .5rem; min-width: 0; }
.conn-dot {
  width: 7px; height: 7px; border-radius: 999px; flex-shrink: 0;
  margin-top: calc((1.4em - 7px) / 2); /* centered on the FIRST text line, even if the label wraps */
  background: var(--status-syncing);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-syncing) 22%, transparent);
  transition: background-color .25s var(--ease), box-shadow .25s var(--ease);
}
.conn-dot.status-online  { background: var(--status-online);  box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-online) 22%, transparent); }
.conn-dot.status-syncing { background: var(--status-syncing); box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-syncing) 22%, transparent); animation: connDotPulse 1.4s ease-in-out infinite; }
.conn-dot.status-offline { background: var(--status-offline); box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-offline) 22%, transparent); }
@keyframes connDotPulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .conn-dot.status-syncing { animation: none; } }
/* Name + state: allowed to wrap onto a 2nd line instead of being cut to
   "Greg · saved data from 1:2..." -- clamped at 2 lines so a very long
   name can't push the footer around; the full text is in the title. */
#connStatus {
  flex: 1 1 auto; min-width: 0; line-height: 1.4;
  white-space: normal; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden; padding-bottom: 1px;
}
#connStatus + .logout-btn { margin-top: -.2rem; }
/* Last-pulled-data timestamp under the connection status -- see app.js
   updateLastSyncDisplay_(). Indented past the dot so it visually reads as
   a sub-line of the status above it; turns amber (.stale) if the live
   ticket-poll heartbeat hasn't landed in a while, same signal language as
   the dot's own green/red. */
.sidebar-footer-sync {
  padding-left: 1.05rem; font-size: .68rem; line-height: 1.4; min-height: 1.4em;
  color: var(--text-faint); opacity: .85;
  /* 2026-09-24: never clipped or ellipsized. "Updated 59 minutes ago" fits
     on one line in the 236px sidebar; a longer string (e.g. "Saved copy
     from Sep 23, 12:59 PM" or a larger user font size) wraps to a second
     line instead of spilling over the main column or being cut off. */
  white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums; /* seconds counter doesn't jitter */
}
.sidebar-footer-sync.stale { color: var(--amber, #b45309); opacity: 1; }
/* Build Version & Refresh Indicator (bottom-left of the app shell, under
   the last-sync line -- see initBuildVersionIndicator_() in app.js).
   Subtle/non-intrusive by default (same faint sizing as the sync line
   above it); swaps into an amber "click to refresh" pill the moment a
   background poll detects the currently-served login.html has changed
   (a new deploy went out) via the response's ETag/Last-Modified header --
   no manual version bump required at deploy time. */
.sidebar-footer-build {
  display: flex; align-items: center; gap: .3rem; width: 100%;
  padding: 0 0 0 1.05rem; margin-top: -.05rem;
  font-size: .66rem; color: var(--text-faint); opacity: .7; font-family: inherit;
  background: none; border: none; text-align: left; cursor: default;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar-footer-build.update-available {
  opacity: 1; color: var(--amber, #b45309); font-weight: 600; cursor: pointer;
}
.sidebar-footer-build.update-available:hover { text-decoration: underline; }
.sidebar-footer-build-dot {
  width: 6px; height: 6px; border-radius: 999px; background: var(--amber); flex-shrink: 0; display: none;
}
.sidebar-footer-build.update-available .sidebar-footer-build-dot { display: block; animation: buildUpdatePulse 1.6s ease-in-out infinite; }
@keyframes buildUpdatePulse { 0%, 100% { opacity: .4; } 50% { opacity: 1; } }
/* Embedded In-App Support Ticket System (2026-09-10) -- "Contact Support"
   link, same faint/subtle sidebar-footer treatment as the build-version
   line above it until hovered, since this is a rarely-needed escape
   hatch, not a primary nav item. */
.sidebar-footer-support {
  display: flex; align-items: center; gap: .35rem; width: 100%;
  padding: .3rem 0 0 1.05rem; margin-top: .1rem;
  font-size: .68rem; color: var(--text-faint); font-family: inherit;
  background: none; border: none; text-align: left; cursor: pointer;
}
.sidebar-footer-support:hover { color: var(--text-dim); text-decoration: underline; }
.sidebar-footer-support svg { flex-shrink: 0; opacity: .8; }
.logout-btn {
  background: none; border: none; color: var(--text-faint); cursor: pointer; padding: .2rem;
  display: flex; border-radius: var(--radius-sm); transition: color .15s var(--ease), background .15s var(--ease);
}
.logout-btn svg { width: 16px; height: 16px; }
.logout-btn:hover { color: var(--text); background: var(--surface-2); }

/* Off-canvas drawer scrim — only ever shown on mobile/tablet widths, see
   the responsive block at the end of this file. */
.sidebar-backdrop {
  display: none; position: fixed; inset: 0; background: rgba(6,8,14,.6);
  z-index: 39; opacity: 0; transition: opacity .2s var(--ease);
}
body.sidebar-open .sidebar-backdrop { opacity: 1; }

/* Hamburger — hidden on desktop, the sidebar is always visible there */
.menu-btn { display: none; }

/* Main column */
.main-col { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }

.topbar {
  height: 68px; flex-shrink: 0; display: flex; align-items: center; gap: .7rem;
  padding: 0 1.5rem; border-bottom: 1px solid var(--border-soft);
}
.search-wrap { position: relative; width: 380px; max-width: 40vw; }
.search-icon { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-faint); pointer-events: none; }
.search-input {
  width: 100%; background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--radius-pill); padding: .58rem .9rem .58rem 2.35rem; font-size: .82rem; font-family: inherit;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease), background .15s var(--ease);
}
.search-input::placeholder { color: var(--text-faint); }
.search-input:focus { outline: none; border-color: var(--accent); background: var(--surface-hover); box-shadow: 0 0 0 3px var(--accent-ring); }

.loading-pill {
  font-size: .72rem; color: var(--text-dim); background: var(--surface-2);
  padding: .3rem .65rem; border-radius: var(--radius-pill); border: 1px solid var(--border);
}
.topbar-spacer { flex: 1; }

/* .user-chip is a real <button> now (User Profile Avatar menu, Global Top
   Menu Consistency pass, 2026-09-11), not a plain <div> -- reset button
   chrome back to the same look it always had. */
.user-chip {
  display: flex; align-items: center; background: none; border: none;
  padding: 0; margin: 0; cursor: pointer; border-radius: 999px;
}
.user-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.user-avatar {
  width: 38px; height: 38px; border-radius: 999px; flex-shrink: 0;
  background: linear-gradient(135deg, var(--accent), var(--blue));
  color: #fff; font-weight: 700; font-size: .8rem;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 0 0 2px var(--surface-solid), 0 0 0 3px var(--border);
}

/* ---------- User Profile Avatar menu (feature: Global Top Menu
   Consistency, 2026-09-11) -- same dropdown shape as .assignments-panel
   above, just right-aligned under the avatar instead of the bell. ---------- */
.user-menu-wrap { position: relative; }
.user-menu-panel {
  position: absolute; top: calc(100% + 8px); right: 0; width: 220px;
  max-width: calc(100vw - 2rem); background: var(--surface-solid);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: 0 18px 40px -12px rgba(0,0,0,.35); z-index: 60; padding: .4rem;
}
.user-menu-panel.hidden { display: none; }
.user-menu-header {
  display: flex; flex-direction: column; gap: .1rem;
  padding: .5rem .6rem .6rem; margin-bottom: .2rem; border-bottom: 1px solid var(--border-soft);
}
.user-menu-name { font-size: .85rem; font-weight: 700; color: var(--text); }
.user-menu-meta { font-size: .72rem; color: var(--text-dim); }
.user-menu-item {
  display: flex; align-items: center; gap: .55rem; width: 100%;
  background: none; border: none; text-align: left; cursor: pointer;
  padding: .55rem .6rem; border-radius: var(--radius-sm, 8px);
  font-size: .82rem; font-weight: 500; color: var(--text); font-family: inherit;
}
.user-menu-item svg { flex-shrink: 0; width: 16px; height: 16px; }
.user-menu-item:hover { background: var(--surface-hover); }
.user-menu-item-danger { color: var(--on-red-soft); }
.user-menu-item-danger:hover { background: var(--red-soft); }

.notif-banner {
  margin: 1rem 1.5rem 0; padding: .7rem 1rem; border-radius: var(--radius-md);
  background: var(--green-soft); border: 1px solid rgba(53,211,145,.3); color: var(--on-green-soft);
  font-size: .8rem; font-weight: 500; display: flex; align-items: center; gap: .55rem;
  animation: rise .35s var(--ease);
}
.notif-banner.warn { background: var(--amber-soft); border-color: rgba(245,165,36,.35); color: var(--on-amber-soft); }
.notif-banner-icon { width: 16px; height: 16px; flex-shrink: 0; }
.notif-banner.error { background: var(--red-soft); border-color: rgba(243,89,107,.35); color: var(--on-red-soft); }

/* Settings > Send Test Alert message preview. Reuses the same bordered/soft
   look as .autocomplete-results so it reads as "part of this panel" rather
   than a separate component. */
.test-alert-preview {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-soft); padding: .55rem .7rem; font-size: .8rem;
}
.test-alert-preview summary { cursor: pointer; color: var(--text-faint); user-select: none; }
.test-alert-preview[open] summary { margin-bottom: .5rem; }
.test-alert-preview-subject { font-weight: 600; margin-top: .5rem; color: var(--text); }
.test-alert-preview-body {
  white-space: pre-wrap; word-break: break-word; margin: .4rem 0 0;
  font-family: inherit; color: var(--text); max-height: 240px; overflow-y: auto;
}

.viewport { flex: 1; overflow: auto; padding: 1.5rem; overscroll-behavior-y: contain; }

/* Unified Settings Save Button (item 1, 2026-09-06): pinned to the bottom
   of the scrolling .viewport (not the document -- .viewport is the actual
   scroll container, see login.html's <main class="main-col"> layout) so
   #settingsMasterSaveBtn stays reachable without scrolling back up no
   matter how long the Settings page gets. Negative side margins bleed it
   back out to the .viewport's own edges so the bar spans full width
   instead of stopping at .settings-panel's inner padding. */
.settings-master-save {
  position: sticky; bottom: -1.5rem; z-index: 5;
  display: flex; align-items: center; gap: .6rem;
  margin: 1.2rem -1.5rem -1.5rem; padding: .9rem 1.5rem;
  background: var(--surface-solid); border-top: 1px solid var(--border);
  box-shadow: 0 -6px 16px rgba(0,0,0,.10);
}

/* Pull-to-refresh indicator (mobile/PWA) -- see wirePullToRefresh_() in
   app.js. Collapsed (height:0) until a touch-drag grows it; the drag
   itself sets `.pulling` (no CSS transition, tracks the finger 1:1) while
   release/refresh-in-flight/snap-back use the transition below. */
.pull-refresh-indicator {
  height: 0; overflow: hidden; display: flex; align-items: center; justify-content: center;
  transition: height .22s var(--ease);
}
.pull-refresh-indicator.pulling { transition: none; }
.pull-refresh-spinner {
  width: 22px; height: 22px; border-radius: 999px; flex-shrink: 0;
  border: 2.5px solid var(--border); border-top-color: var(--accent);
}
.pull-refresh-spinner.spin { animation: view-refresh-spin .7s linear infinite; }

/* ==========================================================================
   Stat cards / panels
   ========================================================================== */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }

.stat-card {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 1.15rem 1.25rem;
  transition: transform .18s var(--ease), box-shadow .18s var(--ease), border-color .18s var(--ease);
}
.stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--border-soft); }

/* Dashboard tiles double as filters into Tickets/Billing (see
   applyDashboardDrill_() in app.js) — this is the clickable-affordance
   layer on top of the existing hover lift above. */
.stat-card-clickable { cursor: pointer; }
.stat-card-clickable:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.stat-card-clickable:active { transform: translateY(0); }

/* Same clickable affordance as .stat-card-clickable above, for a
   .chart-card that also drills through (2026-09-16 -- Financial Snapshot's
   click-to-open-Financials fix, see data-drill="financials" in login.html
   and applyDashboardDrill_() in app.js). Kept as its own rule rather than
   just adding .chart-card to the selector above since .chart-card already
   has its own :hover lift a couple rules down. */
.chart-card-clickable { cursor: pointer; }
.chart-card-clickable:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.chart-card-clickable:active { transform: translateY(0); }

/* Applied by renderDashboard() to the "Awaiting Dispatch" tile whenever its
   count is > 0 -- the plain red icon alone (shared with e.g. Emergency)
   wasn't prominent enough on its own for something meant to read as an
   active alert, so the whole card gets a red-tinted border/background too,
   the same red-soft treatment .notif-banner.error already uses elsewhere. */
.stat-card-alert {
  border-color: rgba(243,89,107,.4);
  background: var(--red-soft);
}
.stat-card-alert:hover { border-color: var(--red); }

/* Small dismissible pill shown in the Tickets toolbar when a Dashboard tile
   has applied a filter that isn't representable by the status dropdown
   alone (e.g. "SLA At Risk") — see ticketExtraFilter/updateTicketDrillChip_()
   in app.js. */
.drill-chip {
  display: inline-flex; align-items: center; gap: .4rem;
  background: var(--accent-soft, var(--blue-soft)); color: var(--accent, var(--blue));
  border: 1px solid var(--border-soft); border-radius: var(--radius-pill);
  padding: .35rem .5rem .35rem .75rem; font-size: .75rem; font-weight: 600;
}
.drill-chip.hidden { display: none; }
.drill-chip-clear {
  background: none; border: none; color: inherit; cursor: pointer;
  font-size: 1rem; line-height: 1; padding: 0 .15rem; opacity: .75;
}
.drill-chip-clear:hover { opacity: 1; }

/* Archive view's Status/Priority/Technician/date-range filter row (see
   populateArchiveFilterOptions_()/filteredArchive_() in app.js). */
.archive-filter-bar { margin-bottom: .5rem; }
.customer-filter-bar { margin-bottom: .5rem; }
.archive-date-field {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .78rem; color: var(--text-dim);
}
.archive-date-field input[type="date"] { min-width: 0; }
.filter-clear-btn {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim);
  border-radius: var(--radius-pill); padding: .5rem .85rem; font-size: .78rem; font-weight: 600;
  cursor: pointer; transition: background .15s var(--ease), color .15s var(--ease), border-color .15s var(--ease);
}
.filter-clear-btn:hover { background: var(--surface-hover); color: var(--text); border-color: var(--border-soft); }
.stat-icon {
  width: 34px; height: 34px; border-radius: var(--radius-sm); margin-bottom: .8rem;
  display: flex; align-items: center; justify-content: center;
}
.stat-icon svg { width: 18px; height: 18px; }
.stat-icon-blue { background: var(--blue-soft); color: var(--blue); }
.stat-icon-amber { background: var(--amber-soft); color: var(--amber); }
.stat-icon-red { background: var(--red-soft); color: var(--red); }
.stat-icon-purple { background: var(--purple-soft); color: var(--purple); }
.stat-icon-green { background: var(--green-soft); color: var(--green); }
.stat-icon-cyan { background: var(--cyan-soft); color: var(--cyan); }
.stat-label { font-size: .72rem; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.stat-value { font-size: 1.9rem; font-weight: 800; color: var(--text); margin-top: .3rem; letter-spacing: -.02em; }
.stat-sub { font-size: .72rem; color: var(--text-faint); margin-top: .25rem; }

/* ==========================================================================
   Dashboard charts
   ========================================================================== */
.chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.chart-card {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 1.15rem 1.25rem 1.05rem;
  display: flex; flex-direction: column;
  transition: transform .18s var(--ease), box-shadow .18s var(--ease), border-color .18s var(--ease);
}
.chart-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--border-soft); }
@media (min-width: 900px) { .chart-card-wide { grid-column: span 2; } }
/* Tile dimensions (2026-09-10, Universal Customizable Dashboard batch) --
   the Customize Dashboard "Wide" per-tile toggle (see
   injectCustomizeControls_()/applyDashboardLayout_() in app.js). Same
   span-2 + >=900px-only idiom as the pre-existing .chart-card-wide above
   (Tickets Created vs. SLA Due / Technician Workload), just made available
   to any dashboard tile -- stat cards or chart cards -- via a plain
   user-facing toggle instead of being hardcoded to those two charts only.
   Below 900px every tile stays single-column regardless of this setting,
   same mobile-safety reasoning .chart-card-wide already had. */
@media (min-width: 900px) { [data-widget-id].widget-size-wide { grid-column: span 2; } }
.chart-card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; margin-bottom: .95rem; }
.chart-card-title { font-size: .82rem; font-weight: 700; color: var(--text); }
.chart-card-sub { font-size: .68rem; color: var(--text-faint); margin-top: .2rem; }
.chart-card-body { position: relative; flex: 1; min-height: 190px; }
.chart-card-body canvas { max-width: 100%; }
.chart-split { display: flex; align-items: center; gap: 1.1rem; }
/* Fixed-size donut, flex-shrink:0 so it can't be pushed around, and the
   legend list takes the rest — without an explicit size here the donut
   wrapper's flex-basis:auto used to fall back to the <canvas>'s pre-resize
   intrinsic size (300×150), which squeezed .chart-legend-label to 0 width
   in the row next to it (see the min-width:0 note below too). */
.chart-donut-wrap { position: relative; }
.chart-split .chart-donut-wrap { flex: 0 0 148px; width: 148px; height: 148px; }
.chart-donut-center {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center; pointer-events: none;
}
.chart-donut-center-value { font-size: 1.35rem; font-weight: 800; color: var(--text); letter-spacing: -.01em; }
.chart-donut-center-label { font-size: .6rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: .04em; margin-top: .15rem; }
.chart-legend-list { display: flex; flex-direction: column; gap: .5rem; font-size: .75rem; flex: 1; min-width: 0; }

/* ==========================================================================
   Role-Based Customizable Dashboard + Drag-and-Drop Widget Layout
   (2026-09-07) -- see DASHBOARD_WIDGETS_/renderDashboardCustomizeUI_() in
   app.js. Cards themselves (.stat-card/.chart-card) are untouched; this
   only adds the toolbar, the per-card overlay controls injected while
   customizing, and the drag feedback states.
   ========================================================================== */
.dashboard-overview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin: 0 0 .8rem .2rem; flex-wrap: wrap; }
.dashboard-overview-toolbar .section-heading { margin: 0; }

.dashboard-customize-bar {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap;
  background: var(--accent-soft, var(--blue-soft)); border: 1px solid var(--border-soft);
  border-radius: var(--radius-md); padding: .6rem .85rem; margin: 0 0 .9rem;
}
.dashboard-customize-bar-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* Applied to .stat-grid/.chart-grid while customize mode is on -- gives
   every [data-widget-id] card inside a dashed, slightly lifted "editable"
   look so it reads as different from normal browsing at a glance, without
   needing to restyle each card class individually. */
.dashboard-widgets-customizing [data-widget-id] {
  outline: 1.5px dashed var(--border-soft); outline-offset: 2px; cursor: grab; user-select: none;
}
.dashboard-widgets-customizing [data-widget-id]:active { cursor: grabbing; }
.dashboard-widgets-customizing .stat-card-clickable,
.dashboard-widgets-customizing .chart-card-clickable { cursor: grab; } /* overrides the normal pointer affordance while customizing */
.widget-dragging { opacity: .45; }
.widget-drag-over { outline: 2px solid var(--accent) !important; outline-offset: 2px; }

/* Small overlay row (checkbox + drag-handle icon) JS injects into every
   [data-widget-id] card only while customizing -- see
   renderDashboardCustomizeUI_(). Absolutely positioned so it never
   reflows a card's existing content. */
.widget-customize-controls {
  position: absolute; top: .5rem; right: .5rem; z-index: 2;
  display: flex; align-items: center; gap: .4rem;
  background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: .2rem .5rem; font-size: .68rem; box-shadow: var(--shadow-sm);
}
.widget-customize-controls label { display: flex; align-items: center; gap: .3rem; cursor: pointer; margin: 0; }
.widget-customize-controls .widget-drag-handle { cursor: grab; color: var(--text-faint); letter-spacing: -1px; font-weight: 700; }
/* Tile dimensions "Wide" toggle (2026-09-10) -- a small left border sets it
   apart from the visibility checkbox+label just before it in the same
   pill, so the row doesn't read as one unbroken clickable strip. */
.widget-size-toggle-label { padding-left: .4rem; border-left: 1px solid var(--border); color: var(--text-faint); }
.stat-card, .chart-card { position: relative; } /* anchors .widget-customize-controls above */

/* ---- Financial Snapshot dashboard widget (compact, no chart canvas) ---- */
.dashboard-fin-snapshot-body { display: flex; flex-direction: column; }
.dashboard-fin-snapshot-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
.dashboard-fin-snapshot-tile { background: var(--surface-2, var(--surface-hover)); border-radius: var(--radius-md); padding: .65rem .75rem; }
.dashboard-fin-snapshot-label { font-size: .65rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: .03em; }
.dashboard-fin-snapshot-value { font-size: 1.1rem; font-weight: 800; color: var(--text); margin-top: .15rem; }
.chart-legend-row { display: flex; align-items: center; gap: .55rem; color: var(--text-dim); }
.chart-legend-swatch { width: 9px; height: 9px; border-radius: 3px; flex-shrink: 0; }
/* min-width:0 is required alongside flex:1 here — a flex item's default
   min-width is auto (≈ its content's width), which silently overrides
   flex-shrink and can collapse a sibling to 0 in a tight row; see above. */
.chart-legend-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-legend-value { font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.chart-empty-note { font-size: .78rem; color: var(--text-faint); display: flex; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 1rem; }

/* Audit Log Dashboard — quick-filter preset chips + Important Events panel
   (2026-09-07 follow-up to the searchable dashboard). Presets are their
   own class rather than .drill-chip (that one represents a single ACTIVE
   dismissible filter on the Tickets view) -- these are one-click
   shortcuts that populate the filter fields below, not a persistent
   active-filter indicator, though the clicked one does get an .active
   state so it's clear which quick filter (if any) is currently applied. */
.audit-preset-row { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; }
.audit-preset-btn {
  font-size: .76rem; font-weight: 600; padding: .4rem .85rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-dim); cursor: pointer;
  transition: all .15s var(--ease);
}
.audit-preset-btn:hover { color: var(--text); border-color: var(--border-soft); background: var(--surface-hover); }
.audit-preset-btn.active { background: var(--accent); color: #fff; border-color: transparent; }

/* Audit Log isolated Refresh button (2026-09-14, see refreshAuditLogFeed_()
   in app.js) -- deliberately its own class, NOT .view-refresh-btn, so it
   is never picked up by that class's global delegated click listener
   (which would run the heavy, whole-tenant-bootstrap refreshCurrentView_()
   instead of this panel-only refetch). Reuses the same spin keyframes
   (view-refresh-spin, defined near .view-refresh-btn below) for a visually
   consistent spinner without duplicating the animation. */
.audit-refresh-btn {
  display: inline-flex; align-items: center; gap: .4rem; font-size: .78rem; font-weight: 600;
  padding: .4rem .85rem; border-radius: var(--radius-pill); border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-dim); cursor: pointer; transition: all .15s var(--ease);
  flex-shrink: 0;
}
.audit-refresh-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border-soft); background: var(--surface-hover); }
.audit-refresh-btn:disabled { opacity: .6; cursor: default; }
.audit-refresh-btn.spinning svg { animation: view-refresh-spin .8s linear infinite; }

/* Important Events feed -- a filtered, human-readable highlight list
   (deletions, closures, transfers, reassignments, settings changes)
   sitting above the full chronological table/results. */
.audit-important-events { display: flex; flex-direction: column; gap: 0; max-height: 320px; overflow-y: auto; }
.audit-important-event-row {
  display: flex; align-items: center; gap: .65rem; padding: .55rem .1rem; border-bottom: 1px solid var(--border-soft);
  font-size: .78rem;
}
.audit-important-event-row:last-child { border-bottom: none; }
.audit-important-event-row.ticket-clickable-row { cursor: pointer; transition: background .12s var(--ease); }
.audit-important-event-row.ticket-clickable-row:hover { background: var(--surface-hover); }
.audit-important-event-icon {
  width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center;
  font-size: .68rem; font-weight: 700; color: #fff;
}
.audit-important-event-body { flex: 1; min-width: 0; }
.audit-important-event-title { color: var(--text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.audit-important-event-meta { color: var(--text-faint); font-size: .7rem; margin-top: .1rem; }
.audit-important-event-time { color: var(--text-faint); font-size: .7rem; white-space: nowrap; flex-shrink: 0; }

.panel {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--radius-lg); overflow: hidden;
}
.panel-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-soft);
}
.panel-title { font-size: .88rem; font-weight: 700; color: var(--text); }

.view-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.1rem; gap: .75rem; flex-wrap: wrap; }

.toggle-group { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 3px; gap: 2px; }
.toggle-btn {
  font-size: .78rem; font-weight: 600; padding: .4rem .95rem; border: none; background: transparent;
  color: var(--text-dim); border-radius: var(--radius-pill); cursor: pointer; transition: all .15s var(--ease);
}
.toggle-btn:hover { color: var(--text); }
.toggle-btn.active { background: var(--accent); color: #fff; box-shadow: 0 2px 8px -2px rgba(109,107,247,.6); }

/* Scheduled Time window presets (ticket modal) — visually matches .toggle-group/
   .toggle-btn but kept as its own class so it can't be caught by the global
   document.querySelectorAll('.toggle-btn') handlers wired for the Tickets
   Grid/Kanban and Dispatch Grid/Timeline toggles elsewhere in app.js. */
.schedule-window-group { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 3px; gap: 2px; margin-top: .3rem; }
.schedule-window-btn {
  font-size: .78rem; font-weight: 600; padding: .4rem .95rem; border: none; background: transparent;
  color: var(--text-dim); border-radius: var(--radius-pill); cursor: pointer; transition: all .15s var(--ease);
}
.schedule-window-btn:hover { color: var(--text); }
.schedule-window-btn.active { background: var(--accent); color: #fff; box-shadow: 0 2px 8px -2px rgba(109,107,247,.6); }

/* Ticket modal's Flags section (see openTicketModal() in site/app.js).
   Unlike .schedule-window-group (mutually-exclusive single choice), any
   number of these can be active at once, so each button is its own free-
   standing pill rather than sharing one pill-shaped background track. */
.flag-toggle-group { display: flex; flex-wrap: wrap; gap: .45rem; }
.flag-toggle-btn {
  font-size: .76rem; font-weight: 600; padding: .35rem .8rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-dim);
  cursor: pointer; transition: all .15s var(--ease);
}
.flag-toggle-btn:hover { color: var(--text); border-color: var(--text-faint); }
.flag-toggle-btn.active { background: var(--accent); color: #fff; border-color: transparent; box-shadow: 0 2px 8px -2px rgba(109,107,247,.6); }

/* Custom (non-preset) flags a dispatcher typed in, rendered below the
   preset row -- each removable via its own "x" without touching the
   preset toggles above. Empty (no custom flags yet) renders nothing, so it
   doesn't leave a visible gap when unused. */
.flag-custom-list { display: flex; flex-wrap: wrap; gap: .45rem; margin-top: .5rem; }
.flag-custom-list:empty { margin-top: 0; }
.flag-custom-chip {
  display: inline-flex; align-items: center; gap: .35rem; font-size: .76rem; font-weight: 600;
  padding: .35rem .5rem .35rem .8rem; border-radius: var(--radius-pill);
  background: var(--slate-soft); color: var(--on-slate-soft);
}
.flag-chip-remove {
  border: none; background: transparent; color: inherit; opacity: .65; cursor: pointer;
  font-size: .95rem; line-height: 1; padding: 0 .15rem;
}
.flag-chip-remove:hover { opacity: 1; }

.flag-add-row { display: flex; gap: .5rem; margin-top: .7rem; align-items: center; flex-wrap: wrap; }

/* ==========================================================================
   Tables
   ========================================================================== */
.section-heading { font-size: .78rem; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; margin: 0 0 .8rem .2rem; }

.table-card { background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }

/* Price Guide category header bar -- background/color set inline per
   category from one of the existing --X-soft/--on-X-soft badge color
   tokens (see priceGuideCategoryColorVars_() in app.js), which already
   have separate light/dark values defined elsewhere in this file, so this
   rule itself needs no dark-mode override. */
.price-guide-category-header {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  padding: .65rem 1rem;
}
table.data-table { width: 100%; font-size: .82rem; border-collapse: collapse; }
table.data-table th {
  text-align: left; padding: .8rem 1rem; background: var(--surface-2);
  border-bottom: 1px solid var(--border); color: var(--text-dim);
  font-weight: 600; font-size: .72rem; text-transform: uppercase; letter-spacing: .03em;
}
/* Clickable Tickets-grid column headers (ticketsTable()'s opts.sortable) --
   Dashboard's Recent Tickets / After Hours Tickets reuse the same table
   markup without this class, so they stay plain. */
table.data-table th.sortable-th { cursor: pointer; user-select: none; transition: color .12s var(--ease), background .12s var(--ease); }
table.data-table th.sortable-th:hover { color: var(--text); background: var(--surface-hover); }
table.data-table th.sortable-th.sort-active { color: var(--accent); }
table.data-table td { padding: .75rem 1rem; border-bottom: 1px solid var(--border-soft); color: var(--text); }
table.data-table tr:last-child td { border-bottom: none; }
table.data-table tr.ticket-clickable-row,
table.data-table tr.invoice-clickable-row,
table.data-table tr.technician-clickable-row,
table.data-table tr.customer-clickable-row { cursor: pointer; transition: background .12s var(--ease); }
table.data-table tr.ticket-clickable-row:hover td,
table.data-table tr.invoice-clickable-row:hover td,
table.data-table tr.technician-clickable-row:hover td,
table.data-table tr.customer-clickable-row:hover td { background: var(--surface-hover); }

.assignee-chip { display: inline-flex; align-items: center; gap: .45rem; color: var(--text); }
.assignee-chip-extra { display: inline-block; font-size: .68rem; font-weight: 700; color: var(--text-dim); background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: .05rem .4rem; margin-left: .3rem; vertical-align: middle; }

/* ---- Photos (Tickets + Assets, added 2026-08-29) ---- */
.photo-gallery { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .3rem; }
.photo-thumb-link { position: relative; display: block; width: 84px; height: 84px; border-radius: var(--radius-sm, 8px); overflow: hidden; border: 1px solid var(--border); }
/* Opens the lightbox (2026-08-31 photo UX pass) -- a plain keyboard-operable
   <div role="button">, not an <a>, since clicking it no longer navigates
   anywhere (see photoGalleryHtml_()/wirePhotoGalleryClicks_() in app.js). */
.photo-thumb-click { display: block; width: 100%; height: 100%; cursor: pointer; }
.photo-thumb-click:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.photo-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Now a real <button> (was a non-interactive <span>) so it can open Maps on
   its own click, separate from the thumbnail's lightbox click -- reset the
   default button chrome, keep the same round pin-badge look. */
.photo-geo-badge {
  position: absolute; bottom: 2px; right: 2px; width: 18px; height: 18px; border-radius: 50%;
  background: rgba(0,0,0,.55); border: none; padding: 0; cursor: pointer;
  font-size: .7rem; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.photo-geo-badge:hover, .photo-geo-badge:focus-visible { background: rgba(0,0,0,.8); }
.photo-delete-btn {
  position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; border-radius: 50%;
  background: rgba(0,0,0,.55); color: #fff; border: none; padding: 0; cursor: pointer;
  font-size: .85rem; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.photo-delete-btn:hover:not(:disabled) { background: rgba(185,28,28,.9); }
.photo-delete-btn:disabled { opacity: .5; cursor: not-allowed; }

/* Customer Profile Photo Repository -- pin (ticket galleries only, top-left
   corner, opposite the delete button) and unpin (Customer profile's Pinned
   Equipment Photos gallery, replaces the delete button in its same
   top-right spot -- see photoGalleryHtml_()'s opts.showPinButton/unpinMode
   in app.js). */
.photo-pin-btn {
  position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%;
  background: rgba(0,0,0,.55); border: none; padding: 0; cursor: pointer;
  font-size: .7rem; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.photo-pin-btn:hover:not(:disabled) { background: rgba(0,0,0,.8); }
.photo-pin-btn.is-pinned, .photo-pin-btn:disabled { background: var(--accent); opacity: .85; cursor: default; }
.photo-unpin-btn {
  position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; border-radius: 50%;
  background: rgba(0,0,0,.55); color: #fff; border: none; padding: 0; cursor: pointer;
  font-size: .85rem; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.photo-unpin-btn:hover:not(:disabled) { background: rgba(185,28,28,.9); }
.photo-unpin-btn:disabled { opacity: .5; cursor: not-allowed; }

/* ---- Photo lightbox (2026-08-31 photo UX pass) ----
   Its own panel shape rather than .modal-panel -- a photo viewer wants to be
   image-led (dark, near-edge-to-edge, no header bar) instead of the usual
   card-with-header modal, so this is a deliberate one-off. Reuses
   .modal-overlay for the backdrop/centering/z-index/Esc-safe-area handling. */
.photo-lightbox-panel {
  position: relative; max-width: 92vw; max-height: 88vh; display: flex; flex-direction: column;
  align-items: center; background: #05070c; border-radius: var(--radius-lg); overflow: hidden;
  box-shadow: var(--shadow-lg); animation: rise .22s var(--ease);
}
.photo-lightbox-img { max-width: 92vw; max-height: 74vh; object-fit: contain; display: block; background: #05070c; }
.photo-lightbox-close {
  position: absolute; top: .6rem; right: .6rem; z-index: 1;
  background: rgba(0,0,0,.55); color: #fff; border-color: transparent;
}
.photo-lightbox-close:hover { background: rgba(0,0,0,.8); color: #fff; }
.photo-lightbox-footer {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: .8rem;
  padding: .7rem 1rem; background: var(--surface-solid);
}
.photo-lightbox-caption { font-size: .78rem; color: var(--text-faint); }
.photo-lightbox-footer .modal-save-btn { margin-top: 0; text-decoration: none; display: inline-block; }
@media (max-width: 640px) {
  .photo-lightbox-panel { max-width: 100vw; max-height: 100vh; border-radius: 0; }
  .photo-lightbox-img { max-height: 66vh; }
  .photo-lightbox-footer { flex-direction: column; align-items: stretch; text-align: center; }
}

/* ---- Install/Closeout Checklist (added 2026-08-29) ---- */
.checklist-list { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .3rem; }
.checklist-item-row { display: flex; align-items: flex-start; gap: .55rem; font-size: .85rem; color: var(--text); cursor: pointer; }
.checklist-item-row input[type="checkbox"] { margin-top: .2rem; flex-shrink: 0; width: 16px; height: 16px; }

/* ---- Customer Assets / equipment (added 2026-08-29) ---- */
.customer-asset-row { padding: .6rem 0; border-bottom: 1px solid var(--border-soft); }
.customer-asset-row:first-child { padding-top: 0; }

/* ---- Ask RunSheets AI assistant (Dashboard, added 2026-08-29) ---- */
.ai-assistant-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius, 12px); padding: 1rem 1.1rem; margin-bottom: 1.4rem; }
.ai-answer-box { margin-top: .8rem; padding: .8rem .9rem; background: var(--surface-solid); border: 1px solid var(--border); border-radius: 8px; font-size: .85rem; line-height: 1.5; white-space: pre-wrap; color: var(--text); }

/* ---- RunSheets Copilot (full-page "Ask RunSheets") ---- */
.copilot-suggestions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .9rem; }
.copilot-suggestion-chip {
  background: var(--surface-solid); border: 1px solid var(--border); border-radius: 999px;
  padding: .4rem .9rem; font-size: .76rem; color: var(--text-dim); cursor: pointer;
  transition: all .15s var(--ease); font-family: inherit;
}
.copilot-suggestion-chip:hover { background: var(--surface-hover); color: var(--text); border-color: var(--border-soft); }
.copilot-feed { display: flex; flex-direction: column; gap: .9rem; }
.copilot-qa-card {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 1rem 1.1rem; animation: rise .18s var(--ease);
}
.copilot-qa-card-error { border-color: #b91c1c66; }
.copilot-qa-question {
  display: flex; align-items: baseline; justify-content: space-between; gap: .8rem;
  font-weight: 700; font-size: .86rem; color: var(--text); margin-bottom: .55rem;
}
.copilot-qa-time { font-weight: 500; font-size: .68rem; color: var(--text-faint); white-space: nowrap; }
.copilot-qa-answer { font-size: .85rem; line-height: 1.55; color: var(--text-dim); white-space: pre-wrap; }
.copilot-empty-state { text-align: center; padding: 2.4rem 1rem; color: var(--text-faint); font-size: .82rem; background: var(--surface-2); border: 1px dashed var(--border); border-radius: var(--radius-md); }
@media (max-width: 640px) {
  .copilot-qa-question { flex-direction: column; align-items: flex-start; gap: .2rem; }
}

.assignee-avatar {
  width: 22px; height: 22px; border-radius: 999px; color: #fff; font-size: .62rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  object-fit: cover; object-position: center top; /* only affects the <img> variant (a real technician photo) — no-op on the initials <div> */
}
.assignee-unassigned { color: var(--text-faint); }

/* Pagination bar — sits as a footer inside .table-card, below the table */
.pagination-bar {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .75rem;
  padding: .75rem 1rem; border-top: 1px solid var(--border-soft); background: var(--surface-2);
}
.pagination-info { font-size: .74rem; color: var(--text-faint); }
.pagination-controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.pagination-size-label {
  display: flex; align-items: center; gap: .45rem; font-size: .72rem; color: var(--text-faint);
  font-weight: 500; white-space: nowrap;
}
.pagination-size { padding: .3rem .55rem; font-size: .76rem; }
.pagination-nav { display: flex; align-items: center; gap: .3rem; }
.pagination-btn {
  width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-solid); border: 1px solid var(--border); color: var(--text-dim);
  border-radius: var(--radius-sm); font-size: .8rem; cursor: pointer; transition: all .15s var(--ease);
}
.pagination-btn:hover:not(:disabled) { background: var(--surface-hover); color: var(--text); border-color: var(--border-soft); }
.pagination-btn:disabled { opacity: .35; cursor: not-allowed; }
.pagination-page { font-size: .74rem; color: var(--text-dim); padding: 0 .3rem; white-space: nowrap; }

/* ==========================================================================
   Badges / pills
   ========================================================================== */
.badge { font-size: .68rem; padding: .22rem .62rem; border-radius: var(--radius-pill); font-weight: 700; letter-spacing: .01em; display: inline-block; }
.badge-New { background: var(--accent-soft); color: var(--on-accent-soft); }
.badge-Assigned { background: var(--blue-soft); color: var(--on-blue-soft); }
.badge-EnRoute { background: var(--cyan-soft); color: var(--on-cyan-soft); }
.badge-InProgress { background: var(--amber-soft); color: var(--on-amber-soft); }
.badge-OnHold { background: var(--slate-soft); color: var(--on-slate-soft); }
.badge-PendingBilling { background: var(--purple-soft); color: var(--on-purple-soft); }
.badge-Closed { background: var(--green-soft); color: var(--on-green-soft); }
.badge-Complete { background: var(--green-soft); color: var(--on-green-soft); } /* Ticket Status & Permission Guardrails (2026-09-05): Technicians can reach Complete; only Office Staff/Admin can go the rest of the way to Closed -- see the Status dropdown filtering in openTicketModal() and the server-side guardrail in Code.gs's apiUpdate(). Same green as Closed since both read as "done" at a glance; the schedule-screen status coloring (see scheduleStatusColor_() in app.js) groups them the same way. */
.badge-Unscheduled { background: var(--red-soft); color: var(--on-red-soft); }

.badge-Low { background: var(--slate-soft); color: var(--on-slate-soft); }
.badge-Medium { background: var(--blue-soft); color: var(--on-blue-soft); }
.badge-High { background: var(--amber-soft); color: var(--on-amber-soft); }
.badge-Emergency { background: var(--red-soft); color: var(--on-red-soft); }

/* Tags a ticket auto-created by Nagoh's after-hours answering service
   (OnCallClerk) — see afterHoursTag_() in app.js. Reuses the same
   purple-soft token as badge-PendingBilling but is never shown alongside it
   (different column), so sharing the color isn't confusing in practice. */
.badge-AfterHours { background: var(--purple-soft); color: var(--on-purple-soft); }

/* Tags a ticket whose linked Customer record was auto-created (never
   reviewed by a person) -- see isAutoCreatedCustomer_()/newCustomerTag_() in
   app.js. Amber to match the callout below and the dashboard's existing
   "at risk" warning tone. */
.badge-NewCustomer { background: var(--amber-soft); color: var(--on-amber-soft); }

/* One per dispatcher-added flag on a ticket (VIP, Warranty Work, a custom
   one they typed, ...) -- see manualFlagsHtml_()/FLAG_PRESETS_ in
   site/app.js. Deliberately neutral/slate rather than reusing a status
   color, so a manually-added flag never gets mistaken for the purple
   After Hours / amber New Customer tags it sits next to in the same
   .flags-cell. */
.badge-ManualFlag { background: var(--slate-soft); color: var(--on-slate-soft); }

/* Per-flag color overrides (2026-09-10 Reece AC feedback batch, item 4:
   "Update the Engle flag color styling to #007AFF (Blue)") -- layered on
   top of the neutral .badge-ManualFlag base via FLAG_COLOR_CLASS_ in
   site/app.js, so most flags stay the deliberately-neutral slate above and
   only these named ones get their own color. #007AFF is Reece's exact
   requested hex rather than the theme's --blue-soft/--accent tokens, since
   this one flag's color was a specific, deliberate ask -- not meant to
   shift with the active color theme the way status/priority badges do. */
.badge-ManualFlag-engle { background: rgba(0,122,255,.16); color: #007AFF; }
.badge-ManualFlag-readyforquote { background: var(--green-soft); color: var(--on-green-soft); }
.badge-ManualFlag-contactattempted { background: var(--blue-soft); color: var(--on-blue-soft); }

/* Callout inside the ticket modal's Customer section prompting a dispatcher
   to fill in the gaps on an auto-created customer record (see
   openCustomerFormModal_() in app.js). Sized for a modal-section rather than
   reusing .notif-banner's page-level margins. */
.customer-review-callout {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap;
  padding: .65rem .9rem; border-radius: var(--radius-md);
  background: var(--amber-soft); border: 1px solid rgba(245,165,36,.35); color: var(--on-amber-soft);
  font-size: .8rem; font-weight: 500; margin-bottom: .8rem;
}
.customer-review-callout .modal-save-btn { margin: 0; white-space: nowrap; }

/* Lets the ticket ID + its optional badge-AfterHours tag sit side by side in
   the Tickets/After-Hours grid and wrap gracefully on narrow columns, instead
   of the tag getting clipped or forcing the ID cell wider. */
.ticket-id-cell { display: inline-flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
/* Flags column (After Hours / New Customer tags) in the Tickets grid — same wrap/gap behavior as .ticket-id-cell, split out into its own labeled column so the tags aren't unlabeled decoration on the Ticket cell. */
.flags-cell { display: inline-flex; align-items: center; gap: .4rem; flex-wrap: wrap; }

/* Groups the left/right clusters of a .view-toolbar (whatever filter/toggle
   controls a view already has) so .view-toolbar's own
   `justify-content: space-between` always has exactly two things to split
   apart, no matter how many controls are on either side. */
.view-toolbar-start,
.view-toolbar-end { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }

/* Manual refresh button (see refreshCurrentView_() in app.js). Global Top
   Menu Consistency pass (2026-09-11): used to be one per view's own
   toolbar (17 of them); now there's exactly one, permanently in the
   topbar (#globalRefreshBtn), and this class just carries its spinning/
   failed visual states -- the .icon-btn circular styling underneath is
   unchanged either way. */
.view-refresh-btn.spinning svg { animation: view-refresh-spin .8s linear infinite; }
.view-refresh-btn.refresh-failed { color: var(--red); border-color: var(--red); }
@keyframes view-refresh-spin { to { transform: rotate(360deg); } }

/* ==========================================================================
   Kanban
   ========================================================================== */
.kanban-lane { background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-lg); min-height: 60vh; padding: .65rem; transition: outline-color .15s var(--ease); }
.kanban-lane h3 { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-dim); padding: .5rem .45rem .7rem; margin: 0; }
.kanban-card {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: .7rem .8rem; margin-bottom: .55rem; font-size: .78rem; color: var(--text);
  cursor: grab; box-shadow: var(--shadow-sm); transition: transform .12s var(--ease), box-shadow .12s var(--ease), border-color .12s var(--ease);
}
.kanban-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--border-soft); }
.kanban-card .text-slate-500 { color: var(--text-dim) !important; margin: .2rem 0 .4rem; }
.kanban-card.dragging { opacity: .4; }
.kanban-lane.drop-target { outline: 2px dashed var(--accent); outline-offset: -2px; }

.sla-bar { height: 4px; border-radius: 2px; background: var(--border); overflow: hidden; margin-top: .4rem; }
.sla-bar > div { height: 100%; border-radius: 2px; }

/* ==========================================================================
   Dispatch board
   ========================================================================== */
.dispatch-section-header { display: flex; align-items: baseline; gap: .6rem; margin-bottom: .85rem; }
.dispatch-section-title { font-size: .95rem; font-weight: 700; color: var(--text); }
.dispatch-section-time { font-size: .74rem; color: var(--text-faint); }
.dispatch-section-count {
  font-size: .68rem; font-weight: 700; color: var(--on-accent-soft); background: var(--accent-soft);
  padding: .18rem .6rem; border-radius: var(--radius-pill); margin-left: auto;
}

.dispatch-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: .9rem; }
.dispatch-block { margin-bottom: 1.5rem; }
.dispatch-block-lg { margin-bottom: 2rem; }

/* Business Hours' 8-1 / 1-5 / Not Yet Scheduled split (see
   renderBusinessHoursSections_ in app.js) -- an eyebrow label over the three
   stacked renderShiftSection blocks, each already carrying its own header +
   grid, so this only needs to add breathing room between them. */
.dispatch-supersection-title {
  display: flex; align-items: baseline; gap: .6rem; font-size: .72rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 1rem;
}
.dispatch-subsection { margin-bottom: 1.5rem; }
.dispatch-subsection:last-child { margin-bottom: 0; }

.kanban-board { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: .75rem; }
@media (max-width: 1300px) { .kanban-board { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 760px) { .kanban-board { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.tech-lane {
  background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-lg);
  min-height: 150px; display: flex; flex-direction: column; overflow: hidden;
  transition: border-color .15s var(--ease), outline-color .15s var(--ease);
}
.tech-lane-header {
  display: flex; align-items: center; gap: .55rem; padding: .75rem .85rem;
  border-bottom: 1px solid var(--border-soft); background: var(--surface-2);
}
.tech-avatar {
  width: 30px; height: 30px; border-radius: 999px; color: #fff;
  font-size: .68rem; font-weight: 700; display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; box-shadow: 0 0 0 2px rgba(255,255,255,.06);
  object-fit: cover; object-position: center top; /* only affects the <img> variant (a real technician photo) — no-op on the initials <div> */
}
/* Same avatar, two more sizes: the Technicians management table row, and the
   larger live preview in the add/edit modal. All three (plus .assignee-avatar
   above) share the same fallback/photo logic via techAvatarHtml() in app.js. */
.tech-avatar-md {
  width: 40px; height: 40px; border-radius: 999px; color: #fff;
  font-size: .78rem; font-weight: 700; display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; object-fit: cover; object-position: center top;
}
.tech-avatar-lg {
  width: 72px; height: 72px; border-radius: 999px; color: #fff;
  font-size: 1.15rem; font-weight: 700; display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; object-fit: cover; object-position: center top; box-shadow: 0 0 0 3px var(--border-soft);
}
.tech-avatar-lg-wrap { position: relative; width: 72px; height: 72px; flex-shrink: 0; }
.tech-avatar-lg-wrap .tech-avatar-lg { position: absolute; inset: 0; }
.tech-lane-name { font-size: .82rem; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tech-lane-count {
  font-size: .68rem; font-weight: 700; color: var(--text-dim); margin-left: auto; background: var(--surface-hover);
  min-width: 20px; height: 20px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; padding: 0 .3rem;
}
.oncall-tag {
  font-size: .6rem; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
  background: var(--amber-soft); color: var(--on-amber-soft); padding: .15rem .4rem; border-radius: var(--radius-sm);
}
.tech-lane-body { padding: .7rem; flex: 1; min-height: 96px; }

/* Dispatch Board Helper/Lead Layout (2026-09-10 Reece AC feedback, item
   11) -- a subtly stronger left border marks a permanent lead column at a
   glance, and the small text field beneath its header is where a
   dispatcher notes who's helping that lead today. See
   renderShiftSection()/attachDispatchHelperFieldSave_() in site/app.js. */
.tech-lane-lead { border-left: 3px solid var(--accent); }
.tech-lane-helper { position: relative; padding: .45rem .85rem; border-bottom: 1px solid var(--border-soft); background: var(--surface-1, var(--surface-solid)); }
.tech-lane-helper-input {
  width: 100%; font-size: .72rem; padding: .3rem .5rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-solid); color: var(--text);
}
.tech-lane-helper-input:focus { outline: none; border-color: var(--accent); }
/* Multi-Helper Selection Dropdown (2026-09-11, mobile audit) -- this
   component (helperOptionsHtml_/wireHelperDropdownDelegation_ in app.js)
   had no CSS of its own at all until now, so it was rendering as bare
   unstyled markup. Deliberately NOT absolutely positioned as a floating
   popover: .tech-lane has overflow:hidden (so the lane's drag-and-drop
   card list clips cleanly), which would silently clip a position:absolute
   menu the moment it opened, worst of all on narrow phone screens. Instead
   the menu opens in normal document flow directly beneath the toggle
   button and pushes the lane taller -- that's inherently safe from both
   horizontal overflow and ancestor clipping at every viewport width, no
   extra @media rule required. */
.tech-lane-helper-btn {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: .4rem;
  font-size: .72rem; padding: .4rem .55rem; min-height: 34px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-solid); color: var(--text);
  cursor: pointer; text-align: left; transition: border-color .15s var(--ease);
}
.tech-lane-helper-btn:hover { border-color: var(--border-soft); }
.tech-lane-helper-btn-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tech-lane-helper-btn-label.placeholder { color: var(--text-faint); }
.tech-lane-helper-caret { flex-shrink: 0; color: var(--text-faint); }
.tech-lane-helper-menu {
  margin-top: .4rem; padding: .5rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-solid); box-shadow: var(--shadow-md);
}
.tech-lane-helper-menu.hidden { display: none; }
.tech-lane-helper-menu-list { max-height: 190px; overflow-y: auto; display: flex; flex-direction: column; gap: .05rem; margin-bottom: .5rem; }
.tech-lane-helper-menu-empty { font-size: .72rem; color: var(--text-faint); padding: .3rem .2rem; }
.tech-lane-helper-option {
  display: flex; align-items: center; gap: .5rem; min-height: 38px; padding: .3rem .3rem;
  border-radius: var(--radius-sm); font-size: .76rem; color: var(--text); cursor: pointer;
}
.tech-lane-helper-option:hover { background: var(--surface-hover); }
.tech-lane-helper-option input[type="checkbox"] { width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--accent); }
.tech-lane-helper-save-btn {
  width: 100%; font-size: .72rem; font-weight: 700; padding: .5rem; min-height: 36px;
  border-radius: var(--radius-sm); border: none; background: var(--accent); color: #fff; cursor: pointer;
}
.tech-lane-helper-save-btn:hover { filter: brightness(1.08); }
.tech-lane-empty {
  height: 100%; min-height: 84px; display: flex; align-items: center; justify-content: center;
  border: 1.5px dashed var(--border); border-radius: var(--radius-md); color: var(--text-faint); font-size: .74rem; text-align: center; padding: .5rem;
}
.tech-lane.drop-target { outline: 2px dashed var(--accent); outline-offset: -2px; }

.dispatch-ticket-card {
  background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--slate);
  border-radius: var(--radius-md); padding: .55rem .65rem; margin-bottom: .5rem; font-size: .76rem;
  cursor: grab; box-shadow: var(--shadow-sm); transition: transform .12s var(--ease), box-shadow .12s var(--ease);
}
.dispatch-ticket-card:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.dispatch-ticket-card:last-child { margin-bottom: 0; }
.dispatch-ticket-card.dragging { opacity: .4; }
.dispatch-ticket-card.priority-Emergency { border-left-color: var(--red); }
.dispatch-ticket-card.priority-High { border-left-color: var(--amber); }
.dispatch-ticket-card.priority-Medium { border-left-color: var(--blue); }
.dispatch-ticket-card.priority-Low { border-left-color: var(--slate); }
/* Overdue-and-still-open ticket (see isPastDueForBoard_ in app.js) -- wins
   over the priority-based left border above since it always comes after it
   in the cascade here, regardless of which priority class is also present. */
.dispatch-ticket-card.past-due { border-left-color: var(--red); box-shadow: 0 0 0 1px rgba(243,89,107,.25); }

/* Schedule Screen Status Color Coding & Persistence (item 2, 2026-09-05) --
   applied alongside the existing .priority-X border-left color: this tints
   the WHOLE block background by status bucket (see scheduleStatusClass_()/
   scheduleStatusColor_() in app.js), so a dispatcher scanning a packed
   Dispatch board or Day Timeline gets "stalled / underway / urgent-hold /
   done" at a glance without reading every card's text. */
/* 2026-09-24: --status-color (set inline from statusColor_() in app.js --
   the tenant's/template's own color for that exact status) wins; the hex
   after the comma is the original bucket color, used if it's missing. */
.dispatch-ticket-card.schedule-status-grey { background: color-mix(in srgb, var(--status-color, #94a3b8) 14%, var(--surface-solid)); }
.dispatch-ticket-card.schedule-status-yellow { background: color-mix(in srgb, var(--status-color, #f59e0b) 16%, var(--surface-solid)); }
.dispatch-ticket-card.schedule-status-red { background: color-mix(in srgb, var(--status-color, #dc2626) 14%, var(--surface-solid)); }
.dispatch-ticket-card.schedule-status-green { background: color-mix(in srgb, var(--status-color, #16a34a) 14%, var(--surface-solid)); }
.unassigned-tray .dispatch-ticket-card.schedule-status-grey,
.unassigned-tray .dispatch-ticket-card.schedule-status-yellow,
.unassigned-tray .dispatch-ticket-card.schedule-status-red,
.unassigned-tray .dispatch-ticket-card.schedule-status-green,
.pastdue-tray .dispatch-ticket-card.schedule-status-grey,
.pastdue-tray .dispatch-ticket-card.schedule-status-yellow,
.pastdue-tray .dispatch-ticket-card.schedule-status-red,
.pastdue-tray .dispatch-ticket-card.schedule-status-green {
  /* Needs Dispatch / Past Due trays keep their plain surface background --
     everything in them is by definition NOT yet underway, so the tinting
     would be redundant/noisy there (every card would just be grey anyway). */
  background: var(--surface-solid);
}

/* Return Call workflow improvements (item 6) -- deliberately louder/more
   saturated than the neutral flag-chip styling manualFlagsHtml_() uses for
   every other manual flag, so it reads as "needs attention" rather than
   just another tag. */
.badge-ReturnCall { background: var(--red); color: #fff; font-weight: 800; }

/* POS Integration Module (2026-09-07) -- Connected/Action Needed/
   Disconnected status vocabulary, same soft-token badge pattern as every
   other status badge above. See renderPOSConnections_() in app.js. */
.badge-Connected { background: var(--green-soft); color: var(--on-green-soft); }
.badge-ActionNeeded { background: var(--amber-soft); color: var(--on-amber-soft); }
.badge-Disconnected { background: var(--slate-soft); color: var(--on-slate-soft); }
.badge-Pending { background: var(--blue-soft); color: var(--on-blue-soft); }

.pos-provider-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: .8rem; margin: .6rem 0; }
.pos-provider-card { border: 1px solid var(--border); border-radius: .6rem; padding: 1rem .8rem; text-align: center; cursor: pointer; background: var(--surface); transition: border-color .15s, box-shadow .15s; }
.pos-provider-card:hover { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.pos-provider-card.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.pos-provider-card[disabled], .pos-provider-card.coming-soon { opacity: .55; cursor: not-allowed; }
.pos-provider-card .pos-provider-name { display: block; font-weight: 700; margin-top: .4rem; }
.pos-provider-card .pos-provider-tag { display: block; font-size: .7rem; color: var(--text-muted); margin-top: .2rem; }
.pos-connection-row { display: flex; align-items: center; gap: .8rem; padding: .8rem; border: 1px solid var(--border); border-radius: .6rem; margin-bottom: .6rem; flex-wrap: wrap; }
.pos-connection-row .pos-connection-main { flex: 1; min-width: 200px; }
.pos-connection-row .pos-connection-name { font-weight: 700; }
.pos-connection-row .pos-connection-meta { font-size: .75rem; color: var(--text-muted); margin-top: .15rem; }
.pos-connection-row .pos-connection-error { font-size: .75rem; color: var(--red); margin-top: .25rem; }
.pos-connection-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

.pastdue-badge {
  display: inline-block; background: var(--red-soft); color: var(--on-red-soft); font-size: .62rem;
  font-weight: 700; letter-spacing: .02em; padding: .1rem .38rem; border-radius: var(--radius-sm); margin-left: .35rem;
}
.dispatch-ticket-id { font-weight: 700; color: var(--text); }
.dispatch-ticket-scheduled { color: var(--text-dim); font-size: .72rem; margin-top: .12rem; }
.dispatch-ticket-customer { color: var(--text-dim); margin-top: .12rem; }
.dispatch-ticket-address { color: var(--text-faint); font-size: .68rem; margin-top: .12rem; white-space: normal; }

/* Dispatch board card quick-flags (2026-09-10 Reece AC feedback batch,
   items 1 & 8) -- see dispatchCardQuickFlags_()/ticketCardHtml() in
   site/app.js. Shown directly on the card, no detail-view click required.
   :empty hides the wrapper entirely for a ticket with neither flag, so it
   never leaves a stray gap on the card. */
.dispatch-ticket-quickflags:empty { display: none; }
.dispatch-ticket-quickflags { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .25rem; }
.quick-flag-badge {
  display: inline-block; font-size: .62rem; font-weight: 700; letter-spacing: .01em;
  padding: .16rem .45rem; border-radius: 999px; white-space: nowrap;
}
/* "prominent green 'Ready for Quote' badge/button" per item 8 -- the
   brightest, most attention-grabbing badge on the card by design (solid
   fill, not the soft/tinted treatment every other badge here uses),
   because the whole point is that a dispatcher notices it without having
   to look for it. */
.quick-flag-ready-for-quote { background: #16a34a; color: #ffffff; }
.quick-flag-contact-attempted { background: var(--blue-soft); color: var(--on-blue-soft); }

/* Tap-to-call / tap-to-map links used inline in ticket cards, ticket/customer
   modals, and the Customers/Technicians grids (see contactLinkHtml_ and
   addressLinkHtml_ in app.js). Inherits the surrounding element's size so it
   drops into faint/dim text (like dispatch tile fields) without looking
   like a mismatched blue browser-default link. */
.link-inline { color: inherit; text-decoration: underline; text-decoration-color: var(--accent-ring); text-underline-offset: 2px; }
.link-inline:hover { color: var(--accent); text-decoration-color: var(--accent); }

.unassigned-tray {
  background: var(--amber-soft); border: 1px solid rgba(245,165,36,.3); border-radius: var(--radius-lg); padding: 1rem;
}
.unassigned-tray-header { display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem; }
.unassigned-tray-title { font-size: .84rem; font-weight: 700; color: var(--on-amber-soft); }
.unassigned-tray-cards { display: flex; flex-wrap: wrap; gap: .55rem; }
.unassigned-tray .dispatch-ticket-card { width: 210px; margin-bottom: 0; background: var(--surface-solid); }
.unassigned-tray-empty { font-size: .78rem; color: var(--on-amber-soft); }
/* Dispatch board UI/UX batch (2026-09-10 Reece AC feedback), item 4: the
   Needs Dispatch tray never had a drop-target affordance because it never
   had a drop handler at all -- see the new #unassignedCards drop listener
   in attachDispatchDragDrop() (site/app.js). Same outline treatment as
   .tech-lane.drop-target. */
.unassigned-tray-cards.drop-target { outline: 2px dashed var(--accent); outline-offset: -2px; border-radius: var(--radius-md); }

/* Awaiting Parts dispatch column (2026-09-10 Reece AC feedback, item 7) --
   reuses .unassigned-tray's layout via the shared class, distinguished with
   the palette's blue-soft tokens instead of amber so it reads as its own
   column at a glance. */
.awaitingparts-tray { background: var(--blue-soft); border-color: rgba(75,155,255,.35); }
.awaitingparts-tray-title, .awaitingparts-tray .unassigned-tray-empty { color: var(--on-blue-soft); }
.awaitingparts-tray #awaitingPartsCards.drop-target { outline: 2px dashed var(--accent); outline-offset: -2px; border-radius: var(--radius-md); }

.pastdue-tray {
  background: var(--red-soft); border: 1px solid rgba(243,89,107,.35); border-radius: var(--radius-lg); padding: 1rem;
}
.pastdue-tray-header { display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem; }
.pastdue-tray-title { font-size: .84rem; font-weight: 700; color: var(--on-red-soft); }
.pastdue-tray-cards { display: flex; flex-wrap: wrap; gap: .55rem; }
.pastdue-tray .dispatch-ticket-card { width: 210px; margin-bottom: 0; background: var(--surface-solid); }

/* ==========================================================================
   Modals
   ========================================================================== */
.modal-overlay {
  position: fixed; inset: 0; background: rgba(6,8,14,.65); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; z-index: 100;
  animation: fadeIn .18s var(--ease);
  /* Same standalone-PWA safe-area reasoning as .app-shell -- matters most on
     phones, where .modal-panel below goes full-screen (100%/100%) with a
     sticky header/footer that would otherwise sit under the status bar or
     home indicator. */
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
.modal-overlay.hidden { display: none; }
.modal-panel {
  background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-xl);
  /* Fluid width instead of a flat 640px -- scales up with the viewport on
     larger monitors (up to 780px) instead of leaving a narrow modal
     stranded in the middle of a wide screen, while max-width:92vw still
     shrinks it down on anything narrower. See .modal-panel-wide below for
     the more generous version used by data-table-heavy modals. */
  width: clamp(640px, 42vw, 780px); max-width: 92vw; max-height: 88vh; overflow-y: auto; box-shadow: var(--shadow-lg);
  animation: rise .22s var(--ease);
}
/* Ticket modal only (see index.html's ticketModalOverlay) -- scales fluidly
   between the shared 640px modal width and a wider desktop layout, so a big
   monitor shows more of the ticket (fewer fields hidden behind a scroll)
   while a phone still gets the full-screen treatment from the
   max-width:640px override further down, which targets .modal-panel and
   wins there on source order. auto-fit lets each field-grid inside this
   modal pick up extra columns as the panel widens, instead of staying
   pinned at 2 columns like the narrower modals (technician/customer/invoice
   forms) still using plain .modal-field-grid. */
.modal-panel-wide { width: clamp(760px, 62vw, 1200px); max-width: 95vw; }
.modal-panel-wide .modal-field-grid { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.modal-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 1.1rem 1.4rem; border-bottom: 1px solid var(--border-soft);
  position: sticky; top: 0; background: var(--surface-solid); z-index: 1; border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}
/* Sticky Save bar for the ticket modal (see modalFooter in app.js's
   openTicketModal()) -- a direct sibling of .modal-body inside the same
   scrolling .modal-panel, mirroring .modal-header's sticky top:0 so Save is
   reachable without scrolling to the very bottom of a long ticket. Empty
   until a ticket is loaded, so it doesn't show a stray border on load. */
.modal-footer {
  display: flex; align-items: center; gap: .6rem;
  padding: .85rem 1.4rem; border-top: 1px solid var(--border-soft);
  position: sticky; bottom: 0; background: var(--surface-solid); z-index: 1;
  border-radius: 0 0 var(--radius-xl) var(--radius-xl);
}
.modal-footer:empty { display: none; }
.modal-footer .modal-save-btn { margin-top: 0; }
.modal-ticket-id { font-size: 1.08rem; font-weight: 700; color: var(--text); margin-right: .6rem; }
.modal-close {
  font-size: 1.4rem; line-height: 1; color: var(--text-faint); background: var(--surface-2); border: 1px solid var(--border);
  width: 32px; height: 32px; border-radius: 999px; cursor: pointer; transition: all .15s var(--ease);
}
.modal-close:hover { color: var(--text); background: var(--surface-hover); }
.modal-body { padding: 1.25rem 1.4rem 1.6rem; font-size: .85rem; }
.modal-section {
  margin-bottom: 1.1rem; padding: 1rem 1.1rem; background: var(--bg-soft);
  border: 1px solid var(--border-soft); border-radius: var(--radius-md);
}
.modal-section:last-child { margin-bottom: 0; }
.modal-section-title {
  font-size: .68rem; font-weight: 700; text-transform: uppercase; color: var(--text-dim);
  letter-spacing: .05em; margin-bottom: .65rem;
}
.modal-section-edit-btn {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  font-family: inherit; font-size: .72rem; font-weight: 600; text-transform: none;
  letter-spacing: normal; color: var(--accent);
}
.modal-section-edit-btn:hover { text-decoration: underline; }

/* Ticket UI Header Cleanup (item 5) -- collapsed SLA/Created/Status History
   disclosure inside the ticket modal's Ticket section. Native <details>, no
   JS needed to expand/collapse. */
.modal-advanced-details { border-top: 1px dashed var(--border-soft); padding-top: .6rem; }
.modal-advanced-details summary {
  cursor: pointer; font-size: .72rem; color: var(--text-faint); font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em;
}
.modal-advanced-details summary:hover { color: var(--text-dim); }
/* Ticket Status Audit Trail (item 3) */
.status-history-table { width: 100%; border-collapse: collapse; font-size: .74rem; }
.status-history-table th {
  text-align: left; color: var(--text); font-weight: 600; padding: .3rem .5rem;
  border-bottom: 1px solid var(--border-soft);
}
.status-history-table td { padding: .3rem .5rem; border-bottom: 1px solid var(--border-soft); }
.modal-field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem 1.3rem; }
.modal-field-label { color: var(--text); font-size: .7rem; margin-bottom: .15rem; }
.modal-field-value { color: var(--text); line-height: 1.45; }

/* ==========================================================================
   Map view
   ========================================================================== */
.map-container { width: 100%; height: 70vh; border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--surface-2); }
.map-status { font-size: .75rem; color: var(--text-faint); margin-top: .7rem; }

.map-legend { display: flex; align-items: center; gap: 1.2rem; margin-bottom: .85rem; font-size: .78rem; color: var(--text-dim); }
.map-legend-item { display: flex; align-items: center; gap: .4rem; }
.legend-dot { width: 10px; height: 10px; border-radius: 999px; display: inline-block; flex-shrink: 0; }

.map-pin { display: flex; align-items: center; gap: .4rem; cursor: pointer; transform: translateY(-6px); }
.map-pin-dot { width: 14px; height: 14px; border-radius: 999px; border: 2px solid #10141f; flex-shrink: 0; box-shadow: 0 1px 6px rgba(0,0,0,.5); }
.map-pin-label {
  background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .3rem .6rem;
  font-size: .68rem; line-height: 1.35; white-space: nowrap; box-shadow: var(--shadow-md); color: var(--text);
}
.map-pin-ticket { font-weight: 700; color: var(--text); }
.map-pin-customer { color: var(--text-dim); }
.map-pin-tech { color: var(--on-purple-soft); font-weight: 600; }

/* Live/last-known GPS presence overlay (2026-09-07) -- see
   renderUserLocationOverlay_()/placeUserLocationBubble_() in app.js. A
   distinct bubble shape from .map-pin above (rounded pill with an avatar
   circle) so a teammate's live position is never visually confused with a
   ticket's dispatch pin, even when they land near each other on the map. */
.map-user-bubble { display: flex; align-items: center; gap: .5rem; cursor: pointer; transform: translateY(-10px); }
.map-user-bubble-avatar {
  width: 30px; height: 30px; border-radius: 999px; flex-shrink: 0; overflow: hidden;
  background: var(--purple, #7c3aed); color: #fff; display: flex; align-items: center; justify-content: center;
  font-size: .68rem; font-weight: 700; border: 2px solid #10141f; box-shadow: 0 1px 6px rgba(0,0,0,.5);
}
.map-user-bubble-avatar img { width: 100%; height: 100%; object-fit: cover; }
.map-user-bubble-info {
  background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .3rem .6rem;
  font-size: .68rem; line-height: 1.35; white-space: nowrap; box-shadow: var(--shadow-md); color: var(--text);
}
.map-user-bubble-name { font-weight: 700; color: var(--text); }
.map-user-bubble-time { color: var(--text-dim); }

/* Live Technician Status & GPS Map List + Ping Data Visualizer
   (2026-09-08) -- a side panel next to the map listing every technician
   currently logged into RunSheets (Active Sessions, same
   apiGetActiveSessions() action Settings > Active Sessions already uses)
   cross-referenced with their last-known GPS ping (DATA.userLocations,
   opt-in -- see the Live GPS Presence overlay above) and technician
   photo/avatar (DATA.technicians by TechID). Consolidated 2026-09-10 into
   ONE bottom-bar table (see .map-bottom-bar below) -- see
   renderTeamPresenceBottomBar_() in app.js. */
.map-layout { display: flex; gap: 1rem; align-items: flex-start; }
.map-main { flex: 1; min-width: 0; }
.map-team-panel-empty { padding: 1.4rem 1rem; font-size: .78rem; color: var(--text-faint); text-align: center; }
.map-team-row-avatar { position: relative; flex-shrink: 0; display: inline-block; vertical-align: middle; margin-right: .4rem; }
.map-team-row-loc { font-size: .68rem; color: var(--text-faint); margin-top: .1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Force-Ping Device Location Sync (2026-09-10) -- Admin/Staff-only button
   in each Team Presence & Telemetry row, see wireTeamPresencePingButtons_()/
   apiAdminForcePingLocation in site/app.js. */
.map-team-ping-btn {
  flex-shrink: 0; font-size: .68rem; font-weight: 600; padding: .3rem .6rem;
  border-radius: .4rem; border: 1px solid var(--border); background: var(--surface);
  color: var(--text-dim); cursor: pointer; white-space: nowrap;
}
.map-team-ping-btn:hover:not(:disabled) { background: var(--surface-hover, var(--surface)); color: var(--text); border-color: var(--accent, var(--border)); }
.map-team-ping-btn:disabled { opacity: .6; cursor: default; }

/* Team Presence + Telemetry consolidated bottom bar (2026-09-10) -- see
   the .map-layout comment above for why this replaced the old side panel
   + separate below-map table. A collapsible drawer, not a fixed/sticky
   overlay, so it never covers the map itself; collapsing it just frees
   vertical page space below the map. */
.map-bottom-bar {
  margin-top: 1rem; border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface-solid); overflow: hidden;
}
.map-bottom-bar-header {
  width: 100%; display: flex; align-items: center; gap: .6rem; padding: .8rem 1rem;
  background: none; border: none; cursor: pointer; text-align: left; font: inherit;
  color: var(--text);
}
.map-bottom-bar-title { font-size: .84rem; font-weight: 700; }
.map-bottom-bar-count { font-size: .7rem; font-weight: 600; color: var(--text-dim); }
.map-bottom-bar-chevron { margin-left: auto; flex-shrink: 0; transition: transform .15s ease; }
.map-bottom-bar-collapsed .map-bottom-bar-chevron { transform: rotate(-90deg); }
.map-bottom-bar-body {
  padding: 0 1rem 1rem; border-top: 1px solid var(--border-soft);
  max-height: 420px; overflow-y: auto;
}
.map-bottom-bar-collapsed .map-bottom-bar-body { display: none; }
.map-bottom-bar-body .map-ping-section-sub { margin-top: .8rem; }

@media (max-width: 980px) {
  /* Mobile Map scaling fix (2026-09-13): .map-layout sets
     `align-items: flex-start` for the DESKTOP row layout (so the side
     content next to the map doesn't get vertically stretched to match the
     map's height). align-items also controls the CROSS axis, and flipping
     to `flex-direction: column` here rotates the cross axis from vertical
     to horizontal -- so without an override, that same flex-start made
     .map-main (and #mapContainer inside it, even with its own
     `width: 100%`) shrink-to-fit its content's natural width instead of
     stretching to the full column width, rendering the map as a narrow
     vertical strip with dead space beside it on phones. `align-items:
     stretch` (the flexbox default, restated explicitly here since the row
     layout above needs its own flex-start) makes every child span the
     full available width in the stacked mobile layout. */
  .map-layout { flex-direction: column; align-items: stretch; }
  .map-main, .map-container { width: 100%; max-width: 100%; }
}

@media (max-width: 640px) {
  /* Fluid map height + overlay/control alignment on phone widths --
     70vh (the desktop height) can be awkwardly tall or push the toolbar/
     legend/team-locations checkbox and the bottom data panel off-balance
     on a short landscape phone screen; cap it and let width drive it
     instead. */
  .map-container { height: min(70vh, 420px); border-radius: var(--radius-md, var(--radius-lg)); }
  .map-legend { flex-wrap: wrap; row-gap: .5rem; gap: .6rem 1rem; }
  .map-bottom-bar-header { flex-wrap: wrap; row-gap: .3rem; }
  .map-bottom-bar-body { max-height: 320px; }
}

/* Mobile Map View fixes (2026-09-10, code-audit pass) -- callout touch
   targets. .map-pin-dot was 14px and .map-pin-label's font/padding were
   tuned for a mouse pointer, not a finger; on a phone-width viewport
   (matching this app's existing "phone" breakpoint used elsewhere for
   table-stacking, see feature_oncall_schedule_management.md) both the
   ticket-pin markers and the Live GPS Presence teammate bubbles get
   larger dots/avatars, more label padding, and a bigger gap between the
   dot and its label so adjacent pins are actually distinguishable and
   tappable with a fingertip instead of a mouse cursor. The whole .map-pin/
   .map-user-bubble element is already the click/tap target (gmpClickable
   on the AdvancedMarkerElement wraps dot+label together, see
   placeTicketMarker()/placeUserLocationBubble_() in app.js) -- this only
   changes how big that target renders, not what's clickable. */
@media (max-width: 700px) {
  .map-pin { gap: .55rem; }
  .map-pin-dot { width: 20px; height: 20px; border-width: 3px; }
  .map-pin-label { padding: .45rem .7rem; font-size: .78rem; }
  .map-user-bubble { gap: .6rem; }
  .map-user-bubble-avatar { width: 38px; height: 38px; font-size: .78rem; border-width: 3px; }
  .map-user-bubble-info { padding: .45rem .7rem; font-size: .78rem; }
}

/* Technician Ping Data Visualizer -- Name/Status/Coordinates/Last Ping
   (relative + exact)/a small live sparkline of ping recency built
   client-side each poll tick (no new GPS history Sheet tab --
   UserLocations is deliberately one-row-per-user, not an append-only
   trail, see that file's own doc comment; a full history log would blow
   past that same constraint). Lives inside .map-bottom-bar-body since the
   2026-09-10 consolidation. */
.map-ping-section-sub { font-size: .74rem; color: var(--text-faint); margin-bottom: .8rem; }
.ping-status-dot { width: 9px; height: 9px; border-radius: 999px; display: inline-block; margin-right: .4rem; vertical-align: middle; }
.ping-status-dot.status-live { background: var(--green); }
.ping-status-dot.status-idle { background: var(--amber); }
.ping-status-dot.status-stale { background: var(--text-faint); }
.ping-coords { font-variant-numeric: tabular-nums; color: var(--text-dim); font-size: .78rem; }
.ping-spark { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 20px; vertical-align: middle; }
.ping-spark-bar { width: 4px; min-height: 2px; border-radius: 1px; background: var(--border); }
.ping-spark-bar.status-live { background: var(--green); }
.ping-spark-bar.status-idle { background: var(--amber); }
.ping-spark-bar.status-stale { background: var(--red); }

/* ==========================================================================
   My Layout Templates (Settings) -- profile/layout template rows, both
   the personal list and the Admin/Staff "Team Layout Templates" oversight
   list share this shape (see renderProfileTemplatesSection_() in app.js).
   ========================================================================== */
.profile-template-list { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1rem; }
.profile-template-row {
  display: flex; align-items: center; gap: .75rem; padding: .6rem .8rem; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-2);
}
.profile-template-row-info { flex: 1 1 auto; min-width: 0; }
.profile-template-row-name { font-weight: 600; color: var(--text); font-size: .85rem; display: flex; align-items: center; gap: .4rem; }
.profile-template-row-desc { font-size: .72rem; color: var(--text-dim); margin-top: .1rem; }
.profile-template-row-actions { display: flex; gap: .4rem; flex-shrink: 0; }
.profile-template-empty { font-size: .78rem; color: var(--text-faint); padding: .5rem 0; }

/* Color Theme Selector (Settings) -- swatch-card grid, one per curated
   palette (see COLOR_THEMES_/renderColorThemeSection_() in app.js and the
   token blocks in this file's "Color Theme Selector" section above). Cards
   share the .profile-template-row's card/border language above but use a
   grid of tappable swatch buttons instead of a list, since picking a
   palette benefits from seeing several side by side. */
.color-theme-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .7rem; margin-bottom: .6rem;
}
.color-theme-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: .55rem; text-align: left;
  padding: .75rem .85rem; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-2); cursor: pointer; font-family: inherit;
  transition: border-color .15s var(--ease), background .15s var(--ease), transform .15s var(--ease);
}
.color-theme-card:hover { border-color: var(--accent); transform: translateY(-1px); }
.color-theme-card.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); background: var(--accent-soft); }
.color-theme-swatches { display: flex; gap: .3rem; }
.color-theme-dot { width: 16px; height: 16px; border-radius: 999px; flex-shrink: 0; box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); }
.color-theme-card-body { display: flex; flex-direction: column; gap: .2rem; }
.color-theme-card-name { font-weight: 600; color: var(--text); font-size: .85rem; display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
.color-theme-card-desc { font-size: .72rem; color: var(--text-dim); line-height: 1.4; }

/* ==========================================================================
   Dispatch timeline
   ========================================================================== */
.timeline-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .75rem; margin-bottom: .75rem; }
.timeline-legend { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.legend-item { display: inline-flex; align-items: center; gap: .4rem; font-size: .72rem; color: var(--text-dim); }
.legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }

.timeline-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface-solid); }
.timeline-header-row { display: flex; border-bottom: 1px solid var(--border); }
.timeline-tech-col-spacer { width: 150px; flex-shrink: 0; border-right: 1px solid var(--border); position: sticky; left: 0; background: var(--surface-solid); z-index: 4; }
.timeline-hour-label { flex-shrink: 0; font-size: .66rem; color: var(--text-faint); text-align: center; padding: .6rem 0; border-right: 1px solid var(--border-soft); }
.timeline-hour-label-business { color: var(--text-dim); background: var(--accent-soft); }
.timeline-row { display: flex; border-bottom: 1px solid var(--border-soft); }
.timeline-tech-col {
  width: 150px; flex-shrink: 0; display: flex; align-items: center; gap: .55rem;
  padding: .55rem .8rem; border-right: 1px solid var(--border); background: var(--surface-2);
  position: sticky; left: 0; z-index: 3; font-size: .8rem; font-weight: 600; color: var(--text);
}
/* flex-shrink:0 is load-bearing, not cosmetic -- see the dayRowWidth
   comment in renderTimeline() (app.js). Without it this shrinks to fit
   the visible scroll port instead of staying 24-hours wide, which breaks
   both the sticky technician-name column and reachability of most of
   the day, worst on narrow (mobile/tablet) viewports. */
.timeline-track { position: relative; flex-shrink: 0; }
.timeline-business-band { position: absolute; top: 0; bottom: 0; background: var(--accent-soft); z-index: 0; }
.timeline-now-line { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--red); z-index: 2; box-shadow: 0 0 8px rgba(243,89,107,.7); }
.timeline-block {
  position: absolute; border-radius: var(--radius-sm); overflow: hidden;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding: .25rem .5rem; cursor: pointer; color: var(--text); z-index: 1;
  transition: transform .12s var(--ease), box-shadow .12s var(--ease);
}
.timeline-block:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); z-index: 2; }
.timeline-block-time { font-size: .62rem; font-weight: 600; opacity: .8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
.timeline-block-label { font-size: .72rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }

/* ==========================================================================
   Time & Material / Invoice UI
   ========================================================================== */
.tm-log-row { display: flex; justify-content: space-between; font-size: .8rem; padding: .45rem 0; border-bottom: 1px solid var(--border-soft); color: var(--text); }
.tm-log-row:last-child { border-bottom: none; }
.tm-add-row { display: flex; gap: .5rem; margin-top: .75rem; align-items: center; flex-wrap: wrap; }

/* Invoice(s) generated from a ticket, listed in the ticket modal's Time &
   Material section (see ticketInvoices/generateInvoiceBtn in app.js) --
   real <button> elements (not styled divs) so they're keyboard-operable
   without extra JS, reset to look like a clickable row/card instead of the
   pill-shaped .modal-save-btn. */
.ticket-invoice-list { display: flex; flex-direction: column; gap: .4rem; margin-top: .4rem; }
.ticket-invoice-row {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  width: 100%; padding: .5rem .7rem; border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
  background: var(--surface-solid); cursor: pointer; font: inherit; font-size: .8rem; color: var(--text); text-align: left;
  transition: background .12s var(--ease), border-color .12s var(--ease);
}
.ticket-invoice-row:hover, .ticket-invoice-row:focus-visible { background: var(--surface-hover); border-color: var(--accent-ring); outline: none; }
.ticket-invoice-total { color: var(--text-dim); font-weight: 600; margin-left: auto; }

/* ==========================================================================
   Settings
   ========================================================================== */
.settings-panel { padding: 1.4rem 1.5rem; font-size: .85rem; color: var(--text-dim); line-height: 1.6; }
.settings-panel p { margin: 0 0 .8rem; }
.settings-code {
  font-family: 'SFMono-Regular', Menlo, Consolas, monospace; font-size: .78rem; color: var(--text);
  background: var(--bg-soft); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .6rem .75rem;
}
.settings-section {
  margin: 1.1rem 0; padding: 1rem 1.1rem; background: var(--bg-soft);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
}
.settings-section .checkbox-row { margin-top: 0; }

/* Settings > Notifications & Logging — a list of toggle rows, each a
   checkbox next to a title + one-line plain-English description, so the
   whole panel stays scannable without needing a legend or separate help
   text elsewhere. .checkbox-row-toggle only adds top-alignment (the shared
   .checkbox-row base is vertically centered, which looks wrong once the
   label wraps to two lines) -- it does not replace the base rule, so the
   Technicians modal's single-line "Active" checkbox is unaffected. */
.settings-toggle-list { display: flex; flex-direction: column; gap: .9rem; }
.checkbox-row-toggle { align-items: flex-start; }
.settings-toggle-title { display: block; font-weight: 600; color: var(--text); }
.settings-toggle-desc { display: block; font-size: .75rem; color: var(--text-dim); margin-top: .15rem; line-height: 1.5; }

/* ==========================================================================
   Settings Tile Grid + Modal Redesign (2026-09-07)
   ========================================================================== */
.settings-group { margin-bottom: 1.7rem; }
.settings-group:last-of-type { margin-bottom: 0; }
.settings-group-title {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); margin-bottom: .7rem; display: flex; align-items: baseline; gap: .4rem;
}
.settings-group-subtitle { text-transform: none; font-weight: 500; letter-spacing: 0; font-size: .72rem; color: var(--text-faint); opacity: .8; }
.settings-tile-grid {
  /* auto-fit (not auto-fill) collapses empty/unused column tracks instead
     of reserving space for them -- fixes a real full-screen/wide-monitor
     bug where a settings-group with fewer tiles than a row could fit
     (e.g. Team & Security's 2-3 tiles) rendered narrow and left-aligned
     with a large dead strip of blank space to the right of it, because
     auto-fill always reserves every column track the container's width
     supports even when nothing is placed in it. auto-fit reflows the SAME
     real tiles to stretch and fill the row instead, at any viewport width
     -- including a maximized/full-screen window on a wide monitor. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: .85rem;
}
.settings-tile {
  display: flex; align-items: flex-start; gap: .75rem; text-align: left; width: 100%;
  background: var(--surface-solid); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: .95rem 1.05rem; cursor: pointer; font: inherit; color: inherit;
  transition: border-color .15s var(--ease), transform .15s var(--ease), box-shadow .15s var(--ease);
}
.settings-tile:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: var(--shadow-md); }
.settings-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.settings-tile.hidden { display: none; }
.settings-tile-icon {
  flex-shrink: 0; width: 38px; height: 38px; border-radius: var(--radius-md); font-size: 1.15rem;
  display: flex; align-items: center; justify-content: center; background: var(--accent-soft);
}
.settings-tile-body { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
.settings-tile-title { font-weight: 600; font-size: .87rem; color: var(--text); display: flex; align-items: center; gap: .4rem; }
.settings-tile-desc { font-size: .74rem; color: var(--text-dim); line-height: 1.45; }
.settings-tile-badge {
  font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  color: var(--on-accent-soft); background: var(--accent-soft); border-radius: var(--radius-pill);
  padding: .12rem .5rem;
}

/* Secondary/Cancel action in a modal footer -- ghost-style button sitting
   next to .modal-save-btn, same idiom as .modal-danger-btn (transparent +
   border) just neutral-colored instead of red. Used by every Settings tile
   modal's footer (see login.html's settings*ModalOverlay blocks). */
.modal-cancel-btn {
  background: transparent; color: var(--text-dim); font-size: .78rem; font-weight: 600; padding: .5rem 1rem;
  border-radius: var(--radius-pill); border: 1px solid var(--border); cursor: pointer;
  transition: background .15s var(--ease), color .15s var(--ease), transform .15s var(--ease);
}
.modal-cancel-btn:hover { background: var(--surface-hover); color: var(--text); }

@media (max-width: 480px) {
  .settings-tile-grid { grid-template-columns: 1fr; }
}


/* ==========================================================================
   Misc utility overrides (kept for template class compatibility)
   ========================================================================== */
.text-red-600, .text-red-400 { color: var(--red) !important; }
.text-amber-600 { color: var(--amber) !important; }
.text-slate-400, .text-slate-500 { color: var(--text-faint) !important; }

/* ==========================================================================
   Motion
   ========================================================================== */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes rise { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } }

.viewport > * { animation: rise .28s var(--ease); }

/* ==========================================================================
   Loading skeleton — shown while the first bootstrap fetch is in flight
   ========================================================================== */
@keyframes shimmer { 0% { background-position: -240px 0; } 100% { background-position: 240px 0; } }
.skel-line {
  height: .68rem; border-radius: var(--radius-sm); margin-top: .5rem; width: 45%;
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-hover) 37%, var(--surface-2) 63%);
  background-size: 480px 100%; animation: shimmer 1.4s linear infinite;
}
.skel-line:first-child { margin-top: 0; }
.skel-icon { width: 34px; height: 34px; border-radius: var(--radius-sm); margin-bottom: .4rem; }
.skel-card { pointer-events: none; }
.skel-row { display: flex; align-items: center; gap: 2rem; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-soft); }
.skel-row:last-child { border-bottom: none; }
.skel-row .skel-line { margin-top: 0; height: .72rem; }

/* 2026-08-29: clearer first-login status -- previously the only feedback
   on a first (cold) bootstrap was the small topbar .loading-pill, easy to
   miss/mistake for a hang since a cold load can take 15-30s on a tenant
   without the perf fixes redeployed yet. Sits above the existing skeleton,
   see renderSkeleton_() in app.js. */
.first-load-status {
  display: flex; align-items: center; gap: .6rem; font-size: .82rem; color: var(--text-dim);
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: .75rem 1rem; margin-bottom: 1rem;
}
.first-load-spinner {
  width: 16px; height: 16px; flex-shrink: 0; border-radius: 999px;
  border: 2px solid var(--border); border-top-color: var(--accent);
  animation: view-refresh-spin .8s linear infinite;
}

/* Save button while a request is in flight -- disabled (see
   .modal-save-btn:disabled above) plus this for the "wait" cursor; the
   button's own text already switches to "Saving…" via #modalSaveStatus. */
.modal-save-btn.is-saving { cursor: wait; }

/* Save Changes button visual state feedback (2026-09-11) -- see
   wireSaveButtonDirtyState_ in app.js. Grey/disabled (.save-btn-clean) with
   no pending form changes, purple/active (.save-btn-dirty, the same
   var(--accent-strong) .modal-save-btn already defaults to everywhere else)
   the instant one is detected. --slate is this app's existing neutral-grey
   token (badges, muted text) -- reused here rather than inventing a new
   grey. Deliberately scoped to these two state classes, not a change to
   .modal-save-btn's own base color, so every button that doesn't opt into
   dirty-tracking (every plain "Add X"/"Create X" button) is unaffected. */
.modal-save-btn.save-btn-clean {
  background: var(--slate); opacity: .6; cursor: not-allowed;
}
.modal-save-btn.save-btn-clean:hover { filter: none; transform: none; }
.modal-save-btn.save-btn-dirty { background: var(--accent-strong); opacity: 1; }

/* ==========================================================================
   Responsive — phone & tablet
   Breakpoints: 900px sidebar drawer (matches the JS resize handler in
   app.js), 700px table→card, 640px full-screen modals / topbar wrap,
   480px single-column stat grid.
   ========================================================================== */

html, body { overflow-x: hidden; }
.viewport, .timeline-scroll { -webkit-overflow-scrolling: touch; }

/* ---- Tablet & phone: off-canvas sidebar drawer ---- */
@media (max-width: 900px) {
  .menu-btn { display: inline-flex; }

  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 40;
    transform: translateX(-100%);
    transition: transform .25s var(--ease);
    box-shadow: var(--shadow-lg);
    /* position:fixed is positioned against the real viewport, not
       .app-shell -- it does NOT inherit .app-shell's safe-area padding
       above, so the drawer needs its own top/left safe-area padding or its
       nav items render under the status bar when opened on a phone. */
    padding-top: env(safe-area-inset-top);
    padding-left: env(safe-area-inset-left);
    padding-bottom: env(safe-area-inset-bottom);
  }
  body.sidebar-open .sidebar { transform: translateX(0); }
  body.sidebar-open .sidebar-backdrop { display: block; }
  body.sidebar-open { overflow: hidden; }

  .app-shell { height: 100dvh; }

  .pagination-btn { width: 32px; height: 32px; }
  .pagination-size { padding: .45rem .6rem; font-size: .8rem; }
  .pagination-controls { gap: .6rem; }
  .modal-close { width: 38px; height: 38px; }
  .toggle-btn { padding: .5rem .85rem; }
}

/* ---- Topbar: search drops to its own full-width row ---- */
@media (max-width: 640px) {
  .topbar {
    height: auto; min-height: 60px; flex-wrap: wrap; padding: .6rem .9rem; row-gap: .55rem;
  }
  .search-wrap { order: 5; width: 100%; max-width: none; }
  .topbar-spacer { display: none; }
  .loading-pill { display: none; }
  .btn-primary-label { display: none; }
  #newTicketBtn { padding: .55rem; }

  .viewport { padding: .9rem; }
  .notif-banner { margin: .75rem .9rem 0; }

  /* Unified Settings Save Button (item 1) -- .settings-master-save's
     negative margins bleed it out to .viewport's OWN padding so the bar
     spans full width; .viewport drops to .9rem here (from 1.5rem at
     desktop widths), so the bleed amount has to match or the bar
     overshoots the actual edge and causes horizontal overflow. */
  .settings-master-save { margin: 1.2rem -.9rem -.9rem; padding: .9rem; bottom: -.9rem; }
}

@media (max-width: 420px) {
  .user-avatar { width: 32px; height: 32px; }
  /* Mobile audit (2026-09-11): this used to shrink .icon-btn to 34px here
     -- SMALLER than its own 38px desktop size, and below the ~40px
     comfortable minimum touch target on a phone (the exact screens this
     rule targets). Bumped to 40px instead of shrinking. */
  .icon-btn { width: 40px; height: 40px; }
  .login-card { padding: 1.6rem 1.3rem; }
}

/* The login screen's hero stacks to a centered column below 860px (see
   about.css's .hero media query) — the card itself is a fixed-width block,
   so text-align:center alone won't center it; give it its own margin.
   NOTE: this originally targeted .login-card-wrap, which does nothing --
   .login-card-wrap has no width rule of its own, so as a plain block box
   it already fills 100% of its container both here and in the >860px fix
   above, leaving margin:auto with no leftover space to split. .login-card
   is the one with an actual fixed width, so the margin has to go on it. */
@media (max-width: 860px) {
  #loginScreen .login-card { margin-inline: auto; }
}

/* ---- Stat grid: single column on phones ---- */
@media (max-width: 480px) {
  .stat-grid { grid-template-columns: 1fr; }
}

/* ---- Responsive tables → stacked cards ---- */
@media (max-width: 700px) {
  table.data-table thead { display: none; }
  table.data-table, table.data-table tbody, table.data-table tr, table.data-table td {
    display: block; width: 100%;
  }
  table.data-table tr { padding: .7rem .9rem; border-bottom: 1px solid var(--border-soft); }
  table.data-table tr:last-child { border-bottom: none; }
  table.data-table td {
    display: flex; align-items: center; justify-content: space-between; gap: .8rem;
    padding: .32rem 0; border-bottom: none; text-align: right;
  }
  table.data-table td::before {
    content: attr(data-label); color: var(--text-faint); font-size: .68rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .03em; text-align: left; flex-shrink: 0;
  }
  table.data-table td .sla-bar { width: 90px; }
  .pagination-bar { padding: .7rem .9rem; }

  .dispatch-grid { grid-template-columns: 1fr; }
  .dispatch-section-header { flex-wrap: wrap; }
  .unassigned-tray .dispatch-ticket-card { width: 100%; }
  .pastdue-tray .dispatch-ticket-card { width: 100%; }
}

/* ---- Kanban: single column on phones (already 2-col at 760px) ---- */
@media (max-width: 620px) {
  .kanban-board { grid-template-columns: 1fr; }
  /* 60vh min-height per lane makes sense across 3-7 side-by-side columns,
     but stacked single-column it means scrolling through mostly empty
     lanes to reach the next status. Let lanes size to their cards instead,
     with just enough room to see the "no cards" empty state and be a
     drop target. */
  .kanban-lane { min-height: 120px; }
}

/* ---- Full-screen modal sheets on phones ---- */
@media (max-width: 640px) {
  .modal-panel, .modal-panel-wide {
    width: 100%; max-width: 100%; height: 100%; max-height: 100%;
    border-radius: 0; animation: none;
  }
  .modal-header, .modal-footer { border-radius: 0; }
  .modal-field-grid, .modal-panel-wide .modal-field-grid { grid-template-columns: 1fr; }

  .tm-add-row { flex-direction: column; align-items: stretch; }
  .tm-add-row input, .tm-add-row button { width: 100% !important; }
  .tm-add-row #tmAddStatus { text-align: center; }

  .flag-add-row { flex-direction: column; align-items: stretch; }
  .flag-add-row input, .flag-add-row button { width: 100% !important; }

  .ai-assistant-card .tm-add-row { flex-direction: column; align-items: stretch; }
  .ai-assistant-card .tm-add-row input, .ai-assistant-card .tm-add-row button { width: 100% !important; }
}

/* ---------- Update 3.0, Feature 1: Live Chats (nav badge + split-pane inbox) ---------- */
.nav-label { flex: 1; }
.nav-badge {
  flex-shrink: 0; min-width: 20px; height: 20px; padding: 0 .4rem;
  border-radius: var(--radius-pill); background: var(--accent); color: #fff;
  font-size: .68rem; font-weight: 700; display: flex; align-items: center; justify-content: center;
}

.livechat-layout {
  display: grid; grid-template-columns: 320px 1fr; gap: 1rem;
  height: calc(100vh - 190px); min-height: 420px;
}
.livechat-list-pane {
  background: var(--surface-solid); border: 1px solid var(--border-soft); border-radius: var(--radius-lg);
  overflow-y: auto;
}
.livechat-session-list { display: flex; flex-direction: column; }
.livechat-session-item {
  display: flex; flex-direction: column; gap: .2rem; padding: .75rem .9rem;
  border-bottom: 1px solid var(--border-soft); cursor: pointer; transition: background .15s var(--ease);
}
.livechat-session-item:hover { background: var(--surface-2); }
.livechat-session-item.active { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
.livechat-session-item-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.livechat-session-item-right { display: flex; align-items: center; gap: .35rem; flex-shrink: 0; }
.livechat-session-delete-btn {
  display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
  border-radius: var(--radius-pill); border: none; background: transparent; color: var(--text-faint);
  cursor: pointer; transition: all .15s var(--ease); flex-shrink: 0;
}
.livechat-session-delete-btn svg { width: 14px; height: 14px; }
.livechat-session-delete-btn:hover, .livechat-session-delete-btn:focus-visible { background: var(--red-soft); color: var(--red); }
.livechat-session-name { font-size: .82rem; font-weight: 600; color: var(--text); }
.livechat-session-time { font-size: .66rem; color: var(--text-faint); flex-shrink: 0; white-space: nowrap; }
.livechat-session-preview {
  font-size: .74rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.livechat-session-item.unread .livechat-session-name::before {
  content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); margin-right: .4rem; vertical-align: middle;
}
.livechat-session-item.unread .livechat-session-preview { color: var(--text); font-weight: 600; }
.livechat-session-badges { display: flex; gap: .3rem; margin-top: .15rem; flex-wrap: wrap; }
.livechat-session-badge {
  font-size: .62rem; font-weight: 700; padding: .12rem .45rem; border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--text-dim);
}
.livechat-session-badge.closed { background: var(--slate-soft); color: var(--on-slate-soft); }

/* Form Submissions Dashboard (2026-09-22) -- see tpl-formsubmissions in
   login.html and renderFormSubmissions()/renderFormSubmissionsTable_() in
   app.js. */
.formsub-table-wrap { overflow-x: auto; }
/* Form Submissions search/filter/sort toolbar (2026-09-22) */
.formsub-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0 0 .9rem; }
.formsub-toolbar .formsub-search { flex: 1 1 240px; min-width: 200px; cursor: text; }
.formsub-toolbar input[type="date"].select-input { cursor: text; }
.formsub-clear-btn { background: transparent; border: 1px solid var(--border); color: var(--text-muted, var(--text)); border-radius: var(--radius-md); padding: .5rem .75rem; font-size: .8rem; font-family: inherit; cursor: pointer; }
.formsub-clear-btn:hover { background: var(--surface-hover); color: var(--text); }
.formsub-count { margin-left: auto; white-space: nowrap; }
.formsub-show-more-row td { text-align: center; }
.formsub-show-more { background: transparent; border: 1px solid var(--border); color: var(--accent); border-radius: var(--radius-md); padding: .4rem .9rem; font-size: .8rem; cursor: pointer; font-family: inherit; }
.formsub-show-more:hover { background: var(--surface-hover); }
@media (max-width: 640px) { .formsub-count { margin-left: 0; width: 100%; } }
.formsub-table-wrap table.data-table { min-width: 760px; }
.formsub-msg-cell {
  max-width: 260px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--text-dim); font-size: .78rem;
}
.formsub-badge {
  display: inline-flex; align-items: center; font-size: .68rem; font-weight: 700;
  padding: .2rem .55rem; border-radius: var(--radius-pill); white-space: nowrap;
}
.formsub-badge-new { background: var(--accent-soft); color: var(--on-accent-soft, var(--accent)); }
.formsub-badge-converted { background: var(--green-soft); color: var(--green); }
.formsub-badge-ignored { background: var(--slate-soft); color: var(--on-slate-soft); }
.formsub-linked-ticket {
  font-size: .74rem; font-weight: 600; color: var(--accent); cursor: pointer; text-decoration: underline;
}
.formsub-row-actions { display: flex; gap: .4rem; flex-wrap: wrap; justify-content: flex-end; }
.formsub-convert-btn, .formsub-ignore-btn {
  font-size: .72rem; font-weight: 600; padding: .32rem .65rem; border-radius: var(--radius-md);
  border: 1px solid var(--border); background: var(--surface-solid); color: var(--text); cursor: pointer;
  transition: background .12s var(--ease), border-color .12s var(--ease);
}
.formsub-convert-btn { background: var(--accent); border-color: var(--accent); color: var(--on-accent, #fff); }
.formsub-convert-btn:hover { filter: brightness(1.06); }
.formsub-ignore-btn:hover { background: var(--surface-hover); }
.formsub-convert-btn:disabled, .formsub-ignore-btn:disabled { opacity: .55; cursor: default; }

/* Form Submissions: clickable rows, Processed status, detail modal (2026-09-24). */
.formsub-badge-processed { background: var(--cyan-soft); color: var(--cyan); }
.formsub-processed-btn {
  font-size: .72rem; font-weight: 600; padding: .32rem .65rem; border-radius: var(--radius-md);
  border: 1px solid color-mix(in srgb, var(--cyan) 45%, var(--border)); background: var(--surface-solid); color: var(--text); cursor: pointer;
  transition: background .12s var(--ease);
}
.formsub-processed-btn:hover { background: var(--cyan-soft); }
.formsub-processed-btn:disabled { opacity: .55; cursor: default; }
tr.formsub-row { cursor: pointer; }
tr.formsub-row:hover td { background: var(--surface-hover); }
tr.formsub-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
tr.formsub-row-handled td { opacity: .62; }
tr.formsub-row-handled:hover td { opacity: .9; }
.formsub-detail-status { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; }
.formsub-detail-grid { display: grid; grid-template-columns: minmax(110px, max-content) 1fr; gap: .45rem 1rem; align-items: baseline; }
.formsub-detail-label { font-size: .7rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }
.formsub-detail-value { font-size: .88rem; color: var(--text); overflow-wrap: anywhere; }
.formsub-detail-value a { color: var(--accent); }
.formsub-detail-message {
  margin-top: .4rem; padding: .8rem .9rem; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--bg-soft); white-space: pre-wrap; overflow-wrap: anywhere; font-size: .88rem; line-height: 1.5;
  max-height: 40vh; overflow-y: auto;
}
.formsub-detail-extras { margin-top: 1rem; }
.formsub-detail-extras summary { cursor: pointer; font-size: .78rem; font-weight: 600; color: var(--text-dim); margin-bottom: .6rem; }
.formsub-detail-footer { display: flex; justify-content: space-between; align-items: center; gap: .75rem; flex-wrap: wrap; }
.formsub-detail-footer .formsub-row-actions { justify-content: flex-start; }
@media (max-width: 640px) { .formsub-detail-grid { grid-template-columns: 1fr; gap: .15rem; } .formsub-detail-value { margin-bottom: .5rem; } }

/* Mobile: the "responsive" requirement -- collapse to a stacked card per
   submission instead of a horizontally-scrolling table below ~640px, same
   breakpoint the rest of this app's mobile layout switches at. */
@media (max-width: 640px) {
  .formsub-table-wrap table.data-table thead { display: none; }
  .formsub-table-wrap table.data-table, .formsub-table-wrap table.data-table tbody, .formsub-table-wrap table.data-table tr, .formsub-table-wrap table.data-table td {
    display: block; width: 100%;
  }
  .formsub-table-wrap table.data-table { min-width: 0; }
  .formsub-table-wrap table.data-table tr {
    border-bottom: 1px solid var(--border-soft); padding: .7rem .2rem; margin: 0;
  }
  .formsub-table-wrap table.data-table td {
    border-bottom: none; padding: .18rem .2rem; text-align: left !important;
  }
  .formsub-table-wrap table.data-table td::before {
    content: attr(data-label); display: block; font-size: .62rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: .03em; color: var(--text-faint); margin-bottom: .1rem;
  }
  .formsub-msg-cell { max-width: none; white-space: normal; }
  .formsub-row-actions { justify-content: flex-start; }
}


.livechat-thread-pane {
  background: var(--surface-solid); border: 1px solid var(--border-soft); border-radius: var(--radius-lg);
  display: flex; flex-direction: column; overflow: hidden;
}
.livechat-empty-state {
  flex: 1; display: flex; align-items: center; justify-content: center;
  color: var(--text-faint); font-size: .85rem; text-align: center; padding: 2rem;
}
.livechat-thread { display: flex; flex-direction: column; height: 100%; }
.livechat-thread-header {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem;
  padding: .9rem 1.1rem; border-bottom: 1px solid var(--border-soft); flex-wrap: wrap;
}
.livechat-thread-name { font-size: .95rem; font-weight: 700; color: var(--text); }
.livechat-thread-meta { font-size: .74rem; color: var(--text-faint); margin-top: .15rem; }
.livechat-thread-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

.livechat-messages { flex: 1; overflow-y: auto; padding: 1rem 1.1rem; display: flex; flex-direction: column; gap: .65rem; }
.chat-bubble-row { display: flex; }
.chat-bubble-row.customer { justify-content: flex-start; }
.chat-bubble-row.owner { justify-content: flex-end; }
.chat-bubble {
  max-width: 70%; padding: .55rem .8rem; border-radius: var(--radius-md); font-size: .82rem; line-height: 1.4;
  overflow-wrap: break-word; /* NOT "anywhere": anywhere shrinks min-content to one character, so a shrink-to-fit bubble collapsed to a sliver and wrapped every word onto its own line */
  word-break: normal;
  hyphens: manual;
  white-space: pre-wrap;
}
.chat-bubble-row.customer .chat-bubble { background: var(--surface-2); color: var(--text); border-bottom-left-radius: 4px; }
.chat-bubble-row.owner .chat-bubble { background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
.chat-bubble-time { font-size: .62rem; color: var(--text-faint); margin-top: .2rem; }
.chat-bubble-row.owner .chat-bubble-time { text-align: right; }
/* Internal Team Chat (2026-09-11) reuses these same generic .chat-bubble-*
   classes the customer-facing Live Chats widget above already defines --
   .mine/.theirs stand in for that widget's .owner/.customer (this is a
   peer-to-peer channel, not an owner-replying-to-a-customer one), plus a
   .pending modifier for a message's own optimistic local echo (see
   sendTeamChatMessage_ in app.js) while it awaits the server's real,
   persisted row. */
.chat-bubble-row.mine { justify-content: flex-end; }
.chat-bubble-row.theirs { justify-content: flex-start; }
.chat-bubble-row.mine .chat-bubble { background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
.chat-bubble-row.theirs .chat-bubble { background: var(--surface-2); color: var(--text); border-bottom-left-radius: 4px; }
.chat-bubble-row.mine .chat-bubble-time { text-align: right; }
.chat-bubble-row.pending .chat-bubble { opacity: .6; }
.team-chat-sender-name { font-size: .68rem; font-weight: 700; color: var(--text-dim); margin-bottom: .15rem; }

/* Message Reactions / iMessage-style tapbacks (2026-09-11 add-on to
   Internal Team Chat) -- see renderTeamChatMessages_/sendTeamChatReaction_
   in app.js. .chat-bubble-wrap is the positioning anchor for the
   react-trigger button and the picker popup, both absolutely positioned
   off it rather than the row itself so they sit right against the
   bubble regardless of the sender-name line's presence. */
.chat-bubble-wrap { position: relative; max-width: 70%; min-width: min(11rem, 100%); width: fit-content; }
.chat-bubble-row.mine .chat-bubble-wrap { margin-left: auto; }
.team-chat-react-trigger {
  position: absolute; top: -.4rem; width: 1.35rem; height: 1.35rem; line-height: 1.35rem;
  text-align: center; padding: 0; border: 1px solid var(--border-soft); border-radius: 999px;
  background: var(--surface-1, #fff); font-size: .78rem; cursor: pointer;
  opacity: .45; transition: opacity .12s ease;
}
.chat-bubble-row.theirs .team-chat-react-trigger { right: -.5rem; }
.chat-bubble-row.mine .team-chat-react-trigger { left: -.5rem; }
.chat-bubble-wrap:hover .team-chat-react-trigger,
.team-chat-react-trigger:focus { opacity: 1; }

.team-chat-reactions { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .3rem; }
.chat-bubble-row.mine .team-chat-reactions { justify-content: flex-end; }
.team-chat-reaction-pill {
  display: inline-flex; align-items: center; gap: .25rem; padding: .08rem .45rem;
  border-radius: 999px; border: 1px solid var(--border-soft); background: var(--surface-2);
  color: var(--text); font-size: .72rem; line-height: 1.5; cursor: pointer;
}
.team-chat-reaction-pill span { font-size: .68rem; color: var(--text-faint); }
.team-chat-reaction-pill.mine { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.team-chat-reaction-pill.mine span { color: var(--accent); }

.team-chat-reaction-picker {
  position: absolute; bottom: 100%; margin-bottom: .3rem; display: flex; gap: .15rem;
  padding: .3rem .35rem; border-radius: var(--radius-md); border: 1px solid var(--border-soft);
  background: var(--surface-1, #fff); box-shadow: 0 6px 18px rgba(0,0,0,.18); z-index: 5;
}
.chat-bubble-row.theirs .team-chat-reaction-picker { left: 0; }
.chat-bubble-row.mine .team-chat-reaction-picker { right: 0; }
.team-chat-reaction-pick-btn {
  border: none; background: none; padding: .2rem; border-radius: 6px; font-size: 1.05rem;
  line-height: 1; cursor: pointer;
}
.team-chat-reaction-pick-btn:hover { background: var(--surface-2); }

.livechat-reply-bar { display: flex; gap: .6rem; padding: .8rem 1.1rem; border-top: 1px solid var(--border-soft); align-items: flex-end; }
.livechat-reply-input {
  flex: 1; resize: none; border-radius: var(--radius-md); border: 1px solid var(--border-soft);
  background: var(--surface-2); color: var(--text); padding: .55rem .7rem; font-size: .82rem; font-family: inherit;
}
.livechat-reply-input:focus { outline: none; border-color: var(--accent); }

@media (max-width: 900px) {
  .livechat-layout { grid-template-columns: 1fr; height: auto; }
  .livechat-list-pane { max-height: 40vh; }
  .livechat-thread-pane { min-height: 60vh; }
}

/* ==========================================================================
   Field Service Enhancements batch (2026-08-31): Customer Navigation &
   Multi-Location Support, In-Line Customer Creation, Upcoming Installs
   calendar, Internal Tracking Sheet. All theme-correct for free -- every
   rule below reuses existing tokens (var(--surface-*), var(--border*),
   var(--text*), var(--radius-*), badge-* color tokens) rather than
   introducing new light/dark pairs, same approach the Price Guide
   category-color follow-up used.
   ========================================================================== */

/* Clickable customer name inside the ticket modal's Customer card (opens
   the profile modal stacked on top -- see openCustomerFromTicketBtn in
   app.js). Reset to a plain inline button, then borrow .link-inline's
   underline treatment so it reads exactly like every other in-modal link. */
.customer-name-link {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  font: inherit; font-weight: 600; color: inherit;
  text-decoration: underline; text-decoration-color: var(--accent-ring); text-underline-offset: 2px;
}
.customer-name-link:hover { color: var(--accent); text-decoration-color: var(--accent); }

/* ---------- Service Locations (customer profile modal) ---------- */
.service-location-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .8rem;
  padding: .6rem 0; border-bottom: 1px solid var(--border-soft);
}
.service-location-row:first-child { padding-top: 0; }
.service-location-row:last-child { border-bottom: none; }
.service-location-row-main { flex: 1; min-width: 0; }
.service-location-label { font-weight: 600; color: var(--text); font-size: .85rem; margin-bottom: .15rem; }
.service-location-row-actions { display: flex; gap: .7rem; flex-shrink: 0; padding-top: .15rem; }

/* ---------- Internal Tracking Sheet (ticket modal) ---------- */
.internal-tracking-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .8rem;
  padding: .5rem 0; border-bottom: 1px solid var(--border-soft); font-size: .82rem; color: var(--text);
}
.internal-tracking-row:last-child { border-bottom: none; }
.internal-tracking-row-main { display: flex; flex-direction: column; gap: .15rem; }
.internal-tracking-row-main > span { line-height: 1.35; }

/* ---------- Upcoming Installs calendar ---------- */
.calendar-toolbar { display: flex; align-items: center; gap: .5rem; margin-bottom: .8rem; }
.calendar-month-label { font-size: .95rem; font-weight: 700; color: var(--text); min-width: 150px; text-align: center; }
.calendar-today-btn {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--radius-pill); padding: .35rem .85rem; font-size: .76rem; font-weight: 600;
  cursor: pointer; transition: all .15s var(--ease);
}
.calendar-today-btn:hover { background: var(--surface-hover); border-color: var(--border-soft); }

.calendar-grid {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  border: 1px solid var(--border-soft); border-radius: var(--radius-lg); overflow: hidden;
  background: var(--border-soft);
  gap: 1px;
}
.calendar-day-header {
  background: var(--surface-2); color: var(--text-faint); font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; text-align: center; padding: .5rem 0;
}
.calendar-day-cell {
  background: var(--surface-solid); min-height: 92px; padding: .35rem .4rem; display: flex; flex-direction: column; gap: .25rem;
}
.calendar-day-cell.other-month { background: var(--surface-2); opacity: .55; }
.calendar-day-cell.today { box-shadow: inset 0 0 0 2px var(--accent); }
.calendar-day-number { font-size: .74rem; font-weight: 700; color: var(--text-faint); }
.calendar-day-cell.today .calendar-day-number { color: var(--accent); }
.calendar-day-chips { display: flex; flex-direction: column; gap: .2rem; overflow: hidden; }
.calendar-ticket-chip {
  border: none; border-radius: 5px; padding: .18rem .4rem; font-size: .68rem; font-weight: 600;
  text-align: left; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  width: 100%; transition: transform .1s var(--ease);
}
.calendar-ticket-chip:hover { transform: translateY(-1px); }
/* Status colors (2026-09-24): a faded tint of the status color with a solid
   left edge -- readable text in both themes, whatever color a tenant picks. */
.calendar-ticket-chip.status-chip {
  background: color-mix(in srgb, var(--status-color, #94a3b8) 22%, var(--surface-solid));
  border-left: 3px solid var(--status-color, #94a3b8);
  color: var(--text);
}
.calendar-ticket-chip.status-chip:hover { background: color-mix(in srgb, var(--status-color, #94a3b8) 34%, var(--surface-solid)); }
/* Settings > Status Colors */
.status-color-rows { display: grid; grid-template-columns: 1fr auto auto; gap: .5rem .9rem; align-items: center; }
.status-color-name { font-size: .85rem; color: var(--text); }
.status-color-rows input[type="color"] { width: 44px; height: 30px; padding: 0; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-solid); cursor: pointer; }
.status-color-reset { background: none; border: none; color: var(--text-dim); font-size: .74rem; cursor: pointer; padding: .2rem .3rem; font-family: inherit; }
.status-color-reset:hover { color: var(--accent); }
.status-color-reset[disabled] { visibility: hidden; }
.status-color-preview { display: flex; flex-wrap: wrap; gap: .4rem; margin: .9rem 0 0; }
.status-color-preview .calendar-ticket-chip { width: auto; max-width: 180px; }
.calendar-more-label { font-size: .64rem; color: var(--text-faint); padding: 0 .1rem; }

.calendar-unscheduled-row {
  display: flex; align-items: center; gap: .6rem; padding: .5rem 0; border-bottom: 1px solid var(--border-soft);
  font-size: .8rem; color: var(--text); cursor: pointer;
}
.calendar-unscheduled-row:last-child { border-bottom: none; }
.calendar-unscheduled-row:hover { color: var(--accent); }

@media (max-width: 640px) {
  .calendar-day-cell { min-height: 64px; padding: .25rem .3rem; }
  .calendar-day-header { font-size: .6rem; padding: .35rem 0; }
  .calendar-ticket-chip { font-size: .6rem; padding: .12rem .3rem; }
  .calendar-month-label { min-width: 120px; font-size: .85rem; }
  .calendar-unscheduled-row { flex-wrap: wrap; }
}

/* ---------- Monthly Calendar View (2026-09-11) ----------
   Grid/day-cell/toolbar chrome is the shared .calendar-* component above
   (see the doc comment on <template id="tpl-calendar"> in login.html) --
   only what's genuinely new to THIS calendar lives here: the day-cell
   click-to-drill-into-Dispatch affordance and the status-color legend/dots
   (this view spans every ticket status, unlike Upcoming Installs' single
   Installation category, so a status breakdown per day earns its own
   summary row where Upcoming Installs never needed one). */
.monthcal-day-cell:not(.other-month) { cursor: pointer; }
.monthcal-day-cell:not(.other-month):hover { background: var(--surface-hover); }
/* The ticket chips inside a day cell open that ticket directly (existing
   shared .calendar-ticket-chip delegated handler) -- clicks on THEM must
   not also trigger the day-cell's own drill-into-Dispatch handler underneath. */
.monthcal-day-cell .calendar-day-chips { pointer-events: none; }
.monthcal-day-cell .calendar-ticket-chip, .monthcal-day-cell .calendar-more-label { pointer-events: auto; }
.monthcal-day-summary { display: flex; align-items: center; gap: .25rem; flex-wrap: wrap; margin-top: auto; padding-top: .15rem; }
.monthcal-status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.monthcal-day-count { font-size: .62rem; font-weight: 700; color: var(--text-faint); }
.monthcal-legend { display: flex; align-items: center; gap: .9rem; margin-left: auto; flex-wrap: wrap; }
.monthcal-legend-item { display: flex; align-items: center; gap: .3rem; font-size: .68rem; color: var(--text-faint); font-weight: 600; }
.monthcal-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
@media (max-width: 640px) {
  .monthcal-day-count { display: none; } /* the day cell is already tight at phone width -- the dots alone still convey the mix */
  .calendar-toolbar { flex-wrap: wrap; }
  .monthcal-legend { margin-left: 0; width: 100%; order: 3; }
}

/* ---------- Internal Team Chat drawer (2026-09-11) ----------
   Body-level slide-in drawer (see #teamChatDrawer in login.html) -- NOT a
   modal overlay, deliberately: it sits alongside whatever view is open
   (same "doesn't block the rest of the app" feel as the assignments-panel
   dropdown above) rather than dimming the page. Two stacked panes
   (.team-chat-pane-list / .team-chat-pane-thread) sliding horizontally via
   a .showing-thread class toggle on the drawer itself -- see
   openTeamChatThread_/backToTeamChatThreadList_ in app.js -- so the same
   drawer works as a single narrow column on mobile without a second,
   separate mobile layout. */
.team-chat-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: 360px; max-width: 100vw;
  background: var(--surface-solid); border-left: 1px solid var(--border);
  box-shadow: -18px 0 40px -12px rgba(0,0,0,.35); z-index: 80;
  display: flex; overflow: hidden;
  transform: translateX(0); transition: transform .2s var(--ease);
}
.team-chat-drawer.hidden { transform: translateX(100%); display: flex; visibility: hidden; }
.team-chat-pane {
  flex: 0 0 100%; width: 100%; display: flex; flex-direction: column; min-width: 0;
  transition: margin-left .2s var(--ease);
}
.team-chat-pane-list { margin-left: 0; }
.team-chat-pane-thread { margin-left: -100%; }
/* Slide to the thread pane -- both panes sit side by side (2 x 100% width)
   inside the drawer's flex row, and this shifts the strip left by one pane
   width so only the thread pane pane is in view; unshown gets undone by
   removing the class. */
.team-chat-drawer.showing-thread .team-chat-pane-list { margin-left: -100%; }
.team-chat-drawer.showing-thread .team-chat-pane-thread { margin-left: 0; }
.team-chat-drawer:not(.showing-thread) { }
.team-chat-drawer .team-chat-pane-thread { order: 2; }
.team-chat-drawer .team-chat-pane-list { order: 1; }

.team-chat-drawer-header {
  display: flex; align-items: center; gap: .6rem; padding: .9rem 1rem;
  border-bottom: 1px solid var(--border-soft); flex-shrink: 0;
}
.team-chat-drawer-title { font-size: .95rem; font-weight: 700; color: var(--text); flex: 1; }
/* Thread pane's header keeps the back button first, then the title, then
   the connection dot, all inline -- so #teamChatBackBtn (only present in
   .team-chat-pane-thread) doesn't need its own layout rule. */
.team-chat-pane-thread .team-chat-drawer-header #teamChatThreadTitle { flex: 1; }

.team-chat-conn-status {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--text-faint);
}
.team-chat-conn-status.live { background: var(--green); }
.team-chat-conn-status.polling { background: var(--amber, #d97706); }
/* 2 or more consecutive failed REST refreshes while there's no live
   WebSocket -- see updateTeamChatSyncStatus_() in app.js. */
.team-chat-conn-status.error { background: var(--red); }

.team-chat-thread-list { flex: 1; overflow-y: auto; padding: .4rem; }
.team-chat-start-dm { border-top: 1px solid var(--border-soft); padding: .6rem .8rem 1rem; flex-shrink: 0; }
.team-chat-start-dm-label {
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-faint); margin-bottom: .3rem;
}
.team-chat-thread-item {
  display: flex; align-items: center; gap: .6rem; padding: .55rem .6rem; border-radius: var(--radius-md);
  cursor: pointer; transition: background .12s var(--ease);
}
.team-chat-thread-item:hover { background: var(--surface-hover); }
.team-chat-thread-item.active { background: var(--accent-soft); }
.team-chat-thread-avatar {
  position: relative; width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
  background: var(--surface-2); color: var(--text-dim); font-size: .74rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}
.team-chat-thread-avatar.online::after {
  content: ''; position: absolute; bottom: -1px; right: -1px; width: 9px; height: 9px;
  border-radius: 50%; background: var(--green); border: 2px solid var(--surface-solid);
}
.team-chat-thread-main { flex: 1; min-width: 0; }
.team-chat-thread-name { font-size: .82rem; font-weight: 600; color: var(--text); }
.team-chat-thread-item.unread .team-chat-thread-name { font-weight: 700; }
.team-chat-thread-preview {
  font-size: .72rem; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.team-chat-thread-item.unread .team-chat-thread-preview { color: var(--text); }
.team-chat-thread-unread-badge {
  flex-shrink: 0; min-width: 18px; height: 18px; padding: 0 .35rem; border-radius: var(--radius-pill);
  background: var(--accent); color: #fff; font-size: .64rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}

/* Self-Contained Team Chat Window (2026-09-11 scroll containment pass) --
   .team-chat-page is now the ONE bounded flex column for this whole view,
   sized to exactly fill .viewport's available content-box height (100%
   resolves correctly here because .viewport itself gets a definite used
   height from the .main-col flex layout above it -- see .main-col/
   .viewport). Its three children stack top to bottom: the toolbar/
   subhead (auto-height, flex-shrink:0), .team-chat-messages (the ONLY
   child with flex:1 -- takes whatever's left), and the composer
   (auto-height, flex-shrink:0, pinned at the bottom simply by being
   last). min-height:0 on .team-chat-page and .team-chat-messages is
   load-bearing, not decorative: without it a flex child's default
   min-height:auto lets it grow to fit its content instead of being
   capped and forced to scroll, which was the ROOT CAUSE of the old
   "whole page scrolls, composer disappears off the bottom" bug -- this
   selector used to not exist at all, so .team-chat-page had no bounded
   height for .team-chat-messages' pre-existing flex:1 to ever act on. */
.team-chat-page { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.team-chat-toolbar { flex-shrink: 0; }
.team-chat-subhead { flex-shrink: 0; margin: 0 0 .8rem; }
.team-chat-messages { flex: 1; min-height: 0; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: .5rem; -webkit-overflow-scrolling: touch; }
/* The actual bubble stack, nested one level inside .team-chat-messages
   (see <template id="tpl-teamchat"> in login.html) so the Load Older
   Messages control above it (a sibling, not a bubble) survives
   renderTeamChatMessages_()'s innerHTML swap -- only #teamChatMessageList
   gets replaced on every render, never #teamChatMessages itself. */
#teamChatMessageList { display: flex; flex-direction: column; gap: .65rem; }
/* Chat Data Offloading / "Load Older Messages" (2026-09-11) */
.team-chat-load-older-wrap { display: flex; justify-content: center; flex-shrink: 0; }
.team-chat-load-older-btn {
  font-size: .78rem; font-weight: 600; color: var(--text-dim); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--radius-pill); padding: .6rem 1.1rem; min-height: 38px;
  cursor: pointer; transition: all .15s var(--ease);
}
.team-chat-load-older-btn:hover { background: var(--surface-hover); color: var(--text); }
.team-chat-load-older-btn:disabled { opacity: .6; cursor: default; }
.team-chat-composer {
  display: flex; gap: .5rem; align-items: flex-end; padding: .7rem .8rem; border-top: 1px solid var(--border-soft); flex-shrink: 0;
}
.team-chat-composer.disabled { opacity: .55; pointer-events: none; }
.team-chat-composer-input {
  flex: 1; resize: none; max-height: 100px; padding: .55rem .7rem; border-radius: var(--radius-md);
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text); font: inherit; line-height: 1.3;
}
.team-chat-composer-input:focus { outline: none; border-color: var(--accent); }
.team-chat-send-btn { flex-shrink: 0; }

@media (max-width: 640px) {
  .team-chat-drawer { width: 100vw; }
}

/* ---------- On-Call schedule modal (rebuilt 2026-09-02, widened 2026-09-02) ----------
   See openOnCallModal()/renderOnCallModal_() in app.js. The calendar itself
   reuses .calendar-grid/.calendar-day-cell/.calendar-day-header/
   .calendar-day-number/.calendar-day-chips/.calendar-more-label/
   .calendar-toolbar/.calendar-month-label/.calendar-today-btn as-is (same
   month-grid component renderUpcomingInstalls() already established) --
   only the shift chip itself needs its own class, since it's colored per
   TECHNICIAN (techColor(), a solid hex background) rather than per ticket
   priority like .calendar-ticket-chip's badge-* classes are.

   Panel width: this modal carries `.modal-panel-wide` (same fluid-width
   class the ticket modal uses -- min(1040px, 95vw)) PLUS this modifier,
   which pushes it wider still. A 7-column calendar grid keeps gaining
   useful room (bigger day cells, more of a long technician legend on one
   line) well past where the ticket modal's field-grid stops benefiting
   from extra width, and Greg specifically asked for "as much scalable
   screen as possible" here. Still capped (not 100vw) so it doesn't look
   absurd on an ultrawide monitor. Wrapped in its own min-width query
   (rather than relying on cascade order against the shared max-width:640px
   modal override elsewhere in this file, which this rule would otherwise
   win against since it comes later in the file) so phones/small tablets
   always get the full-screen modal treatment regardless of where either
   rule sits. */
@media (min-width: 641px) {
  .oncall-modal-panel { width: min(1280px, 96vw); max-width: 96vw; }
}

/* ---------- Manage Users modal: full-width scaling (2026-09-08) ----------
   Settings > Manage Users carries a data-table-heavy Existing Users grid
   (7 columns) plus, as of the granular permissions feature, a full
   Permissions Matrix inside the edit form -- both need real room to
   breathe, the same "as much scalable screen as possible" reasoning
   .oncall-modal-panel above already documents for the On-Call calendar.
   `.modal-panel-wide` alone (clamp(760px, 62vw, 1200px)) still left this
   looking like a centered, narrow box on anything wider than a laptop --
   this modifier pushes it to near-full-viewport-width instead, same
   min(...)/max-width pairing and same `min-width: 641px` wrapper as
   `.oncall-modal-panel` so phones/small tablets still fall through to the
   shared max-width:640px full-screen modal rule elsewhere in this file
   (which only ever targets the base `.modal-panel`/`.modal-panel-wide`
   classes, so it applies here unmodified regardless of source order). */
@media (min-width: 641px) {
  .users-modal-panel { width: min(1680px, 97vw); max-width: 97vw; }
}
/* Existing Users table + the Permissions Matrix grid both want to use
   every inch of that extra width rather than staying pinned to the
   narrower auto-fit column count `.modal-panel-wide .modal-field-grid`
   picks for smaller modals (technician/customer/invoice forms sharing
   that same base rule). minmax(240px,1fr) (vs. the shared 210px) plus the
   wider container means more permission toggle groups sit side-by-side
   on a desktop monitor instead of stacking into one long column. */
.users-modal-panel .modal-field-grid { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.users-modal-panel .data-table { min-width: 720px; }

.oncall-shift-chip {
  border: none; border-radius: 5px; padding: .18rem .4rem; font-size: .66rem; font-weight: 700;
  color: #fff; text-align: left; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  width: 100%; transition: transform .1s var(--ease), filter .1s var(--ease);
}
.oncall-shift-chip:hover { transform: translateY(-1px); filter: brightness(1.08); }
/* Two labels inside the chip (see renderOnCallModal_() in app.js) -- the
   full name by default, swapped for a single bold initial once the phone
   breakpoint below turns the chip into a small round dot instead of a
   text pill. */
.oncall-chip-initial { display: none; }
/* A day with no shift yet is still clickable (starts a new one there) --
   only the this-month cells, matching the day-cell click handler in app.js
   which ignores .other-month cells (those belong to a different visible
   month once you click into them via Prev/Next instead). */
.oncall-day-cell:not(.other-month) { cursor: pointer; }
.oncall-day-cell:not(.other-month):hover { background: var(--surface-hover); }
/* A past (but not yet purged -- see PAST_SHIFT_RETENTION_DAYS_) day within
   the CURRENT visible month reads as history rather than something to plan
   around, same intent as .other-month's dimming but scoped to "already
   happened" instead of "not this month". */
.calendar-day-cell.past-day { opacity: .55; }
.calendar-day-cell.past-day:hover { opacity: .75; }

.oncall-now-row { display: flex; flex-wrap: wrap; gap: .5rem; }
.oncall-now-chip {
  display: inline-flex; flex-direction: column; color: #fff; padding: .4rem .7rem;
  border-radius: var(--radius-md); font-size: .78rem; font-weight: 700; line-height: 1.35;
}
.oncall-now-chip-sub { font-size: .64rem; font-weight: 500; opacity: .9; }

.oncall-legend { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; margin-top: .8rem; font-size: .72rem; color: var(--text-dim); }
.oncall-legend-item { display: flex; align-items: center; gap: .35rem; }
.oncall-legend-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; display: inline-block; }

/* Past rows in the "Upcoming Shifts" list (only shown at all once "Show
   past shifts" is checked) -- same visual demotion as .past-day above. */
tr.oncall-past-row td { opacity: .6; }

@media (max-width: 640px) {
  .oncall-shift-chip { font-size: .6rem; padding: .12rem .3rem; }
  .oncall-now-chip { font-size: .72rem; padding: .35rem .6rem; }
}

/* Phone-width calendar (matches .stat-grid's own 480px single-column
   break): a 7-column month grid only has ~45-55px per day cell here, where
   a name pill just truncates to an unreadable "Sh…" -- and there's no
   hover on a touchscreen to fall back on the chip's `title` tooltip the
   way a mouse would. Below this width every chip becomes a small round
   dot with one bold initial instead (still in the tech's own color, still
   click-to-edit, still cross-referenced by the legend underneath). */
@media (max-width: 480px) {
  .oncall-shift-chip {
    width: 20px; height: 20px; padding: 0; margin: 0 auto; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
  }
  .oncall-chip-full { display: none; }
  .oncall-chip-initial { display: block; font-size: .62rem; line-height: 1; }
  .calendar-day-chips { align-items: center; }
}

/* ---------- Customer Merge modal (2026-09-01) ----------
   See openCustomerMergeModal_() in app.js and #customerMergeModalOverlay in
   login.html. Reuses .modal-section/.modal-field-label/.customer-review-callout
   etc. from elsewhere in this file wherever they already fit; these rules
   are only for the pieces unique to the compare/merge UI. */

.merge-suggestion-list { display: flex; flex-direction: column; gap: .5rem; }
.merge-suggestion-chip {
  display: flex; align-items: center; justify-content: space-between; gap: .8rem;
  width: 100%; text-align: left; background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: .55rem .75rem; cursor: pointer;
  font: inherit; color: var(--text); transition: border-color .15s var(--ease), transform .1s var(--ease);
}
.merge-suggestion-chip:hover { border-color: var(--accent); transform: translateY(-1px); }
.merge-suggestion-name { font-weight: 600; font-size: .84rem; }
.merge-suggestion-reason { font-size: .72rem; color: var(--text-faint); white-space: nowrap; }

.merge-keep-picker { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
.merge-keep-card {
  display: flex; flex-direction: column; gap: .2rem; text-align: left; cursor: pointer;
  background: var(--surface-solid); border: 2px solid var(--border); border-radius: var(--radius-md);
  padding: .7rem .85rem; font: inherit; color: var(--text); transition: border-color .15s var(--ease);
}
.merge-keep-card:hover { border-color: var(--accent-ring); }
.merge-keep-card.is-selected { border-color: var(--accent); background: var(--accent-soft); }
.merge-keep-name { font-weight: 700; font-size: .88rem; }

.merge-field-table { display: flex; flex-direction: column; }
.merge-field-row {
  display: grid; grid-template-columns: 140px 1fr 1fr; gap: .6rem; align-items: center;
  padding: .5rem 0; border-bottom: 1px solid var(--border-soft); font-size: .8rem;
}
.merge-field-row:last-child { border-bottom: none; }
.merge-field-row-header { font-size: .68rem; font-weight: 700; text-transform: uppercase; color: var(--text-faint); letter-spacing: .04em; }
.merge-field-row-identical { opacity: .6; }
.merge-field-label-cell { color: var(--text-faint); font-weight: 600; }
.merge-field-option {
  display: flex; align-items: flex-start; gap: .45rem; cursor: pointer; color: var(--text); line-height: 1.35;
}
.merge-field-option input[type="radio"] { margin-top: .2rem; flex-shrink: 0; accent-color: var(--accent); }
.merge-field-option input[type="radio"]:disabled { opacity: .5; }

.merge-notes-option { display: flex; align-items: center; gap: .5rem; font-size: .82rem; color: var(--text); cursor: pointer; }
.merge-notes-option input[type="checkbox"] { accent-color: var(--accent); }

.merge-move-list { margin: 0 0 .7rem; padding-left: 1.2rem; font-size: .82rem; color: var(--text); }
.merge-move-list li { margin-bottom: .2rem; }

/* Same visual weight as .customer-review-callout but red-toned, for the
   "this can't be undone" warning and the "already merged" short-circuit
   view in openCustomerModal(). */
.merge-warning-callout {
  padding: .65rem .9rem; border-radius: var(--radius-md);
  background: var(--red-soft); border: 1px solid rgba(243,89,107,.35); color: var(--on-red-soft);
  font-size: .8rem; font-weight: 500;
}

@media (max-width: 640px) {
  .merge-keep-picker { grid-template-columns: 1fr; }
  .merge-field-row { grid-template-columns: 1fr; gap: .2rem; }
  .merge-field-row-header { display: none; }
  .merge-field-label-cell { font-weight: 700; color: var(--text); margin-bottom: .1rem; }
}

/* ============================================================
   REPORTS MODULE
   ============================================================
   Dashboard (stat-grid/chart-grid reused as-is from the main Dashboard),
   Report Builder, Documents (upload + AI analysis), Scheduled (custom
   scheduled report emails). See renderReports_() and its per-tab renderers
   in app.js, and the <template id="tpl-reports"> markup in login.html. */

.reports-tab-bar {
  display: flex; gap: .35rem; margin-bottom: 1.2rem; padding: .3rem;
  background: var(--bg-soft); border: 1px solid var(--border); border-radius: var(--radius-lg);
  overflow-x: auto; -webkit-overflow-scrolling: touch;
}
.reports-tab-btn {
  flex: 1 1 auto; white-space: nowrap; border: none; background: transparent; color: var(--text-dim);
  font-size: .78rem; font-weight: 700; padding: .55rem .9rem; border-radius: var(--radius-md);
  cursor: pointer; transition: background .12s var(--ease), color .12s var(--ease);
}
.reports-tab-btn:hover { background: var(--surface-hover); color: var(--text); }
.reports-tab-btn.active { background: var(--accent); color: var(--on-accent); }
.reports-tab-panel.hidden { display: none; }

.report-builder-card, .report-upload-card {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 1.1rem 1.2rem;
}
.report-builder-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;
}
.report-metric-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: .55rem .9rem;
  margin-top: .5rem;
}
.report-metric-checkbox {
  display: flex; align-items: center; gap: .5rem; font-size: .8rem; color: var(--text);
  background: var(--bg-soft); border: 1px solid var(--border-soft); border-radius: var(--radius-md);
  padding: .5rem .7rem; cursor: pointer;
}
.report-metric-checkbox input { width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--accent); }

.report-builder-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .8rem; margin-bottom: 1rem; }
.report-builder-summary-tile {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: .75rem .85rem;
}
.report-builder-summary-tile .stat-label { font-size: .68rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: .03em; }
.report-builder-summary-tile .stat-value { font-size: 1.35rem; font-weight: 800; color: var(--text); margin-top: .2rem; }

/* Documents tab */
#reportDocFileInput {
  font-size: .78rem; color: var(--text-dim); max-width: 100%;
}
.report-doc-card {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: .9rem 1rem; margin-bottom: .8rem; display: flex; flex-direction: column; gap: .4rem;
}
.report-doc-card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
.report-doc-name { font-weight: 700; font-size: .85rem; color: var(--text); word-break: break-word; }
.report-doc-meta { font-size: .68rem; color: var(--text-faint); margin-top: .1rem; }
.report-doc-status { font-size: .65rem; font-weight: 700; padding: .18rem .55rem; border-radius: var(--radius-pill); white-space: nowrap; }
.report-doc-status-Analyzed { background: var(--green-soft); color: var(--on-green-soft); }
.report-doc-status-Analyzing { background: var(--amber-soft); color: var(--on-amber-soft); }
.report-doc-status-Failed { background: var(--red-soft); color: var(--on-red-soft); }
.report-doc-summary { font-size: .8rem; color: var(--text-dim); line-height: 1.5; }
.report-doc-error { font-size: .78rem; color: var(--red, #dc2626); }
.report-doc-metric-row { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .2rem; }
.report-doc-metric-chip {
  font-size: .68rem; font-weight: 700; background: var(--bg-soft); border: 1px solid var(--border-soft);
  border-radius: var(--radius-pill); padding: .2rem .6rem; color: var(--text-dim);
}
.report-doc-metric-chip b { color: var(--text); font-weight: 800; }
.report-doc-actions { display: flex; gap: .5rem; }

/* Dashboard tab -- AI-Extracted Insights (from uploaded documents), kept
   visually distinct from the native stat-grid/chart-grid above it so a
   figure pulled from a PDF is never mistaken for one this app computed
   from its own Sheet data. */
.ai-insights-card {
  background: var(--surface-2); border: 1px dashed var(--border-soft); border-radius: var(--radius-lg);
  padding: 1rem 1.1rem; margin-bottom: 1.4rem;
}
.ai-insights-card .section-heading { margin: 0 0 .6rem; }
.ai-insight-doc-row { padding: .6rem 0; border-top: 1px solid var(--border-soft); }
.ai-insight-doc-row:first-of-type { border-top: none; padding-top: 0; }
.ai-insight-doc-title { font-size: .78rem; font-weight: 700; color: var(--text); }
.ai-insight-doc-summary { font-size: .76rem; color: var(--text-dim); margin: .2rem 0 .4rem; line-height: 1.5; }

/* Scheduled tab */
.scheduled-report-card {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: .9rem 1.1rem; margin-bottom: .8rem;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
}
.scheduled-report-info { min-width: 0; flex: 1 1 260px; }
.scheduled-report-name { font-weight: 700; font-size: .85rem; color: var(--text); }
.scheduled-report-meta { font-size: .7rem; color: var(--text-faint); margin-top: .2rem; }
.scheduled-report-actions { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }

@media (max-width: 640px) {
  .reports-tab-btn { font-size: .72rem; padding: .5rem .6rem; }
  .report-builder-grid, .report-metric-grid { grid-template-columns: 1fr; }
  .scheduled-report-card { flex-direction: column; align-items: stretch; }
  .scheduled-report-actions { justify-content: flex-start; }
}

/* ---------- Log Indirect Time: clock dial + shift history (2026-09-10) ---------- */
.iti-clock-dial { display: flex; flex-direction: column; align-items: center; gap: .25rem; cursor: pointer; touch-action: none; user-select: none; width: fit-content; }
.iti-dial-face { fill: var(--surface-2); stroke: var(--border); stroke-width: 1.5; }
.iti-dial-hand { stroke: var(--accent); stroke-width: 3; stroke-linecap: round; }
.iti-dial-center { fill: var(--accent); }
.iti-dial-label { font-size: .7rem; color: var(--text-dim); font-weight: 600; }
.iti-history-list { display: flex; flex-direction: column; gap: .3rem; max-height: 220px; overflow-y: auto; }
.iti-history-row { display: flex; align-items: center; gap: .6rem; font-size: .76rem; padding: .4rem .55rem; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-2); }
.iti-history-time { color: var(--text-faint); min-width: 4.6rem; }
.iti-history-label { flex: 1; color: var(--text); }
.iti-history-dur { color: var(--text-dim); font-weight: 600; }

@media (max-width: 640px) {
  .iti-clock-dial svg { width: 68px; height: 68px; }
}

/* ---------- Admin Overview: Time Logs dashboard (2026-09-10) ---------- */
.time-logs-filter-bar { display: flex; align-items: flex-end; gap: .8rem; flex-wrap: wrap; margin-bottom: .9rem; }
.time-logs-filter-bar .modal-input { min-width: 9rem; }
.time-logs-summary { display: flex; gap: 1.4rem; flex-wrap: wrap; font-size: .78rem; color: var(--text-dim); padding: .6rem .8rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-md); }
.time-logs-summary strong { color: var(--text); }

@media (max-width: 640px) {
  .time-logs-filter-bar { flex-direction: column; align-items: stretch; }
  .time-logs-filter-bar .modal-input { min-width: 0; }
}

/* ============================================================================
   PHASE 4: CSS & Z-INDEX HIERARCHY + RESPONSIVE DESIGN (2026-09-18)
   ============================================================================ */

/* Z-INDEX HIERARCHY & MODAL LAYERING */
:root {
  --z-modal-base: 50;
  --z-modal-overlay: 45;
  --z-dropdown: 30;
  --z-sticky-header: 20;
  --z-normal: 1;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: var(--z-modal-overlay);
  transition: opacity 0.2s ease;
}

.modal-overlay.hidden {
  display: none;
  opacity: 0;
}

.modal-overlay:not(.hidden) {
  opacity: 1;
}

.modal-panel {
  background: white;
  border-radius: 0.75rem;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
              0 10px 10px -5px rgba(0, 0, 0, 0.04);
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
  z-index: 1;
  min-width: 400px;
  animation: slideUp 0.3s ease-out;
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.modal-panel-wide {
  max-width: 90vw;
  width: 900px;
}

@media (prefers-color-scheme: dark) {
  .modal-panel {
    background: #1f2937;
    color: #f3f4f6;
  }

  .modal-overlay {
    background: rgba(0, 0, 0, 0.8);
  }
}

/* MODAL HEADER & FOOTER */
.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.25rem;
  border-bottom: 1px solid #e5e7eb;
  position: sticky;
  top: 0;
  background: white;
  z-index: 10;
}

@media (prefers-color-scheme: dark) {
  .modal-header {
    background: #1f2937;
    border-bottom-color: #374151;
  }
}

.modal-ticket-id {
  font-size: 1.125rem;
  font-weight: 600;
  color: #111827;
}

@media (prefers-color-scheme: dark) {
  .modal-ticket-id {
    color: #f3f4f6;
  }
}

.modal-close {
  background: none;
  border: none;
  font-size: 1.75rem;
  color: #6b7280;
  cursor: pointer;
  padding: 0;
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 0.375rem;
  transition: background-color 0.2s;
}

.modal-close:hover {
  background-color: #f3f4f6;
  color: #111827;
}

@media (prefers-color-scheme: dark) {
  .modal-close:hover {
    background-color: #374151;
    color: #f3f4f6;
  }
}

.modal-body {
  padding: 1.5rem;
  overflow-y: auto;
  max-height: calc(90vh - 140px);
}

.tech-modal-actions {
  display: flex;
  gap: 0.75rem;
  padding: 1.5rem;
  border-top: 1px solid #e5e7eb;
  position: sticky;
  bottom: 0;
  background: white;
  z-index: 10;
}

@media (prefers-color-scheme: dark) {
  .tech-modal-actions {
    background: #1f2937;
    border-top-color: #374151;
  }
}

/* MODAL FORM STYLING */
.modal-field-label {
  display: block;
  font-size: 0.9rem;
  font-weight: 500;
  color: #111827;
  margin-bottom: 0.4rem;
}

@media (prefers-color-scheme: dark) {
  .modal-field-label {
    color: #f3f4f6;
  }
}

.modal-input,
.modal-notes-textarea {
  width: 100%;
  padding: 0.625rem 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  font-size: 0.95rem;
  font-family: inherit;
  transition: border-color 0.2s;
}

.modal-input:focus,
.modal-notes-textarea:focus {
  outline: none;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

@media (prefers-color-scheme: dark) {
  .modal-input,
  .modal-notes-textarea {
    background: #111827;
    border-color: #374151;
    color: #f3f4f6;
  }

  .modal-input:focus,
  .modal-notes-textarea:focus {
    border-color: #60a5fa;
    box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
  }
}

.modal-section {
  margin-bottom: 1.5rem;
}

.modal-section-title {
  font-size: 0.95rem;
  font-weight: 600;
  color: #111827;
  margin-bottom: 0.75rem;
}

@media (prefers-color-scheme: dark) {
  .modal-section-title {
    color: #f3f4f6;
  }
}

.modal-field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
}

@media (max-width: 768px) {
  .modal-field-grid {
    grid-template-columns: 1fr;
  }
}

.modal-save-btn {
  padding: 0.625rem 1.25rem;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 0.375rem;
  font-size: 0.95rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s;
}

.modal-save-btn:hover {
  background: #2563eb;
}

.modal-save-btn:disabled {
  background: #9ca3af;
  cursor: not-allowed;
}

.modal-cancel-btn {
  padding: 0.625rem 1.25rem;
  background: #f3f4f6;
  color: #374151;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  font-size: 0.95rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s;
}

.modal-cancel-btn:hover {
  background: #e5e7eb;
}

/* RESPONSIVE DESIGN - MOBILE ADJUSTMENTS */
@media (max-width: 768px) {
  .modal-panel {
    min-width: 90vw;
    max-width: 95vw;
    border-radius: 0.5rem;
  }

  .modal-panel-wide {
    width: auto;
  }

  .modal-header {
    padding: 1rem;
  }

  .modal-body {
    padding: 1rem;
    max-height: calc(90vh - 120px);
  }

  .tech-modal-actions {
    padding: 1rem;
    flex-direction: column;
  }

  .modal-save-btn,
  .modal-cancel-btn {
    width: 100%;
  }

  .modal-field-label {
    font-size: 0.875rem;
  }

  .modal-input,
  .modal-notes-textarea {
    font-size: 0.9rem;
  }
}

/* UNIFIED USER MANAGEMENT PANEL - RESPONSIVE */
@media (max-width: 1024px) {
  .user-list-panel,
  .user-form-panel {
    /* Stack vertically on smaller screens */
  }
}

.user-list-item {
  border-bottom: 1px solid #f3f4f6;
  padding: 0.8rem;
  cursor: pointer;
  transition: background-color 0.15s;
}

.user-list-item:hover {
  background-color: #f9fafb;
}

@media (prefers-color-scheme: dark) {
  .user-list-item {
    border-bottom-color: #374151;
  }

  .user-list-item:hover {
    background-color: #111827;
  }
}

.user-edit-btn {
  padding: 0.4rem 0.8rem;
  font-size: 0.8rem;
  background: #f3f4f6;
  border: 1px solid #d1d5db;
  border-radius: 0.3rem;
  cursor: pointer;
  color: #374151;
  transition: all 0.2s;
}

.user-edit-btn:hover {
  background: #e5e7eb;
  border-color: #9ca3af;
}

/* KEYBOARD ACCESSIBILITY - HIGH CONTRAST FOCUS STATES */
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

@media (prefers-color-scheme: dark) {
  button:focus-visible,
  input:focus-visible,
  select:focus-visible,
  textarea:focus-visible {
    outline-color: #60a5fa;
  }
}

/* UTILITY CLASSES */
.hidden {
  display: none;
}

.text-xs {
  font-size: 0.75rem;
}

.text-slate-400 {
  color: #cbd5e1;
}

@media (prefers-color-scheme: dark) {
  .text-slate-400 {
    color: #64748b;
  }
}

/* END PHASE 4 CSS */

/* ==========================================================================
   THEME-AWARE CONTRAST FIX (2026-09-19)
   The Phase 4 modal/user-management rules above hardcode light-mode greys and
   switch on prefers-color-scheme, but the app themes via :root[data-theme].
   Result: white-on-white text in dark mode. These overrides route everything
   through the theme tokens so every modal follows the app theme in both modes.
   ========================================================================== */
.modal-panel { background: var(--surface-solid); color: var(--text); }
.modal-header, .tech-modal-actions { background: var(--surface-solid); border-color: var(--border); }
.modal-ticket-id, .modal-field-label, .modal-section-title { color: var(--text); }
.modal-close { color: var(--text-dim); }
.modal-close:hover { background-color: var(--surface-hover); color: var(--text); }
.modal-input, .modal-notes-textarea { background: var(--bg-soft); border-color: var(--border); color: var(--text); }
.modal-input::placeholder, .modal-notes-textarea::placeholder { color: var(--text-faint); }
.modal-input:focus, .modal-notes-textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
.modal-cancel-btn { background: var(--surface-hover); color: var(--text); border: 1px solid var(--border); }
.modal-cancel-btn:hover { background: var(--border); color: var(--text); }
.user-list-item { border-bottom-color: var(--border-soft); color: var(--text); }
.user-list-item:hover { background-color: var(--surface-hover); }
.user-edit-btn { background: var(--surface-hover); border: 1px solid var(--border); color: var(--text); }
.user-edit-btn:hover { background: var(--border); border-color: var(--text-faint); }
.text-slate-400 { color: var(--text-faint); }
.modal-panel select, .modal-panel select option { background: var(--bg-soft); color: var(--text); }


/* ========================================================================== 
   Phase 6: Tenant Interface Customization (2026-09-18)
   Dynamic theming support with CSS variables applied by TenantCustomization class
   Mobile-first responsive design with touch-friendly interaction targets
   WCAG AA accessibility compliance
   ========================================================================== */

/* Customization-specific CSS variables (applied dynamically by TenantCustomization) */
:root {
  --customization-primary: #0066CC;
  --customization-secondary: #FF6600;
  --customization-success: #28A745;
  --customization-danger: #DC3545;
  --customization-font-family: 'Inter, sans-serif';
  --customization-font-size: 1rem;
  --customization-density-factor: 1;
}

/* Mobile (480px): Optimized layout for small screens */
@media (max-width: 480px) {
  .modal-field-grid {
    grid-template-columns: 1fr !important;
  }

  .modal-input {
    font-size: 16px !important; /* Prevent iOS zoom on input focus */
    min-height: 44px; /* Touch target minimum */
  }

  .modal-cancel-btn,
  .modal-save-btn {
    min-height: 44px; /* Touch target minimum */
    font-size: 14px;
    padding: 10px 16px;
  }

  .modal-panel {
    margin: 0 1rem;
    max-height: 85vh;
  }

  .modal-body {
    padding: 1rem !important;
    max-height: 60vh !important;
  }
}

/* Tablet (768px): Medium layout optimizations */
@media (min-width: 481px) and (max-width: 768px) {
  .modal-field-grid {
    grid-template-columns: repeat(2, 1fr) !important;
  }

  .modal-input {
    min-height: 40px;
  }

  .modal-panel {
    max-width: 90vw;
  }
}

/* Desktop (1200px+): Full-featured layout */
@media (min-width: 1200px) {
  .modal-field-grid {
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) !important;
  }

  .modal-panel-wide {
    max-width: 900px;
  }
}

/* Customization modal accessibility enhancements */
#settingsCustomizationModalOverlay input[type="color"] {
  cursor: pointer;
  min-height: 40px;
  border-radius: 4px;
}

#settingsCustomizationModalOverlay input[type="checkbox"] {
  cursor: pointer;
  min-width: 18px;
  min-height: 18px;
  accent-color: var(--customization-primary, #0066CC);
}

#settingsCustomizationModalOverlay select {
  cursor: pointer;
}

/* Focus states for keyboard navigation (WCAG AA) */
.modal-input:focus,
#settingsCustomizationModalOverlay input:focus,
#settingsCustomizationModalOverlay select:focus {
  outline: 2px solid var(--customization-primary, #0066CC);
  outline-offset: 2px;
}

/* High contrast mode support (WCAG AAA) */
@media (prefers-contrast: more) {
  .modal-input {
    border: 2px solid currentColor;
  }

  .modal-cancel-btn,
  .modal-save-btn {
    border: 2px solid currentColor;
  }
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  .modal-overlay,
  .modal-panel {
    animation: none !important;
    transition: none !important;
  }
}

/* Dark mode support for customization modal */
@media (prefers-color-scheme: dark) {
  #settingsCustomizationModalOverlay {
    --modal-bg: var(--surface-solid, #1e293b);
    --modal-text: var(--text, #f1f5f9);
  }
}

/* Customization theme application class (applied by TenantCustomization.apply()) */
body.customization-theme-applied {
  /* These are overridden by inline styles set by TenantCustomization class */
  --accent: var(--customization-primary, #0066CC);
}

/* ==========================================================================
   THEME-DRIVEN MODAL OVERRIDES (2026-09-19)
   The generic "MODAL" block above hardcodes light-mode colors (#111827 text,
   white header/panel) and only switches to dark via the OS-level
   prefers-color-scheme media query -- but the app's theme is driven by
   <html data-theme="light|dark">. With an OS in light mode and the app in
   dark mode, those hardcoded colors won the cascade: dark labels/section
   titles on dark cards, and a white modal header. This block re-binds the
   same selectors to the theme tokens so they follow data-theme, and it
   must stay LAST in the file to win on source order.
   ========================================================================== */
:root[data-theme] .modal-panel { background: var(--surface-solid); color: var(--text); }
:root[data-theme] .modal-header { background: var(--surface-solid); border-bottom-color: var(--border-soft); }
:root[data-theme] .modal-ticket-id { color: var(--text); box-shadow: none; }
:root[data-theme] .modal-section-title { color: var(--text-dim); }
:root[data-theme] .modal-field-label { color: var(--text); }
:root[data-theme] .tech-modal-actions { background: var(--surface-solid); border-top-color: var(--border-soft); }
:root[data-theme] .modal-input,
:root[data-theme] .modal-notes-textarea {
  background: var(--surface-2); color: var(--text); border-color: var(--border);
}
:root[data-theme] .modal-input:focus,
:root[data-theme] .modal-notes-textarea:focus {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring);
}
:root[data-theme] .modal-cancel-btn { background: var(--surface-2); color: var(--text); border-color: var(--border); }
:root[data-theme] .modal-cancel-btn:hover { background: var(--surface-hover); }
/* Disabled buttons: keep the label readable (inline teal backgrounds on
   Send Dispatch / Generate Invoice were fading to unreadable at .4). */
:root[data-theme="dark"] .modal-save-btn:disabled { opacity: .65; color: #e5e7eb; }
:root[data-theme="dark"] .modal-save-btn.save-btn-clean:disabled { background: var(--surface-hover); color: var(--text-dim); opacity: 1; }

/* Team chat bubble width fix (2026-09-19): .chat-bubble-wrap already caps
   width at 70% of the row, so the bubble inside must not ALSO cap at 70%
   (that compounded to ~49% and, with overflow-wrap:anywhere, squeezed short
   messages into one-word-per-line columns). */
.chat-bubble-wrap > .chat-bubble, .chat-bubble-wrap .chat-bubble { max-width: 100%; }

/* ---------- Prior Service History (rollout-2026-09-28) -- ticket modal box
   for pasted legacy records; see customerHistorySectionHtml_() in app.js. ---------- */
.customer-history-summary { display: flex; align-items: center; gap: .6rem; cursor: pointer; list-style: none; min-height: 44px; }
.customer-history-summary::-webkit-details-marker { display: none; }
.customer-history-summary::after {
  content: ""; width: 8px; height: 8px; margin-left: auto; margin-right: .3rem;
  border-right: 2px solid var(--text-dim); border-bottom: 2px solid var(--text-dim);
  transform: translateY(-2px) rotate(45deg); transition: transform .2s var(--ease);
}
details[open] > .customer-history-summary::after { transform: translateY(2px) rotate(-135deg); }
.customer-history-title { margin: 0 !important; }
.customer-history-pill { flex-shrink: 0; white-space: nowrap; font-size: .68rem; font-weight: 700; padding: .15rem .55rem; border-radius: 999px; background: var(--slate-soft); color: var(--on-slate-soft); }
.customer-history-pill.has { background: var(--cyan-soft); color: var(--on-cyan-soft); }
.customer-history-view {
  white-space: pre-wrap; overflow-wrap: anywhere; font-size: .8rem; line-height: 1.5; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: var(--radius-md, 10px);
  padding: .7rem .8rem; max-height: 320px; overflow-y: auto;
}
.customer-history-input { min-height: 180px; font-size: .82rem; line-height: 1.5; }
.customer-history-actions { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin-top: .5rem; }
.customer-history-edit-btn { margin-top: .6rem; }
.rs-picker-trigger { cursor: pointer; caret-color: transparent; }
