/* TruckSpots SPA — "Gallery" design system: radical minimal monochrome. Pure
   white surfaces, #eaeaea hairlines as the only separation, ink text, and lots
   of white space. Terracotta (#c2410c) is the single accent — primary CTAs,
   active nav, status stamps — color never appears in chrome otherwise. Cards
   are frosted glass (translucent fill + backdrop blur); corners are square.
   Oswald (uppercase) for display, Inter for body, Spline Sans Mono for data.
   Fonts are the vendored woff2 served from /static/fonts so the SPA runs fully
   offline. All colors live in :root — no raw hex sprawl.

   Amendments (2026-07 live production review, founder-approved):
   1. Soft neutral elevation (--shadow-rest/--shadow-hover) on INTERACTIVE
      result cards and the modal ONLY — form/section containers stay flat
      hairline (.ts-card--interactive marks "this opens the detail modal").
   2. A narrow --danger red exception on the destructive delete action
      (danger-zone heading marker + delete button border/text).
   3. The modal carries the heaviest shadow in the system (--shadow-modal).
   Square corners, monochrome surfaces, and the single terracotta accent
   remain law. */
:root {
  --bg: #ffffff;
  --surface: #ffffff;
  --surface-2: #f7f7f7;
  --border: #eaeaea;
  /* Form-control boundary: hairlines measure ~1.2:1 on white, below the 3:1
     WCAG minimum for UI components — controls get this ~3.2:1 gray instead. */
  --border-strong: #8f8f8f;
  --text: #111111;
  --text-muted: #6f6f6f;

  /* Terracotta — the single accent (primary CTAs, active nav, stamps). */
  --accent: #c2410c;
  --accent-hover: #9a3412;
  --on-accent: #ffffff;

  /* Status colors live in data (banners, checks, connection state) only. */
  --ok: #15803d;
  --warn: #a16207;
  --danger: #b91c1c;
  --ring: rgba(194, 65, 12, 0.35);

  /* Elevation (Amendments 1 + 3): neutral ink-tinted shadows. Interactive
     result cards + the modal only — never containers or form cards. */
  --shadow-rest: 0 1px 2px rgba(17, 17, 17, 0.04), 0 4px 12px rgba(17, 17, 17, 0.06);
  --shadow-hover: 0 2px 4px rgba(17, 17, 17, 0.06), 0 12px 28px rgba(17, 17, 17, 0.12);
  --shadow-modal: 0 24px 64px rgba(17, 17, 17, 0.24);

  /* Frosted-glass card treatment. */
  --glass: rgba(255, 255, 255, 0.55);
  --glass-border: rgba(17, 17, 17, 0.08);

  /* Neutral chart bars (busy-times). Darker than a hairline so bars read as
     data, quieter than ink; the accent marks the current hour only. */
  --chart-bar: #d6d3d0;

  --radius: 0px;

  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-display: "Oswald", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "Spline Sans Mono", ui-monospace, SFMono-Regular, "SF Mono", monospace;
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/inter-latin-variable.woff2") format("woff2");
}
@font-face {
  font-family: "Oswald";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/oswald-latin-variable.woff2") format("woff2");
}
@font-face {
  font-family: "Spline Sans Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("/static/fonts/spline-sans-mono-latin-variable.woff2") format("woff2");
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

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

/* Display treatment: Oswald, uppercase, tracked. */
.ts-brand,
.ts-page-header h1,
.ts-card-title,
.ts-btn,
.ts-nav-link,
.ts-stamp {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* Mono for numeric/data values. */
.ts-mono,
.ts-zip,
.ts-stat strong { font-family: var(--font-mono); }

/* Tracked-out micro-labels. */
.ts-section-title,
.ts-table th,
.ts-stat span { text-transform: uppercase; letter-spacing: 0.1em; }

a { color: var(--text); text-decoration: underline; }
a:hover { color: var(--accent); }

/* Skip link for keyboard users. */
.ts-skip {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--accent);
  color: var(--on-accent);
  padding: 8px 14px;
  z-index: 100;
  text-decoration: none;
}
.ts-skip:focus { left: 0; }

/* ====================== App shell (sidebar layout) ====================== */
.ts-shell { display: flex; min-height: 100vh; }

.ts-sidebar {
  width: 248px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--border);
  transition: width 0.16s ease;
}
.ts-shell.is-collapsed .ts-sidebar { width: 72px; }

/* Sidebar header row: brand + the desktop collapse control beside it. The
   hairline lives on the row so brand and toggle share one underline. */
.ts-sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding-right: 12px;
  border-bottom: 1px solid var(--border);
}
.ts-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 18px 20px;
  font-size: 1.05rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
}
.ts-sidebar-head .ts-brand { flex: 1; min-width: 0; }
.ts-brand .ts-spark { color: var(--accent); }

.ts-sidebar-toggle {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  font-size: 1.05rem;
  color: var(--text-muted);
}
.ts-sidebar-toggle:hover { color: var(--text); }
/* Collapsed: stack mark over toggle so the way back stays reachable at 72px. */
.ts-shell.is-collapsed .ts-sidebar-head {
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  padding: 0 0 12px;
}
.ts-shell.is-collapsed .ts-sidebar-head .ts-brand { padding: 18px 0 4px; }

.ts-nav {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 12px;
  overflow-y: auto;
}

.ts-nav-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.82rem;
  white-space: nowrap;
  overflow: hidden;
}
.ts-nav-link:hover { background: var(--surface-2); color: var(--text); }
.ts-nav-link.ts-active {
  background: var(--surface-2);
  color: var(--text);
  box-shadow: inset 3px 0 0 var(--accent);
}
.ts-nav-ico { width: 18px; text-align: center; flex-shrink: 0; }

.ts-avatar {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: var(--radius);
  background: var(--text);
  color: var(--bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 600;
}
.ts-user-meta { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.ts-user-name { font-weight: 600; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-user-email { color: var(--text-muted); font-size: 0.78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ts-shell.is-collapsed .ts-hide-collapsed { display: none; }
/* The compact "T·S" mark shows only when the sidebar is collapsed. */
.ts-show-collapsed { display: none; }
.ts-shell.is-collapsed .ts-show-collapsed { display: inline; }
.ts-shell.is-collapsed .ts-nav-link,
.ts-shell.is-collapsed .ts-brand { justify-content: center; }

.ts-mainwrap { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.ts-topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 24px;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  /* Frosted chrome: content scrolling beneath shows through the blur. */
  background: rgba(255, 255, 255, 0.8);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  z-index: 5;
}

.ts-iconbtn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  width: 38px;
  height: 38px;
  border-radius: var(--radius);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
}
.ts-iconbtn:hover { border-color: var(--text); }

.ts-topbar-title {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.04em;
  font-size: 1.02rem;
}

/* Right edge of the topbar: connection status + the account avatar menu. */
.ts-topbar-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
}

/* Quiet connection summary — hairline chip, muted text; the green check is
   connection state (data), same treatment as the connect list. */
.ts-connstat {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 0.78rem;
  text-decoration: none;
  white-space: nowrap;
}
.ts-connstat:hover { border-color: var(--text); color: var(--text); }
.ts-connstat-item { display: inline-flex; align-items: center; gap: 6px; }

/* Avatar button + dropdown (Settings / Account / Sign out). */
.ts-avatarbtn {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  border-radius: var(--radius);
}
.ts-avatarbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ts-usermenu { position: relative; display: inline-flex; }
.ts-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 10px);
  min-width: 210px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.12);
  padding: 6px 0;
  z-index: 60;
}
.ts-menu-id {
  padding: 8px 14px 10px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 6px;
}
.ts-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 9px 14px;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.04em;
  font-size: 0.78rem;
  color: var(--text);
  cursor: pointer;
}
.ts-menu-item:hover { background: var(--surface-2); }
.ts-menu-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  box-shadow: none;
}

.ts-content { flex: 1; padding: 32px 28px 56px; max-width: 1040px; width: 100%; }

.ts-drawer-toggle, .ts-scrim { display: none; }

@media (max-width: 760px) {
  /* The in-sidebar collapse control is desktop-only; on mobile the sidebar is
     an off-canvas drawer driven from the topbar. */
  .ts-sidebar-toggle { display: none; }
  /* A labeled "Menu" button, not a bare hamburger glyph. */
  .ts-drawer-toggle {
    display: inline-flex;
    width: auto;
    min-height: 44px;
    padding: 0 14px;
    font-family: var(--font-display);
    text-transform: uppercase;
    font-weight: 600;
    letter-spacing: 0.06em;
    font-size: 0.78rem;
  }
  /* Primary nav affordance on touch — comfortable 44px target. */
  .ts-iconbtn { width: 44px; height: 44px; }
  /* The persistent topbar controls (messages bell + account avatar) are the
     only way to reach messages / settings / sign-out on a phone — give them
     the same 44px touch target as every other control. */
  .ts-bell, .ts-avatarbtn {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* A desktop-persisted collapsed state must not shrink the drawer. */
  .ts-shell.is-collapsed .ts-sidebar { width: 248px; }
  .ts-shell.is-collapsed .ts-hide-collapsed { display: revert; }
  .ts-shell.is-collapsed .ts-show-collapsed { display: none; }
  .ts-shell.is-collapsed .ts-nav-link,
  .ts-shell.is-collapsed .ts-brand { justify-content: flex-start; }
  .ts-shell.is-collapsed .ts-sidebar-head { flex-direction: row; padding: 0 12px 0 0; }
  .ts-shell.is-collapsed .ts-sidebar-head .ts-brand { padding: 18px 20px; }
  /* Connection detail lives in Settings on small screens. */
  .ts-connstat { display: none; }
  .ts-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 40;
    transform: translateX(-100%);
    transition: transform 0.18s ease;
  }
  .ts-shell.is-open .ts-sidebar { transform: translateX(0); }
  .ts-shell.is-open .ts-scrim {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(17, 17, 17, 0.35);
    z-index: 30;
  }
}

/* ====================== Public (auth) layout ====================== */
/* Centered so the white space frames the form instead of hanging under it. */
.ts-public {
  max-width: 440px;
  margin: 0 auto;
  padding: 2rem 1.25rem;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* gap: 0 — the bare "Truck" text node is a flex item; the brand gap would
   split the wordmark apart. */
.ts-public-brand { padding: 0 0 1.5rem; border-bottom: none; font-size: 1.2rem; gap: 0; }

/* ====================== Headings ====================== */
.ts-page-header { margin-bottom: 1.4rem; }
.ts-page-header h1 { margin: 0; font-size: 1.5rem; }
.ts-subtitle { margin: 0.25rem 0 0; color: var(--text-muted); }
/* PageHeader's optional actions slot (Food Menu's "Sync from Square" / "Paste
   menu" — the first view to use it). Wraps under the title on narrow screens
   rather than the header ever scrolling sideways. */
.ts-page-header-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
.ts-page-header-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; flex-shrink: 0; }
.ts-section-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.8rem;
  color: var(--text-muted);
  margin: 0.5rem 0 0.75rem;
}

/* ====================== Cards (frosted glass) ====================== */
.ts-card {
  background: var(--glass);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  padding: 1.4rem;
  margin-bottom: 1.4rem;
}
.ts-card-title { margin: 0 0 0.85rem; font-size: 1rem; }

/* Interactive result cards (Amendment 1): soft neutral elevation marks "this
   opens the detail modal". TopSpots grid cards + Find-a-Spot tickets only —
   never form/section containers. Under prefers-reduced-motion the global
   kill-switch stops the transition and the hover lift is removed below; the
   rest→hover shadow swap still reads state without motion. */
.ts-card--interactive {
  border: 1px solid var(--border);
  box-shadow: var(--shadow-rest);
  cursor: pointer;
  transition: box-shadow 150ms ease, transform 150ms ease;
}
.ts-card--interactive:hover {
  box-shadow: var(--shadow-hover);
  transform: translateY(-1px);
}
.ts-card--interactive:focus-visible {
  outline: none;
  box-shadow: var(--shadow-hover), 0 0 0 2px var(--ring);
}

/* Amendment 2: the danger-zone heading carries a small --danger marker. */
.ts-card--danger .ts-card-title::before {
  content: "";
  display: inline-block;
  width: 0.55em;
  height: 0.55em;
  background: var(--danger);
  margin-right: 0.5em;
}

/* ====================== Forms ====================== */
/* Visible to screen readers only: labels and live-region announcements whose
   text would be redundant on screen but is the only cue a non-visual user
   gets. Never use it to hide something a sighted user also needs. */
.ts-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.ts-form { display: flex; flex-direction: column; gap: 0.85rem; max-width: 520px; }
.ts-inline-form { display: flex; gap: 0.75rem; align-items: flex-end; flex-wrap: wrap; }
.ts-block { display: block; }

label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.82rem; color: var(--text-muted); }

/* A field group that wraps more than one control (so it can't be a <label> —
   a label only ever implicitly names the first control inside it) but still
   needs to read exactly like one: label's own box model and text styling,
   reproduced on a plain div + span. */
.ts-fieldgroup { display: flex; flex-direction: column; gap: 0.3rem; }
.ts-fieldgroup-label { font-size: 0.82rem; color: var(--text-muted); }

input, textarea, select {
  /* Block, not inline: inside block labels (.ts-block) a capped-width inline
     input would sit beside the label text instead of under it. */
  display: block;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  color: var(--text);
  padding: 0.55rem 0.7rem;
  font: inherit;
  width: 100%;
  /* Short values in metre-wide fields read as unconsidered — cap them. */
  max-width: 420px;
}
textarea { max-width: 680px; }
input[type="checkbox"] { display: inline-block; width: auto; accent-color: var(--accent); }
/* A checkbox with its label on one line (e.g. Settings' "remind me weekly"
   toggle) — plain labels default to a stacked column for text-above-input
   fields, which reads wrong for a single checkbox + sentence. */
.ts-checkrow { flex-direction: row; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: var(--text); margin-top: 0.75rem; }
input:focus-visible, textarea:focus-visible, select:focus-visible,
.ts-btn:focus-visible, .ts-nav-link:focus-visible, .ts-iconbtn:focus-visible, a:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--ring);
  border-color: var(--accent);
}
/* Borderless elements (nav links, text links): border-color is a no-op there,
   and the soft ring alone is ~1.7:1 — give them a crisp accent outline. */
a:focus-visible, .ts-nav-link:focus-visible, .ts-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: none;
}

/* ====================== Buttons ====================== */
.ts-btn {
  display: inline-block;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.55rem 1rem;
  font-size: 0.82rem;
  cursor: pointer;
  text-decoration: none;
}
.ts-btn:hover { border-color: var(--text); }
/* Disabled is neutral gray, not half-opacity accent — faded terracotta reads
   as a wrong brand color rather than an unavailable action. */
.ts-btn:disabled,
.ts-btn-primary:disabled {
  background: var(--surface-2);
  color: var(--text-muted);
  border-color: var(--border);
  cursor: not-allowed;
}
.ts-btn-primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.ts-btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.ts-btn-danger { background: var(--danger); color: #fff; border-color: var(--danger); }
/* Amendment 2: the disarmed delete button reads as danger — --danger
   border/text on white — and fills red only once armed (enabled). */
.ts-btn-danger:disabled {
  background: var(--surface);
  color: var(--danger);
  border-color: var(--danger);
  cursor: not-allowed;
}
.ts-link { background: none; border: none; color: var(--text); padding: 0; text-decoration: underline; font-family: var(--font); text-transform: none; letter-spacing: 0; cursor: pointer; }
.ts-link:hover { color: var(--accent); }

.ts-actions { display: flex; gap: 0.75rem; margin-top: 0.5rem; flex-wrap: wrap; }

/* A small side-by-side figure row (Dashboard sales tile: yesterday / last 7
   days) — each stat is a muted label over a bold value. */
.ts-stat-row { display: flex; gap: 2rem; flex-wrap: wrap; }
.ts-stat p { margin: 0; }
.ts-stat-value { font-size: 1.1rem; font-weight: 600; }

.ts-muted { color: var(--text-muted); }
.ts-source { color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem; font-family: var(--font-mono); }
/* Inline per-field validation message (e.g. onboarding's ZIP format check) —
   smaller and tighter than a page-level banner since it sits right under the
   offending input. */
.ts-field-error { color: var(--danger); font-size: 0.85rem; margin: -0.5rem 0 0.75rem; }

/* ====================== Banners ====================== */
/* A left accent rule, not a full outlined box — full-width accent borders
   shout louder than the hairline language around them. */
.ts-banner { padding: 0.7rem 0.9rem; border-radius: var(--radius); margin-bottom: 1rem; border: none; border-left: 3px solid; }
.ts-banner-error { background: rgba(185, 28, 28, 0.06); border-color: var(--danger); }
.ts-banner-success { background: rgba(21, 128, 61, 0.06); border-color: var(--ok); }
.ts-banner-info { background: rgba(194, 65, 12, 0.06); border-color: var(--accent); }

/* A dismissible nudge banner (e.g. Settings' "Update permissions" prompt) is
   an info banner with its copy and a close control on one line. Scoped to its
   own modifier class — flexing .ts-banner-info itself would break every
   existing info banner whose children are inline text + links (Events, Menu,
   FindASpot, …) into spread-apart flex items. */
.ts-banner-dismissible { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.ts-dismiss {
  background: transparent;
  border: none;
  color: var(--text-muted);
  width: 24px;
  height: 24px;
  min-width: 24px;
  border-radius: var(--radius);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}
.ts-dismiss:hover { color: var(--text); }

.ts-spinner { padding: 2rem; text-align: center; color: var(--text-muted); }

/* Demo-parse badge: a calm, informational note (not an error/warning color) —
   the mock LLM's canned output is by design, not a malfunction. */
.ts-demo-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.7rem;
  margin: 0 0 0.75rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 0.82rem;
  /* Was a raw rgba(0,0,0,0.015) outside :root — a token violation. --surface-2
     is visually equivalent (a faint neutral tint) and already lives in :root. */
  background: var(--surface-2);
}

/* ====================== Cuisine chips ====================== */
.ts-cuisines { display: flex; flex-wrap: wrap; gap: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); padding: 0.75rem; }
.ts-cuisines legend { padding: 0 0.4rem; color: var(--text-muted); }
.ts-chip { flex-direction: row; align-items: center; gap: 0.4rem; background: var(--surface-2); padding: 0.35rem 0.6rem; border-radius: var(--radius); color: var(--text); font-size: 0.85rem; }
.ts-chip-custom { border: 1px solid var(--accent); color: var(--accent); }
.ts-chip-x { background: none; border: none; color: inherit; cursor: pointer; font-size: 1rem; line-height: 1; padding: 0; min-width: 24px; min-height: 24px; display: inline-flex; align-items: center; justify-content: center; }
.ts-add-cuisine { display: flex; gap: 0.5rem; align-items: center; flex-basis: 100%; margin-top: 0.4rem; }
.ts-add-cuisine input { max-width: 220px; }

/* ====================== Checklist ====================== */
.ts-checklist { list-style: none; padding: 0; margin: 0 0 0.75rem; }
.ts-checklist li { padding: 0.35rem 0; color: var(--text-muted); }
.ts-checklist li.ts-done { color: var(--text); }
.ts-check { display: inline-block; width: 1.5rem; color: var(--ok); }

/* ====================== Tables / feed ====================== */
.ts-table { width: 100%; border-collapse: collapse; }
.ts-table th, .ts-table td { text-align: left; padding: 0.55rem 0.4rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.ts-table th { color: var(--text-muted); font-weight: 600; font-size: 0.78rem; }

.ts-feed { list-style: none; padding: 0; margin: 0; }
.ts-feed li { padding: 0.6rem 0; border-bottom: 1px solid var(--border); }

.ts-stat { display: flex; flex-direction: column; }
.ts-stat span { font-size: 0.72rem; color: var(--text-muted); }

/* Terracotta status stamp. */
.ts-stamp {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border-radius: var(--radius);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--accent);
  border: 1px solid var(--accent);
  background: rgba(194, 65, 12, 0.06);
  white-space: nowrap;
}
.ts-events { margin: 0.25rem 0 0.5rem; padding-left: 1.1rem; }
.ts-events li { margin: 0.1rem 0; }

/* ====================== Placement advisor ====================== */
/* Results header: the section title on the left, the secondary "Get advice"
   action on the right (Search stays the terracotta primary). */
.ts-results-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.ts-results-head .ts-section-title { margin-bottom: 0; }

/* The advisor's ranked summary panel: a plain card with a numbered list keyed
   to the candidate cards below (which carry a matching "#n advisor pick" stamp). */
.ts-advisor-panel { margin-bottom: 1.4rem; }
.ts-advisor-title {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ts-advisor-list { list-style: none; padding: 0; margin: 0.75rem 0 0; }
.ts-advisor-list li {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--border);
}
.ts-advisor-list li:first-child { border-top: none; }
.ts-advisor-list strong { font-size: 0.95rem; }
.ts-advisor-list p { margin: 0.15rem 0 0; }
/* The rank chip is status/data, so it may carry the accent. */
.ts-advisor-rank {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.6rem;
  height: 1.6rem;
  padding: 0 0.4rem;
  font-size: 0.8rem;
  font-family: var(--font-mono);
  color: var(--on-accent);
  background: var(--accent);
}

/* ====================== Connection status dot (W12) ====================== */
/* Traffic-light connection indicator for Square/Calendar: green = connected &
   healthy, amber = a real permissions/connection-issue signal already
   computed by the backend, red = not connected. Never color-only — a visible
   text label always sits beside the dot (see StatusDot in components.tsx), so
   the state reads for a color-blind operator and a screen reader alike. Hues
   are chosen for status contrast, not brand — terracotta stays the only
   accent; these never appear as decoration. */
.ts-status { display: inline-flex; align-items: center; gap: 0.45rem; }
.ts-status-dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  flex: none;
}
.ts-status-dot--ok { background: var(--ok); }
.ts-status-dot--warn { background: var(--warn); }
.ts-status-dot--off { background: var(--danger); }
.ts-status-label {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* ====================== Connect-both nudge ====================== */
.ts-connect-list { list-style: none; padding: 0; margin: 0 0 0.75rem; }
.ts-connect-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}
.ts-connect-list li:last-child { border-bottom: none; }
.ts-conn-label { flex: 1; }
/* ts-conn-on/off remain for the topbar's compact ConnStatus summary
   (app.tsx) — out of W12's scope (Settings' two connection cards + the
   connect-both nudge, which now use .ts-status-dot above). */
.ts-conn-on { color: var(--ok); font-family: var(--font-mono); }
.ts-conn-off { color: var(--text-muted); font-family: var(--font-mono); }

/* ====================== Modal (detail + onboarding wizard) ====================== */
.ts-modal-scrim {
  position: fixed;
  inset: 0;
  /* Ink scrim, opacity only. No backdrop-filter here: blur on the scrim
     breaks the sticky modal header in Chromium. */
  background: rgba(17, 17, 17, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4vh 1rem;
  z-index: 100;
  overflow-y: auto;
}
/* Amendment 3: the modal carries the heaviest shadow in the system. The panel
   is its own scroll container (85vh cap) so the header stays sticky inside. */
.ts-modal {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  width: 100%;
  max-width: 640px;
  max-height: 85vh;
  overflow-y: auto;
  outline: none;
}
.ts-modal--detail { max-width: 560px; }
.ts-modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  background: var(--bg);
  z-index: 1;
}
.ts-modal-title {
  margin: 0;
  font-size: 1.05rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* Muted line under the modal title (address / ZIP fallback + chip). */
.ts-modal-sub {
  margin: 0.25rem 0 0;
  color: var(--text-muted);
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}
/* Touch-comfortable close target (≥44px). */
.ts-modal-close { width: 44px; height: 44px; flex-shrink: 0; }
.ts-modal-body { padding: 1.25rem; }

/* Hairline-divided sections inside the detail modal body. */
.ts-detail-section { padding: 0.9rem 0; border-top: 1px solid var(--border); }
.ts-detail-section:first-child { border-top: none; padding-top: 0; }
.ts-detail-section h3 {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.ts-detail-section p { margin: 0.25rem 0; }
.ts-detail-coords { font-size: 0.8rem; }
/* Terracotta action link ("Open in Maps") — a CTA, so it may carry the accent. */
.ts-accent-link { color: var(--accent); }
.ts-accent-link:hover { color: var(--accent-hover); }

/* ====================== Dashboard top-spots list ====================== */
/* Compact hairline-separated rows — the dashboard summary of Find-a-Spot's
   heavy order tickets. The whole row is one link to /find. */
.ts-spot-controls { display: flex; gap: 0.9rem; flex-wrap: wrap; margin: 0.35rem 0 0.6rem; }
.ts-spot-controls select { width: auto; min-width: 150px; }
/* Find a Spot's W10 search filters (date/time-of-day/radius/category) — a
   distinct row above the results from .ts-spot-controls above (that one only
   re-slices already-fetched candidates; this one re-runs the search). A
   hairline + align-items keeps it visually its own group, not a continuation
   of the ZIP form card above it. */
.ts-search-filters {
  align-items: flex-end;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}
.ts-search-filters input[type="date"] { width: auto; min-width: 150px; }
.ts-microlabel {
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.72rem;
  color: var(--text-muted);
}
/* Cards / List segmented toggle — monochrome: the active segment inverts to
   ink, no accent (accent stays reserved for CTAs / active nav / stamps). */
.ts-viewtoggle-wrap { display: flex; flex-direction: column; gap: 0.3rem; }
.ts-viewtoggle {
  display: inline-flex;
  width: max-content;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.ts-vt-btn {
  background: var(--surface);
  border: none;
  padding: 0.5rem 0.85rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.04em;
  font-size: 0.72rem;
  color: var(--text-muted);
  cursor: pointer;
}
.ts-vt-btn + .ts-vt-btn { border-left: 1px solid var(--border-strong); }
.ts-vt-btn:hover { color: var(--text); }
.ts-vt-btn[aria-pressed="true"] { background: var(--text); color: var(--bg); }
.ts-vt-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--ring);
  position: relative;
  z-index: 1;
}

/* Card view: compact Gallery cards in a responsive grid (2-3 across desktop,
   one column on narrow screens via auto-fill). Whole card is one link. */
.ts-spotgrid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
}
.ts-spotcard {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  height: 100%;
  padding: 0.9rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
}
.ts-spotcard:hover { border-color: var(--text); }
.ts-spotcard:hover .ts-spot-title { color: var(--accent); }
/* Stamps hug their label. As a direct child of the column-flex card a stamp
   would otherwise stretch to the full card width (align-items: stretch),
   reading as an empty bordered bar instead of a compact status marker. */
.ts-spotcard > .ts-stamp { align-self: flex-start; }
.ts-spotcard-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.ts-spotlist { list-style: none; margin: 0; padding: 0; }
.ts-spotlist li { border-bottom: 1px solid var(--border); }
.ts-spotlist li:last-child { border-bottom: none; }
.ts-spotrow {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.6rem 0;
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
}
.ts-spotrow:hover .ts-spot-title { color: var(--accent); }
.ts-spotrow:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.ts-spot-rank { color: var(--text-muted); font-size: 0.78rem; }
.ts-spot-title {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.04em;
  font-size: 0.88rem;
}
.ts-spot-meta { font-size: 0.78rem; }
.ts-spot-note { font-size: 0.8rem; margin: 0.5rem 0 0; }
.ts-skeleton-row {
  height: 40px;
  margin: 0.4rem 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--surface-2) 0%, var(--border) 50%, var(--surface-2) 100%);
  background-size: 200% 100%;
  animation: ts-shimmer 1.4s ease-in-out infinite;
}

/* ====================== Dashboard skeleton ====================== */
.ts-skeleton-grid { display: grid; gap: 1.4rem; margin-top: 1.4rem; }
.ts-skeleton-card {
  height: 120px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--surface-2) 0%, var(--border) 50%, var(--surface-2) 100%);
  background-size: 200% 100%;
  animation: ts-shimmer 1.4s ease-in-out infinite;
}
@keyframes ts-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ====================== Enrichment status (Find-a-Spot) ====================== */
/* Live chip while background signal compilation settles. Hairline chip, muted
   text; the terracotta dot is status/data, so it may carry the accent. The
   reduced-motion kill-switch below stops the pulse — the dot stays visible as
   a static marker, so the state still reads without motion. */
.ts-enrich-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.4rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1rem;
  color: var(--text-muted);
  font-size: 0.85rem;
}
.ts-enrich-dot {
  width: 7px;
  height: 7px;
  flex: none;
  background: var(--accent);
  animation: ts-enrich-pulse 1.3s ease-in-out infinite;
}
@keyframes ts-enrich-pulse {
  50% { opacity: 0.25; }
}
/* Quiet settled/timeout line once (or if) the pending chip retires. */
.ts-enrich-note { color: var(--text-muted); font-size: 0.85rem; margin: 0 0 1rem; }

/* ====================== Busy times (popular-times) ====================== */
/* The detail modal's 24-bar day chart, the candidate-card peak line, and the
   dashboard best-time tile. Monochrome bars on a hairline baseline; the
   terracotta accent marks exactly one thing — the current hour. */
.ts-busy { margin-top: 0.35rem; }
.ts-busy-days { display: flex; gap: 0.3rem; margin-bottom: 0.65rem; }
/* Day selector: monochrome segmented buttons (accent stays reserved); the
   active day inverts to ink, same treatment as the cards/list view toggle. */
.ts-busy-day {
  width: 30px;
  height: 30px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  cursor: pointer;
}
.ts-busy-day[aria-pressed="true"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}
.ts-busy-day:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ts-busy-figure { margin: 0; }
.ts-busy-svg { display: block; width: 100%; height: 88px; }
.ts-busy-bar { fill: var(--chart-bar); }
.ts-busy-bar--now { fill: var(--accent); }
.ts-busy-baseline { stroke: var(--border); stroke-width: 1; }
.ts-busy-ticks { position: relative; height: 0.9rem; }
.ts-busy-tick {
  position: absolute;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: var(--text-muted);
}
.ts-busy-caption {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.8rem;
  margin-top: 0.45rem;
}
/* Candidate-card peak line — data-adjacent copy, muted like the event count. */
.ts-busy-chip { color: var(--text-muted); font-size: 0.85rem; }
/* Dashboard best-time tile: a small flat hairline tile above Top spots. */
.ts-besttime {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.7rem 0.9rem;
  margin-bottom: 1rem;
  background: var(--surface);
}
.ts-besttime-when { font-size: 1.05rem; }
.ts-besttime-where { font-size: 0.85rem; }

/* Respect reduced-motion: drop the sidebar/drawer transitions + skeleton
   shimmer, and the interactive-card hover lift (the rest→hover shadow swap
   still reads without motion; focus keeps the ring). */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  .ts-card--interactive:hover { transform: none; }
}

/* ============================================================================
   Human-first redesign (Today answer card, Your Week, Money, Messages).
   Extends the Gallery system — tokens only, square corners, single terracotta
   accent, Oswald display for the answer. Mobile-first; the Today card is tuned
   to read cleanly at 375px.
   ========================================================================== */

/* Button size modifiers (base .ts-btn unchanged). */
.ts-btn-lg { padding: 0.8rem 1.4rem; font-size: 0.95rem; }
.ts-btn-sm { padding: 0.35rem 0.7rem; font-size: 0.75rem; }
/* A compact button (.ts-btn-sm) still needs a real touch target — bump the
   tap area without touching its compact font/padding, the same override
   pattern .ts-iconbtn/.ts-bell use for their own 44px minimum. */
.ts-btn-touch { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }

/* --- Today: the answer card ------------------------------------------------ */
.ts-today { max-width: 40rem; }
.ts-answer {
  border: 1px solid var(--border);
  border-top: 3px solid var(--accent);
  background: var(--surface);
  padding: 1.75rem 1.5rem;
  box-shadow: var(--shadow-rest);
}
.ts-answer--set { border-top-color: var(--ok); }
.ts-answer-kicker {
  margin: 0;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.ts-answer-spot {
  margin: 0.35rem 0 0.15rem;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 2rem;
  line-height: 1.1;
  color: var(--text);
}
.ts-answer-window {
  margin: 0 0 0.75rem;
  font-family: var(--font-mono);
  font-size: 1rem;
  color: var(--accent);
}
.ts-answer-badge { margin-left: 0.6rem; font-family: var(--font); vertical-align: middle; }
.ts-answer-why {
  margin: 0 0 1.25rem;
  font-size: 1.05rem;
  line-height: 1.5;
  color: var(--text);
}
.ts-answer-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.ts-answer-actions .ts-btn-lg { flex: 1 1 auto; text-align: center; }
.ts-answer-set { margin: 0; font-size: 1.05rem; color: var(--ok); font-weight: 600; }
.ts-answer-check { display: inline-block; margin-right: 0.25rem; }
.ts-today-links {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 1.25rem;
}
.ts-today-links .ts-link { text-decoration: none; color: var(--text-muted); text-align: left; }
.ts-today-links .ts-link:hover { color: var(--accent); }

/* --- Today: the current calendar week as day columns ----------------------- */
/* Desktop/tablet: seven columns, always the full week, never a partial or
   overflowing grid — `minmax(0, 1fr)` lets each column shrink together as the
   viewport narrows instead of one column overflowing its cell. */
.ts-weekgrid {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.6rem;
  /* Stretch (the grid default) so every column matches the row's tallest —
     a result day (short) and a suggestion day (why text + 3 buttons) would
     otherwise end at very different heights, leaving a ragged bottom edge.
     Pairs with .ts-weekcol-btnrow's margin-top: auto below, which needs the
     extra column height to push into. */
  align-items: stretch;
}
.ts-weekcol {
  border: 1px solid var(--border);
  border-top: 3px solid var(--border);
  background: var(--surface);
  padding: 0.75rem 0.65rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  min-width: 0;
}
/* Tiers, ground-truth-first (docs/goals/ground-truth-first.md G4): a result
   already happened (strongest tier this view has); approved is the owner's
   own committed pick; suggested is the agent's opinion and reads as one —
   dashed, the same "this is not a plain actual" language the modal chart's
   hatch treatment uses for un-attributed money (.ts-chart-bar--shared). */
.ts-weekcol--actual { border-top-color: var(--text-muted); background: var(--surface-2); }
.ts-weekcol--approved { border-top-color: var(--ok); }
.ts-weekcol--suggested { border-top-style: dashed; border-top-color: var(--text-muted); }
/* Today is anchored independently of its tier color — an accent ring so it
   reads as "you are here" regardless of whether today is done/approved
   /suggested. */
.ts-weekcol--today { box-shadow: 0 0 0 2px var(--accent) inset; }
.ts-weekcol-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.35rem; }
.ts-weekcol-day {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  color: var(--text-muted);
}
.ts-weekcol--today .ts-weekcol-day { color: var(--accent); }
.ts-weekcol-date { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted); }
.ts-weekcol-kicker {
  margin: 0;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.68rem;
  color: var(--text-muted);
}
.ts-weekcol-spot { margin: 0; font-weight: 600; font-size: 0.92rem; line-height: 1.25; }
.ts-weekcol-window { margin: 0; font-family: var(--font-mono); font-size: 0.78rem; color: var(--accent); }
.ts-weekcol-why { margin: 0; color: var(--text-muted); font-size: 0.82rem; line-height: 1.4; }
.ts-weekcol-result { margin: 0; font-size: 0.92rem; }
.ts-weekcol-badge { margin: 0.2rem 0 0; font-size: 0.65rem; padding: 2px 7px; }
.ts-weekcol-again { display: flex; flex-direction: column; gap: 0.35rem; align-items: flex-start; }
.ts-weekcol-btnrow { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: auto; padding-top: 0.2rem; }

/* --- Your Week ------------------------------------------------------------- */
.ts-week-cta { margin-bottom: 1rem; }
.ts-week { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.ts-week-row {
  display: flex;
  gap: 0.9rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  background: var(--surface);
  padding: 0.9rem 1rem;
}
.ts-week-row--approved { border-left-color: var(--ok); }
.ts-week-row--done { border-left-color: var(--text-muted); background: var(--surface-2); }
.ts-week-day {
  flex: 0 0 4.5rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.8rem;
  color: var(--text-muted);
  padding-top: 0.1rem;
}
.ts-week-body { flex: 1 1 auto; min-width: 0; }
.ts-week-spot { font-weight: 600; font-size: 1.05rem; }
.ts-week-window { font-weight: 400; color: var(--accent); font-family: var(--font-mono); font-size: 0.85rem; }
.ts-week-why { margin: 0.3rem 0 0.6rem; color: var(--text-muted); font-size: 0.92rem; line-height: 1.45; }
.ts-week-result { margin: 0.3rem 0 0.6rem; font-size: 1rem; }
.ts-week-actions, .ts-again { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.ts-week-ok { color: var(--ok); font-weight: 600; font-size: 0.85rem; }
.ts-again-label { color: var(--text-muted); font-size: 0.9rem; }
.ts-again-recorded { color: var(--text); font-size: 0.9rem; font-weight: 600; }
/* Transient landing highlight when "Plan for it" (Events tab) jumps here. */
.ts-week-row--highlight { border-left-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }

/* --- Events ------------------------------------------------------------ */
.ts-events-list { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.ts-event-row {
  display: flex;
  gap: 0.9rem;
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 0.9rem 1rem;
}
.ts-event-day {
  flex: 0 0 7rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.8rem;
  color: var(--text-muted);
  padding-top: 0.1rem;
}
.ts-event-body { flex: 1 1 auto; min-width: 0; }
.ts-event-title { font-weight: 600; font-size: 1.05rem; }
.ts-event-actions { margin-top: 0.5rem; }

/* Swap picker list (in the modal). */
.ts-swap-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.ts-swap-item {
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 0.7rem 0.9rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.ts-swap-item:hover { border-color: var(--text); }
.ts-swap-item:disabled { color: var(--text-muted); cursor: not-allowed; }
.ts-swap-name { font-weight: 600; }
.ts-swap-meta { color: var(--text-muted); font-size: 0.82rem; }

/* --- Money ----------------------------------------------------------------- */
.ts-money-hero {
  border: 1px solid var(--border);
  border-top: 3px solid var(--accent);
  padding: 1.5rem;
  margin-bottom: 1rem;
  box-shadow: var(--shadow-rest);
}
.ts-money-kicker {
  margin: 0;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.ts-money-total {
  margin: 0.2rem 0 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 2.6rem;
  line-height: 1;
  color: var(--text);
}
.ts-money-best { margin: 0 0 0.75rem; font-weight: 600; }
.ts-money-spots { list-style: none; margin: 0; padding: 0; }
.ts-money-spot {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}
.ts-money-spot:last-child { border-bottom: none; }
.ts-money-spot-name { font-weight: 500; }
.ts-money-spot-figs { white-space: nowrap; }

/* --- Your spots (calendar⋈Square actuals) --------------------------------- */
.ts-hist-spot { align-items: flex-start; }
.ts-hist-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.ts-hist-name { font-weight: 600; }
.ts-hist-trend { font-size: 0.85rem; color: var(--text-muted); }
.ts-hist-trend--up { color: var(--ok); }
.ts-hist-figs { text-align: right; white-space: nowrap; }
.ts-hist-typical { font-weight: 600; }
.ts-hist-sub { display: block; font-size: 0.85rem; color: var(--text-muted); }
.ts-hist-footer { margin: 0.7rem 0 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }
.ts-hist-reconnect { margin: 0 0 0.5rem; line-height: 1.5; }

/* --- Messages + bell ------------------------------------------------------- */
.ts-bell {
  position: relative;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text);
  font-size: 1.1rem;
  padding: 0.25rem 0.4rem;
  line-height: 1;
}
.ts-bell:hover { color: var(--accent); }
.ts-bell-badge {
  position: absolute;
  top: -0.1rem;
  right: -0.1rem;
  min-width: 1rem;
  height: 1rem;
  padding: 0 0.2rem;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.65rem;
  line-height: 1rem;
  text-align: center;
  font-family: var(--font-mono);
}
.ts-msg-cta { margin-bottom: 0.5rem; }
.ts-msg-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.ts-msg {
  width: 100%;
  text-align: left;
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 0.85rem 1rem;
  cursor: pointer;
}
.ts-msg:hover { border-color: var(--text); }
.ts-msg--unread { border-left: 3px solid var(--accent); background: var(--surface); }
.ts-msg-dot { flex: 0 0 auto; width: 0.6rem; height: 0.6rem; margin-top: 0.35rem; background: var(--text-muted); border-radius: 50%; }
.ts-msg-dot--result { background: var(--ok); }
.ts-msg-dot--today { background: var(--accent); }
.ts-msg-dot--week_ready { background: var(--text-muted); }
.ts-msg-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.ts-msg-title { font-weight: 600; }
.ts-msg-body { color: var(--text-muted); font-size: 0.9rem; line-height: 1.4; }

/* --- Onboarding confirmation ---------------------------------------------- */
.ts-onboard-alt { margin-top: 1rem; color: var(--text-muted); }
.ts-confirm-line { margin: 0.75rem 0 0.35rem; font-size: 1.05rem; }

/* --- Small screens (375px): the answer must read cleanly ------------------ */
@media (max-width: 460px) {
  .ts-answer { padding: 1.4rem 1.15rem; }
  .ts-answer-spot { font-size: 1.7rem; }
  .ts-answer-actions { flex-direction: column; }
  .ts-answer-actions .ts-btn-lg { width: 100%; }
  .ts-money-total { font-size: 2.1rem; }
  .ts-week-row { flex-direction: column; gap: 0.35rem; }
  .ts-week-day { flex-basis: auto; }
  /* Seven columns cannot fit 375px — scroll instead of squeezing (never a
     broken/illegible grid). Snap-scrolling keeps one day roughly full-width
     with a peek of the next, so it still reads as "columns", not a list. */
  .ts-weekgrid {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    padding-bottom: 0.3rem;
    -webkit-overflow-scrolling: touch;
  }
  .ts-weekcol { flex: 0 0 82%; scroll-snap-align: start; }
}

/* Money first-week empty state: named next step + pending plan rows. */
.ts-money-empty { margin: 0 0 0.75rem; line-height: 1.5; }
.ts-money-spot--pending .ts-money-spot-name { color: var(--text-muted); }

/* Square-confirm name suggestion (never a silent overwrite). */
.ts-name-suggestion { margin: 0.35rem 0 0.75rem; color: var(--text-muted); font-size: 0.9rem; }

/* ============================================================================
   Owner exclusions ("Not for me" / Spots you've hidden) — the feasibility fix.
   Extends the Gallery system: tokens only, quiet by default so hiding a spot
   never competes with the primary action on a card.
   ========================================================================== */

/* The quiet "Not for me" control on a candidate card / list row. Muted until
   hovered so it reads as a secondary escape hatch, not a call to action. */
.ts-notforme {
  align-self: flex-start;
  margin-top: 0.15rem;
  padding: 0;
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: var(--font);
  font-size: 0.72rem;
  text-transform: none;
  letter-spacing: 0;
  text-decoration: underline;
  cursor: pointer;
}
.ts-notforme:hover { color: var(--accent); }
.ts-notforme:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ts-spotrow .ts-notforme { margin-top: 0; margin-left: auto; }

/* A quieter button variant for "Don't suggest this spot" / "Not feasible": a
   bordered ghost that stays out of the way of the primary action beside it. */
.ts-btn-quiet {
  background: none;
  color: var(--text-muted);
  border-color: var(--border);
}
.ts-btn-quiet:hover { color: var(--accent); border-color: var(--accent); }

/* Today card's inline "Not for me — find another": a plain link under the
   actions, so the answer stays the hero. */
.ts-answer-notforme {
  display: inline-block;
  margin-top: 0.9rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}
.ts-answer-notforme:hover { color: var(--accent); }

/* Settings "Spots you've hidden" list. */
.ts-hidden-list { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.ts-hidden-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}
.ts-hidden-row:last-child { border-bottom: none; }
.ts-hidden-name { font-size: 0.9rem; }

/* ============================================================================
   Money — per-spot cards + drill-down (goal actionable-spots-and-money, W2).
   One card per spot he has actually worked, each opening a filterable modal.
   Gallery law holds: white surfaces, #eaeaea hairlines, square corners, Oswald
   uppercase for display, and terracotta as the ONLY accent. Charts use exactly
   two data colors — --accent for the one emphasized mark, --chart-bar for
   everything else — and both are tokens, so the figures follow the system
   (including any future dark theme) instead of pinning their own hex.
   ========================================================================== */

.ts-spots-section { margin: 1.5rem 0; }
.ts-spots-title { margin-bottom: 0.85rem; }
/* The honest covered-period line under the section title ("Since March 2023").
   It pulls up under the title (which keeps its own bottom margin for the
   no-coverage case) so the two read as a title + subtitle pair. */
.ts-spots-coverage { margin: -0.6rem 0 0.85rem; font-size: 0.85rem; color: var(--text-muted); }

/* Auto-fitting grid: two-up on a laptop, one-up under ~420px. The min track is
   sized so the "$1,240 a typical day" lead never wraps mid-figure. */
.ts-moneycards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
/* The <li> is a semantic wrapper only (it carries the list count for screen
   readers); the card inside is the grid item that should stretch. */
.ts-moneycards > li { display: flex; }
.ts-moneycards > li > .ts-moneycard { flex: 1; }

/* The card is a single button — one target, one tab stop, identical by mouse,
   keyboard and touch. Elevation marks it as "this opens the detail modal"
   (Amendment 1); `overflow: hidden` keeps the full-bleed backdrop graph inside
   the square corners. */
.ts-moneycard {
  position: relative;
  overflow: hidden;
  display: block;
  width: 100%;
  max-width: none;
  text-align: left;
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-rest);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.ts-moneycard:hover { box-shadow: var(--shadow-hover); transform: translateY(-2px); }
.ts-moneycard:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); border-color: var(--accent); }

/* The background growth graph. It is pinned to the card's BOTTOM BAND while the
   figures occupy the band above (via the body's padding-bottom), so legibility
   is structural — the numbers and the graph never contend for the same pixels,
   rather than relying on opacity alone to keep text readable. */
.ts-moneycard-graph {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 62px;
  pointer-events: none;
}
.ts-moneycard-body { position: relative; display: block; padding: 1.15rem 1.15rem 4.6rem; }

/* align-items is pinned rather than left to the default: this card is a
   <button>, and the UA button styles centre flex children. */
.ts-moneycard-head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.15rem; margin-bottom: 0.9rem; }
.ts-moneycard-name {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.02em;
  font-size: 1rem;
  line-height: 1.25;
}

/* The lead figure: the one number he should read first. The unit rides directly
   under it so "$1,240" is never a bare figure whose period must be guessed. */
.ts-moneycard-lead { display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; margin-bottom: 0.9rem; }
.ts-moneycard-amount {
  font-family: var(--font-display);
  font-size: 2rem;
  line-height: 1;
  font-weight: 600;
}
.ts-moneycard-unit { font-size: 0.82rem; color: var(--text-muted); }

.ts-moneycard-stats { display: flex; flex-wrap: wrap; gap: 0.35rem 1.4rem; }
.ts-moneycard-stat { display: flex; flex-direction: column; align-items: flex-start; gap: 0.05rem; }
.ts-moneycard-stat-v { font-weight: 600; font-size: 0.95rem; }
.ts-moneycard-stat-l { font-size: 0.75rem; color: var(--text-muted); }

/* The affordance sits at the bottom, over the graph's quiet left edge. */
.ts-moneycard-more {
  position: absolute;
  left: 1.15rem;
  bottom: 1rem;
  font-size: 0.78rem;
  color: var(--text-muted);
}
.ts-moneycard:hover .ts-moneycard-more { color: var(--accent); }

/* ---- Shared chart marks (card backdrop + modal charts) ---- */
/* The area fill is what makes the card backdrop read as texture rather than as
   a chart demanding to be measured; the line gives it just enough definition to
   show direction. */
.ts-spotgraph-area { fill: var(--accent); opacity: 0.08; }
.ts-spotgraph-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  opacity: 0.3;
  vector-effect: non-scaling-stroke;
}

/* ---- Modal: revenue-over-time ---- */
.ts-chart { margin: 0; }
.ts-chart-plot { position: relative; }
.ts-chart-svg { display: block; width: 100%; height: 120px; }
/* A hairline zero baseline: with the fill gone the line needs something to sit
   against, and it makes the zero-anchored scale visible rather than implied. */
.ts-chart-base { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }
.ts-chart-caption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  margin-top: 0.4rem;
  font-size: 0.75rem;
  border-top: 1px solid var(--border);
  padding-top: 0.35rem;
}
.ts-chart-legend { text-align: center; }
.ts-chart-empty { margin: 0.3rem 0; font-size: 0.9rem; }

/* ---- Modal: weekday emphasis columns ---- */
/* Note on type sizes below: nothing in this block goes under 0.75rem (12px).
   These labels carry the derivation of every figure ("median of 51 days",
   "51d"), and they are read at --text-muted in a truck in daylight — the one
   place the system must not get precious about small type. */
.ts-wd { margin: 0; }
.ts-wd-cols { display: flex; align-items: flex-end; gap: 0.3rem; }
.ts-wd-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.25rem; min-width: 0; }
/* 56px, not 76px: a single worked weekday renders as one solid terracotta
   block, and at 76px that block dominated the modal for what is a one-line
   fact ("you work Fridays here"). */
.ts-wd-track { width: 100%; height: 56px; display: flex; align-items: flex-end; }
.ts-wd-bar { width: 100%; background: var(--chart-bar); border-radius: var(--radius); }
/* The single emphasized column. Terracotta here is an implicit recommendation,
   which is why bestWeekday() requires repeat visits before awarding it. */
.ts-wd-bar--best { background: var(--accent); }
/* A weekday never worked: a hairline stub at the baseline, so the absence sits
   where a bar would start instead of floating at the top of the column. */
.ts-wd-none { width: 100%; height: 2px; background: var(--border); }
.ts-wd-fig { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); }
.ts-wd-fig--best { color: var(--accent); font-weight: 500; }
.ts-wd-day { font-size: 0.75rem; color: var(--text-muted); }
.ts-wd-day--best { color: var(--text); font-weight: 600; }
.ts-wd-count { font-size: 0.75rem; color: var(--text-muted); min-height: 0.9em; }
.ts-wd-caption { justify-content: flex-start; }
.ts-wd-key { font-family: var(--font-mono); }

/* ---- Modal: layout ---- */
/* The date range keeps each date whole. Inline after the sentence it broke
   mid-date at 375px ("Wed, Jul" / "15, 2026"), which reads as a broken range;
   on its own line it fits intact. */
.ts-spotmodal-range { display: inline-block; }
@media (max-width: 480px) {
  .ts-spotmodal-range { display: block; margin-top: 0.15rem; }
}

.ts-spotmodal-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}
/* Date/weekday controls are short values — the global 420px input cap is far too
   wide for them and would read as unconsidered. */
.ts-spotmodal-filters input,
.ts-spotmodal-filters select { width: auto; min-width: 9.5rem; max-width: 100%; }
.ts-spotmodal-clear { font-size: 0.82rem; padding-bottom: 0.55rem; }

.ts-spotmodal-figs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 1rem;
  padding: 1rem 0;
  border-bottom: 1px solid var(--border);
}
/* Every modal figure carries label + value + derivation, so a filtered number
   can never be ambiguous about the period or the maths behind it. */
.ts-spotfig { display: flex; flex-direction: column; gap: 0.1rem; }
.ts-spotfig-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); font-family: var(--font-display); }
.ts-spotfig-value { font-size: 1.3rem; font-weight: 600; font-family: var(--font-display); }
.ts-spotfig-note { font-size: 0.75rem; color: var(--text-muted); }

.ts-spotmodal-section { padding: 1rem 0; border-bottom: 1px solid var(--border); }
.ts-spotmodal-section:last-child { border-bottom: none; padding-bottom: 0; }
.ts-spotmodal-h {
  margin: 0 0 0.6rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  font-weight: 600;
}
.ts-spotmodal-lede { margin: 0 0 0.7rem; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }

/* ---- Modal: the calendar⋈Square correlation table ---- */
.ts-corr { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.ts-corr th {
  text-align: left;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  font-weight: 600;
  padding: 0.35rem 0.5rem 0.35rem 0;
  border-bottom: 1px solid var(--border);
}
.ts-corr td { padding: 0.4rem 0.5rem 0.4rem 0; border-bottom: 1px solid var(--border); }
.ts-corr tr:last-child td { border-bottom: none; }
/* The left pad is what keeps two ADJACENT right-aligned columns apart: with the
   receipt's new "vs typical" column, "VS TYPICAL" and "SQUARE SALES" ran
   together into one word at 375px. */
.ts-corr-num {
  text-align: right;
  padding-right: 0 !important;
  padding-left: 0.5rem !important;
  white-space: nowrap;
}

/* The honest block: real money we deliberately refuse to attribute. Set on the
   quiet grey surface so it reads as a caveat, not as a second set of results. */
.ts-corr-honest { margin-top: 1rem; padding: 0.8rem; background: var(--surface-2); }
.ts-corr-honest-h { margin: 0 0 0.35rem; font-size: 0.85rem; font-weight: 600; }
.ts-corr-honest-note { margin: 0 0 0.6rem; font-size: 0.8rem; line-height: 1.5; }

/* Not-yet-available sections state their absence in words; they must never look
   like data (ground-truth-first: no hypothetical where a fact belongs). */
.ts-spotmodal-soon p { font-size: 0.85rem; line-height: 1.5; margin: 0; }

@media (max-width: 480px) {
  /* At 375px a single column keeps the lead figure whole and the tap target
     full-width. */
  .ts-moneycards { grid-template-columns: 1fr; }
  .ts-moneycard-amount { font-size: 1.75rem; }
  .ts-spotmodal-filters input,
  .ts-spotmodal-filters select { min-width: 0; width: 100%; }
  .ts-spotmodal-filters label { flex: 1 1 8rem; }
  /* Seven columns of money labels don't fit 375px — drop the per-column figure
     and keep the bars, day letters and the spoken aria description. */
  .ts-wd-fig { display: none; }
  .ts-wd-cols { gap: 0.15rem; }
  /* Three items on one caption row squeeze the end dates onto two lines each.
     Give the legend its own row and keep the dates as a clean pair. */
  .ts-chart-caption { flex-wrap: wrap; }
  .ts-chart-legend { order: 3; flex-basis: 100%; text-align: left; }
}

@media (prefers-reduced-motion: reduce) {
  .ts-moneycard:hover { transform: none; }
}

/* ====================== Operating-cost reminder toast (W6) ====================== */
/* A floating card, not an inline banner — it must survive navigation until
   acted on, so it sits fixed above the chrome. z-index sits below the modal
   scrim (100) so an open modal always wins if both are ever on screen. The
   accent-left rule + white surface matches .ts-banner-info; the elevation
   (--shadow-modal) is what marks it as floating rather than in-flow. */
.ts-costtoast {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 90;
  width: min(380px, calc(100vw - 2.5rem));
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  padding: 1rem 1.1rem;
}
.ts-costtoast-title {
  margin: 0 0 0.35rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: 0.85rem;
  letter-spacing: 0.03em;
}
.ts-costtoast-copy { margin: 0 0 0.85rem; font-size: 0.85rem; line-height: 1.5; color: var(--text-muted); }
.ts-costtoast-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
/* A quiet text action, not a button — "turn off" is a real, findable setting
   (it's also in Settings), so it doesn't need equal visual weight to Save/Not now. */
.ts-costtoast-off {
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 0.8rem;
  text-decoration: underline;
  cursor: pointer;
  /* min-height + inline-flex, not just padding: a fixed floor keeps the tap
     target at 44px regardless of font metrics, matching the 44px minimum the
     rest of the app holds to. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding: 0 0.2rem;
  margin-left: auto;
}
.ts-costtoast-off:hover { color: var(--text); }
.ts-costtoast-off:disabled { color: var(--border-strong); cursor: default; }

@media (max-width: 480px) {
  /* Full-width, docked to the bottom edge at 375px rather than a floating
     card with margins on both sides. */
  .ts-costtoast { left: 0.75rem; right: 0.75rem; bottom: 0.75rem; width: auto; }
  .ts-costtoast-off { margin-left: 0; }
}

/* ====================================================================== *
 * U10 — SpotModal "What sold here": item sell-mix + prep suggestion.
 * Gallery tokens only, no new hex. Actuals (ItemGroup, SpotModal.tsx) reuse
 * the existing .ts-corr table — no new rule needed there. The block below is
 * ONLY the prep suggestion's distinct treatment: dashed border + tinted
 * surface, the same "not-actual" language .ts-weekcol--suggested already
 * established (ground-truth-first: a suggestion must never be mistaken for a
 * fact printed right above it).
 * ====================================================================== */
.ts-spotmodal-items { margin-top: 0.9rem; }
.ts-spotmodal-items:first-child { margin-top: 0; }

.ts-spotmodal-suggest {
  margin-top: 1rem;
  padding: 0.85rem 0.9rem;
  border: 1px dashed var(--border-strong);
  background: var(--surface-2);
}
.ts-spotmodal-suggest-label {
  display: block;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.68rem;
  color: var(--text-muted);
  margin-bottom: 0.3rem;
}
.ts-spotmodal-suggest-copy { margin: 0 0 0.6rem; font-size: 0.85rem; line-height: 1.5; }
.ts-spotmodal-suggest-list { list-style: none; margin: 0 0 0.6rem; padding: 0; }
.ts-spotmodal-suggest-list li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.3rem 0;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
}
.ts-spotmodal-suggest-list li:first-child { border-top: none; }
.ts-spotmodal-suggest-cost { margin: 0; font-size: 0.8rem; color: var(--text-muted); }
/* ============================================================================
   Owner-booking tier ("On your calendar") — U14, WEEK-CAL c. Ground-truth
   -first (docs/goals/ground-truth-first.md G4): an owner calendar entry is
   his own stated fact, so it outranks every other tier on both the Today
   grid and the Week list — suggested, approved, and even an already
   -resolved done/actual day. Gallery tokens only: accent is the strongest
   treatment the system has (already used for stamps/primary CTAs), given a
   heavier border than the --ok (approved) and dashed-muted (suggested)
   tiers so it reads as visually distinct, not merely first in DOM order.
   The absence of an actions row (no Approve/Swap/Not feasible) is itself
   part of the distinction — there is no agent suggestion here to act on.
   ========================================================================== */

/* Today grid column. */
.ts-weekcol--owner {
  border-top-width: 4px;
  border-top-style: solid;
  border-top-color: var(--accent);
  background: var(--surface);
}
.ts-weekcol-bookings { display: flex; flex-direction: column; gap: 0.5rem; }
/* Multi-stop day: a hairline between entries so neither reads as buried
   under the other (rule: neither booking is implied to be "the real one"). */
.ts-weekcol-booking + .ts-weekcol-booking {
  padding-top: 0.5rem;
  border-top: 1px dashed var(--border);
}
.ts-weekcol-spot--owner { color: var(--accent); }

/* Week list row. */
.ts-week-row--owner { border-left-width: 4px; border-left-color: var(--accent); }
.ts-week-booking + .ts-week-booking {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px dashed var(--border);
}
.ts-week-spot--owner { color: var(--accent); }

/* ============================================================================
   Menu — import + items (U16 design pass). Extends the Gallery system, tokens
   only. Root cause this block fixes: MenuView previously had no CSS of its
   own at all — the whole surface (upload card, Square import card, item
   table) rode entirely on generic .ts-form/.ts-table/.ts-muted primitives, so
   nothing distinguished "an action you can take" from "your data" and a
   failed parse looked identical to nothing having happened.
   ========================================================================== */

/* The "Current menu" card is the one card on this page holding HIS data (the
   parse status + item table) — everything above it is an input/action. A
   heavier top rule marks that split, the same hero-card device Today/Money
   already use, and its color reports the state at a glance without reading
   the status line. */
.ts-menu-current { border-top: 3px solid var(--border); }
.ts-menu-current--ok { border-top-color: var(--ok); }
.ts-menu-current--failed { border-top-color: var(--danger); }

/* Parse-status line. Plain by default (e.g. "pending"); the ok modifier gives
   a successful parse the same confirmed-state treatment other views use
   (ts-week-ok, ts-answer-set) instead of reading like an unstyled log line. A
   failed parse does not use this class at all — MenuView renders an explicit
   error Banner with a next step instead of a status word next to an empty
   table. */
.ts-menu-status { margin: 0 0 0.75rem; color: var(--text-muted); }
.ts-menu-status--ok { color: var(--ok); font-weight: 600; }
.ts-menu-status--ok span { margin-right: 0.3rem; }

/* Founder amendment: item cards replace the paged table (S2). Grouped by
   category; auto-fill/minmax already collapses to one column under ~460px
   content width with no separate breakpoint needed — verified at 375px. */
.ts-menu-items { margin-top: 0.25rem; }
.ts-menu-category-heading {
  margin: 1.1rem 0 0.6rem;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.8rem;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  /* Square-imported categories arrive Title Case ("Tacos"); hand-parsed ones
     arrive lowercase ("dogs") — normalize display only, the stored category
     string (his data) is never altered. */
  text-transform: capitalize;
}
.ts-menu-category-heading:first-child { margin-top: 0; }
.ts-menu-item-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}
.ts-menu-item-card {
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 0.85rem 1rem;
}
.ts-menu-item-top { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; }
.ts-menu-item-name { font-weight: 600; }
.ts-menu-item-price { font-family: var(--font-mono); white-space: nowrap; }
.ts-menu-item-desc {
  margin: 0.3rem 0 0;
  color: var(--text-muted);
  font-size: 0.85rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ============================================================================
   Financials — per-visit bars, real prep quantities, best sellers on the card
   (U21/U22/U23, founder batch 2026-07-22). Gallery tokens only; no hex outside
   :root. Three founder complaints, three blocks below:

   A. "The Money cards still do not show what items sold best at a location."
      -> .ts-moneycard-sellers: the top-3 sell-mix on the CARD.
   B. "The suggestion is worthless." -> .ts-spotmodal-suggest-* regains a
      quantity-first three-column row; the block itself now returns null with
      no quantities, so the styles below are only ever seen carrying numbers.
   C. "This data should be represented on the bargraph above it."
      -> .ts-chart-bar*: one bar per worked day on a real date axis, with the
      hatch treatment for money we refuse to attribute and a selection stroke
      shared with the receipt table's .ts-corr-row--on.

   Palette discipline is unchanged: two data colors total (--accent for the one
   emphasized mark, --chart-bar for everything else). Selection is a STROKE and
   a surface tint, never a third hue — "you are pointing at this" is not a fact
   about the data.
   ========================================================================== */

/* ---- A. Best sellers on the money card ---- */
/* Sits between the stat row and the "See every day" affordance. The label is
   quiet and the value carries the weight: the ITEM is the answer here, the
   quantity is its evidence. Two lines at 375px, one on desktop. */
.ts-moneycard-sellers {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  margin-top: 0.75rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
  /* Reserved height: once ANY card in the grid has a sell-mix, every card
     keeps this row so the grid does not go ragged. A card with no item data
     holds the space empty rather than printing a placeholder row. */
  min-height: 2.3rem;
}
.ts-moneycard-sellers-l {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  font-family: var(--font-display);
}
.ts-moneycard-sellers-v {
  font-size: 0.85rem;
  line-height: 1.35;
  /* Two lines max: a truck with a 30-item menu must not push the card's
     affordance off the bottom. The modal has the full list. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ---- C. Per-visit bars on a real date axis ---- */
.ts-chart-bar {
  fill: var(--chart-bar);
  /* Bars can be a fraction of a viewBox unit wide on a dense history; without
     this they anti-alias into a grey wash. */
  shape-rendering: crispEdges;
}
/* The single emphasized mark: his best day here. Same meaning terracotta
   already carries on .ts-wd-bar--best. */
.ts-chart-bar--best { fill: var(--accent); }
/* Money we deliberately refuse to attribute to one booking. Hatched, not a
   third color — the treatment says "this is real and it is not counted",
   which is the same thing .ts-corr-honest says in words. */
.ts-chart-bar--shared {
  fill: url(#ts-chart-hatch);
  /* The outline is what makes a 4px-wide hatched bar readable at all — inside
     that width only one or two hatch stripes land, so the bar's EDGE has to
     carry the "this one is different" signal. */
  stroke: var(--text);
  stroke-width: 0.75;
  stroke-dasharray: 2 2;
}
.ts-chart-hatch-line { stroke: var(--border-strong); stroke-width: 2.5; }
/* Selection: an outline, applied on top of whatever fill the bar already has. */
.ts-chart-bar--on { stroke: var(--text); stroke-width: 1.5; paint-order: stroke; }
/* Full-height invisible hit target — a $40 day is a couple of viewBox units
   tall and would otherwise be untappable on a phone. Widths come from
   spotBars.hitSlots and never overlap, so two adjacent calendar days stay
   individually selectable (SVG hit testing is document order: an overlapping
   target would simply swallow its neighbour). */
.ts-chart-hit { fill: transparent; cursor: pointer; }

/* The caption's centre slot holds either the legend or the pointed-at day's
   readout. One slot, so moving across the bars never reflows the caption. */
.ts-chart-readout { text-align: center; font-size: 0.75rem; }

/* ---- C. The receipt table under the chart ---- */
.ts-corr-details { margin-top: 0.9rem; }
.ts-corr-summary {
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  /* 44px floor, same as every other tap target in the app. */
  min-height: 44px;
  display: flex;
  align-items: center;
}
.ts-corr-summary:hover { color: var(--text); }
.ts-corr--linked tr { cursor: pointer; }
.ts-corr--linked tr:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The other half of the two-way link with the chart. A surface tint, matching
   the stroke on the bar rather than competing with it. */
.ts-corr-row--on td { background: var(--surface-2); }
/* A shared row is money not counted in the figures above: muted, and the row
   says so in words in its own amount cell. */
.ts-corr-row--shared td { color: var(--text-muted); }
/* The competing-bookings line wraps on its own row under the date. Inline, it
   is one unbreakable run that widens the Day column until the money column is
   pushed out of the modal. */
.ts-corr-shared-note { display: block; margin-top: 0.2rem; font-size: 0.75rem; line-height: 1.4; }
/* The receipt can be wider than a 375px modal; it scrolls in its own box
   rather than letting the page scroll sideways. */
.ts-corr-details .ts-corr { min-width: 20rem; }
.ts-corr-scroll { overflow-x: auto; }

/* "vs typical" — the column that replaces the dead "Your booking" one. Mono so
   the percentages form a readable column; the tone is a tint, not a badge,
   because every row carries one and 52 badges is a wall. */
.ts-corr-vs { font-family: var(--font-mono); font-size: 0.8rem; }
.ts-corr-vs--up { color: var(--ok); }
.ts-corr-vs--down { color: var(--text-muted); }
.ts-corr-vs--flat { color: var(--text-muted); }

/* The honest note now sits under the chart (the shared days are drawn there)
   rather than heading a second table of its own. */
.ts-corr-honest-note--inline { margin: 0.6rem 0 0; }

/* ---- B. Quantity-first prep suggestion ---- */
/* Three columns: the quantity (the instruction), the item, and what that many
   is worth at his own prices (the reason). The quantity column is fixed-width
   so the numbers form a scannable column instead of ragging against the names
   — that column IS the content, which is the whole point of B2. */
.ts-spotmodal-suggest-head {
  margin: 0 0 0.5rem;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.4;
}
.ts-spotmodal-suggest-list li {
  display: grid;
  grid-template-columns: 3.2rem 1fr auto;
  gap: 0.6rem;
  align-items: baseline;
  justify-content: initial;
}
.ts-spotmodal-suggest-qty { font-weight: 600; text-align: right; }
.ts-spotmodal-suggest-name { min-width: 0; }
.ts-spotmodal-suggest-worth { color: var(--text-muted); font-size: 0.8rem; }
/* The basis drops UNDER the numbers and loses the method narration it used to
   lead with. One clause, muted, stating the evidence and nothing else. */
.ts-spotmodal-suggest-basis {
  margin: 0 0 0.5rem;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text-muted);
}

@media (max-width: 480px) {
  /* The worth column is the first thing to go at 375px — the quantity and the
     item are the instruction, the money is the justification. */
  .ts-spotmodal-suggest-list li { grid-template-columns: 2.8rem 1fr; }
  .ts-spotmodal-suggest-worth { grid-column: 2; text-align: left; }
  /* Three caption slots do not fit; the readout takes its own row. */
  .ts-chart-readout { order: 3; flex-basis: 100%; text-align: left; }
}

/* === Week outlook, day detail, and Events range ========================== */
/* Added 2026-07-22 (swarm/week-dash-events). ONE block, appended at the very
   end of the file, so it can never re-open or swallow a rule above it. Every
   comment opened in this block is closed on its own line. */

/* --- The per-day dollar outlook ---------------------------------------- */
/* A FOURTH visual tier, deliberately distinct from owner / actual / approved /
   suggested: the figure is generated from his history, so it must not be
   mistaken for money he actually made (docs/goals/ground-truth-first.md). It is
   quieter than an actual, dashed rather than solid, and the basis is always on
   screen beside it - never a tooltip. */
.ts-outlook {
  margin: 0.35rem 0 0.6rem;
  padding: 0.4rem 0 0.4rem 0.65rem;
  border-left: 2px dashed var(--border-strong);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.6rem;
  font-size: 0.9rem;
  line-height: 1.4;
}
.ts-outlook-figure {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 1rem;
  color: var(--text);
}
.ts-outlook-basis { color: var(--text-muted); font-size: 0.85rem; }
/* Low confidence is stated, not implied: a visible chip, not a lighter grey. */
.ts-outlook-flag {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.68rem;
  color: var(--warn);
  border: 1px solid var(--warn);
  padding: 0.05rem 0.35rem;
  white-space: nowrap;
}
.ts-outlook--low .ts-outlook-figure { color: var(--text-muted); }
.ts-outlook--none { border-left-style: dotted; color: var(--text-muted); font-size: 0.85rem; }

/* --- Day label as the "show me this location" affordance ---------------- */
.ts-week-day--link {
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  flex: 0 0 4.5rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.8rem;
  color: var(--text-muted);
  padding: 0.1rem 0 0;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
}
.ts-week-day--link:hover { color: var(--accent); }
.ts-week-day--link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- Day detail panel --------------------------------------------------- */
.ts-daydetail-section { margin-top: 1rem; }
.ts-daydetail-h {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.78rem;
  color: var(--text-muted);
}
.ts-daydetail-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 0.6rem;
  margin: 0 0 0.5rem;
}
.ts-daydetail-stats dt {
  font-size: 0.75rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.ts-daydetail-stats dd { margin: 0.15rem 0 0; font-family: var(--font-mono); font-size: 1.05rem; }

/* --- Events range controls ---------------------------------------------- */
.ts-events-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.75rem;
}
.ts-segmented { display: inline-flex; border: 1px solid var(--border-strong); }
.ts-seg {
  background: var(--surface);
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: 0.85rem;
  padding: 0.4rem 0.9rem;
  color: var(--text-muted);
  min-height: 2.25rem;
}
.ts-seg + .ts-seg { border-left: 1px solid var(--border-strong); }
.ts-seg-on { background: var(--text); color: var(--bg); font-weight: 600; }
.ts-seg:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ts-stepper { display: inline-flex; align-items: center; gap: 0.4rem; }
.ts-events-datepick input {
  font: inherit;
  font-size: 0.85rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  min-height: 2.25rem;
}
/* The list area alone shows the pending state - the controls and header stay
   mounted, so stepping never flashes the whole page. */
.ts-events-body { min-height: 8rem; }
.ts-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
@media (max-width: 640px) {
  .ts-events-controls { flex-direction: column; align-items: stretch; }
  /* Stretched to full width, the two segments split it evenly — a half-filled
     control with dead space to the right reads as a layout bug. */
  .ts-segmented { display: flex; }
  .ts-seg { flex: 1 1 0; text-align: center; }
  .ts-stepper { justify-content: space-between; }
  .ts-events-datepick { flex: 1 1 auto; }
  .ts-events-datepick input { width: 100%; }
}
/* Matches the existing 460px rule for .ts-week-day: the row stacks, so the day
   label must not hold a 4.5rem column reservation. */
@media (max-width: 460px) {
  .ts-week-day--link { flex-basis: auto; }
}
/* === end week outlook / day detail / events range ======================== */
