/* =========================================================================
   SeeWork — web prototype
   Design tokens follow Apple's Human Interface Guidelines: the system colour
   palette, the macOS type scale (13px body, not 16px), standard control
   heights, and materials rather than flat greys for the sidebar and toolbar.
   Icons are hand-drawn SVG, never SF Symbols — the SF font licence doesn't
   cover shipping them in a web app.
   ========================================================================= */

/* ---------- Tokens ---------- */

:root {
  color-scheme: light dark;

  /* The brand palette. Kept as given wherever white sitting on the colour
     still reads; orange, yellow, green and teal are nudged darker in light
     mode only, because white on the originals falls under 3:1.
     Tools/build-palette.mjs derives these and checks every one. */
  --purple: #9a69b3;
  --on-purple: #160f19;
  --red: #f05a52;
  --on-red: #220d0b;
  --orange: #dc7a17;
  --on-orange: #1f1103;
  --yellow: #b98f02;
  --on-yellow: #1a1400;
  --green: #47a554;
  --on-green: #0a170c;
  --blue: #0098d6;
  --on-blue: #00151e;
  /* Not in the six, but the app needs the names. Mixed from neighbours so
     they belong to the same family. */
  --indigo: #557ec3;
  --on-indigo: #0c121b;
  --teal: #2ca3a3;
  --on-teal: #061717;
  --pink: #ce6079;
  --on-pink: #1d0d11;
  --brown: #855a2e;
  --on-brown: #ffffff;
  --gray:   #8e8e93;
  --on-gray: #ffffff;

  --accent: var(--blue);

  /* Surfaces */
  --bg-window: #eceef1;
  --bg-content: #ffffff;
  /* The surface cards sit *on*. Grey behind white is how macOS separates a
     grouped list from its background; two whites and a hairline is not
     separation, it's a rumour. */
  --bg-page: #f1f1f5;
  --bg-card: #ffffff;
  --bg-inset: rgba(120, 120, 128, 0.08);
  --bg-inset-strong: rgba(120, 120, 128, 0.14);
  --bg-sidebar: rgba(246, 246, 248, 0.68);
  --bg-titlebar: rgba(246, 246, 248, 0.72);
  --bg-hover: rgba(0, 0, 0, 0.045);

  /* Labels */
  --label: #000000;
  --label-secondary: rgba(60, 60, 67, 0.62);
  --label-tertiary: rgba(60, 60, 67, 0.36);
  --label-quaternary: rgba(60, 60, 67, 0.20);
  --on-accent: #ffffff;

  --separator: rgba(60, 60, 67, 0.14);
  --separator-opaque: #d8d8dc;

  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.07), 0 1px 1px rgba(0, 0, 0, 0.04),
                 0 0 0 0.5px rgba(0, 0, 0, 0.08);
  --shadow-raised: 0 8px 28px rgba(0, 0, 0, 0.16), 0 0 0 0.5px rgba(0, 0, 0, 0.08);

  /* Radii — Apple's continuous corners, approximated */
  --r-control: 6px;
  --r-field: 8px;
  --r-card: 12px;
  --r-panel: 16px;

  --sidebar-w: 216px;
  --titlebar-h: 52px;

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
          "Helvetica Neue", "Segoe UI", system-ui, sans-serif;
  --font-rounded: -apple-system, "SF Pro Rounded", var(--font);

  --ease: cubic-bezier(0.32, 0.72, 0, 1);
}

/* System preference first; the explicit [data-theme] block below overrides it
   in both directions, so the in-app toggle always wins. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* The same six, lifted only where they'd vanish against a dark card. */
    --purple: #a275b9;
    --on-purple: #17101a;
    --red: #f05a52;
    --on-red: #220d0b;
    --orange: #e8841f;
    --on-orange: #201204;
    --yellow: #fcc50d;
    --on-yellow: #231c02;
    --green: #67be73;
    --on-green: #0e1b10;
    --blue: #0098d6;
    --on-blue: #00151e;
    --indigo: #6388c8;
    --on-indigo: #0e131c;
    --teal: #2ea9a9;
    --on-teal: #061818;
    --pink: #d26d84;
    --on-pink: #1d0f12;
    --brown: #ac743b;
    --on-brown: #181008;
    --gray:   #98989d;
    --on-gray: #ffffff;

    --bg-window: #1c1c1e;
    --bg-content: #1e1e20;
    --bg-page: #161618;
    --bg-card: #2a2a2c;
    --bg-inset: rgba(120, 120, 128, 0.20);
    --bg-inset-strong: rgba(120, 120, 128, 0.30);
    --bg-sidebar: rgba(38, 38, 41, 0.62);
    --bg-titlebar: rgba(38, 38, 41, 0.70);
    --bg-hover: rgba(255, 255, 255, 0.06);
  /* The selected segment needs to be lighter than the track, not darker. */
  --segmented-on: #48484a;

    --label: #ffffff;
    --label-secondary: rgba(235, 235, 245, 0.62);
    --label-tertiary: rgba(235, 235, 245, 0.34);
    --label-quaternary: rgba(235, 235, 245, 0.18);

    --separator: rgba(255, 255, 255, 0.12);
    --separator-opaque: #3a3a3c;

    --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.36), 0 1px 1px rgba(0, 0, 0, 0.24),
                   0 0 0 0.5px rgba(255, 255, 255, 0.08);
    --shadow-raised: 0 10px 34px rgba(0, 0, 0, 0.5), 0 0 0 0.5px rgba(255, 255, 255, 0.08);
  }
}

/* Explicit override beats the media query in both directions. */
:root[data-theme="dark"] {
  /* The same six, lifted only where they'd vanish against a dark card. */
  --purple: #a275b9;
  --on-purple: #17101a;
  --red: #f05a52;
  --on-red: #220d0b;
  --orange: #e8841f;
  --on-orange: #201204;
  --yellow: #fcc50d;
  --on-yellow: #231c02;
  --green: #67be73;
  --on-green: #0e1b10;
  --blue: #0098d6;
  --on-blue: #00151e;
  --indigo: #6388c8;
  --on-indigo: #0e131c;
  --teal: #2ea9a9;
  --on-teal: #061818;
  --pink: #d26d84;
  --on-pink: #1d0f12;
  --brown: #ac743b;
  --on-brown: #181008;
  --gray:   #98989d;
  --on-gray: #ffffff;

  --bg-window: #1c1c1e;
  --bg-content: #1e1e20;
  --bg-page: #161618;
  --bg-card: #2a2a2c;
  --bg-inset: rgba(120, 120, 128, 0.20);
  --bg-inset-strong: rgba(120, 120, 128, 0.30);
  --bg-sidebar: rgba(38, 38, 41, 0.62);
  --bg-titlebar: rgba(38, 38, 41, 0.70);
  --bg-hover: rgba(255, 255, 255, 0.06);
  /* The selected segment needs to be lighter than the track, not darker. */
  --segmented-on: #48484a;

  --label: #ffffff;
  --label-secondary: rgba(235, 235, 245, 0.62);
  --label-tertiary: rgba(235, 235, 245, 0.34);
  --label-quaternary: rgba(235, 235, 245, 0.18);

  --separator: rgba(255, 255, 255, 0.12);
  --separator-opaque: #3a3a3c;

  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.36), 0 1px 1px rgba(0, 0, 0, 0.24),
                 0 0 0 0.5px rgba(255, 255, 255, 0.08);
  --shadow-raised: 0 10px 34px rgba(0, 0, 0, 0.5), 0 0 0 0.5px rgba(255, 255, 255, 0.08);
}

/* ---------- Reset ---------- */

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

html, body {
  height: 100%;
  margin: 0;
}

body {
  font-family: var(--font);
  font-size: 13px;
  line-height: 1.42;
  color: var(--label);
  background: var(--bg-window);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow: hidden;
}

h1, h2, h3, h4, p, figure { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; }
input, textarea, select { font: inherit; color: inherit; }

:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset: 1px;
  border-radius: 5px;
}

/* ---------- The app's own frame ----------

   This used to draw a Mac window on a desk: a 1440px-wide card floating in
   the middle of the page with rounded corners, a drop shadow and three
   traffic-light dots in the corner. It was a way of showing what the native
   app would look like, and it stopped being worth the trade — the app IS
   the page now, so the browser's own window is the window, and a screen
   wider than 1440px gets used rather than framed.

   What stayed: the titlebar, which is a real toolbar with real controls in
   it, and the sidebar, which is unchanged. Only the pretend part went. */

.window {
  height: 100%;
  display: grid;
  grid-template-rows: var(--titlebar-h) minmax(0, 1fr);
  background: var(--bg-page);
  overflow: hidden;
}

.titlebar {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  align-items: center;
  background: var(--bg-titlebar);
  backdrop-filter: saturate(180%) blur(30px);
  -webkit-backdrop-filter: saturate(180%) blur(30px);
  border-bottom: 0.5px solid var(--separator);
}

.titlebar__left {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px 0 14px;
  height: 100%;
}

.titlebar__main {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 100%;
  padding: 0 14px 0 16px;
  min-width: 0;
}

/* Add actions sit next to the title. */
.titlebar__leading {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}
.titlebar__leading:empty { display: none; }

/* Everything else is pushed to the far end. */
.titlebar__actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

/* Both belong to the phone and are hidden everywhere else — a Mac has a
   sidebar and two panes side by side, so neither has anything to do.

   The back button is scoped through `.titlebar` rather than named on its own:
   it also carries `.btn`, which sets a display of its own further down the
   file, and a bare `.titlebar__back` ties with it and loses on source order. */
.tabbar { display: none; }
.titlebar .titlebar__back { display: none; }

/* ---------- Split view ---------- */

.split {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 0;
}

.window[data-sidebar="hidden"] .split,
.window[data-sidebar="hidden"] .titlebar { grid-template-columns: 0 minmax(0, 1fr); }
.window[data-sidebar="hidden"] .sidebar { transform: translateX(-100%); }
.window[data-sidebar="hidden"] .titlebar__left { padding-right: 4px; }

.sidebar {
  background: var(--bg-sidebar);
  backdrop-filter: saturate(180%) blur(30px);
  -webkit-backdrop-filter: saturate(180%) blur(30px);
  border-right: 0.5px solid var(--separator);
  overflow-y: auto;
  overflow-x: hidden;
  padding: 8px 10px 20px;
  transition: transform 0.3s var(--ease);
  scrollbar-width: thin;
}

.detail {
  background: var(--bg-page);
  overflow-y: auto;
  min-width: 0;
  scroll-behavior: smooth;
}

/* Wrapper the render loop replaces each time. It generates no box of its own,
   so full-height workspaces still measure against .detail. */
.detail__host { display: contents; }

.sidebar-scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.3);
  z-index: 40;
}

/* ---------- Sidebar contents ---------- */

.side-section { margin-bottom: 12px; }

/* The header is a disclosure control, so it's a button. */
/* The heading is a row: the title collapses, and whatever sits beside it acts
   on the section. Siblings rather than nested, because a button inside a
   button is invalid and the inner one can't be reached by keyboard. */
.side-section__head {
  display: flex;
  align-items: center;
  /* Matched to `.side-row`'s own right padding, so the switch on a heading
     and the switches on the rows under it land on one line. */
  padding-right: 9px;
  gap: 4px;
}

.side-section__title {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  font-size: 11px;
  font-weight: 600;
  color: var(--label-tertiary);
  padding: 6px 10px 3px;
  letter-spacing: 0.01em;
  text-align: left;
  cursor: pointer;
  border-radius: 5px;
}
.side-section__title:hover { color: var(--label-secondary); }

/* Hidden until you go near the heading, like the chevron on the other side —
   a control on every section at rest would read as a row of buttons rather
   than a list of labels. Focus reveals it too, or it would be unreachable
   without a mouse. */
.side-section__more {
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--label-tertiary);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.12s ease, background 0.12s ease;
}
.side-section:hover .side-section__more,
.side-section__more:focus-visible { opacity: 1; }
.side-section__more:hover { background: var(--bg-inset); color: var(--label); }

/* Hidden until you go near it, so the headings read as labels rather than a
   row of controls. Faded rather than removed, so the title doesn't shift
   sideways the moment it appears. */
.side-section__chevron {
  display: grid;
  place-items: center;
  color: var(--label-quaternary);
  opacity: 0;
  transition: transform 0.2s var(--ease), opacity 0.15s ease;
}
.side-section:hover .side-section__chevron,
.side-section__title:focus-visible .side-section__chevron {
  opacity: 1;
}

/* A shut section keeps it: without the chevron nothing says there's more
   underneath, and the dot only appears when your place is in there. */
.side-section--collapsed .side-section__chevron {
  opacity: 1;
  transform: rotate(-90deg);
}

/* Marks a shut section that holds wherever you are, so collapsing doesn't
   lose all trace of your place. */
.side-section__dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  margin-left: 2px;
  display: none;
}
.side-section--collapsed .side-section__dot { display: block; }

/* The inner div is what clips and what animates. These two rules are only the
   resting states — open is `auto`, so the section still fits its content when
   rows are added or removed underneath it, and shut is zero.

   The movement between them is run from JS with the Web Animations API, on
   measured pixel heights. A CSS transition can't do it: `auto` is not an
   interpolable value, and the grid-template-rows 1fr → 0fr trick that gets
   around that didn't animate reliably here. Measuring is unglamorous and
   always works. */
.side-section__rows > div {
  overflow: hidden;
  min-height: 0;
}
.side-section--collapsed .side-section__rows > div { height: 0; }

.side-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 9px;
  border: 0;
  background: transparent;
  border-radius: var(--r-control);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
  color: var(--label);
  transition: background 0.12s ease;
}
.side-row:hover { background: var(--bg-hover); }
.side-row .icon { color: var(--accent); flex: none; }
.side-row__label {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.side-row__count {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--label-tertiary);
}
.side-row[aria-current="true"] {
  background: var(--accent);
  color: var(--on-accent);
}
.side-row[aria-current="true"] .icon,
.side-row[aria-current="true"] .side-row__count { color: var(--on-accent); }
.side-row[aria-current="true"] .side-row__count { opacity: 0.8; }

.side-row--add { color: var(--label-secondary); }
.side-row--add .icon { color: var(--label-tertiary); }

.side-badge {
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--red);
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  display: grid;
  place-items: center;
  font-variant-numeric: tabular-nums;
}
.side-row[aria-current="true"] .side-badge { background: rgba(255, 255, 255, 0.28); }

/* ---------- Sidebar edit mode ---------- */

/* Counts and badges are about what needs doing, which isn't the question
   while you're deciding what the sidebar should contain. They step out, and
   each row is left saying only what it is. */
.sidebar--editing .side-row__count,
.sidebar--editing .side-badge { display: none; }

/* Headings stop being disclosure controls while editing — every section is
   forced open, so there's nothing left to disclose — and become handles
   instead. The chevron goes with the job it was doing. */
.sidebar--editing .side-section__head { cursor: grab; }
.sidebar--editing .side-section__head:active { cursor: grabbing; }
.sidebar--editing .side-section__chevron { display: none; }

/* Matched to the heading it sits on rather than dimmer. At 18% opacity it
   was there but unreadable, which is the one thing a handle can't be — its
   whole job is to say the heading can be dragged. */
.side-section__grip {
  display: grid;
  place-items: center;
  color: var(--label-tertiary);
  flex: none;
}
.sidebar--editing .side-section__head:hover .side-section__grip {
  color: var(--label);
}

/* Left behind rather than hidden: you're moving it, and it helps to see
   where it currently sits as the others part around it. */
.side-section--dragging { opacity: 0.4; }
.side-section--dragging .side-section__head { cursor: grabbing; }

/* A row you've taken out. Still legible, because you have to read it to
   decide to put it back, but plainly no longer part of the sidebar. */
.side-row--off .side-row__label { color: var(--label-tertiary); }
.side-row--off .icon { opacity: 0.4; }

/* Tinted rather than filled: one of these sits on every row at once, and a
   column of solid red dots reads as a warning rather than a control.

   The glyph is mixed toward the label instead of being the raw colour — the
   same recipe the badges use. Straight --red on a pale red tint measures
   2.25:1, and this is a one-pixel stroke, so it needs the help. */
.side-row__toggle {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  flex: none;
  border-radius: 50%;
  color: color-mix(in srgb, var(--red) 62%, var(--label));
  background: color-mix(in srgb, var(--red) 22%, transparent);
}
.side-row__toggle .icon { color: currentColor; }
.side-row--off .side-row__toggle {
  color: color-mix(in srgb, var(--green) 62%, var(--label));
  background: color-mix(in srgb, var(--green) 24%, transparent);
}
.side-row--off .side-row__toggle .icon { opacity: 1; }

/* The same switch on a whole section. Same colours as the row's, because it
   does the same thing at a larger scale and a second visual language for one
   more control would be a second thing to learn. */
.side-section__toggle {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  color: color-mix(in srgb, var(--red) 62%, var(--label));
  background: color-mix(in srgb, var(--red) 22%, transparent);
}
.side-section__toggle .icon { color: currentColor; }
.side-section--off .side-section__toggle {
  color: color-mix(in srgb, var(--green) 62%, var(--label));
  background: color-mix(in srgb, var(--green) 24%, transparent);
}

/* A section you've taken out, while you're still looking at it. It folds shut
   and drops to the foot of the list, so what's left at the top is what you
   actually use — the heading stays as the handle to bring it back.

   Same fold the collapse uses, so there's one mechanism rather than two that
   have to be kept looking alike. */
.side-section--off .side-section__title { color: var(--label-quaternary); }
.side-section--off .side-section__rows > div { height: 0; }
.side-section--off .side-section__chevron { display: none; }

/* ---------- Colour swatches ---------- */

.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.swatch {
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--swatch);
  cursor: pointer;
  /* The ring is drawn outside the colour rather than around it, so choosing
     one doesn't change its size and shuffle the row along. */
  box-shadow: 0 0 0 0 var(--bg-card), 0 0 0 0 var(--swatch);
  transition: box-shadow 0.15s var(--ease);
}
.swatch[aria-checked="true"] {
  box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--swatch);
}
.swatch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.sidebar__hint {
  font-size: 11px;
  line-height: 1.45;
  color: var(--label-tertiary);
  padding: 5px 10px 0;
}

/* ---------- Generic layout ---------- */

.page {
  padding: 20px 24px 60px;
  max-width: 1100px;
}
.page--wide { max-width: none; }
.page--narrow { max-width: 720px; }

.stack { display: flex; flex-direction: column; gap: 16px; }
.row { display: flex; align-items: center; gap: 10px; }
.row--wrap { flex-wrap: wrap; }
.spacer { flex: 1; }

.section-heading {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  font-weight: 600;
  color: var(--label-secondary);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin-bottom: 8px;
}
.section-heading .icon { color: var(--label-tertiary); }
/* A heading is uppercase; a control that sits in one is not. Both inherit
   down without this, and "STREET VIEW" on a button reads as shouting. */
.section-heading .btn,
.section-heading .segmented { text-transform: none; letter-spacing: normal; }

.muted { color: var(--label-secondary); }
.dim { color: var(--label-tertiary); }
.caption { font-size: 11px; color: var(--label-secondary); }
.mono-nums { font-variant-numeric: tabular-nums; }
.title-lg { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
.title-md { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.headline { font-size: 13px; font-weight: 600; }

/* ---------- Controls ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 26px;
  padding: 0 11px;
  border-radius: var(--r-control);
  border: 0.5px solid var(--separator);
  background: var(--bg-card);
  box-shadow: 0 0.5px 1px rgba(0, 0, 0, 0.06);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: filter 0.12s ease, background 0.12s ease;
}
.btn:hover { filter: brightness(0.97); }
.btn:active { filter: brightness(0.93); }
.btn[disabled] { opacity: 0.4; cursor: default; filter: none; }

.btn--primary {
  background: var(--accent);
  color: var(--on-accent);
  border-color: transparent;
}
.btn--primary:hover { filter: brightness(1.06); }
.btn--primary .icon { color: currentColor; }

.btn--plain {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--accent);
  padding: 0 4px;
}
.btn--plain:hover { background: var(--bg-hover); filter: none; }

.btn--destructive { color: var(--red); }
.btn--large { height: 32px; padding: 0 14px; font-size: 13px; }

.icon-btn {
  display: grid;
  place-items: center;
  width: 28px;
  height: 26px;
  border: 0;
  border-radius: var(--r-control);
  background: transparent;
  color: var(--label-secondary);
  cursor: pointer;
  flex: none;
  transition: background 0.12s ease, color 0.12s ease;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--label); }

/* Coloured icon buttons. Both of these need a matching :hover rule, and
   both must sit after .icon-btn:hover — that rule carries a pseudo-class
   and would otherwise outrank a plain modifier, dropping the colour at the
   moment the pointer is on it, which is when it most needs to be there. */

/* The one that destroys something, in the same `--red` .btn--destructive
   uses. */
.icon-btn--destructive { color: var(--red); }
.icon-btn--destructive:hover {
  color: var(--red);
  background: color-mix(in srgb, var(--red) 14%, transparent);
}

/* The ordinary way in, in the app's own accent — the colour the sidebar
   icons already carry, by the same token rather than by a matching hex, so
   the two cannot drift apart if the accent is ever changed. */
.icon-btn--accent { color: var(--accent); }
.icon-btn--accent:hover {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.segmented {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  background: var(--bg-inset);
  border-radius: 8px;
}
.segmented button {
  border: 0;
  background: transparent;
  border-radius: 6px;
  padding: 3px 11px;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  color: var(--label);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.segmented button[aria-pressed="true"] {
  background: var(--segmented-on, var(--bg-card));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.05);
}
/* For a card header, where the control is an accessory rather than the
   subject and shouldn't out-weigh the title beside it. */
.segmented--sm button { padding: 2px 9px; font-size: 11px; }

/* Two lines per segment, for switching between whole screens rather than
   filtering one list. The second line is what stops "Booking page" and
   "Customer app" reading as two names for the same thing. */
.segmented--wide { display: flex; width: 100%; }
.segmented--wide button {
  flex: 1;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: 7px 11px;
  font-size: 13px;
  font-weight: 600;
  white-space: normal;
}
.segmented__sub {
  font-size: 11px;
  font-weight: 400;
  color: var(--label-secondary);
  line-height: 1.3;
}

.segmented button:disabled {
  color: var(--label-quaternary);
  cursor: default;
}
.segmented button:disabled[aria-pressed="true"] { background: transparent; box-shadow: none; }

/* ---------- Price list ---------- */

.price {
  padding: 10px 0;
  border-bottom: 0.5px solid var(--separator);
}
.price:last-child { border-bottom: 0; padding-bottom: 0; }
.price:first-child { padding-top: 0; }
.price__name { margin-bottom: 6px; }
.price__money { gap: 6px; flex-wrap: wrap; }
.price__note { margin: 6px 0 0; }

/* ---------- Products and orders ----------

   A row that is mostly a button — pressing the name opens it — with live
   controls parked on the right. Deliberately not a .list-row: that whole row
   is one button, and a stepper nested inside a button is a thing you cannot
   click straight. */

.product {
  /* Two buttons, the field between them, and the gaps: 36 + 4 + 58 + 4 + 36.
     Named because the bundle count that replaces the stepper has to match
     it exactly, and because the mobile rule below grows both at once. */
  --stepper-w: 138px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 0.5px solid var(--separator);
}
.product:last-child { border-bottom: 0; }

/* The picture, or the space one goes in. A fixed square either way: a list
   where only some rows carry a thumbnail sets the names at two different
   left edges and stops reading as a list. */
.product__thumb {
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  overflow: hidden;
  background: var(--bg-inset);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.product__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.product__thumb--empty { border: 0.5px dashed var(--separator); }
.product__thumb--empty .icon { color: var(--label-tertiary); }
/* In the sheet it is a preview beside the buttons, not a target of its own. */
.product__thumb--sheet { width: 46px; height: 46px; }

/* Text, not a target: the pencil is the only way into the sheet, so this
   carries no hover and no pointer — an area that lights up under the mouse
   and then does nothing when clicked is worse than one that never did. */
.product__open {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
  color: inherit;
}

/* The shop's own reference for the thing. Tabular figures and a touch of
   tracking because it is read a character at a time — off a label, down a
   phone — rather than as a word. */
.product__code {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--label-tertiary);
}
.product__name { font-size: 13px; font-weight: 500; }
/* One line, clipped. A description that wraps to three turns a stock list
   into a page you scroll to count. */
.product__sub {
  font-size: 11px;
  color: var(--label-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.product__controls { gap: 8px; flex: none; }

/* A bundle's count, where a stepper would be on an ordinary product.
   Both take their width from the same variable so the numbers sit in one
   column down the page: sized to the field alone, the count drifted right
   by half a button and every row below it looked misaligned. */
.stepper__derived {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--stepper-w);
  font-size: 13px;
  color: var(--label-secondary);
}

/* The offers a product is in, inside the edit sheet. */
.offers {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
}
.offers__row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 6px;
  margin: 0 -6px;
  border-radius: 7px;
  cursor: pointer;
}
.offers__row:hover { background: var(--bg-hover); }
.offers__name { flex: 1; min-width: 0; font-size: 13px; }

/* The managed category list, inside its own sheet. Same row shape as .offers
   above — a name that takes the width and controls that keep theirs — with
   the name and its count stacked, because "2 products · not in your list yet"
   beside a 40-character category is a line that wraps into the buttons. */
.cats { display: flex; flex-direction: column; gap: 2px; margin: 12px 0 0; padding: 0; list-style: none; }
.cats__row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px;
  margin: 0 -6px;
  border-radius: 7px;
}
.cats__row:hover { background: var(--bg-hover); }
.cats__name { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.cats__name strong { font-size: 13px; font-weight: 500; }
.cats__new { display: flex; align-items: center; gap: 8px; }
.cats__new .field { flex: 1; min-width: 0; }

.stepper {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: var(--stepper-w);
}

/* Bigger than a plain .icon-btn, everywhere.
   These two are pressed over and over — a delivery is counted one press at a
   time, usually with a box in the other hand — and a 28 × 26 target is a size
   for a button you use once. The narrow layout below has always made them
   thumb-sized for exactly that reason; this is the same argument applied to
   the window on a desk, where the pointer is doing the same repeated work.

   The field grows to match — see its own rule below — so the three read as
   one control rather than a short box wedged between two tall buttons.
   Change any of them and `--stepper-w` above needs the new sum. */
.product__controls .icon-btn { width: 36px; height: 32px; }

/* Two classes, and it has to be: the element carries `.field` too, and that
   rule sets width:100% further down the file — same specificity, later, so
   it won. The count field has been stretching to fill whatever room the row
   had ever since, which on a phone was a 160px box around a two-digit
   number. */
.stepper .stepper__field {
  width: 58px;
  height: 32px;      /* the buttons either side, so the three are one control */
  flex: none;
  text-align: center;
  padding-left: 4px;
  padding-right: 4px;
}
/* The spinners double up with the two buttons either side, and they make the
   field narrower than the number it holds. */
.stepper__field::-webkit-outer-spin-button,
.stepper__field::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.stepper__field { -moz-appearance: textfield; appearance: textfield; }

@media (max-width: 600px) {
  /* The controls drop under the name rather than squeezing it to nothing.
     The name stops short of the full width so the thumbnail keeps its place
     beside it — at a plain 100% the picture is bumped onto a line of its
     own, which reads as a row that belongs to nothing.

     More room than the desktop row, not less: two lines of content per
     product means the only thing separating one from the next is space,
     where on a wide screen the row itself is the separation. */
  .product {
    /* The bigger touch targets below make the stepper wider: 42 + 4 + 58 +
       4 + 42. The bundle count follows it through the same variable. */
    --stepper-w: 150px;
    flex-wrap: wrap;
    padding: 16px 0;
    row-gap: 12px;
  }
  .product__thumb { width: 48px; height: 48px; }
  .product__open { flex-basis: calc(100% - 60px); }
  .product__controls { flex-basis: 100%; justify-content: flex-end; }
  /* Thumb-sized targets, since this is the row somebody counts a delivery
     on while holding the box — and a real thumb is wider than a pointer, so
     bigger again than the desktop pair. */
  .product__controls .icon-btn { width: 42px; height: 38px; }
  .stepper .stepper__field { height: 38px; }
}

/* Something to paste elsewhere. Scrolls sideways in its own box rather than
   wrapping: a wrapped snippet is one somebody copies wrong. */
.code-block {
  margin: 0;
  padding: 11px 12px;
  border-radius: 8px;
  background: var(--bg-inset);
  border: 0.5px solid var(--separator);
  overflow-x: auto;
  font-size: 11.5px;
  line-height: 1.55;
  white-space: pre;
  color: var(--label-secondary);
}
.code-block code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* ---------- Services (what they can book) ---------- */

/* Two lines: the name and how it's offered, then the sentence that sits under
   it on the booking page. Edited where it sits, like the rate card — a sheet
   per service is three clicks to fix a word a customer is reading now. */
.service {
  padding: 11px 0;
  border-bottom: 0.5px solid var(--separator);
}
.service:first-child { padding-top: 0; }
.service:last-child { border-bottom: 0; padding-bottom: 0; }

.service__top { gap: 8px; }
.service__bottom { gap: 4px; margin-top: 6px; padding-left: 25px; }

.service__icon { display: grid; place-items: center; width: 17px; flex: none; }
.service__name { flex: 1; min-width: 0; font-weight: 500; }
.service__blurb { flex: 1; min-width: 0; font-size: 12px; }
.service__public { flex: none; margin: 0; }

/* The move buttons are one glyph rotated, so there's one icon to maintain
   rather than a matched pair that can drift apart. */
[data-move-service][data-dir="up"] .icon { transform: rotate(-90deg); }
[data-move-service][data-dir="down"] .icon { transform: rotate(90deg); }
[data-move-service]:disabled { opacity: 0.3; cursor: default; }

/* ---------- Menus ---------- */

/* Positioned by script, so it starts wherever and is moved once it has a size
   to measure against the edges of the window. */
.menu {
  position: fixed;
  z-index: 200;
  min-width: 190px;
  padding: 4px;
  border-radius: 8px;
  background: var(--bg-card);
  border: 0.5px solid var(--separator);
  box-shadow:
    0 8px 28px rgba(0, 0, 0, 0.20),
    0 2px 6px rgba(0, 0, 0, 0.12);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .menu {
    box-shadow: 0 10px 34px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.4);
  }
}
:root[data-theme="dark"] .menu {
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.4);
}

.menu__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 9px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--label);
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
}
.menu__item:hover,
.menu__item:focus-visible {
  background: var(--accent);
  color: #fff;
  outline: none;
}
.menu__item .icon { color: var(--label-tertiary); flex: none; }
.menu__item:hover .icon,
.menu__item:focus-visible .icon { color: #fff; }

/* Keeps the labels in a column when only some items carry a glyph. */
.menu__gap { width: 13px; flex: none; }

.menu__item--danger { color: var(--red); }
.menu__item--danger .icon { color: var(--red); }

.menu__rule {
  height: 0.5px;
  margin: 4px 6px;
  background: var(--separator);
}

/* ---------- Setup ---------- */

.setup__head { padding: 4px 2px 0; }

.setup__trades {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 8px;
}

.setup-trade {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "icon label" "icon eg";
  gap: 1px 11px;
  align-items: center;
  padding: 12px 13px;
  border: 1px solid var(--separator);
  border-radius: 11px;
  background: transparent;
  text-align: left;
  font: inherit;
  color: var(--label);
  cursor: pointer;
  transition: border-color 0.12s var(--ease), background 0.12s var(--ease);
}
.setup-trade:hover { background: var(--bg-inset); }

/* Selection is a border and a tint together, never colour alone — the trade's
   own glyph keeps its hue, so it can't double as the state. */
.setup-trade--on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.setup-trade__icon { grid-area: icon; display: grid; place-items: center; }
.setup-trade__label { grid-area: label; font-weight: 600; font-size: 13.5px; }
.setup-trade__eg {
  grid-area: eg;
  font-size: 11.5px;
  color: var(--label-secondary);
  line-height: 1.35;
}

/* What a trade would put on the booking page, as a grid of glyphs rather than
   a stack of rows.

   It's the same question a home screen answers — what is there, at a glance —
   and the answer is short enough to be shaped rather than read. As rows it ran
   most of a screen tall for four words of content. Each service passes its own
   hue in as --tile. */
.setup-services {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 2px;
}

.setup-service {
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 7px;
  padding: 11px 5px 10px;
  border-radius: 12px;
  text-align: center;
}

.setup-service__icon {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 11px;
  background: color-mix(in srgb, var(--tile, var(--accent)) 15%, transparent);
  box-shadow: inset 0 0 0 0.5px
    color-mix(in srgb, var(--tile, var(--accent)) 26%, transparent);
  /* Mixed toward the label, not used neat — same recipe and same reason as a
     card's own glyph: a coloured mark on a tint of its own hue has nothing to
     push against, and the pale end of the palette washes out. */
  color: color-mix(in srgb, var(--tile, var(--accent)) 72%, var(--label));
}

.setup-service__label {
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
}

/* ---------- Albums ---------- */

.gal-thumb {
  width: 34px; height: 34px;
  border-radius: 6px;
  overflow: hidden;
  flex: none;
  display: grid;
  place-items: center;
  background: var(--bg-inset);
  color: var(--label-tertiary);
}
.gal-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Fixed-height rows rather than a masonry. The admin grid is for *finding* a
   photograph and changing it, and a ragged edge makes that harder — the
   visitor's page is where the shapes get to be themselves. */
.gal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
}

.gal-tile {
  margin: 0;
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-inset);
  border: 0.5px solid var(--separator);
  position: relative;
  cursor: grab;
}
.gal-tile:active { cursor: grabbing; }
.gal-tile--dragging { opacity: 0.4; }

.gal-tile__view {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  aspect-ratio: 4 / 3;
}
.gal-tile__view img { width: 100%; height: 100%; object-fit: cover; display: block; }

.gal-tile__cover {
  position: absolute;
  top: 6px; left: 6px;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 6px;
  border-radius: 5px;
  font-size: 10px;
  font-weight: 600;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
}

.gal-tile__bar {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 5px 5px 5px 7px;
}

.gal-tile__caption {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 11px;
  color: var(--label);
  padding: 2px 0;
}
.gal-tile__caption::placeholder { color: var(--label-quaternary); }
.gal-tile__caption:focus { outline: none; color: var(--label); }

/* The lightbox inside a sheet. Capped by viewport height so a tall portrait
   doesn't push the sheet's own buttons off the bottom of the screen. */
.gal-viewer { text-align: center; }
.gal-viewer img {
  max-width: 100%;
  max-height: 62vh;
  border-radius: 8px;
  display: block;
  margin: 0 auto;
}
.gal-viewer .caption { margin: 10px 0 0; }

/* ---------- Place pane ---------- */

.place__stage {
  height: 260px;
  background: var(--bg-inset);
  display: grid;
  place-items: center;
}
.place__canvas { width: 100%; height: 100%; }

/* Google's own controls sit inside the canvas, so the corners have to be
   clipped by the card rather than the map. */
.place .card__body--flush { overflow: hidden; }

/* The same clipping for the pane as a band, which has no flush card body to
   do it — the frame is the edge instead. */
.place__frame {
  border-radius: 8px;
  overflow: hidden;
  border: 0.5px solid var(--separator);
}
.place__where { margin: 8px 0 0; }

.place__note {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 20px 28px;
  text-align: center;
  max-width: 34em;
  color: var(--label-secondary);
  font-size: 12px;
  line-height: 1.5;
}
.place__note .icon { color: var(--label-tertiary); }
.place__note p { margin: 0; }
.place__note code {
  font-size: 11px;
  padding: 1px 4px;
  border-radius: 4px;
  background: var(--bg-card);
}
.place__note .btn { text-decoration: none; }

.field, .select, textarea.field {
  width: 100%;
  height: 26px;
  padding: 0 8px;
  border-radius: var(--r-control);
  border: 0.5px solid var(--separator-opaque);
  background: var(--bg-card);
  font-size: 13px;
}
textarea.field { height: auto; padding: 6px 8px; resize: vertical; line-height: 1.45; }
.field:focus, .select:focus, textarea.field:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}
.select { appearance: none; padding-right: 22px;
  background-image: linear-gradient(transparent, transparent); }

.search {
  position: relative;
  display: flex;
  align-items: center;
}
.search .icon {
  position: absolute;
  left: 7px;
  color: var(--label-tertiary);
  pointer-events: none;
}
.search input {
  height: 26px;
  width: 100%;
  padding: 0 8px 0 26px;
  border-radius: 13px;
  border: 0.5px solid transparent;
  background: var(--bg-inset);
  font-size: 12.5px;
}
.search input:focus {
  outline: none;
  background: var(--bg-card);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ---------- A list's own search and filters ---------- */

/* A bar above the card, not an accessory inside its header. Products carries
   a search box, a category and a kind, and none of those fit beside a title,
   a count and a plus — on a narrow pane they would wrap the header into three
   lines, at which point it stops reading as a header at all. Out here they
   wrap into a second row of controls, which is what they are. */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

/* The box takes what's left, down to a width where a word is still readable;
   below that the controls drop underneath it rather than crushing it. */
.filters .search { flex: 1 1 190px; min-width: 0; }

/* Shaped like the search box beside it rather than like a field in a sheet:
   these narrow a list you are looking at, they are not questions being asked.
   `width: auto` because `.select` is full-width by default, which would put
   one category picker across the whole pane. */
.filters .select {
  width: auto;
  max-width: 14em;
  border-radius: 13px;
  border-color: transparent;
  background-color: var(--bg-inset);
  font-size: 12.5px;
}
.filters .select:focus { border-color: var(--accent); }

/* ---------- The search in the titlebar ---------- */

/* Shrinks before it disappears. `.titlebar__main` is a flex row whose right
   cluster is held there by margin-left:auto, so an item with no basis of its
   own gets squeezed to nothing by a view with a lot of buttons. A floor
   rather than a fixed width: on a phone there is very little room, and a
   short field you can still type into beats a field that isn't there. */
.search--global {
  flex: 0 1 210px;
  min-width: 96px;
}

/* Out here on its own because the titlebar is a stacking context — see
   index.html. Fixed, and positioned from the field it belongs to.

   90 sits above everything inside .window (the sidebar's phone sheet is the
   highest at 50) and deliberately below the sheet backdrop at 100. A modal
   is modal: a results list floating over one, still clickable, would be a
   way out of a dialog that the dialog doesn't know about. */
.gsearch {
  position: fixed;
  z-index: 90;
  max-height: min(60vh, 520px);
  overflow-y: auto;
  padding: 5px 0;
  background: var(--bg-card);
  border: 0.5px solid var(--separator);
  border-radius: 12px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.16), 0 2px 8px rgba(0, 0, 0, 0.10);
}
.gsearch[hidden] { display: none; }

.gsearch__group + .gsearch__group { border-top: 0.5px solid var(--separator); }

/* Says which kind of thing these are, and — when there were more than fit —
   how many were left out. A list that silently stops at six looks like the
   whole answer. */
.gsearch__heading {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 7px 12px 3px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--label-secondary);
}
.gsearch__more {
  margin-left: auto;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--label-tertiary);
}

.gsearch__hit {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 12px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.gsearch__hit > .icon { flex: none; color: var(--label-tertiary); }

/* One class for both, so the keyboard and the pointer light the same row
   the same way — two highlights that disagree is worse than either. */
.gsearch__hit:hover,
.gsearch__hit--active { background: color-mix(in srgb, var(--accent) 12%, transparent); }

.gsearch__text { min-width: 0; }
.gsearch__title,
.gsearch__sub {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gsearch__title { font-size: 13px; }
.gsearch__sub { font-size: 11px; color: var(--label-secondary); }

.gsearch__nothing {
  margin: 0;
  padding: 12px;
  font-size: 12.5px;
  color: var(--label-secondary);
}

.switch {
  appearance: none;
  width: 38px; height: 22px;
  border-radius: 11px;
  background: var(--bg-inset-strong);
  position: relative;
  cursor: pointer;
  flex: none;
  transition: background 0.2s var(--ease);
}
.switch::after {
  content: "";
  position: absolute;
  top: 2px; left: 2px;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  transition: transform 0.2s var(--ease);
}
.switch:checked { background: var(--green); }
.switch:checked::after { transform: translateX(16px); }

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

.card {
  background: var(--bg-card);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}
/* No fill. The glyph carries the colour on its own, and a page of cards is
   quieter for it — six tinted bars down a detail pane read as six things
   wanting attention, when the tint was only ever saying what the title says
   in words. The hairline is what separates the header from the body now. */
.card__header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  border-bottom: 0.5px solid var(--separator);
}
/* Only the card's own glyph. Accessory buttons in the header keep the accent,
   because they're links and links are blue everywhere else in the app.

   Used neat now the bar has gone. The old recipe mixed it toward the label to
   claw back contrast against a tint of its own hue — on the card's own
   background there's nothing to claw back, and the same mix only greyed the
   colour off at the moment it became the one thing carrying it. */
.card__header > .icon,
/* Named rather than loosened to a descendant selector. A collapsible card
   moves its glyph inside the disclosure button — but an accessory's glyph
   sits inside a button too, and that one is meant to stay blue. */
.card__disclosure > .icon {
  color: var(--card-colour, var(--accent));
}
.card__title {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.005em;
}
.card__body { padding: 12px 14px; }
.card__body--flush { padding: 0; }
.card__footer {
  padding: 8px 14px;
  border-top: 0.5px solid var(--separator);
  font-size: 11px;
  color: var(--label-secondary);
}

.card--tappable { cursor: pointer; }

/* ---------- A card that opens and shuts ---------- */

/* The header is the handle, so the title and its glyph are the button. It has
   to be stripped back to looking like the text it replaced — a card that grew
   a grey rounded rectangle in its corner the day it became collapsible would
   be announcing a mechanism nobody asked about. */
.card__disclosure {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* Always there, open or shut. The sidebar can afford to hide its chevron
   until you hover, because a sidebar is a short list you learn once; a card
   header that folds and gives no sign of it is a thing you have to be told
   about, and nobody reads a header hoping it's a button.

   Tertiary rather than the sidebar's quaternary. At 0.20 it's a smudge you
   have to go looking for, which is the wrong end of the trade for a mark
   whose whole job is to be noticed without being asked. */
.card__chevron {
  display: grid;
  place-items: center;
  color: var(--label-tertiary);
  transition: transform 0.2s var(--ease), color 0.15s ease;
}
/* The pointer is told the whole bar is live, because it is. Without this the
   chevron appears on hover over a header that then looks inert everywhere
   except the title, which is a worse offer than no chevron at all. */
.card--collapsible > .card__header { cursor: pointer; }
/* …but not over an accessory, which does its own thing and keeps its own. */
.card--collapsible > .card__header button:not(.card__disclosure),
.card--collapsible > .card__header a { cursor: revert; }

/* Hover firms it up rather than summoning it. Read from the header rather
   than the whole card: a chevron that darkens because the pointer is four
   hundred pixels below it, somewhere in a list of invoices, is answering
   something the pointer isn't asking. */
.card--collapsible > .card__header:hover .card__chevron,
.card__disclosure:focus-visible .card__chevron { color: var(--label-secondary); }
.card--collapsed .card__chevron { transform: rotate(-90deg); }

/* The outer box clips and the inner one is measured — see `animateHeight`. */
.card__collapse > div { overflow: hidden; min-height: 0; }
.card--collapsed .card__collapse > div { height: 0; }

/* A shut card is a header on its own, and the header's hairline would be a
   line ruled under nothing. */
.card--collapsed .card__header { border-bottom: 0; }

/* A section inside a card, for when several things are one thing. The
   negative margin runs the hairline out to the card's own edges — a divider
   that stops short reads as a mistake rather than a separation. */
.card-band {
  margin: 14px -14px 0;
  padding: 14px 14px 0;
  border-top: 0.5px solid var(--separator);
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 16px;
  align-items: start;
}

/* ---------- Today widgets ---------- */

/* Packed, not ruled. The same reasoning as the detail panes: a grid row is
   as tall as its tallest widget, so a five-row Reminders card beside the
   month calendar leaves a hole under it, and Today — the one page that's
   meant to be taken in at a glance — reads as half-empty.

   `columns: 340px` rather than a count, so the number of columns still comes
   from the width the way `auto-fill` did. The cost is that the order runs
   down a column and then to the next, instead of along a row: the widget you
   put second now sits under the first rather than beside it. That's inherent
   to packing, it's what the detail panes already do, and the order you
   dragged them into is still the order you read them in. */
.widgets {
  columns: 340px;
  column-gap: 16px;
}
/* No row-gap in multi-column, so the spacing is a margin. `break-inside` is
   what stops a widget being sawn in half at the foot of a column — which
   would also saw its remove button off. */
.widgets > .widget {
  break-inside: avoid;
  margin-bottom: 16px;
}

/* Lifted further off the page than a plain card: these are objects you can
   pick up and move, so they should look like it even when you can't. */
.card--widget {
  border-radius: 14px;
  box-shadow:
    0 4px 14px rgba(0, 0, 0, 0.07),
    0 1px 3px rgba(0, 0, 0, 0.06),
    0 0 0 0.5px rgba(0, 0, 0, 0.05);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .card--widget {
    box-shadow:
      0 6px 18px rgba(0, 0, 0, 0.42),
      0 1px 3px rgba(0, 0, 0, 0.3),
      0 0 0 0.5px rgba(255, 255, 255, 0.07);
  }
}
:root[data-theme="dark"] .card--widget {
  box-shadow:
    0 6px 18px rgba(0, 0, 0, 0.42),
    0 1px 3px rgba(0, 0, 0, 0.3),
    0 0 0 0.5px rgba(255, 255, 255, 0.07);
}

.widget { position: relative; }

.widget--editing { cursor: grab; }
.widget--editing:active { cursor: grabbing; }
.widget--editing .card--widget {
  outline: 1.5px dashed color-mix(in srgb, var(--accent) 45%, transparent);
  outline-offset: 3px;
  transition: transform 0.15s var(--ease), box-shadow 0.15s var(--ease);
}
.widget--editing:hover .card--widget {
  transform: translateY(-2px);
  box-shadow: var(--shadow-raised);
}

/* In edit mode the card's own controls would compete with dragging it. */
.widget--editing .card * { pointer-events: none; }
.widget--editing .widget__remove { pointer-events: auto; }

.widget--dragging { opacity: 0.4; }
.widget--dragging .card--widget { transform: scale(0.98); }

.widget__remove {
  position: absolute;
  top: -7px;
  left: -7px;
  z-index: 4;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--red);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
.widget__remove:hover { filter: brightness(1.1); }

/* ---------- Mini calendar (Today widget) ---------- */

.mini-cal {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.mini-cal__dow {
  text-align: center;
  font-size: 10px;
  font-weight: 600;
  color: var(--label-tertiary);
  padding-bottom: 4px;
}
.mini-cal__day {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--label);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  /* Square-ish, because the tint is the point: a day is a block of colour to
     be read at a glance, not a line of text with a background. */
  min-height: 30px;
}
.mini-cal__day:hover { background: var(--bg-hover); }
.mini-cal__day--outside { color: var(--label-quaternary); }

/* How full the day is. Both the colour and the strength of the tint are set
   per day, so one rule covers the whole scale and the key below the grid can
   reuse it.

   The scale ramps in density as well as hue, and that isn't decoration: in
   light mode the palette's yellow is a dark mustard (it has to be legible as
   text), which at equal opacity sits far too close to the orange to tell
   apart at 30px. Getting denser as it gets hotter separates the three
   whatever the hues are doing, and reads as "more" on its own.

   A tint rather than a solid fill, so the date on top keeps its contrast. */
.mini-cal__day--busy {
  background: color-mix(in srgb, var(--load) var(--load-mix), transparent);
}
.mini-cal__day--busy:hover {
  background: color-mix(in srgb, var(--load) var(--load-mix-hover), transparent);
}

/* Today marks the numeral rather than filling the cell — the cell background
   belongs to the heat scale now, and red there would mean two things at once.
   Matches how the full Calendar marks today. */
.mini-cal__day--today > span {
  min-width: 19px;
  height: 19px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--red);
  color: #fff;
  font-weight: 600;
}

.mini-cal__key {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  margin-top: 9px;
  font-size: 10px;
  color: var(--label-tertiary);
}
.mini-cal__key i {
  width: 17px;
  height: 10px;
  border-radius: 3px;
  /* The same mix the days use, so the key is a sample rather than an
     approximation of one. */
  background: color-mix(in srgb, var(--load) var(--load-mix), transparent);
}

/* ---------- Rows ---------- */

.rows { display: flex; flex-direction: column; }

.list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 14px;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
  border-top: 0.5px solid var(--separator);
  transition: background 0.1s ease;
  color: inherit;
}
.rows > .list-row:first-child { border-top: 0; }
.list-row:hover { background: var(--bg-hover); }

/* A row that holds a second line of controls rather than being one button.
   It isn't clickable itself — the things inside it are — so it drops the
   pointer and the hover, which would otherwise promise something the row
   doesn't do. */
.list-row--stack {
  display: block;
  cursor: default;
}
.list-row--stack:hover { background: transparent; }
.list-row[aria-current="true"] { background: color-mix(in srgb, var(--accent) 12%, transparent); }
/* These are spans so the whole row can be a <button>; without stacking them
   the title and subtitle run together on one line. */
.list-row__main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.list-row__title {
  display: block;
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-row__sub {
  display: block;
  font-size: 11px;
  color: var(--label-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-row__trailing {
  text-align: right;
  flex: none;
  font-size: 12px;
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}
/* A <br> inside a flex column collapses; the trailing slot uses one instead
   of nested elements, so give it something to break on. */
.list-row__trailing br { display: none; }
.list-row .chevron { color: var(--label-quaternary); flex: none; }

.time-chip {
  flex: none;
  width: 52px;
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--label-secondary);
}

/* Pinned to the foot of a list column and floating over the rows as they
   scroll past. Sticky rather than fixed so it stays inside its own pane. */
.list-total {
  position: sticky;
  bottom: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 14px;
  font-size: 11.5px;
  color: var(--label-secondary);
  background: var(--bg-titlebar);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-top: 0.5px solid var(--separator);
  box-shadow: 0 -1px 8px rgba(0, 0, 0, 0.05);
}
/* The label and count stack so a narrow column doesn't squeeze the figure. */
.list-total__meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.list-total__label {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 10px;
  color: var(--label-tertiary);
}
.list-total__count {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-total__value {
  flex: none;
  font-size: 16px;
  font-weight: 600;
  color: var(--label);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

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

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 18px;
  padding: 0 7px;
  border-radius: 9px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  background: color-mix(in srgb, var(--badge-color, var(--gray)) 16%, transparent);
  /* 72/28 is the weakest mix that clears 4.5:1 for every colour in the
     palette, in both appearances — solved for, not guessed. The tint keeps
     the hue; the text goes as dark as the background demands, and inverts
     for free in dark mode because --label does. */
  color: color-mix(in srgb, var(--badge-color, var(--gray)) 72%, var(--label));
}
.badge--solid {
  background: var(--badge-color, var(--gray));
  color: #fff;
}
.badge--dot::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.avatar {
  width: 30px; height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 11.5px;
  font-weight: 600;
  flex: none;
  letter-spacing: 0.01em;
  background: var(--gray);
  color: var(--on-gray);
}

/* Each fill carries whichever text colour actually reads on it. Derived and
   checked by Tools/build-palette.mjs — white for the darker colours, a deep
   version of the same hue for the lighter ones. */
.avatar--blue   { background: var(--blue);   color: var(--on-blue); }
.avatar--indigo { background: var(--indigo); color: var(--on-indigo); }
.avatar--purple { background: var(--purple); color: var(--on-purple); }
.avatar--pink   { background: var(--pink);   color: var(--on-pink); }
.avatar--orange { background: var(--orange); color: var(--on-orange); }
.avatar--green  { background: var(--green);  color: var(--on-green); }
.avatar--teal   { background: var(--teal);   color: var(--on-teal); }
.avatar--brown  { background: var(--brown);  color: var(--on-brown); }
.avatar--lg { width: 44px; height: 44px; font-size: 15px; }
.avatar--sm { width: 24px; height: 24px; font-size: 10px; }

/* ---------- Tags ---------- */

.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 24px;
  padding: 0 6px 0 10px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: color-mix(in srgb, var(--accent) 72%, var(--label));
  white-space: nowrap;
}
/* The label is the way through to everybody who has this tag, now that the
   sidebar no longer lists them. Styled to disappear into the chip: it should
   read as the chip being clickable, not as a button inside a button. */
.tag-chip__open {
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.tag-chip:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }

.tag-chip__x {
  display: grid;
  place-items: center;
  width: 16px; height: 16px;
  border: 0;
  border-radius: 50%;
  padding: 0;
  background: transparent;
  color: inherit;
  opacity: 0.55;
  cursor: pointer;
}
.tag-chip__x:hover { opacity: 1; background: color-mix(in srgb, var(--accent) 22%, transparent); }

/* Tags already in use elsewhere, offered so spellings stay consistent. */
.tag-chip--suggest {
  padding: 0 10px 0 7px;
  border: 0.5px dashed var(--separator-opaque);
  background: transparent;
  color: var(--label-secondary);
  cursor: pointer;
}
.tag-chip--suggest:hover {
  border-style: solid;
  border-color: var(--accent);
  color: var(--accent);
}

/* Group membership, shown on the client. Filing is done in the sidebar, so
   this only states where they are and offers a way out. */
.group-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 24px;
  padding: 0 6px 0 9px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
  background: color-mix(in srgb, var(--group-color) 16%, transparent);
  color: color-mix(in srgb, var(--group-color) 72%, var(--label));
}

/* ---------- Dragging a client into a group ---------- */

.list-row[draggable="true"] { cursor: grab; }
.list-row[draggable="true"]:active { cursor: grabbing; }

/* While a name is in flight, every group row shows it will take it. */
.is-dragging-client .side-row--drop-target {
  outline: 1.5px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset: -1.5px;
}
.side-row--drop-over {
  background: var(--accent) !important;
  color: var(--on-accent) !important;
  outline-color: transparent !important;
}
.side-row--drop-over .icon,
.side-row--drop-over .side-row__count { color: var(--on-accent) !important; }

/* Remove-from-group, only shown inside a group. */
.row-remove {
  display: grid;
  place-items: center;
  width: 20px; height: 20px;
  border-radius: 50%;
  color: var(--label-tertiary);
  cursor: pointer;
}
.row-remove:hover {
  background: color-mix(in srgb, var(--red) 16%, transparent);
  color: var(--red);
}

/* ---------- Picture of the day ---------- */

.potd {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: var(--bg-inset);
  cursor: zoom-in;
  line-height: 0;
  /* A fixed frame so one tall photo can't stretch the card past its
     neighbours in the grid. The image fills it and crops. */
  aspect-ratio: 4 / 3;
  overflow: hidden;
}
.potd img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Phone photos carry their rotation in EXIF; honour it rather than
     showing them on their side. This is the browser default, said out loud
     because the whole widget depends on it. */
  image-orientation: from-image;
  transition: transform 0.4s var(--ease);
}
.potd:hover img { transform: scale(1.02); }

/* ---------- Empty states ---------- */

.empty {
  display: grid;
  place-items: center;
  gap: 6px;
  padding: 48px 20px;
  text-align: center;
  color: var(--label-secondary);
}
.empty .icon { color: var(--label-quaternary); }
.empty h3 { font-size: 15px; font-weight: 600; color: var(--label); }
.empty p { font-size: 12px; max-width: 34ch; }

/* ---------- Weather ---------- */

/* The greeting and the forecast share a line; the chip drops underneath when
   there isn't room for both. */
.today-header {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

.weather-chip {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 15px 8px 11px;
  border: 0;
  border-radius: 15px;
  color: #fff;
  background: linear-gradient(135deg, var(--sky-top, #3d8bd8), var(--sky-bottom, #1c5aa8));
  box-shadow: var(--shadow-card);
  cursor: pointer;
  text-align: left;
  transition: filter 0.15s ease, transform 0.15s var(--ease);
}
.weather-chip:hover { filter: brightness(1.07); }
.weather-chip:active { transform: scale(0.98); }
.weather-chip__temp {
  font-size: 25px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.weather-chip__meta { display: flex; flex-direction: column; gap: 1px; }
.weather-chip__condition { font-size: 12px; font-weight: 600; line-height: 1.25; }
.weather-chip__place { font-size: 10.5px; opacity: 0.8; line-height: 1.25; }


.weather {
  border-radius: var(--r-panel);
  padding: 16px 18px;
  color: #fff;
  background: linear-gradient(135deg, var(--sky-top, #3d8bd8), var(--sky-bottom, #1c5aa8));
  box-shadow: var(--shadow-card);
  position: relative;
  overflow: hidden;
}
.weather::after {
  /* A soft highlight, the way Apple's weather cards catch light. */
  content: "";
  position: absolute;
  top: -60px; right: -30px;
  width: 220px; height: 220px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.20), transparent 70%);
  pointer-events: none;
}
.weather__top {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
}
.weather__temp {
  font-size: 46px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.weather__label { font-size: 11px; opacity: 0.78; text-transform: uppercase; letter-spacing: 0.04em; }
.weather__condition { font-size: 14px; font-weight: 600; }
.weather__place { font-size: 12px; opacity: 0.78; }
.weather__metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 10px 16px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.18);
}
.weather__metric-label { font-size: 10px; opacity: 0.72; text-transform: uppercase; letter-spacing: 0.04em; }
.weather__metric-value { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.weather__caveat {
  margin-top: 12px;
  font-size: 11px;
  opacity: 0.78;
  display: flex;
  gap: 6px;
  align-items: flex-start;
  line-height: 1.4;
}
.weather__glyph { flex: none; opacity: 0.95; }

/* ---------- Master / detail workspaces ---------- */

.workspace {
  display: grid;
  grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
  height: 100%;
  min-height: 0;
}
.workspace__list {
  border-right: 0.5px solid var(--separator);
  overflow-y: auto;
  background: var(--bg-content);
  min-height: 0;
}
.workspace__list-header {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 10px 12px;
  display: flex;
  gap: 8px;
  align-items: center;
  background: var(--bg-titlebar);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 0.5px solid var(--separator);
}
.workspace__detail {
  overflow-y: auto;
  min-height: 0;
  /* Measured against itself, not the window. The pane is whatever's left
     after the list column, so a viewport media query would be guessing —
     and guessing wrong every time the sidebar is hidden. */
  container-type: inline-size;
}

/* The same for a full-width view. Two containers, not one: a page inside a
   workspace measures the pane it's in, a page on its own measures the whole
   detail area, and the nearest one always wins. */
.detail { container-type: inline-size; }

/* A detail pane is tall and half-empty: six cards in a single column, 900px
   of width doing nothing, and nearly two screens of scrolling to read one
   client. Given the room, they go two abreast.

   Cards only. The heading, the row of stats and the weather panel are the
   page talking about itself and run the full width, which also breaks the
   grid into bands and keeps the reading order obvious.

   HOW THE BANDS ARE MADE, and why not the obvious way. This was `columns: 2`
   over the whole stack with `column-span: all` on the page's own furniture.
   Safari lays that out wrong: the content following a spanner is placed both
   after it AND again beside it, so a card that exists once in the DOM is
   painted twice on the screen — reported on the client screen, where Tags
   appeared in both columns at once.

   So there is no spanner any more. `groupIntoColumns` in js/app.js wraps each
   run of consecutive cards in a `.stack__cols` box, and a band is simply a
   sibling of that box rather than something punched through it. Balancing is
   still the browser's job; only the one buggy feature is gone. */

/* The narrow case, and the shape `.stack` would have given these cards had
   they not been wrapped: one column, 16px apart. */
.stack__cols { display: flex; flex-direction: column; gap: 16px; }

/* 820 on the container, which after the page's own 24px padding and the
   16px gutter leaves columns of 378px and up. Tried it lower to catch a
   1280 laptop's detail pane: at 680 the columns come out near 330 and an
   email address stops fitting in its own field, which is a poor trade for
   half a screen of scrolling. */
@container (min-width: 820px) {
  /* Multi-column rather than a grid, so the cards pack against each other.
     A grid puts them in rows, and a row is as tall as its tallest card — so
     a Notes box beside a job list leaves a hole under it and the page reads
     as half-empty, which is the thing this was meant to fix. Columns have no
     rows to line up with: each card starts where the one above it ended. */
  .detail__host .stack__cols {
    display: block;
    columns: 2;
    column-gap: 16px;
    /* The trailing card's own margin is what spaces cards inside the box.
       Left alone it would also count as space *after* the box, so a band
       following the columns would sit 32px down instead of 16. */
    margin-bottom: -16px;
  }
  /* No row-gap in multi-column, so the spacing is a margin. `break-inside`
     is what stops a card being sawn in half at the foot of a column. */
  .detail__host .stack__cols > * {
    break-inside: avoid;
    margin-bottom: 16px;
  }
}

.group-header {
  position: sticky;
  top: 46px;
  z-index: 1;
  padding: 5px 14px;
  font-size: 11px;
  font-weight: 600;
  color: var(--label-secondary);
  background: color-mix(in srgb, var(--bg-content) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 0.5px solid var(--separator);
}

/* ---------- Detail forms ---------- */

.form-grid {
  display: grid;
  grid-template-columns: 130px minmax(0, 1fr);
  gap: 9px 12px;
  align-items: center;
}
.form-grid > label {
  font-size: 12px;
  color: var(--label-secondary);
  text-align: right;
}
.form-grid > .full { grid-column: 1 / -1; }

/* A read-only line of a record: what it is on the left, what it says on the
   right. The native apps' `FieldRow`, and the reason a record that's mostly
   read shows these rather than a column of inputs — a field with nothing in
   it is left out altogether instead of sitting there as an empty box.

   Native separates the two by size as well as colour (.body against
   .subheadline). That's an Apple optical convention that doesn't carry to a
   browser, where the smaller of the two would just look like the content
   mattered less than its label, so the colour does the work here. */
.field-rows { display: flex; flex-direction: column; gap: 7px; }
.field-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  font-size: 13px;
}
.field-row__label { flex: none; color: var(--label-secondary); }
.field-row__value {
  margin-left: auto;
  text-align: right;
  /* An address is the one of these that arrives with newlines in it. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.kv { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 12px; }
.kv dt { color: var(--label-secondary); }
.kv dd { margin: 0; font-variant-numeric: tabular-nums; }

/* ---------- Calendar ---------- */

/* The month owns the pane, so the page is a column with the grid taking
   whatever the heading and the key leave. It stops being a document you
   scroll and becomes a thing you look at, which is the only reason to draw a
   month rather than a list. */
.cal-page {
  display: flex;
  flex-direction: column;
  gap: 12px;
  height: 100%;
  max-width: none;
  padding: 16px 20px 14px;
  box-sizing: border-box;
}

.cal {
  --cell-min: 92px;
  display: flex;
  flex-direction: column;
  border-top: 0.5px solid var(--separator);
  border-left: 0.5px solid var(--separator);
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  background: var(--bg-card);
}
.cal__dows,
.cal__week {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
/* Each week is its own row *and* its own positioning context, so the lane
   overlay above it can be measured against the week rather than against
   whatever happens to be positioned further up the page. */
.cal__week { position: relative; min-height: 0; }
.cal__dow {
  padding: 7px 8px;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--label-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  text-align: center;
  border-right: 0.5px solid var(--separator);
  border-bottom: 0.5px solid var(--separator);
  background: var(--bg-inset);
}
.cal__cell {
  min-height: var(--cell-min);
  padding: 5px 6px;
  border-right: 0.5px solid var(--separator);
  border-bottom: 0.5px solid var(--separator);
  display: flex;
  flex-direction: column;
  gap: 3px;
  background: transparent;
  text-align: left;
  cursor: default;
}
/* An outline rather than an inset shadow, because the bars are drawn over the
   cell now and an inset ring would be underneath them. */
.cal__cell--selected { outline: 2px solid var(--accent); outline-offset: -2px; }
.cal__cell--outside { background: var(--bg-inset); }
.cal__cell--outside .cal__num { color: var(--label-tertiary); }
.cal__num {
  font-size: 11.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  align-self: flex-start;
  width: 20px; height: 20px;
  display: grid; place-items: center;
  border-radius: 50%;
}
.cal__cell--today .cal__num { background: var(--red); color: #fff; font-weight: 600; }
.cal__dot { width: 6px; height: 6px; border-radius: 50%; flex: none; }

/* ---- The lanes ----
   A layer over the week's cells, on the same seven columns, so a job can be
   one bar across three days rather than three chips pretending to be
   unrelated. It starts below the date numbers and lets clicks through to the
   cells underneath except on the bars themselves. */
.cal__lanes {
  position: absolute;
  inset: 27px 0 3px 0;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  grid-auto-rows: 19px;
  row-gap: 2px;                        /* LANE_GAP in js/views/calendar.js */
  pointer-events: none;
}

.cal__bar {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  margin: 0 3px;
  padding: 0 6px;
  font-size: 11px;
  border-radius: 5px;
  background: var(--bg-inset);
  cursor: pointer;
  border: 0;
  text-align: left;
  color: inherit;
}
.cal__bar:hover { background: var(--bg-inset-strong); }
.cal__bar span {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Work that runs across days is a run, not a stack of separate bookings, so
   it reads as one object: a stronger fill and no gaps at the joins. */
.cal__bar--run { background: var(--bg-inset-strong); font-weight: 500; }

/* Squared off where it carries on past the edge of the week — a rounded end
   in the middle of a run says the job stopped on Sunday and a new one started
   on Monday. */
.cal__bar--from-before {
  margin-left: 0;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.cal__bar--to-after {
  margin-right: 0;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
/* A continuation has nothing to point at with a dot: the colour was stated
   where the run began. */
.cal__bar--from-before .cal__dot { display: none; }

.cal--fill { flex: 1; min-height: 0; }
.cal--fill .cal__week { flex: 1; }
.cal--fill .cal__cell { min-height: 0; overflow: hidden; }
.cal--fill .cal__num { font-size: 12.5px; }

/* What wouldn't fit. A count on its own would say a day is busier than the
   cell admits and leave you nowhere to go, so it's a button: it opens the
   day. */
.cal__more {
  pointer-events: auto;
  justify-self: start;
  align-self: center;
  margin: 0 3px;
  font-size: 10.5px;
  font-weight: 550;
  color: var(--label-secondary);
  background: none;
  border: 0;
  padding: 1px 5px 2px;
  border-radius: 5px;
  cursor: pointer;
}
.cal__more:hover { background: var(--bg-inset-strong); color: var(--label); }

/* ---------- Travel / map ---------- */

.map {
  position: relative;
  height: 300px;
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  background:
    linear-gradient(0deg, rgba(0,0,0,0.03), rgba(0,0,0,0.03)),
    repeating-linear-gradient(0deg, transparent 0 23px, color-mix(in srgb, var(--label) 6%, transparent) 23px 24px),
    repeating-linear-gradient(90deg, transparent 0 23px, color-mix(in srgb, var(--label) 6%, transparent) 23px 24px),
    color-mix(in srgb, var(--teal) 12%, var(--bg-card));
}
.map__note {
  position: absolute;
  left: 10px; bottom: 10px;
  font-size: 10.5px;
  padding: 3px 8px;
  border-radius: 10px;
  background: var(--bg-card);
  box-shadow: var(--shadow-card);
  color: var(--label-secondary);
}
.pin {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 24px; height: 24px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 700;
  color: #fff;
  background: var(--accent);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), 0 0 0 2px var(--bg-card);
}
.pin--problem { background: var(--red); }
.pin--start { background: var(--green); }

.leg {
  display: flex;
  gap: 10px;
  padding: 9px 14px;
  border-top: 0.5px solid var(--separator);
  align-items: center;
}
.leg__rule {
  width: 22px;
  display: grid;
  place-items: center;
  color: var(--label-tertiary);
}

/* ---------- Messages ---------- */

/* Three columns: what came in, what was said, and everything else.
   The third is one card, never a stack — a booking request used to arrive as
   four, and reading four headings to answer one question is what this column
   exists to stop. */
.messages {
  display: grid;
  grid-template-columns: minmax(250px, 320px) minmax(0, 1fr) 330px;
  height: 100%;
  min-height: 0;
}
/* The card scrolls; the answer under it doesn't. A long card mustn't be able
   to put Accept out of reach — it's the thing the card is there to inform. */
.messages__context {
  border-left: 0.5px solid var(--separator);
  background: var(--bg-page);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.messages__context-scroll {
  flex: 1;
  overflow-y: auto;
  min-height: 0;
  padding: 14px;
}
.context-answer {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 14px;
  border-top: 0.5px solid var(--separator);
  background: var(--bg-content);
}
.context-answer .btn { flex: 1 1 auto; justify-content: center; }
/* Accept gets the whole first line: it's the answer most requests get, and a
   third of a row is not how you say that. */
.context-answer .btn--primary { flex-basis: 100%; }
/* A label column sized for the page leaves nothing for the value here. 76px
   still holds "Outstanding" and gives an email address room to be read. */
.messages__context .form-grid { grid-template-columns: 76px minmax(0, 1fr); }
.messages__context .form-grid > .field { min-width: 0; }
.messages__context .card { margin: 0; }
.thread {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
}

.answer-clash {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--red);
}
.answer-clash .icon { flex: none; margin-top: 1px; }

.thread__header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  border-bottom: 0.5px solid var(--separator);
  background: var(--bg-titlebar);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}
.thread__scroll {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-height: 0;
}
.bubble-row { display: flex; }
.bubble-row--mine { justify-content: flex-end; }
.bubble {
  max-width: min(68%, 460px);
  padding: 7px 12px;
  border-radius: 17px;
  font-size: 13px;
  line-height: 1.42;
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--bg-inset);
  color: var(--label);
}
.bubble-row--mine .bubble { background: var(--accent); color: #fff; }
.bubble__meta {
  font-size: 10px;
  color: var(--label-tertiary);
  padding: 2px 8px 8px;
  display: flex;
  gap: 5px;
  align-items: center;
}
.bubble-row--mine + .bubble__meta { justify-content: flex-end; }
.bubble__subject { font-weight: 600; margin-bottom: 3px; opacity: 0.9; }

.pay-card {
  margin-top: 8px;
  border-radius: 12px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.28);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.bubble-row:not(.bubble-row--mine) .pay-card {
  background: var(--bg-card);
  border-color: var(--separator);
}
.pay-card__amount { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; }

.composer {
  border-top: 0.5px solid var(--separator);
  padding: 10px 14px;
  display: flex;
  gap: 8px;
  align-items: flex-end;
  background: var(--bg-content);
}
.composer textarea {
  flex: 1;
  min-height: 30px;
  max-height: 120px;
  padding: 6px 12px;
  border-radius: 15px;
  border: 0.5px solid var(--separator-opaque);
  background: var(--bg-card);
  resize: none;
  font-size: 13px;
  line-height: 1.4;
}
.composer textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.send-btn {
  width: 28px; height: 28px;
  border-radius: 50%;
  border: 0;
  background: var(--accent);
  color: #fff;
  display: grid; place-items: center;
  cursor: pointer;
  flex: none;
}
.send-btn[disabled] { background: var(--bg-inset-strong); color: var(--label-tertiary); cursor: default; }

.channel-dot {
  width: 18px; height: 18px;
  border-radius: 50%;
  display: grid; place-items: center;
  color: #fff;
  flex: none;
}

.banner {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 12px;
  border-radius: var(--r-field);
  font-size: 11.5px;
  background: color-mix(in srgb, var(--orange) 14%, transparent);
  color: color-mix(in srgb, var(--orange) 72%, var(--label));
  line-height: 1.42;
}
.banner .icon { flex: none; margin-top: 1px; }
.banner--info {
  background: color-mix(in srgb, var(--blue) 12%, transparent);
  color: color-mix(in srgb, var(--blue) 72%, var(--label));
}
.banner--good {
  background: color-mix(in srgb, var(--green) 14%, transparent);
  color: color-mix(in srgb, var(--green) 72%, var(--label));
}

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

.bars {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  height: 150px;
  padding-top: 8px;
}
.bars__col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; }
.bars__pair {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 3px;
  width: 100%;
  height: 100%;
}
.bars__bar {
  flex: 1;
  max-width: 18px;
  border-radius: 4px 4px 2px 2px;
  background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 62%, transparent));
  min-height: 3px;
  transition: height 0.4s var(--ease);
}
.bars__bar--out {
  background: linear-gradient(180deg, var(--orange), color-mix(in srgb, var(--orange) 58%, transparent));
}
.bars__key {
  display: flex;
  gap: 14px;
  align-items: center;
  font-size: 11px;
  color: var(--label-secondary);
  margin-top: 10px;
}
.bars__swatch {
  width: 9px; height: 9px;
  border-radius: 3px;
  display: inline-block;
  margin-right: 5px;
  vertical-align: -1px;
}
.bars__label { font-size: 10px; color: var(--label-secondary); }

.donut { display: grid; place-items: center; position: relative; }
.donut__center {
  position: absolute;
  text-align: center;
}

.meter {
  height: 6px;
  border-radius: 3px;
  background: var(--bg-inset);
  overflow: hidden;
}
.meter__fill { height: 100%; border-radius: 3px; background: var(--accent); }

/* ---------- Expenses: upload, receipts ---------- */

.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 26px 20px;
  border: 1.5px dashed var(--separator-opaque);
  border-radius: var(--r-card);
  background: transparent;
  color: var(--label-secondary);
  cursor: pointer;
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.dropzone:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.dropzone .icon { color: var(--accent); }
.dropzone__title { font-size: 13px; font-weight: 600; color: var(--label); }
.dropzone__hint { font-size: 11px; max-width: 42ch; line-height: 1.45; }

/* Shown while files are dragged over the page. Never takes pointer events —
   otherwise crossing it fires dragleave on the view underneath and the
   highlight flickers. */
.drop-overlay {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: grid;
  place-items: center;
  pointer-events: none;
  background: color-mix(in srgb, var(--accent) 12%, rgba(0, 0, 0, 0.18));
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: fade 0.14s var(--ease);
}
.drop-overlay[hidden] { display: none; }
.drop-overlay__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 30px 44px;
  border-radius: var(--r-panel);
  background: var(--bg-card);
  box-shadow: var(--shadow-raised);
  border: 2px dashed var(--accent);
  font-size: 14px;
  font-weight: 600;
}
.drop-overlay__card .icon { color: var(--accent); }

/* ---------- Attachments: a photo or a document on a record ---------- */

/* The open half of an attachment row. A <button> in its own right rather
   than the whole row, because the row also carries rename and remove — and a
   button inside a button is invalid markup the browser quietly rearranges. */
.attach-row__open {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
/* Disabled for the moment it takes to sign an address. */
.attach-row__open:disabled { opacity: 0.55; cursor: progress; }

.attach-row__kind {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  background: var(--bg-inset);
  color: var(--label-secondary);
}
.attach-row__kind--image { color: var(--accent); }

/* Said in the subtitle as well as in the share button's colour: whether the
   customer can open this is the one fact worth reading at a glance, and a
   tinted glyph is not reading. */
.attach-row__shared { color: var(--accent); font-weight: 600; }

/* Dragging files over the card that would catch them. The ring is on the
   card rather than the page because a client record is nine cards deep, and
   a page-wide target would have to guess which one you meant. */
.card.is-dropping {
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
  background: color-mix(in srgb, var(--accent) 7%, var(--bg-card));
}

.receipt {
  display: flex;
  gap: 14px;
  align-items: flex-start;
}
.receipt__thumb {
  flex: none;
  padding: 0;
  border: 0.5px solid var(--separator);
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-inset);
  cursor: zoom-in;
  line-height: 0;
}
.receipt__thumb img {
  display: block;
  width: 108px;
  height: 132px;
  object-fit: cover;
}
.receipt__file {
  flex: none;
  width: 108px;
  height: 132px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  background: var(--bg-inset);
  border: 0.5px solid var(--separator);
  color: var(--label-tertiary);
}

/* The month headers in the expense list carry a running total on the right. */
.group-header { display: flex; align-items: center; gap: 8px; }

/* ---------- Sheets ---------- */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(2px);
  display: grid;
  place-items: start center;
  padding: 8vh 16px 16px;
  z-index: 100;
  animation: fade 0.18s var(--ease);
}
.sheet {
  width: min(560px, 100%);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--bg-card);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-raised);
  animation: rise 0.24s var(--ease);
}
.sheet__header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 18px;
  border-bottom: 0.5px solid var(--separator);
}
.sheet__body { padding: 16px 18px; }
.sheet__footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 18px;
  border-top: 0.5px solid var(--separator);
}

@keyframes fade { from { opacity: 0; } }
@keyframes rise { from { opacity: 0; transform: translateY(10px) scale(0.98); } }

/* ---------- Toasts ---------- */

.toast-host {
  position: fixed;
  bottom: 22px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 200;
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 20px;
  background: var(--bg-card);
  box-shadow: var(--shadow-raised);
  font-size: 12.5px;
  animation: rise 0.24s var(--ease);
}
.toast .icon { color: var(--green); }

/* ---------- Responsive ---------- */

/* Too narrow for a third column, but not for the card. It goes under the
   conversation rather than behind a button: what's in it is the half of the
   decision the thread can't show you, and a decision aid you have to go and
   ask for is one nobody asks for. */
@media (max-width: 1240px) {
  .messages {
    grid-template-columns: minmax(250px, 320px) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) minmax(0, 42%);
  }
  .messages .workspace__list { grid-row: 1 / span 2; }
  .messages__context {
    border-left: 0;
    border-top: 0.5px solid var(--separator);
  }
}

/* Too narrow for two columns side by side. The list stacks above the detail
   rather than being hidden — hiding it leaves no way to pick anything else. */
@media (max-width: 900px) {
  .workspace, .messages {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(140px, 34vh) minmax(0, 1fr);
  }
  /* All three stack, and the card's row is capped rather than left to size
     itself: everything in this column scrolls internally, so an `auto` row
     takes the card's whole height and leaves the conversation none. */
  .messages {
    grid-template-rows: minmax(140px, 26vh) minmax(200px, 1fr) minmax(0, 32vh);
  }
  .messages .workspace__list { grid-row: auto; }
  .workspace__list {
    border-right: 0;
    border-bottom: 0.5px solid var(--separator);
  }
}

/* ---------- The phone ----------

   Below this the app stops pretending to be a Mac window and behaves like the
   iOS app: a tab bar at the foot, and one pane at a time with a back button,
   rather than a list and a record sharing a short screen.

   Everything above this line is untouched. The look is the same — same cards,
   same blur, same type — it is the navigation that changes, because a sidebar
   and a two-pane split are furniture for a screen that isn't there. */
@media (max-width: 760px) {
  :root { --sidebar-w: 236px; }
  /* A third row for the tab bar. `auto` rather than a height, so the safe-area
     inset under it belongs to the bar and not to a magic number here. */
  .window { grid-template-rows: 46px minmax(0, 1fr) auto; }

  /* ---- The tab bar ----
     Five seats, as on the phone: Today, which doesn't move because it's where
     the app opens, and four sections. Everything else is still in the sidebar,
     which is a sheet at this width and reached from the toolbar. */
  .tabbar {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    background: var(--bg-titlebar);
    backdrop-filter: saturate(180%) blur(30px);
    -webkit-backdrop-filter: saturate(180%) blur(30px);
    border-top: 0.5px solid var(--separator);
    /* The home indicator sits under the bar on a modern iPhone, so the bar is
       taller than its contents by whatever the device says it is. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }

  .tabbar__item {
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    padding: 7px 2px 5px;
    font: inherit;
    font-size: 10px;
    font-weight: 500;
    color: var(--label-secondary);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .tabbar__item[aria-current="page"] { color: var(--accent); }
  .tabbar__item:active { opacity: 0.55; }
  .tabbar__label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Only the Inbox has one, and only when it isn't zero — a badge showing
     nothing is a dot that means "look here" about nothing. */
  .tabbar__badge {
    position: absolute;
    top: 3px;
    left: 50%;
    margin-left: 4px;
    min-width: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: var(--red);
    color: #fff;
    font-size: 10px;
    font-weight: 600;
    line-height: 16px;
    text-align: center;
  }

  /* ---- One pane at a time ----
     A list and a record stacked in 380 points of height gives you a third of
     each. So the list is the screen until you pick something, then the record
     is — which is what the phone app does, and what the back button undoes.
     `data-pane` is set by the shell from whether the section has a selection. */
  .window[data-pane="list"] .workspace__detail,
  .window[data-pane="list"] .messages__thread,
  .window[data-pane="list"] .messages__context { display: none; }
  .window[data-pane="detail"] .workspace__list { display: none; }

  .window[data-pane] .workspace,
  .window[data-pane] .messages { grid-template-rows: minmax(0, 1fr); }
  .window[data-pane="detail"] .messages {
    grid-template-rows: minmax(0, 1fr) minmax(0, 34vh);
  }

  /* Shown only where there is somewhere to go back to. */
  .titlebar .titlebar__back { display: inline-flex; }
  .titlebar { grid-template-columns: auto minmax(0, 1fr); }
  .split { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    position: fixed;
    top: 46px; bottom: 0; left: 0;
    width: var(--sidebar-w);
    z-index: 50;
  }
  /* The titlebar still has two children, so it keeps two columns. The split
     must not — and this is the line that decides whether the app renders at
     all on a phone.

     The sidebar is `position: fixed` here, which takes it out of the grid
     flow. With two columns declared, the detail pane becomes the first
     in-flow item and lands in the `auto` column, which now has nothing in it
     to give it width. The page draws every pixel of itself into a column
     0px wide: no error, no blank slot, just nothing on screen. */
  .window[data-sidebar="hidden"] .titlebar { grid-template-columns: auto minmax(0, 1fr); }
  .window[data-sidebar="hidden"] .split { grid-template-columns: minmax(0, 1fr); }
  /* Toolbar buttons drop to icons so the view title survives. A button with
     no icon keeps its label — otherwise it would render as an empty box. */
  .titlebar .btn:has(.icon) .btn__label { display: none; }
  .titlebar .btn:has(.icon) { padding: 0 9px; }

  /* Except back, which keeps its label. A bare chevron is an arrow pointing at
     nothing in particular; "‹ Clients" says where it goes, which is the whole
     of what a back button is for.
     After the rule it overrides rather than more specific than it: `:has()`
     carries the specificity of its argument, so the obvious `.btn.titlebar__back`
     version ties and loses on source order. Order is the clearer lever. */
  .titlebar .btn.titlebar__back .btn__label { display: inline; }
  .titlebar .btn.titlebar__back { padding: 0 8px 0 2px; gap: 1px; }
  .titlebar__leading { gap: 6px; }

  /* One plus, not two.
     A section's own add button and the global one sat side by side up here,
     two plus signs a thumb's width apart doing almost the same thing — which
     is the opposite of what a consistent add button is for. The section's own
     goes; `addAnything` offers it as the first row of the sheet instead, so
     nothing becomes unreachable, including the sections whose add is not one
     of the three the sheet knows about.

     Scoped through .titlebar so this beats `.btn`'s own display without
     needing !important. Only [data-add] — the back button lives in this slot
     too and must stay. */
  .titlebar .titlebar__leading [data-add] { display: none; }

  /* A card header is an icon, a title, and whatever that card offers on the
     right — Prices, Templates, Add line. There is not room for both on a
     phone, and flexbox resolves it by shrinking the only item that can
     shrink: the title, which becomes a column of one word per line while the
     buttons keep every pixel they asked for. Backwards. The title is what the
     card is.

     So the header wraps, and the spacer earns a second job: at `flex-basis:
     100%` it cannot share a line with the title, so it takes the break itself
     and every button lands together on the row beneath. Letting them wrap on
     their own doesn't work — flexbox fills a line before it distributes what
     is left, so two buttons fit beside the title and the third is stranded
     underneath on its own.

     `:has(.spacer + *)` is the test for "this header actually has an
     accessory": without it, every plain card header would gain a wrap it
     never uses and a blank row under the title. */
  .card__header:has(.spacer + *) { flex-wrap: wrap; row-gap: 6px; }
  .card__header:has(.spacer + *) > .spacer { flex-basis: 100%; height: 0; }

  .page { padding: 14px 14px 50px; }
  .grid { grid-template-columns: minmax(0, 1fr); }
  .form-grid { grid-template-columns: minmax(0, 1fr); }
  .form-grid > label { text-align: left; }
  .cal { --cell-min: 62px; font-size: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* =========================================================================
   Sign-in.

   Only ever on screen when js/config.js exists and nobody is signed in —
   the standalone demo never renders this at all.

   Sits above everything on its own layer rather than as a "view", because
   it is not part of the app: the app behind it has no data yet, and a
   half-drawn shell showing through would suggest otherwise.
   ========================================================================= */

.signin {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: grid;
  place-items: center;
  padding: 24px;
  background: var(--bg-window);
  overflow: auto;
}

.signin__card {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 28px;
  border-radius: var(--r-panel);
  background: var(--bg-card);
  box-shadow: var(--shadow-raised);
}

.signin__title {
  margin: 0;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.signin__blurb {
  margin: 0 0 4px;
  font-size: 13px;
  color: var(--label-secondary);
}

.signin__label {
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-size: 12px;
  font-weight: 600;
  color: var(--label-secondary);
}

.signin__input {
  width: 100%;
  padding: 9px 11px;
  font: inherit;
  font-size: 14px;
  color: var(--label);
  background: var(--bg-inset);
  border: 1px solid transparent;
  border-radius: var(--r-field);
}

.signin__input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--bg-content);
}

.signin__error {
  margin: 0;
  font-size: 12.5px;
  color: var(--red);
}

.signin__go {
  margin-top: 4px;
  padding: 10px 14px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--on-accent);
  background: var(--accent);
  border: none;
  border-radius: var(--r-field);
  cursor: pointer;
}

.signin__go:disabled { opacity: 0.6; cursor: default; }

/* =========================================================================
   The enquiries board.

   One column per stage, scrolling sideways when there are more columns than
   fit. Each column scrolls on its own, so a stage with forty enquiries in it
   does not push the others off the bottom of the page.
   ========================================================================= */

.board {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}

.board__bar {
  display: flex;
  justify-content: flex-end;
  padding: 10px 14px;
  border-bottom: 1px solid var(--separator);
}

.board__cols {
  flex: 1;
  min-height: 0;
  display: flex;
  gap: 0;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0;
}

.board__col {
  flex: 0 0 280px;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* A line rather than a gap between columns — the same decision as the Mac.
     Gaps leave the eye to infer where one column stops; a line says. */
  border-right: 1px solid var(--separator);
}

.board__col:last-child { border-right: none; }

/* Sticks while its own column scrolls, so you always know which pile you are
   looking at. */
.board__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 11px 14px;
  background: var(--bg-content);
  border-bottom: 1px solid var(--separator);
}

.board__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

.board__name {
  font-size: 13px;
  font-weight: 650;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.board__gone {
  font-size: 10.5px;
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 4px;
  color: var(--on-accent);
  background: var(--label-tertiary);
}

.board__count {
  margin-left: auto;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--label-secondary);
}

.board__drop {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Only while something is over it. A permanent tint would be a column that
   looks perpetually about to receive something. */
.board__drop.is-over {
  background: var(--bg-inset);
  box-shadow: inset 0 0 0 2px var(--accent);
  border-radius: var(--r-card);
}

.board__empty {
  margin: 6px 2px;
  font-size: 12px;
  color: var(--label-tertiary);
}

.board__card {
  padding: 10px 11px;
  border-radius: var(--r-card);
  background: var(--bg-card);
  box-shadow: var(--shadow-card);
  cursor: grab;
}

.board__card:active { cursor: grabbing; }

/* Kept visible rather than hidden while dragging: the drag image is a copy,
   and removing the original makes the column jump under the cursor. */
.board__card.is-dragging { opacity: 0.4; }

.board__cardtop {
  display: flex;
  align-items: center;
  gap: 6px;
}

.board__cardname {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Clamped in CSS rather than by cutting the string, so the whole message is
   still in the page for anyone reading it with something other than eyes. */
.board__cardmsg {
  margin: 5px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--label-secondary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.board__cardfoot {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-top: 7px;
  font-size: 11px;
  color: var(--label-tertiary);
}

.board__stale { color: var(--orange); }

/* On a phone the columns are most of the screen, and sideways scrolling is
   the point rather than a compromise. */
@media (max-width: 600px) {
  .board__col { flex-basis: 82vw; }
}

/* Mailbox header — replaces the channel chips while one is open. */
.mailbox-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}

.mailbox-head__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
}

.mailbox-head__name {
  font-size: 13px;
  font-weight: 650;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mailbox-head__all {
  margin-left: auto;
  padding: 3px 9px;
  font: inherit;
  font-size: 12px;
  color: var(--label-secondary);
  background: var(--bg-inset);
  border: none;
  border-radius: var(--r-control);
  cursor: pointer;
  white-space: nowrap;
}

.mailbox-head__all:hover { color: var(--label); background: var(--bg-inset-strong); }

/* ---------- Admin: the starting layout ----------------------------------

   A picture of somebody else's app, and the short list that edits it. The
   picture is the point: switches say what is on and an order says what comes
   first, and neither answers "what do they open on a Monday morning".

   Deliberately a sketch — names and shapes, no counts, no badges. It should
   read as a diagram of their sidebar rather than a live copy of it, because
   a live copy invites being trusted for things it doesn't know. */

.layout-preview {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}

.layout-preview__pane { flex: 1 1 200px; min-width: 0; }
.layout-preview__pane > .caption { margin-bottom: 6px; }

.faux {
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: var(--bg-inset);
  overflow: hidden;
  font-size: 11px;
  line-height: 1.3;
}

.faux--sidebar { padding: 6px 0; max-height: 260px; overflow-y: auto; }

.faux__head {
  padding: 8px 10px 3px;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--label-tertiary);
}

.faux__row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  color: var(--label-secondary);
}

.faux__row .icon { opacity: 0.55; flex: none; }
.faux__row--fixed { color: var(--label); font-weight: 500; }

/* The phone: a screen with nothing in it and the bar that matters. */

.faux--phone { display: flex; flex-direction: column; min-height: 120px; }

.faux-phone__screen {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px 10px;
  text-align: center;
}

.faux-phone__bar {
  display: flex;
  border-top: 1px solid var(--hairline);
  background: var(--bg-card);
  padding: 6px 2px;
}

.faux-phone__tab {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  color: var(--label-tertiary);
  font-size: 9px;
}

.faux-phone__tab span {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.faux-phone__tab--on { color: var(--accent); }
.faux-phone__tab--on .icon { color: var(--accent); }

/* The list that edits it. One line per area — the picture above already says
   where each one lands, so there is no blurb and no tab badge here. */

.layout-rows { border-top: 1px solid var(--hairline); }

.layout-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--hairline);
}

.layout-row:last-child { border-bottom: 0; }
.layout-row__name { flex: 1; min-width: 0; }
.layout-row__count { flex: none; }
.layout-row .icon { flex: none; opacity: 0.7; }

.layout-row--off .layout-row__name { color: var(--label-tertiary); }
.layout-row--off .icon { opacity: 0.35; }

.layout-row__grip { display: flex; flex-direction: column; flex: none; }

.layout-row__grip .icon-btn {
  width: 22px;
  height: 16px;
  padding: 0;
  /* Quiet, but findable. Tertiary was quiet enough to be missed entirely,
     and a control nobody can see is not restraint. */
  color: var(--label-secondary);
}

.layout-row__grip .icon-btn:hover { color: var(--label); background: var(--bg-inset); }
.layout-row__grip .icon-btn[disabled] { opacity: 0.2; pointer-events: none; }

/* The finer answer, folded away: most of the time the areas are the decision
   and the rows inside them are a detail. */

.layout-inner { margin-top: 10px; border-top: 1px solid var(--hairline); }

.layout-inner > summary {
  padding: 9px 0 0;
  font-size: 12px;
  color: var(--label-secondary);
  cursor: pointer;
}

.layout-inner__body {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  padding: 10px 0 2px;
}

.layout-inner__group { flex: 1 1 130px; min-width: 0; }
.layout-inner__group > .caption { margin-bottom: 4px; }

.layout-inner__row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
  font-size: 12px;
  cursor: pointer;
}

/* ---------- Orders: one order, as a line -------------------------------
   Its own row rather than the products list's. That one is built around a
   thumbnail and a stock stepper, and an order has neither — it borrowed the
   shape and inherited a text block that is deliberately not a target, which
   as a `button` meant every row wore the browser's default grey box. */

.order-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 0.5px solid var(--separator);
}

.order-row:last-child { border-bottom: 0; }

.order-row__open {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* The chrome, cleared. This is a target — unlike the products list's — so
     it says so with the cursor and the name rather than with a box. */
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.order-row__open:hover .order-row__name { color: var(--accent); }

.order-row__name {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 13px;
  font-weight: 500;
}

/* The shop's reference, read a character at a time rather than as a word —
   and quiet, because you look it up rather than read it. */
.order-row__code {
  flex: none;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--label-tertiary);
  font-weight: 400;
}

.order-row__sub {
  font-size: 12px;
  color: var(--label-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.order-row__controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

/* -------------------------------------------------------------------------
   Order lines, in the order sheet

   A line is what was bought, its picture, and a box to tick as it goes in
   the parcel. The whole label is the target — picture and words included —
   because this list is read with a jumper in one hand, and a 16px checkbox
   is not a thing to aim at twice.
   ------------------------------------------------------------------------- */
.order-lines {
  margin: 14px 0;
  border: 0.5px solid var(--separator);
  border-radius: 8px;
  overflow: hidden;
}

.order-line {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-bottom: 0.5px solid var(--separator);
}

.order-line:last-child { border-bottom: 0; }

.order-line__pick {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
}

/* Not a target where there is nothing to tick — a cancelled order's lines
   are a record, and a pointer over them promises something to press. */
.order-line span.order-line__pick { cursor: default; }

.order-line__pick input {
  flex: none;
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
  cursor: pointer;
}

/* The same square as the products list, a size down: here it is telling you
   which thing to reach for, not being the row. */
.order-line__thumb {
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 6px;
  overflow: hidden;
  background: var(--bg-inset);
  display: flex;
  align-items: center;
  justify-content: center;
}

.order-line__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.order-line__thumb--empty { border: 0.5px dashed var(--separator); }
.order-line__thumb--empty .icon { color: var(--label-tertiary); }

.order-line__text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.order-line__name {
  font-size: 13px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.order-line__sub { font-size: 12px; color: var(--label-secondary); }

.order-line__total { flex: none; font-size: 13px; }

/* Packed: quieter, not struck through. A ticked line is done, but it is
   still what the customer paid for and the price still has to be readable
   against the total underneath. */
.order-line--packed .order-line__name,
.order-line--packed .order-line__sub { color: var(--label-secondary); }
.order-line--packed .order-line__thumb { opacity: 0.55; }

/* The sheet takes focus itself when its first control shouldn't have it —
   see UI.sheet's `autofocus: false`. Programmatic focus on a container is
   not something to draw a ring around; the keyboard is simply inside the
   dialog, which is the whole point. */
.sheet:focus { outline: none; }

/* ---------------------------------------------------------------------------
   The flow strip — how far a quote has got.

   In app.css rather than either app's own sheet because both pages load
   this one, and the business and the customer are looking at the same
   journey from opposite ends: one definition of what it looks like, beside
   the one definition of what the stages are (js/flow.js).

   Green for finished and indigo for the step being waited on, matching the
   badges elsewhere — paid is green, a quote is indigo — so the colours mean
   here what they already mean everywhere else.
   --------------------------------------------------------------------------- */

.flow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
  font-size: 12.5px;
  color: var(--label-secondary);
}

.flow__step { display: flex; align-items: center; gap: 7px; min-width: 0; }

/* The connector belongs to the step it leads into, and is only coloured
   when the step behind it is finished — so the line stops exactly where
   the progress does. */
.flow__step + .flow__step::before {
  content: "";
  height: 2px;
  width: 22px;
  flex: 0 1 22px;
  min-width: 10px;
  margin: 0 9px;
  border-radius: 1px;
  background: var(--separator);
}
.flow__step--done + .flow__step::before { background: var(--green); }

.flow__dot {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: 50%;
  border: 1.5px solid var(--separator-opaque);
  box-sizing: border-box;
}
.flow__step--done .flow__dot { background: var(--green); border-color: var(--green); }
.flow__step--now .flow__dot {
  background: var(--indigo);
  border-color: var(--indigo);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--indigo) 22%, transparent);
}

.flow__step--done .flow__label,
.flow__step--now .flow__label { color: var(--label); font-weight: 500; }

/* Narrow enough and the strip wraps to two rows rather than shrinking the
   words away — a stage nobody can read is not a stage. */
@media (max-width: 460px) {
  .flow { font-size: 12px; }
  .flow__step + .flow__step::before { margin: 0 6px; min-width: 8px; }
}
