/* Redesign layer — Figma design language.
   Loaded after styles.css so it wins, which keeps the 2 500-line original
   intact and makes this change reviewable on its own.
   Reference: ~/.claude/plans/web-app-async-scott.md */

/* --- shared type ------------------------------------------------------- */
.eyebrow {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text);
  margin: 0;
}
.eyebrow.is-lead { font-size: 13px; font-weight: 700; }
.eyebrow.is-strong { font-weight: 700; font-size: 11px; letter-spacing: 0.7px; }

.display-figure {
  font-size: 40px;
  font-weight: 340;
  letter-spacing: -1px;
  line-height: 1;
  color: var(--text);
  margin: 0;
  font-variant-numeric: tabular-nums;
}
.body-line {
  font-size: 14px;
  font-weight: 330;
  letter-spacing: -0.14px;
  line-height: 1.45;
  color: var(--text);
  margin: 0;
}
.pos-delta { color: var(--positive); font-weight: 480; }

/* This system has exactly no shadows. */
.panel,
.section,
.card,
.action-card,
.live-kpi,
.surface { box-shadow: none !important; }

/* --- pills ------------------------------------------------------------- */
button.primary,
.pill-action {
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  border-radius: 50px;
  padding: 12px 22px;
  font-size: 14px;
  font-weight: 480;
  letter-spacing: -0.1px;
  min-height: 44px;
}
button.ghost,
.pill-quiet {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 50px;
  padding: 11px 20px;
  font-size: 14px;
  font-weight: 480;
  min-height: 44px;
}
.pill-selected {
  background: var(--accent-weak);
  color: var(--accent-ink);
  border: 1px solid var(--accent-line);
  border-radius: 50px;
  padding: 11px 20px;
  font-size: 14px;
  font-weight: 480;
  min-height: 44px;
}

/* --- pastel blocks ----------------------------------------------------- */
.block {
  border-radius: 24px;
  padding: 15px 16px;
  color: var(--text);
}
.block-cream { background: var(--block-cream); }
/* Three steps lighter than --block-mint, which stays as it is for the matrix
   bands that use the variable directly. Only the Home "Now vs target" card
   carries this class, so letting it down here reaches exactly that card. */
.block-mint { background: #eaf6ed; }
.block-pink { background: var(--block-pink); }
.block-coral { background: var(--block-coral); }
.block-lilac { background: var(--block-lilac); }

/* --- bottom tab bar ---------------------------------------------------- */
/* Sticky, not fixed: fixed fights the chat composer's visualViewport sizing. */
.workspace {
  display: flex;
  flex-direction: column;
  min-height: calc(100dvh - 56px);
}
.workspace > .view { flex: 1 1 auto; }
.tabbar {
  position: sticky;
  bottom: 0;
  z-index: 20;
  display: flex;
  gap: 3px;
  padding: 8px 4px calc(8px + env(safe-area-inset-bottom));
  margin: 14px -16px 0;
  background: var(--surface);
  border-top: 1px solid var(--hairline-soft);
}
.tabbtn {
  flex: 1;
  min-height: 48px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 6px 0;
  border: 0;
  border-radius: 50px;
  background: transparent;
  color: var(--text);
  position: relative;
}
.tabbtn.is-active { background: var(--accent-weak); color: var(--accent-ink); }
.tabbtn svg { width: 20px; height: 20px; display: block; }
.tabbtn-label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.tabbtn.is-active .tabbtn-label { font-weight: 700; }
.tab-dot {
  position: absolute;
  top: 5px;
  right: calc(50% - 17px);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--positive);
}
.tab-thinking {
  position: absolute;
  top: 8px;
  right: calc(50% - 19px);
  display: inline-flex;
  gap: 2px;
}
.tab-thinking i {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent-ink);
  animation: tabdots 1.2s infinite ease-in-out;
}
.tab-thinking i:nth-child(2) { animation-delay: 0.15s; }
.tab-thinking i:nth-child(3) { animation-delay: 0.3s; }
@keyframes tabdots {
  0%, 60%, 100% { opacity: 0.25; }
  30% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .tab-thinking i { animation: none; opacity: 0.7; }
}

/* --- progress bar with a pace marker ----------------------------------- */
.progress {
  position: relative;
  height: 7px;
  background: rgba(var(--ink-rgb), 0.16);
  border-radius: 50px;
  margin: 6px 0 4px;
}
.progress.is-lead { height: 9px; margin: 10px 0 5px; }
.progress-fill { height: 100%; background: var(--accent-ink); border-radius: 50px; }
.progress-tick {
  position: absolute;
  top: -3px;
  width: 2px;
  height: 13px;
  background: var(--text);
}
.progress.is-lead .progress-tick { height: 15px; }
.progress-caps { display: flex; justify-content: space-between; gap: 8px; }
.cap {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text);
  margin: 0;
}
.cap.is-ok { color: var(--positive); font-weight: 700; }
.cap.is-behind { font-weight: 700; }

/* --- home view --------------------------------------------------------- */
.home-topline {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.icon-button {
  /* 44 not 40: styles.css floors these at min-height 44 AND min-width 56, so
     the "round" control was really a 56x44 ellipse. Both floors are released
     here; 44 is also the touch-target minimum, so circle and finger agree. */
  min-width: 44px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface-soft);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
}
.branch-target + .branch-target {
  padding-top: 7px;
  border-top: 1px solid rgba(var(--ink-rgb), 0.14);
}
.branch-target { padding-bottom: 7px; }
/* Scoped to the card: the same .progress sits under the MTD figure with its
   own is-lead spacing, and elsewhere on its default. */
.branch-target .progress { margin: 5px 0 3px; }
.branch-target:last-child { padding-bottom: 0; }
.branch-target-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}
.branch-target-head strong,
.branch-target-head span {
  font-size: 14px;
  font-weight: 480;
  letter-spacing: -0.14px;
}
.branch-target-head span { font-variant-numeric: tabular-nums; }

/* The MTD tile grid that used to live here is gone: Home now renders the same
   eight-figure card as Live, from kpiCells(), so there is one component and
   one definition of what the figures are. */

/* --- live matrix ------------------------------------------------------- */
.live-matrix-wrap { margin-inline: -16px; }
.live-matrix { width: 100%; border-collapse: collapse; table-layout: fixed; }
.live-matrix caption { display: none; }
.live-matrix th,
.live-matrix td {
  padding: 7px 3px;
  font-size: 11px;
  text-align: right;
  color: var(--text);
  line-height: 1.25;
  border: 0;
  background: transparent;
  font-variant-numeric: tabular-nums;
}
.live-matrix th[scope="row"] {
  text-align: left;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.3px;
  font-weight: 400;
  width: 93px;
  padding-left: 16px;
}
.live-matrix td:last-child { padding-right: 16px; font-weight: 700; }
/* Colouring follows the owner's reference sheet: a figure in its own right
   sits on plain white, and only the rows DERIVED from it are tinted — cream
   under the label, a wash of the band's own green under the numbers. Both
   tints are the app's --block-cream and --block-mint let down over white
   (~60% and ~35%), so the table stays in the same palette as the bands above
   it rather than introducing a second family of colours.
   The derived rows also step in exactly one character — the label column is
   mono, so 1ch is literally one glyph — which is what makes "Share" read as
   belonging to the "Revenue" above it. */
.live-matrix tr.is-sub > th[scope="row"] { padding-left: calc(16px + 1ch); }
/* Owner: the whole table reads bold. The figure rows and the percentages
   derived from them are still told apart — by the row tint and the one-glyph
   indent — so weight is free to be uniform. */
.live-matrix th,
.live-matrix td,
.live-matrix th[scope="row"],
.live-matrix .money-cell,
.live-matrix .info-cell,
.live-matrix .soft-cell,
.live-matrix .warn-cell,
.live-matrix .live-total-cell { font-weight: 700; }
/* a just-updated figure is marked by its own colour, nothing else — see the
   .changed-cell block further down */

/* Section band. The label sits in a <td>, which inherits text-align:right from
   the table — so left has to be stated or it drifts back to the right edge. */
.live-matrix tr.live-band > th,
.live-matrix tr.live-band > td {
  background: var(--block-mint);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  padding-top: 8px;
  padding-bottom: 8px;
}
.live-matrix tr.live-band > th[scope="row"],
.live-matrix tr.live-band > td.band-label {
  text-align: left;
  padding-left: 16px;
}

/* Eight figures in a 4x2 grid, coloured by PAIR the way the owner's sheet is:
   revenue over margin in one colour, so a column reads as one subject. The
   tints are the app's own blocks — accent-weak, cream, coral, lilac, pink —
   rather than a new set, so this card still belongs to the same app. */
.live-kpi-strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-radius: 16px;
  overflow: hidden;
  margin-bottom: 12px;
  gap: 0;
  background: transparent;
}
.live-kpi.kpi-total  { background: var(--accent-weak); }
.live-kpi.kpi-pro    { background: var(--block-cream); }
.live-kpi.kpi-china  { background: var(--block-coral); }
.live-kpi.kpi-basket { background: var(--block-lilac); }
.live-kpi.kpi-bogo   { background: var(--block-pink); }
.live-kpi {
  background: transparent;
  border: 0;
  border-right: 1px solid rgba(var(--ink-rgb), 0.16);
  border-bottom: 1px solid rgba(var(--ink-rgb), 0.16);
  border-radius: 0;
  padding: 11px 12px;
  margin: 0;
  display: block;
}
.live-kpi:nth-child(2n) { border-right: 0; }
.live-kpi:nth-last-child(-n + 2) { border-bottom: 0; }
.live-kpi small {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: 4px;
}
.live-kpi strong {
  font-size: 20px;
  font-weight: 340;
  letter-spacing: -0.4px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* --- Yoyo shortcut chips: one scrolling row ---------------------------- */
.chip-rail-wrap { position: relative; }
.chip-rail {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 2px 0 6px;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.chip-rail::-webkit-scrollbar { display: none; }
/* ...except where the pointer is a mouse. A hidden scrollbar is right on a
   phone, where the rail is swiped, and wrong on a PC, where it left the chips
   past the right edge both invisible and unreachable. A thin bar says "this
   slides" and can be dragged; app.js also lends the rail the wheel. */
@media (pointer: fine) {
  .chip-rail {
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
    padding-bottom: 2px;
  }
  .chip-rail::-webkit-scrollbar { display: block; height: 6px; }
  .chip-rail::-webkit-scrollbar-track { background: transparent; }
  .chip-rail::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 999px;
  }
  .chip-rail:hover::-webkit-scrollbar-thumb { background: var(--muted); }
}
.chip-rail > * { flex: 0 0 auto; white-space: nowrap; }
.chip-rail-fade {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 6px;
  width: 34px;
  pointer-events: none;
  background: linear-gradient(90deg, rgba(var(--surface-rgb), 0), var(--surface) 70%);
}

/* --- More list --------------------------------------------------------- */
.more-row {
  width: 100%;
  min-height: 52px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 0;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  color: var(--text);
  font-size: 14px;
  font-weight: 480;
  letter-spacing: -0.14px;
  text-align: left;
}
.more-row:last-child { border-bottom: 0; }
.more-row .sub {
  display: block;
  font-weight: 330;
  font-size: 13px;
}
/* "There is something here you have not opened." Same dot as the tab bar's,
   inline after the row title rather than floating over it: this row is a line
   of text, not an icon, so the mark belongs in the reading order. */
.more-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-left: 7px;
  vertical-align: 2px;
  border-radius: 50%;
  background: var(--positive);
}

/* --- misc -------------------------------------------------------------- */
/* The one notice in the app: "Yoyo is not available for Save Care yet" under
   the database picker. A filled amber panel gave a fact about one of the two
   databases the weight of a warning, on a screen that is otherwise all pale
   glass. It is a footnote now -- no fill, no border, grey type.

   A literal grey, not --muted: .login-view is pinned to the light palette
   where --muted is pure black, because everywhere else in this app hierarchy
   comes from weight rather than from grey. This screen is the exception, and
   the tone matches the violet-grey it already uses for "Secure connection" --
   only darker, since that one measures 3.5:1 and this line carries a fact. */
.notice {
  border: 0;
  border-radius: 0;
  padding: 2px 0 0;
  background: none;
  font-size: 12.5px;
  font-weight: 400;
  color: #6a6780;
}

/* --- icon buttons must not inherit the pill padding --------------------- */
/* .chat-icon-btn is a fixed 44px square; the generic pill rules above blow it
   out and squash the 20px glyph to nothing. */
button.primary.chat-icon-btn,
button.ghost.chat-icon-btn,
button.chat-icon-btn {
  padding: 0;
  width: 44px;
  flex: 0 0 44px;
  min-height: 44px;
  border-radius: 50%;
}
button.ghost.chat-icon-btn { background: var(--surface); border: 1px solid var(--border); color: var(--text); }
button.primary.chat-icon-btn { background: var(--accent); border: 0; color: var(--on-accent); }
.chat-icon-btn svg { width: 20px; height: 20px; display: block; }

/* --- legends carry meaning, so they carry distinct colour --------------- */
.android-legend { color: var(--text); }
.android-segment-list .android-bar-fill { background: #2c6588; }
.android-segment-list .android-segment:nth-child(2) .android-bar-fill { background: #4a7c59; }
.android-segment-list .android-segment:nth-child(3) .android-bar-fill { background: #7a5aa8; }

.bogo-legend .is-good::before { background: var(--positive); }
.bogo-legend .is-ok::before { background: var(--warn); }
.bogo-legend .is-bad::before { background: var(--danger); }
.android-stack .good { background: var(--positive); }
.android-stack .ok { background: var(--warn); }
.android-stack .bad { background: var(--danger); }

/* --- a cell that just changed ------------------------------------------ */
/* Was a green dot in the corner; now the figure itself turns dark blue, which
   reads at a glance without adding furniture to a dense table. Colour ONLY —
   the cell keeps whatever weight its row already had, so a light share row
   stays light and the bold Total column stays bold. */
/* The !important was here to beat `.live-matrix td { color: var(--text) }` — but an
   author !important also beats a keyframe, so the fade-back below could never
   run. Specificity does the same job without blocking the animation:
   .live-matrix .changed-cell is (0,2,0) against that rule's (0,1,1). */
.live-matrix .changed-cell {
  background: transparent;
  color: var(--changed-ink);
  animation: changedFade 5s ease-out both;
}
.changed-cell::after { content: none !important; }

/* --- home lead row ------------------------------------------------------ */
.home-lead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 4px;
}

/* The refresh control kept the whole heading row 44px tall to carry a 16px
   label. It keeps the 44px a thumb needs — as a transparent ::after, the same
   way .chat-vote does it — and gives the 12px back to the page. */
#homeRefresh {
  position: relative;
  min-height: 32px;
  min-width: 32px;
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
#homeRefresh::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.display-figure.is-bold { font-size: 31px; font-weight: 700; letter-spacing: -1.1px; }

/* The month's total and how it compares share a line, on one baseline: the
   comparison is a property OF that figure, and a line of its own put the
   sparkline between them. Baseline rather than centre — they are one sentence,
   not two stacked blocks. Wrapping is allowed but should not happen; it is
   there so a longer month name on a narrow phone degrades instead of
   overflowing. Neither type size changes. */
.home-figure-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin-bottom: 6px;
}

/* --- view transitions --------------------------------------------------- */
/* Opacity + a 6px translate only: both are compositor-only properties, so this
   costs no layout and no paint. 160ms is under the threshold where a transition
   starts to feel like waiting. */
.view.is-active {
  animation: viewIn 160ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@keyframes viewIn {
  from { opacity: 0; transform: translate3d(0, 6px, 0); }
  to { opacity: 1; transform: none; }
}
.tabbtn { transition: background-color 140ms ease, color 140ms ease; }
.chip { transition: background-color 140ms ease, border-color 140ms ease; }
@media (prefers-reduced-motion: reduce) {
  .view.is-active { animation: none; }
  .tabbtn, .chip { transition: none; }
}

/* --- chat shell: stop double-insetting -------------------------------- */
/* .app-shell already pads 16px; the shell's own 12px made a visible inset box
   inside the screen and cost width the bubbles needed. The 168px fallback also
   predates the tab bar — sizeChatShell() is authoritative, this is just what
   first paint sees. */
.chat-shell {
  padding: 0;
  gap: 8px;
  height: calc(100dvh - 232px);
  min-height: 280px;
}
.chat-thread { padding: 2px 0 6px; }
.chat-composer { padding-bottom: 0; }
/* The rail is already a scroller in styles.css; this only keeps the fade from
   sitting on top of the first chip when there is nothing to scroll. */
.chip-rail-wrap { margin-inline: 0; }

/* "DASHBOARD" is the longest label; let it shrink rather than wrap or clip the
   five-tab bar at 320px. */
.tabbtn-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 359px) {
  .tabbtn-label { font-size: 8px; letter-spacing: 0.2px; }
}

/* --- breathing room between dashboard sections -------------------------- */
/* The h2 sat right on top of the previous section's last bar. A section break
   should read as a break before you get to the words. */
.android-section + .android-section { margin-top: 26px; }
.android-section h2 {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.2px;
  margin: 0 0 2px;
  padding-top: 4px;
}
.android-section + .android-section h2 { border-top: 1px solid var(--rule); padding-top: 22px; }

/* --- tab bar: pinned, not sticky ---------------------------------------- */
/* Sticky rides along with the document during rubber-band overscroll, so the
   bar visibly followed the finger on the chat screen. Fixed pins it to the
   viewport instead. The width is matched to .app-shell so it does not span the
   full window on a desktop-width browser. */
.tabbar {
  position: fixed;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(480px, 100%);
  margin: 0;
  padding: 8px 4px calc(8px + env(safe-area-inset-bottom));
}
/* The bar no longer takes part in layout, so the shell has to reserve its room
   or the last row of every screen hides underneath it. */
.workspace {
  min-height: auto;
  padding-bottom: calc(72px + env(safe-area-inset-bottom));
}

/* --- targets form: two per row, not one field per screenful ------------- */
.target-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 10px;
}
.target-cell { display: block; }
.target-name {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  margin-bottom: 4px;
}
.target-input {
  display: flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0 10px 0 12px;
  min-height: 44px;
  background: var(--surface);
}
.target-input input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: none;
  padding: 0;
  background: transparent;
  font-size: 17px;
  font-weight: 480;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.target-input b {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
}
.target-input:focus-within { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

.target-save {
  display: flex;
  justify-content: center;
  margin-top: 14px;
}

/* --- live band: the Total column head lines up with its numbers --------- */
/* Number cells end 16px from the edge (td:last-child); the band's header cells
   are <th>, which missed that rule, so "TOT" sat closer to the edge than the
   column under it. Same inset, and no wrap if a band label runs long. */
.live-matrix tr.live-band > th:last-child { padding-right: 16px; }
.live-matrix tr.live-band > th { white-space: nowrap; }

/* --- chat screen -------------------------------------------------------- */
/* Compact header: the room title is orientation, not content — every px it
   gives up goes to the conversation. */
.view-header.chat-head { margin-bottom: 2px; }
.view-header.chat-head h2 { font-size: 18px; letter-spacing: -0.2px; }
.view-header.chat-head p {
  margin-top: 1px;
  font-size: 11.5px;
  color: var(--text);
  font-weight: 330;
}
.view-header.chat-head p.is-exhausted { font-weight: 700; }
.chip-rail-fade { transition: opacity 160ms ease; }

/* --- category tree: each level wears its own colour --------------------- */
/* Level 1 (CAT1 parents, Branch Revenue, BOGO CAT4) = dark blue; level 2
   (CAT4 children inside a parent) = green. The expand marker matches its
   level's bar, so [-] tells you at a glance how deep you are. */
.android-bar-track .android-bar-fill { background: var(--accent-ink); }
.android-expand-row .expand-closed,
.android-expand-row .expand-open { color: var(--accent-ink); }
.android-nested-bars .android-bar-track .android-bar-fill { background: var(--positive); }
.android-nested-bars .expand-closed,
.android-nested-bars .expand-open { color: var(--positive); }

/* --- a KPI tile whose value just changed -------------------------------- */
.live-kpi-strip .live-kpi.changed-kpi strong {
  color: var(--changed-ink);
  animation: changedFade 5s ease-out both;
}

/* KPI values are black ink at rest — the old tone-* colours leaked through and
   made every value the same dark blue as the changed state, so "just changed"
   was invisible. Black at rest, dark blue only when the value moved. */
.live-kpi-strip .live-kpi strong { color: var(--text); }
.live-kpi-strip .live-kpi {
  display: grid;
  align-content: start;
  gap: 1px;
  padding: 5px 5px;
  min-height: 52px;
}
.live-kpi-strip .live-kpi small {
  font-size: 8.5px;
  letter-spacing: 0.2px;
  line-height: 1.05;
  /* "China Contribution" cannot fit one line at a quarter of a phone */
  white-space: normal;
}
.live-kpi-strip .live-kpi strong { font-size: 17px; letter-spacing: -0.3px; }

/* --- SKU-level cards sit on the palette's pink -------------------------- */
/* Pink is already the SKU colour in this system (SKU comparison, branch
   picker), so the deepest level of every product tree wears it too instead of
   dissolving into the white page. */
.product-card {
  background: var(--block-pink);
  border-color: rgba(var(--ink-rgb), 0.08);
}
.product-card .code-badge,
.product-card .cat-badge { background: var(--surface); }

/* --- tab bar, 15% shorter ---------------------------------------------- */
/* 8+48+8 ≈ 64px -> 5+44+5 ≈ 54px. sizeChatShell measures the bar live, so the
   chat composer follows automatically. The button stops at 44: it is the
   control every screen depends on, and the two pixels it used to give back
   came out of the only dimension a thumb cares about. */
.tabbar { padding: 5px 4px calc(5px + env(safe-area-inset-bottom)); }
.tabbtn { min-height: 44px; gap: 2px; padding: 4px 0; }
.workspace { padding-bottom: calc(62px + env(safe-area-inset-bottom)); }

/* --- SKU cards: colour is the container, no lines ----------------------- */
.product-card { border: 0; }
.product-card .star-button { border: 0; }

/* --- Sales Dashboard setup: blue surface instead of borders -------------- */
/* The setup card and its branch tiles were white boxes drawn with borders on a
   white page. The pale action-blue surface now does the separating; the tiles
   go white-on-blue with no stroke at all. */
#viewDashboard .panel.field-stack {
  background: var(--accent-weak);
  border: 0;
}
#viewDashboard .company-box label {
  background: var(--surface);
  border: 0;
}
#viewDashboard .segmented.period-control { background: var(--surface); border: 0; }
#viewDashboard #dashboardContent > .empty-state {
  background: var(--accent-weak);
  border: 0;
  color: var(--text);
}

/* --- Admin: one pastel per section, no strokes -------------------------- */
/* Five white boxes drawn with borders on a white page read as one long form.
   Colour separates them at a glance and matches the rest of the app, where a
   pastel block IS the section boundary. */
.admin-panel { border: 0; }
.admin-panel.is-targets { background: var(--block-lilac); }
.admin-panel.is-access { background: var(--block-mint); }
.admin-panel.is-quota { background: var(--block-cream); }
.admin-panel.is-activity { background: var(--block-pink); }
.admin-panel.is-patterns { background: var(--block-coral); }
/* The teaching ledger reuses the cream tint (quota's) rather than minting a
   sixth block colour — the section order keeps them apart on screen. */
.admin-panel.is-improve { background: var(--block-cream); }

.improve-day { display: grid; gap: 8px; margin-top: 10px; }
.improve-day-head {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  margin: 0;
}
.improve-entry {
  background: var(--surface);
  border-radius: 8px;
  padding: 10px 12px;
  display: grid;
  gap: 4px;
}
.improve-meta { font-family: var(--mono); font-size: 10px; color: var(--muted); margin: 0; }
.improve-q { font-size: 12px; color: var(--muted); margin: 0; font-style: italic; }
.improve-diag { font-size: 13px; margin: 0; line-height: 1.4; }
.improve-lesson { font-size: 12px; font-weight: 650; margin: 0; line-height: 1.4; }
/* Anything that used white-on-white to stand out now needs to stay white to
   stand out on the tint instead. */
.admin-panel .admin-list,
.admin-panel .wl-list,
.admin-panel .activity-list { background: transparent; }
.admin-panel input[type="email"],
.admin-panel input[type="text"],
.admin-panel input[type="number"] { background: var(--surface); border-color: rgba(var(--ink-rgb), 0.12); }
.admin-panel .admin-row,
.admin-panel .wl-row,
.admin-panel .activity-row {
  border-bottom: 1px solid rgba(var(--ink-rgb), 0.12);
  background: transparent;
}
.admin-panel .admin-row:last-child,
.admin-panel .wl-row:last-child,
.admin-panel .activity-row:last-child { border-bottom: 0; }
.admin-panel button.ghost { background: var(--surface); border-color: rgba(var(--ink-rgb), 0.12); }
.admin-panel .admin-bar { background: rgba(var(--ink-rgb), 0.12); }
/* A hundred rows is a lot of screen — give the list its own scroller so the
   other three panels stay reachable. */
.admin-panel.is-activity .activity-list {
  max-height: 420px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* --- answer feedback --------------------------------------------------- */
/* Sits under Yoyo's answer, aligned with the bubble (avatar 28 + gap 8), and
   deliberately quiet: this is an invitation, not a call to action. Only open
   questions get it — shortcut output is canned and has nothing to judge. */
.chat-feedback {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-left: 36px;
  margin-top: 4px;
}
.chat-vote {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 32px visual, 44px hit area via the pseudo-element below — the icons would
     look like buttons from a toolbar at full touch size. */
  width: 32px;
  height: 32px;
  padding: 0;
  position: relative;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text);
  opacity: 0.45;
  cursor: pointer;
  transition: background-color 180ms ease, opacity 180ms ease, color 180ms ease;
}
.chat-vote::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.chat-vote:hover,
.chat-vote:focus-visible {
  opacity: 1;
  background: var(--accent-weak);
  color: var(--accent-ink);
}

.chat-feedback.is-open {
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.chat-feedback-q {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text);
  opacity: 0.55;
}
.chat-reasons {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* Pill CTAs, same as everywhere else in this system — but outlined, because
   picking a reason is a smaller act than sending a question. */
.chat-reason {
  font: inherit;
  font-size: 12px;
  font-weight: 480;
  letter-spacing: -0.1px;
  padding: 7px 13px;
  min-height: 34px;
  border: 1px solid rgba(var(--ink-rgb), 0.22);
  border-radius: 50px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease;
}
.chat-reason:hover,
.chat-reason:focus-visible {
  background: var(--accent-weak);
  border-color: var(--accent-ink);
  color: var(--accent-ink);
}

.chat-feedback.is-done {
  padding-left: 36px;
  font-size: 11px;
  font-weight: 330;
  letter-spacing: -0.1px;
  color: var(--text);
  opacity: 0.5;
}

@media (prefers-reduced-motion: reduce) {
  .chat-vote,
  .chat-reason { transition: none; }
}

/* --- admin: question patterns ------------------------------------------ */
/* Counts and fixed labels only — by design there is nothing here to read that
   could identify a question, so the layout optimises for scanning tallies. */
.pattern-head {
  font-size: 13px;
  font-weight: 330;
  letter-spacing: -0.14px;
  line-height: 1.5;
  color: var(--text);
  margin: 0 0 10px;
}
.pattern-head b { font-weight: 700; }

.pattern-tally {
  display: flex;
  gap: 8px;
  align-items: baseline;
  padding: 6px 0;
  border-top: 1px solid rgba(var(--ink-rgb), 0.16);
}
.pattern-tally-title {
  flex: 0 0 74px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text);
  opacity: 0.6;
}
.pattern-tally-items {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
/* The feedback split. Green and coral rather than the neutral chip, because
   this is the one tally where the direction of the number is the point. The
   note beside them carries the raw counts — a percentage of seven ratings is
   not a statistic, and the reader should be able to see that for themselves. */
.pattern-chip.is-good { background: var(--accent-weak); color: var(--accent-ink); }
.pattern-chip.is-bad { background: rgba(214, 89, 74, 0.13); color: #a63b2c; }
[data-theme="dark"] .pattern-chip.is-bad { color: #f0a99d; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pattern-chip.is-bad { color: #f0a99d; }
}
.pattern-note {
  font-size: 11px;
  color: var(--muted);
  opacity: 0.75;
  align-self: center;
}
.pattern-chip {
  font-size: 12px;
  font-weight: 330;
  letter-spacing: -0.1px;
  color: var(--text);
  padding: 2px 8px;
  border-radius: 50px;
  background: rgba(var(--surface-rgb), 0.55);
}
.pattern-chip b {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.pattern-subhead {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--text);
  margin: 14px 0 2px;
}
/* Long enough to scroll on its own rather than pushing the panel off-screen,
   the same treatment the activity log already gets. */
.admin-panel.is-patterns .activity-list {
  max-height: 360px;
  overflow-y: auto;
}
.pattern-row .activity-act {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.pattern-tags {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  opacity: 0.55;
}

/* ===== login landing page ================================================ */
/* The one deliberately soft surface in the app. The owner supplied a pastel
   glass mockup for the login screen specifically (2026-08-03); everything
   after sign-in stays in the flat no-shadow system above. All rules are
   scoped to .login-view / .lp-* so nothing leaks into the workspace. */

.login-view {
  position: relative;
  /* Pinned light for good. This screen is a pastel composition, not a themed
     surface -- the violet gradient, the glass card and the orange wordmark are
     the design, and there is no dark counterpart of them worth inventing.
     color-scheme also keeps the UA from darkening the form controls inside. */
  color-scheme: light;
  /* styles.css centers the stack; with the compact card that strands the
     content mid-screen with a dead band above it. Flow from the top. */
  align-content: start;
  padding: 46px 4px 22px;
  gap: 12px;
  background:
    radial-gradient(120% 90% at 50% 0%, #f3f0fd 0%, #ffffff 55%),
    #ffffff;
}
.login-view > * { position: relative; z-index: 1; }

/* --- ambient background ------------------------------------------------ */
.lp-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.lp-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(58px);
  opacity: 0.5;
  animation: lpDrift 12s ease-in-out infinite alternate;
}
.lp-b1 { width: 340px; height: 340px; top: -12%; left: -22%;
         background: radial-gradient(circle, #c9befa 0%, transparent 70%); }
.lp-b2 { width: 300px; height: 300px; top: 26%; right: -26%;
         background: radial-gradient(circle, #bcd8ea 0%, transparent 70%);
         animation-delay: -4s; animation-duration: 14s; }
.lp-b3 { width: 320px; height: 320px; bottom: -14%; left: 6%;
         background: radial-gradient(circle, #cdead4 0%, transparent 70%);
         animation-delay: -8s; animation-duration: 16s; }
@keyframes lpDrift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(24px, -18px, 0) scale(1.12); }
}

/* floating glass tiles */
.lp-float {
  position: absolute;
  display: grid;
  place-items: center;
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.75);
  box-shadow: 0 10px 26px rgba(124, 108, 232, 0.14);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #7c6ce8;
  animation: lpFloat 7s ease-in-out infinite;
}
.lp-float svg { width: 46%; height: 46%; }
.lp-f1 { width: 40px; height: 40px; top: 8px; left: 10px; }
.lp-f2 { width: 40px; height: 40px; top: 24%; right: 5%; color: #3a7ca5;
         animation-delay: -2s; animation-duration: 8s; display: none; }
.lp-f3 { width: 38px; height: 38px; top: 42%; left: 4%; color: #4f9d6b;
         animation-delay: -4s; animation-duration: 9s; display: none; }
.lp-f4 { width: 42px; height: 42px; top: 12px; right: 10px;
         animation-delay: -5.5s; animation-duration: 7.5s; }
@media (min-width: 720px) {
  .lp-f1 { top: 6%; left: 5%; }
  .lp-f4 { top: 9%; right: 6%; }
  .lp-f2, .lp-f3 { display: grid; }
}
@keyframes lpFloat {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-11px) rotate(2deg); }
}

/* --- entrance ----------------------------------------------------------- */
.lp-rise { animation: lpRise 0.65s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.lp-d1 { animation-delay: 0.05s; }
.lp-d2 { animation-delay: 0.18s; }
.lp-d3 { animation-delay: 0.32s; }
@keyframes lpRise {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* --- brand -------------------------------------------------------------- */
/* Service-hours notice. Amber, not red: the app is not broken and nobody has
   done anything wrong — it is a timetable. It sits above the form because it
   changes whether filling the form is worth doing. */
.lp-hours {
  margin: 0 0 2px;
  padding: 10px 12px;
  border: 1px solid rgba(180, 120, 20, 0.28);
  border-radius: 10px;
  background: #fdf3dd;
  color: #6b4c12;
  font-size: 12px;
  line-height: 1.5;
  text-align: left;
}
.lp-hours strong { display: block; font-weight: 800; margin-bottom: 1px; }
[data-theme="dark"] .lp-hours { background: #2a2415; border-color: rgba(240, 200, 120, 0.24); color: #f0d9a8; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .lp-hours {
    background: #2a2415;
    border-color: rgba(240, 200, 120, 0.24);
    color: #f0d9a8;
  }
}

.lp-brand { text-align: center; display: grid; gap: 3px; }
.lp-title {
  font-size: 31px;
  font-weight: 800;
  font-style: italic;
  letter-spacing: -1px;
  line-height: 1.05;
  background: linear-gradient(95deg, #f0563a 0%, #f97316 48%, #f59e0b 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.lp-brand h1 {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: #201d38;
}
.lp-brand p { font-size: 11px; font-weight: 480; color: #7c6ce8; }

/* --- the card ----------------------------------------------------------- */
.login-view .panel.lp-card {
  border: 1px solid rgba(255, 255, 255, 0.85);
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.74);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 22px 48px rgba(124, 108, 232, 0.16);
  padding: 16px 16px 12px;
  gap: 13px;
}
/* an empty status line must not cost a grid row + gap */
.lp-card .status:empty { display: none; }

/* inputs: lavender rest state, purple focus ring */
.lp-card input:not([type="checkbox"]):not([type="radio"]) {
  border: 1px solid #e5e0f7;
  border-radius: 13px;
  background: #fbfaff;
  min-height: 43px;
  padding: 8px 12px;
  transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
}
.lp-card input:not([type="checkbox"]):not([type="radio"]):focus {
  outline: none;
  border-color: #8b7cf0;
  background: #ffffff;
  box-shadow: 0 0 0 4px rgba(139, 124, 240, 0.16);
}
.lp-card input[type="checkbox"] { accent-color: #7c6ce8; }

/* password eye */
.lp-pw { position: relative; display: block; }
.lp-pw input { width: 100%; padding-right: 44px !important; }
.lp-eye {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: #a49ec2;
  cursor: pointer;
  transition: color 0.18s ease, background-color 0.18s ease;
}
.lp-eye:hover, .lp-eye:focus-visible { color: #7c6ce8; background: #f3f0fd; }
.lp-eye svg { width: 19px; height: 19px; }
.lp-eye .lp-eye-shut { display: none; }
.lp-eye.is-on .lp-eye-open { display: none; }
.lp-eye.is-on .lp-eye-shut { display: block; }

/* database toggle: gradient fades in under the active choice */
.login-view .database-radio { gap: 10px; }
.login-view .database-radio label {
  position: relative;
  border: 1px solid #e5e0f7;
  border-radius: 13px;
  background: #f6f4fe;
  color: #8a86a3;
  font-weight: 800;
  overflow: hidden;
  transition: color 0.22s ease, transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}
.login-view .database-radio label::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, #8b7cf0 0%, #6a5ae0 100%);
  opacity: 0;
  transition: opacity 0.22s ease;
}
.login-view .database-radio label span { position: relative; }
.login-view .database-radio label.is-active {
  border-color: transparent;
  color: #ffffff;
  transform: translateY(-1px);
  box-shadow: 0 10px 22px rgba(106, 90, 224, 0.32);
}
.login-view .database-radio label.is-active::before { opacity: 1; }
.login-view .database-radio label:active { transform: scale(0.97); }

/* submit: gradient pill, press squash, in-button spinner */
.login-view .primary.lp-submit {
  position: relative;
  border-radius: 14px;
  min-height: 46px;
  background: linear-gradient(135deg, #8b7cf0 0%, #6a5ae0 100%);
  box-shadow: 0 14px 28px rgba(106, 90, 224, 0.35);
  transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
}
.login-view .primary.lp-submit:hover { filter: brightness(1.06); }
.login-view .primary.lp-submit:active {
  transform: scale(0.97);
  box-shadow: 0 8px 16px rgba(106, 90, 224, 0.28);
}
.lp-submit-label { transition: opacity 0.15s ease; }
.lp-submit.is-loading .lp-submit-label { opacity: 0.35; }
.lp-submit.is-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: 50%;
  border: 2.5px solid rgba(255, 255, 255, 0.35);
  border-top-color: #ffffff;
  animation: lpSpin 0.7s linear infinite;
}
@keyframes lpSpin { to { transform: rotate(360deg); } }

.lp-secure {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 11.5px;
  font-weight: 480;
  color: #7c6ce8;
  margin-top: 0;
}
.lp-secure svg { width: 14px; height: 14px; }

/* --- success overlay ---------------------------------------------------- */
.lp-success {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: grid;
  place-items: center;
  background: rgba(250, 249, 255, 0.94);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  opacity: 1;
  transition: opacity 0.4s ease;
}
.lp-success.is-leaving { opacity: 0; }
.lp-success-inner { display: grid; justify-items: center; gap: 6px; text-align: center; }
.lp-success-badge {
  width: 84px;
  height: 84px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: linear-gradient(135deg, #8b7cf0 0%, #6a5ae0 100%);
  color: #ffffff;
  box-shadow: 0 18px 40px rgba(106, 90, 224, 0.4);
  margin-bottom: 8px;
  animation: lpPop 0.5s cubic-bezier(0.3, 1.5, 0.5, 1) both;
}
.lp-success-badge svg { width: 38px; height: 38px; }
@keyframes lpPop {
  from { opacity: 0; transform: scale(0.4); }
  to   { opacity: 1; transform: scale(1); }
}
.lp-success-title { font-size: 21px; font-weight: 800; color: #201d38; }
.lp-success-sub { font-size: 13px; color: #8a86a3; }
.lp-success-bar {
  width: 170px;
  height: 5px;
  margin-top: 12px;
  border-radius: 50px;
  background: #e9e5f9;
  overflow: hidden;
}
.lp-success-bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 50px;
  background: linear-gradient(90deg, #8b7cf0, #6a5ae0);
  animation: lpBarFill 1.15s ease-out forwards;
}
@keyframes lpBarFill { to { width: 100%; } }

/* --- accessibility ------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .lp-rise, .lp-blob, .lp-float,
  .lp-success-badge, .lp-success-bar i { animation: none; }
  .lp-rise { opacity: 1; transform: none; }
  .lp-success-bar i { width: 100%; }
  .login-view .database-radio label,
  .login-view .primary.lp-submit,
  .lp-card input { transition: none; }
}

/* --- boot splash --------------------------------------------------------- */
/* Covers the session-resume gap: index.html sets html.booting BEFORE first
   paint when a saved token exists, so the login form never flashes on the way
   into the workspace. endBootSplash() fades it out whichever way boot ends. */
.lp-boot { display: none; }
html.booting .lp-boot {
  display: grid;
  place-items: center;
  /* Same pastel composition as .login-view, and pinned light for the same
     reason -- it is the frame either screen can arrive in. */
  color-scheme: light;
  position: fixed;
  inset: 0;
  z-index: 998;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 0%, #f3f0fd 0%, #ffffff 60%),
    #ffffff;
  opacity: 1;
  transition: opacity 0.45s ease;
}
html.booting .lp-boot.is-leaving { opacity: 0; pointer-events: none; }
.lp-boot .lp-blob { position: absolute; }
.lp-boot-inner {
  display: grid;
  justify-items: center;
  gap: 4px;
  text-align: center;
  animation: lpRise 0.55s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.lp-boot-sub {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: #201d38;
}
.lp-boot-ring {
  width: 44px;
  height: 44px;
  margin: 22px 0 14px;
  border-radius: 50%;
  border: 3.5px solid #e9e5f9;
  border-top-color: #7c6ce8;
  animation: lpSpin 0.85s linear infinite;
}
.lp-boot-note { font-size: 12.5px; color: #8a86a3; }

@media (prefers-reduced-motion: reduce) {
  .lp-boot-inner, .lp-boot-ring { animation: none; }
}

/* --- one-screen lock ----------------------------------------------------- */
/* The login page must never scroll: 100vh on phones includes the strip behind
   the browser chrome, which is exactly the phantom scroll the owner saw. dvh
   tracks the REAL visible height. Locked via :has() only while the login view
   is on stage — workspace views manage their own scrolling and are untouched.
   Browsers without :has()/dvh simply keep today's scrolling page. */
html:has(.login-view),
body:has(.login-view) { height: 100dvh; overflow: hidden; }
/* the shell's own vertical padding was the 52px that still overflowed the
   viewport — the login view carries its own breathing room */
body:has(.login-view) .app-shell { padding-top: 0; padding-bottom: 0; }
.login-view { min-height: 100dvh; }

/* --- wait bar ------------------------------------------------------------ */
/* setStatus() flags any message ending in "..." with .is-wait: the text
   breathes and an indeterminate bar slides beneath it, in the workspace's
   own accent so it belongs to the flat system, not the pastel login. */
.status.is-wait { animation: lpBlink 1.2s ease-in-out infinite; }
.status.is-wait::after {
  content: "";
  display: block;
  width: min(220px, 62%);
  height: 3px;
  margin-top: 7px;
  border-radius: 50px;
  background: linear-gradient(90deg,
    var(--accent-weak) 0%, var(--accent) 45%, var(--accent-weak) 90%);
  background-size: 200% 100%;
  animation: lpBarSlide 1.1s linear infinite;
}
@keyframes lpBlink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}
@keyframes lpBarSlide {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .status.is-wait { animation: none; }
  .status.is-wait::after { animation: none; background-size: 100% 100%; }
}

/* --- workspace headroom -------------------------------------------------- */
/* Owner: the blank band above "MONTH TO DATE" made every screen start ~58px
   late. Three layers stacked there: the shell's 24px roof, an EMPTY status
   line still holding its 20px floor, and the view grid's 14px gap after it. */
.workspace .status:empty { display: none; }
.app-shell { padding-top: max(10px, env(safe-area-inset-top)); }

/* Branch Matrix loads behind the new wait bar, not the old circle */
.matrix-loading .status.is-wait { text-align: center; }
.matrix-loading .status.is-wait::after { margin-inline: auto; }

/* The failed matrix. Centred like the loading state it replaces, so the panel
   does not jump when one becomes the other. */
.matrix-error {
  display: grid;
  justify-items: center;
  gap: 10px;
  padding: 22px 16px;
  text-align: center;
}
.matrix-error .body-line { color: var(--muted); }
.matrix-error .ghost { min-width: 140px; }

/* Home's status line settles into "Updated 08:02" and then stays there all
   session -- the only permanent text in this file's status slot. One step
   down the scale, and its own line box rather than the 20px floor every
   other view needs for a passing sentence: this one is a caption above a
   heading, and six of those twenty pixels were air. */
#homeStatus {
  font-size: 12px;
  line-height: 14px;
  min-height: 14px;
}
/* And then it leaves, on the same twenty seconds as the chart's target
   caption. The box stays — opacity only — so the heading below it never
   moves. The class is added by showHomeStamp and cleared at the top of every
   load, which is what keeps a loading line or an error from inheriting it. */
#homeStatus.is-timestamp { animation: homeStampFade 20s ease-in forwards; }
@keyframes homeStampFade {
  0%, 70% { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* Still gone on time, without the fade — step-end holds each keyframe's
     value and jumps at the end instead of interpolating. */
  #homeStatus.is-timestamp { animation-timing-function: step-end; }
}

/* --- dashboard palette --------------------------------------------------- */
/* The four tones were rendering as two near-identical dark blues: --info,
   --warn and --danger were all #2c6588, and .tone-accent had no fill rule at
   all so it fell through to --accent. Two shops side by side were the same
   colour (the owner photographed exactly that).
   The shop/segment palette now has its OWN tokens, so it can be bright and
   separable without touching the semantic meaning of info/warn/danger. Hues
   are the app's own: accent blue, lilac-block violet, coral-block amber,
   mint-block teal — lifted to bar strength. */
:root,
.login-view,
.lp-boot {
  --shop-1: #2b8cd4;  --shop-1-ink: #1c6fa8;   /* blue   — accent family */
  --shop-2: #7c5ce8;  --shop-2-ink: #5f43c4;   /* violet — lilac block / login gradient */
  --shop-3: #f08a24;  --shop-3-ink: #a85c0c;   /* amber  — coral block / YOU SAVE logo */
  --shop-4: #17a97f;  --shop-4-ink: #0d7a5c;   /* teal   — mint block */

  --seg-retail: #2b8cd4;
  --seg-wholesale: #17a97f;
  --seg-rich: #7c5ce8;
}

/* bars and order-time columns wear the bright value */
.android-bar-fill.tone-accent,
.android-column span.tone-accent { background: var(--shop-1); }
.android-bar-fill.tone-info,
.android-column span.tone-info { background: var(--shop-2); }
.android-bar-fill.tone-warn,
.android-column span.tone-warn { background: var(--shop-3); }
.android-bar-fill.tone-danger,
.android-column span.tone-danger { background: var(--shop-4); }

/* the share percentage is TEXT on white — bright reads at ~3:1, so text takes
   the ink variant (all four clear 4.5:1) while the bar beside it stays bright */
.android-overview-head .tone-accent { color: var(--shop-1-ink); }
.android-overview-head .tone-info { color: var(--shop-2-ink); }
.android-overview-head .tone-warn { color: var(--shop-3-ink); }
.android-overview-head .tone-danger { color: var(--shop-4-ink); }

/* the card's left edge is a 4px identity stripe, so it takes the bright value */
.android-branch-card.tone-info { border-left-color: var(--shop-2); }
.android-branch-card.tone-warn { border-left-color: var(--shop-3); }
.android-branch-card.tone-danger { border-left-color: var(--shop-4); }
.android-branch-card:not(.tone-info):not(.tone-warn):not(.tone-danger) {
  border-left-color: var(--shop-1);
}

/* segments: identity colours, and the legend dots must match them exactly */
.android-bar-fill.tone-seg-retail { background: var(--seg-retail); }
.android-bar-fill.tone-seg-wholesale { background: var(--seg-wholesale); }
.android-bar-fill.tone-seg-rich { background: var(--seg-rich); }
.seg-legend .is-retail::before { background: var(--seg-retail); }
.seg-legend .is-wholesale::before { background: var(--seg-wholesale); }
.seg-legend .is-rich::before { background: var(--seg-rich); }

/* --- semantic colours restored ------------------------------------------ */
/* Collateral damage from the same flattening: --danger was blue, so error
   text did not read as an error and the BOGO badge was a pink pill with blue
   type. Both take the reds/ambers the BOGO legend already uses — no new hue
   enters the app. */
:root,
.login-view,
.lp-boot {
  --warn: #c8891f;
  --danger: #b3453a;
  /* the offline badge's fill; the dark block gives it a dark counterpart */
  --tint-warn: #fff3df;
  /* --warn is chosen to read on WHITE. On the pale amber fill it drops to
     2.65:1, so amber-on-amber gets its own darker value. The border keeps
     --warn -- a 1px edge is not type and does not need 4.5:1. */
  --warn-ink: #8a5c00;
}

/* --- order time: Myanmar clock note -------------------------------------- */
.tz-note {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.4px;
  vertical-align: 2px;
  color: var(--text);
  opacity: 0.5;
}
/* slot ranges ("09:00-10:00") need more room than the old bare hour */
.android-time-row { grid-template-columns: 88px minmax(0, 1fr) 70px; }
.android-time-row strong { font-variant-numeric: tabular-nums; font-size: 12px; }

/* --- view headers in the display mono ------------------------------------ */
/* Owner: Real-Time Sales / Sales Dashboard / Chat with Yoyo take the same
   face as the MONTH TO DATE header — the mono — bold, size unchanged.
   Mono needs its tracking pulled in at 22px or the title turns airy. */
.view-header h2 {
  font-family: var(--mono);
  font-weight: 700;
  letter-spacing: -0.8px;
}

/* --- live header: the Updated stamp ---------------------------------------
   formatUpdated read the VIEWER's device clock, so this line disagreed with
   every other time on the screen. It now carries the Myanmar stamp with the
   time picked out, which is why the separate live clock could go. */
/* The stamp sits inside the h2, so it inherits the title's face, weight and
   size for free — nothing here restates them, which is what keeps the two in
   step if the title ever changes. */
.view-header h2 .hdr-clock { font-variant-numeric: tabular-nums; }
/* digital-watch colon: lit half the second, dark the other half. CSS, not a JS
   timer — an interval drifts against the second, an animation does not. */
.view-header h2 .hdr-clock .lc-colon {
  font-style: normal;
  animation: lcBlink 1s steps(1) infinite;
}
@keyframes lcBlink { 50% { opacity: 0; } }
/* A payload older than the resume threshold stops the blink and dims the
   time: a blinking colon asserts "this is now", and after a phone comes back
   from hours in a pocket that is a lie the screen was actively telling. The
   per-second countdown tick applies the class. */
.view-header h2 .hdr-clock.is-stale .lc-colon { animation: none; opacity: 1; }
.view-header h2 .hdr-clock.is-stale { opacity: 0.45; }
@media (prefers-reduced-motion: reduce) {
  .view-header h2 .hdr-clock .lc-colon { animation: none; }
}

/* --- live matrix: the figure rows lead ------------------------------------ */
/* Revenue / Margin / Cost / Qty are the figures; their derived percentages sit
   under them in lighter type, so the label should say which is which too. */
.live-matrix tr.is-money > th[scope="row"] { font-weight: 700; }

/* --- live KPI card: one baseline for all eight ---------------------------- */
/* "Pro Contribution" wraps to two lines and "Basket Size" does not, which left
   the figures sitting at different heights across the row. Reserving two lines
   for every label — and centring the short ones inside that space — puts both
   the labels and the figures on one line each. */
.live-kpi-strip .live-kpi small {
  display: grid;
  align-content: end;
  min-height: calc(2 * 1.05 * 8.5px);
}

/* --- Back becomes an icon, like Refresh ---------------------------------- */
/* styles.css reserves 82px for the word "Back"; the round control is 40px, so
   that reservation has to be released or the button sits in a wide gap. */
.view-header .nav-back,
.nav-back.icon-button {
  min-width: 44px;
  width: 44px;
  height: 44px;
  padding: 0;
}

/* --- Home: the two cards need air between them -------------------------- */
/* Only fires where a block precedes the card — on Live the card leads and must
   keep sitting tight under the header. */
.block + .live-kpi-strip { margin-top: 10px; }

/* --- view titles come down one step ------------------------------------- */
.view-header h2 { font-size: 20px; }

/* --- period segment: the chosen half looks like the button it commits to --- */
/* It sat white-on-white inside a white control, carrying only a tint of text
   colour. Filling it with the same accent as Run Dashboard says "this is what
   will run" in the same voice as the button underneath. */
.segmented button.is-active,
.tabs button.is-active {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: none;
}

/* --- Yoyo's bubbles ------------------------------------------------------- */
/* White on a white thread left the reply with only a hairline to hold it; the
   owner's pink gives it a body of its own while the outgoing bubble keeps the
   accent, so the two sides stay unmistakable. */
.chat-bubble.in {
  background: #f4d5d4;
  border-color: transparent;
}

/* --- refresh acknowledges the tap ---------------------------------------- */
/* Reuses lpSpin from the login layer rather than declaring a second identical
   rotation. The dimming carries the message on its own, so a reader who has
   asked for reduced motion still sees the tap land. */
.icon-button.is-busy {
  opacity: 0.5;
  pointer-events: none;
}
.icon-button.is-busy svg { animation: lpSpin 0.8s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .icon-button.is-busy svg { animation: none; }
}

/* ===== phase 1: motion ==================================================== */

/* --- bars grow from nothing ---------------------------------------------- */
/* Widths stay inline (the JS writes them); only transform animates, so no
   render code changes and no layout is read back mid-animation. The Live
   matrix has no bars, so the 60s repaint cannot set these off — they run on a
   view switch or a manual refresh, which is exactly when a redraw is expected. */
.android-bar-fill,
.bar-fill,
.progress-fill,
.android-stack span {
  transform-origin: left center;
  animation: barGrow 0.45s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
.android-column span {
  transform-origin: center bottom;
  animation: colGrow 0.45s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@keyframes barGrow { from { transform: scaleX(0); } }
@keyframes colGrow { from { transform: scaleY(0); } }

/* --- a changed figure cools back to black -------------------------------- */
/* Held the accent until the next repaint, which on a quiet minute meant the
   highlight outlived the change it marked. Now it holds long enough to catch
   the eye, then settles — and under reduced motion the animation is dropped,
   which leaves the static accent colour: exactly today's behaviour. */
/* Bright blue for 3.5s of a 5s run, then 1.5s of fade to black. --shop-1 is
   the palette's vivid blue, already used for the level-1 bars — the muted
   --accent-ink it replaced was close enough to black type to be missed. */
:root,
.login-view,
.lp-boot { --changed-ink: var(--shop-1); }
@keyframes changedFade {
  0%, 70% { color: var(--changed-ink); }
  100% { color: var(--text); }
}

/* --- controls answer the finger ------------------------------------------ */
/* The login screen already did this; the workspace did not, so the same tap
   felt different depending on which screen you were on. */
.icon-button,
.primary,
.ghost,
.refresh-button { transition: transform 120ms ease; }
.tabbtn { transition: background-color 140ms ease, color 140ms ease, transform 120ms ease; }
.chip { transition: background-color 140ms ease, border-color 140ms ease, transform 120ms ease; }

.icon-button:active,
.primary:active,
.ghost:active,
.refresh-button:active,
.tabbtn:active,
.chip:not(:disabled):active { transform: scale(0.97); }

@media (prefers-reduced-motion: reduce) {
  .android-bar-fill,
  .bar-fill,
  .progress-fill,
  .android-stack span,
  .android-column span,
  .live-matrix .changed-cell,
  .live-kpi-strip .live-kpi.changed-kpi strong { animation: none; }
  /* the press squash stays — it is a direct answer to a touch, not decoration.
     Only the easing into and out of it is dropped. */
  .icon-button, .primary, .ghost, .refresh-button, .tabbtn, .chip { transition: none; }
}

/* ===== phase 2 (a): first-load skeleton + countdown ring ================== */

/* --- skeleton ------------------------------------------------------------ */
/* Shaped like the screen that is coming, so the figures land into blocks that
   are already the right size instead of shoving the page around. */
.skel-wrap { display: block; }
.skel {
  background: var(--zebra);
  border-radius: 6px;
  animation: skelPulse 1.4s ease-in-out infinite;
}
@keyframes skelPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }

/* .block-mint is on the element too, so the tint can never drift from the real
   card — only the geometry lives here. */
.skel-card {
  margin-top: 14px;
  padding: 18px 16px;
  border-radius: 24px;
}
.skel-card .skel-row + .skel-row { margin-top: 12px; }
.skel-lead, .skel-caps { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.skel-caps { margin-top: 7px; }
.skel-card .skel { background: rgba(var(--ink-rgb), 0.07); }

.skel-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  margin-top: 14px;
  border-radius: 16px;
  overflow: hidden;
  background: var(--border);
}
.skel-tile {
  display: grid;
  justify-items: center;
  padding: 12px 5px 14px;
  background: var(--surface);
}

.skel-table { margin-top: 18px; }
.skel-line { padding: 8px 0; }

/* --- countdown ring ------------------------------------------------------ */
/* The seconds were a sentence ("Auto refresh in 56s"); the ring says the same
   thing at a glance and leaves the number as the exact readout. */
.live-countdown {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.live-countdown svg { display: block; overflow: visible; }
.live-countdown circle {
  fill: none;
  stroke-width: 2.5;
  transform: rotate(-90deg);          /* start the sweep at 12 o'clock */
  transform-origin: 50% 50%;
}
.live-countdown .lc-track { stroke: var(--border); }
.live-countdown .lc-arc {
  stroke: var(--accent);
  stroke-linecap: round;
  stroke-dasharray: 56.549;           /* 2 * pi * 9 — one full turn */
  transition: stroke-dashoffset 0.9s linear;
}
.live-countdown b {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; }
  /* the ring still steps once a second — that is the readout, not decoration.
     Only the sweep between steps is dropped. */
  .live-countdown .lc-arc { transition: none; }
}

/* --- Home sparkline ------------------------------------------------------ */
/* The line stretches horizontally with the column; non-scaling-stroke keeps it
   1.5px regardless, and the today-dot is a CSS element rather than an SVG
   circle so it stays round instead of being stretched into an ellipse. */
/* Four layers in 44px. What keeps it minimal is not the count but the ink
   weight: each layer behind the data line is deliberately fainter than the one
   in front, so the eye lands on the line first and the rest is context it can
   take or leave. Every colour is a token, so dark mode will follow later
   without touching this block again. */
.spark {
  position: relative;
  /* 44 for the chart plus 11 for the date scale hanging below it. */
  height: 55px;
  margin: 4px 0 2px;
}
.spark svg { display: block; width: 100%; height: 44px; overflow: visible; }

/* 1 — the day scale, below the chart rather than inside it, so nothing sits
   behind the data at all. Dates only: the ticks that used to stand above them
   were a second mark for a position the number already gives, and thirty of
   them made a picket fence under a chart whose whole point is one clean line.
   Sunday takes the only colour. Each date is positioned by percentage on the
   same 0-100 axis the polyline uses, so it lands under its own point. */
.spark-days {
  position: absolute;
  left: 0;
  right: 0;
  top: 44px;
  height: 11px;
  display: block;
}
.spark-day {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: 7px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.1px;
  padding-top: 3px;
  color: var(--muted);
  opacity: 0.55;
  white-space: nowrap;
}
/* The two ends anchor to their own edge instead of their centre: centred,
   half of each would hang outside the chart, and a mark pinned past the right
   edge is what widened the whole document once before. */
.spark-day.is-first { transform: none; }
.spark-day.is-last { transform: translateX(-100%); }
/* Sunday, the one day the owner asked to pick out. Soft red, not the danger
   red: nothing is wrong with a Sunday. */
.spark-day.is-sunday { color: var(--danger); opacity: 0.85; }

/* 2 — a flat tint, not a gradient: the workspace has no gradients */
.spark-area { fill: var(--accent); opacity: 0.1; }

/* 3 — the daily pace needed to reach target. non-scaling-stroke matters twice
   here: the dash pattern would otherwise stretch with the viewport too. */
.spark-target {
  stroke: var(--muted);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.35;
  vector-effect: non-scaling-stroke;
}

/* The caption that names it, on the left where reading starts, tucked under
   the line rather than over the data. It leaves after ten seconds because by
   then it has either been read or been ignored, and a chart this small cannot
   afford to carry a permanent legend. */
.spark-target-label {
  position: absolute;
  left: 0;
  font-family: var(--mono);
  font-size: 8px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.06em;
  color: var(--muted);
  white-space: nowrap;
  pointer-events: none;      /* the whole chart is a drag surface underneath */
  opacity: 0.5;
  animation: sparkTargetFade 20s ease-in forwards;
}
/* Still for fourteen of the twenty seconds, then away over the last six.
   Opacity only, so it stays on the compositor and never asks the sparkline to
   relayout while a finger is scrubbing it. */
@keyframes sparkTargetFade {
  0%, 70% { opacity: 0.5; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* It still has to leave on time -- being temporary is the whole point -- so
     it keeps the ten seconds and gives up the fade: step-end holds each
     keyframe's value and jumps at the end instead of interpolating. */
  .spark-target-label { animation-timing-function: step-end; }
}

/* 4 — the data */
.spark polyline {
  stroke: var(--accent-ink);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* 5 — today */
.spark-dot {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  /* Through `color`, so the ring inside it can take the same one with a single
     declaration -- and one class on the dot then repaints both. */
  color: var(--accent-ink);
  background: currentColor;
  transform: translate(-50%, -50%);
}
/* --- a branch row that opens the dashboard ------------------------------- */
.branch-target.is-tappable {
  cursor: pointer;
  border-radius: 12px;
  margin-inline: -6px;
  padding-inline: 6px;
  transition: background-color 140ms ease;
}
.branch-target.is-tappable:hover,
.branch-target.is-tappable:focus-visible { background: rgba(var(--surface-rgb), 0.5); }
/* a chevron, so the row reads as a way in rather than a static readout */
.branch-target.is-tappable .branch-target-head strong::after {
  content: "›";
  margin-left: 6px;
  font-weight: 700;
  opacity: 0.4;
}
@media (prefers-reduced-motion: reduce) {
  .branch-target.is-tappable { transition: none; }
}

/* --- admin controls: two sizes, not five --------------------------------- */
/* The page had five button heights (44 / 40 / 40 / 38 / 32) with no rule
   behind which was which. The 32px Remove already in the whitelist rows is the
   one that felt right, so it becomes the size for EVERY secondary action, and
   the two controls that commit something keep a slightly larger 36. Scoped to
   .admin-panel — the rest of the app is untouched.
   These sit under the 44px touch minimum on purpose: this screen is used by
   one or two admins on their own phone, and two of the buttons here delete
   things, where a target you cannot brush past is a feature. */
/* These were kept at 32px on the theory that a small target is a safeguard
   against deleting something by accident. It never was: clearActivityLog,
   clearPatterns and removeWhitelistEmail each stop on a confirm() the reader
   has to answer, and that is the guard. What the small size actually did was
   make the buttons hard to hit on purpose. */
.admin-panel .admin-refresh,
.admin-panel .admin-reset,
.admin-panel .wl-remove {
  min-height: 44px;
  padding: 4px 14px;
  font-size: 12px;
  font-weight: 700;
  border-radius: 7px;
}

/* the field and the button that fills it have to stay the same height */
.admin-panel .wl-add input,
.admin-panel .wl-add-btn,
.admin-panel #targetsSave {
  min-height: 44px;
}
.admin-panel .wl-add-btn,
.admin-panel #targetsSave {
  padding: 6px 18px;
  font-size: 13px;
  font-weight: 700;
  border-radius: 7px;
}

/* space the buttons were sitting in, reclaimed with them */
.admin-panel .admin-head { margin-bottom: 6px; }
.admin-panel .target-save { margin-top: 10px; }
.admin-panel .wl-add { margin: 4px 0 6px; }


/* ===== dark ============================================================== */
/* One block, at the end, deliberately. Everything the dark theme knows lives
   here; deleting this block returns the app to light exactly as it was.

   Anchored on [data-theme] rather than @media (prefers-color-scheme: dark).
   The switch in More has to be able to overrule the system, and a media query
   cannot be overruled by a selector -- honouring both would mean writing the
   whole block twice, once per condition. Instead the inline script in
   index.html resolves the two inputs (stored choice, else system) into one
   attribute before first paint, so there is a single source of truth here and
   no flash of the wrong theme on the way in.

   The rule that decides every value below: a saturated token LIGHTENS and its
   type DARKENS. --accent goes from #3a7ca5 to a paler blue so it can carry
   small type against a near-black page, and --on-accent stops being white so
   the filled pill still reads. Commit A guessed white would hold either way;
   it does not -- lightening the fill enough to work as type drops white on it
   to 3.3:1, while dark ink on the same fill clears 6.9:1. */

:root[data-theme="dark"] {
  /* form controls, scrollbars and the caret follow. .login-view and
     .lp-boot override this back to light in their own subtree. */
  color-scheme: dark;

  /* Not pure black. The light neutrals all carry a whisper of green
     (#f7f7f5, #eef1ee); the dark ones keep it so the app stays one family. */
  --bg: #111413;
  --surface: #171b19;
  --surface-soft: #1e2320;
  --surface-rgb: 23, 27, 25;

  /* --muted equals --text here for the same reason it equals black in
     light: this app builds hierarchy out of weight, never out of grey. */
  --text: #f2f4f2;
  --muted: #f2f4f2;
  --ink-soft: #f2f4f2;
  --ink-rgb: 242, 244, 242;

  --border: #2f3633;
  --hairline-soft: #232825;
  --rule: #272d2a;
  --zebra: #1f1d15;   /* the label column, warm like its cream in light */

  --accent: #6aa9cf;         /* 7.2:1 as type on --bg */
  --on-accent: #0e1a20;      /* 6.9:1 on that fill */
  --accent-ink: #8fc7e6;
  --accent-weak: #0a3350;   /* blue -- Total Revenue and Margin */
  --accent-line: #2f5f79;

  --positive: #7fbf90;
  --info: #7cb8dc;
  --warn: #e0a94a;
  --danger: #e2685c;

  /* Bars keep their identity hue -- a bar is a shape, not type, and at 4px
     wide the bright value is what makes four shops tellable apart. Only the
     -ink variants move, because those ARE the share percentages. */
  --shop-1-ink: #6fb6e2;
  --shop-2-ink: #a894f0;
  --shop-3-ink: #e59b4a;
  --shop-4-ink: #5fce9f;

  /* The pastel blocks become tinted darks at the same hue -- and the hue has to
     SURVIVE. The first pass took the light pastels and simply darkened them,
     which drains chroma faster than lightness: five tints landed within a few
     points of each other and the KPI grid read as one slab of brown.
     These hold roughly the same luminance (so the white figures and the
     coloured deltas keep the contrast they were measured at) and spend the
     budget on saturation instead, so blue, gold, rust, violet and rose are
     told apart at a glance the way they are in light.

     Gold, rust and rose are three warm hues that sit close together on the
     wheel, and darkening them pulls them closer still. They are pushed apart
     deliberately: gold has no blue at all, rust is red-orange with its green
     pulled down, rose keeps blue in it so it reads pink rather than brown. */
  --block-cream: #3a3300;   /* gold   -- Pro */
  --block-mint:  #0f3323;   /* green  -- the Now vs target card */
  --block-pink:  #4a1236;   /* rose   -- BOGO */
  --block-coral: #521a05;   /* rust   -- China */
  --block-lilac: #2b1650;   /* violet -- Basket */

  /* Cell tints for the Live matrix, named once so the table rules below stay
     readable. Same correction as the blocks above: the first pass left money
     (#262214) and warn (#2a2312) four points apart, which is no distance at
     all, and the whole matrix read as one field of olive. Saturation carries
     the identity now. Money and warn stay in the same yellow family on
     purpose -- they are in light too -- but they are told apart by weight,
     muted gold against bright amber, the way #fbf8e8 and #fff0cf differ on
     white. */
  --tint-good:  #0e2c1a;   /* green */
  --tint-warn:  #3d2c06;   /* amber -- the brighter yellow */
  --tint-bad:   #38150f;   /* red */
  --tint-info:  #0c2733;   /* cyan */
  --tint-money: #26200a;   /* gold  -- the muted yellow */

  --warn-ink: #e0a94a;      /* on a dark fill the bright amber is the readable one */

  /* The dark tile tints are all near-black, so these go the other way: bright
     enough to read on them, where the light-mode values would vanish. */
  --delta-good: #7fbf90;
  --delta-bad: #ec8074;
  --delta-flat: #8fc7e6;
}

/* --- pale neutrals that were written as literals ----------------------- */
/* Scoped to .workspace, not because these classes collide but because the
   bare element selectors -- select, th, input -- would otherwise reach into
   .login-view, and that screen is pinned light. color-scheme stops the UA
   from restyling it; it does not stop author CSS. */
[data-theme="dark"] .workspace select,
[data-theme="dark"] .workspace input,
[data-theme="dark"] .workspace textarea,
[data-theme="dark"] .workspace .tabs,
[data-theme="dark"] .workspace .segmented,
[data-theme="dark"] .workspace .ghost,
[data-theme="dark"] .workspace th,
[data-theme="dark"] .workspace .pill,
[data-theme="dark"] .workspace .bar-track,
[data-theme="dark"] .workspace .android-bar-track,
[data-theme="dark"] .workspace .android-stack,
[data-theme="dark"] .workspace .chip:hover,
[data-theme="dark"] .workspace .android-expand-row[open] > summary.android-bar-row,
[data-theme="dark"] .workspace .chat-input-row textarea:disabled { background: var(--surface-soft); }
[data-theme="dark"] .workspace input,
[data-theme="dark"] .workspace textarea,
[data-theme="dark"] .workspace select { color: var(--text); border-color: var(--border); }
[data-theme="dark"] .workspace .live-section-row th { background: var(--surface-soft) !important; }

/* --- tinted cells and rows --------------------------------------------- */
[data-theme="dark"] .live-head-best { background: var(--tint-good) !important; }
[data-theme="dark"] .live-head-mid { background: var(--tint-warn) !important; border-top-color: var(--warn); }
[data-theme="dark"] .live-head-low { background: var(--tint-bad) !important; }

/* --- tinted badges and panels ------------------------------------------ */
[data-theme="dark"] .fallback-badge,
[data-theme="dark"] .bogo-badge,
[data-theme="dark"] .pill.danger,
[data-theme="dark"] .quota-badge.is-empty { background: var(--tint-bad); }
[data-theme="dark"] .code-badge,
[data-theme="dark"] .pill.warn { background: var(--tint-warn); }
[data-theme="dark"] .android-teaser,
[data-theme="dark"] .android-accordion { background: var(--tint-good); }
[data-theme="dark"] .fallback-badge,
[data-theme="dark"] .star-button,
[data-theme="dark"] .android-teaser,
[data-theme="dark"] .android-accordion,
[data-theme="dark"] .android-expand-row[open] > summary.android-bar-row { border-color: var(--border); }
[data-theme="dark"] .android-expand-body { border-left-color: var(--border); }

/* --- type that was a dark colour ON a pale tint ------------------------- */
/* Those tints are now dark, so the type has to come up with them. */
[data-theme="dark"] .live-kpi.tone-warn strong,
[data-theme="dark"] .android-segment span,
[data-theme="dark"] .android-bar-label span,
[data-theme="dark"] .android-branch-card span,
[data-theme="dark"] .code-badge,
[data-theme="dark"] .pill.warn { color: var(--warn); }
[data-theme="dark"] .pill.danger,
[data-theme="dark"] .daypicker-note,
[data-theme="dark"] .wl-remove { color: var(--danger); }

/* --- chart furniture ---------------------------------------------------- */
[data-theme="dark"] .comparison-grid-line { stroke: var(--rule); }
[data-theme="dark"] .comparison-axis-line { stroke: var(--border); }
[data-theme="dark"] .spinner { border-color: var(--border); border-top-color: var(--accent); }

/* --- redesign.css literals ---------------------------------------------- */
[data-theme="dark"] .block-mint { background: var(--block-mint); }
[data-theme="dark"] .chat-bubble.in { background: var(--block-pink); }
[data-theme="dark"] .android-segment-list .android-bar-fill { background: var(--shop-1); }
[data-theme="dark"] .android-segment-list .android-segment:nth-child(2) .android-bar-fill { background: var(--shop-4); }
[data-theme="dark"] .android-segment-list .android-segment:nth-child(3) .android-bar-fill { background: var(--shop-2); }

/* --- the sparkline's four layers --------------------------------------- */
/* The 10% area tint disappears against a dark card; it needs roughly twice
   the strength to still read as a filled region. */
[data-theme="dark"] .spark-area { opacity: 0.2; }
[data-theme="dark"] .spark-target { opacity: 0.5; }

/* --- appearance row in More ----------------------------------------------- */
/* The last row of the same list. A segmented control here was a second visual
   language on a page built from plain rows and hairlines; the row already had
   a right-hand slot, so the current value simply sits where the chevron does. */
.theme-state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.1px;
  color: var(--muted);
}
.theme-state svg { opacity: 0.75; }

/* --- offline: last known good --------------------------------------------- */
/* Amber, not red. Nothing failed in a way the reader can act on and the
   figures below are real -- they are from a stated moment in the past. Red is
   reserved for "this is wrong", which this is not. */
.stale-badge {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--warn-ink);
  background: var(--tint-warn);
  border: 1px solid var(--warn);
  border-radius: 50px;
  padding: 5px 11px;
  margin-bottom: 12px;
  display: inline-block;
}

/* --- KPI comparison against last month ------------------------------------ */
/* Green is better, red is worse -- and "better" is per tile, not per arrow:
   BOGO Cost is the free stock given away, so its DOWN arrow is the green one
   (kpiCells carries the direction). The blue caption underneath binds the
   eight readings into one group, and a flat reading takes that same blue
   because nothing moved either way.

   These are not --positive / --danger. Those are chosen to read on white; the
   tiles are five pastel tints, and the lilac one is dark enough to drop the
   standard green to 3.3:1. Darker values here clear AA on all five. No
   opacity: the colour is the message now, and dimming it would undo exactly
   the contrast these values were picked for. */
:root,
.login-view,
.lp-boot {
  --delta-good: #2f5c3c;
  --delta-bad: #8f2f26;
  --delta-flat: #204c66;
}
.live-kpi .kpi-delta {
  display: block;
  font-size: 10px;
  line-height: 1.15;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--delta-flat);
  white-space: nowrap;
}
.live-kpi .kpi-delta.is-good { color: var(--delta-good); }
.live-kpi .kpi-delta.is-bad { color: var(--delta-bad); }
.live-kpi .kpi-delta.is-flat { color: var(--delta-flat); }
/* Stated once, under the grid, rather than eight times inside the tiles. */
/* This line names what the deltas in the tiles above are measured against,
   so it belongs to that block -- seventeen pixels of gap (twelve under the
   strip, five over the line) read as the start of a new one. */
#homeContent .live-kpi-strip { margin-bottom: 5px; }
.kpi-basis {
  margin: 3px 0 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  font-family: var(--mono);
  color: var(--accent-ink);
}
.live-kpi .kpi-delta i {
  font-style: normal;
  margin-right: 2px;
  font-size: 8px;
  vertical-align: 1px;
}

/* --- yesterday's-sale badge ----------------------------------------------- */
/* Centred: it is a statement about the whole screen, not about the column it
   happens to sit above. .live-topline had no rules of its own, so the badge
   was taking justify-self: start from the grid it sits in. */
.live-topline {
  display: flex;
  justify-content: center;
  margin-bottom: 10px;
}
.live-topline .fallback-badge { justify-self: center; }

/* --- the previous month arrives after the first paint --------------------- */
/* Cold cache: Home paints the month-to-date immediately and the comparison
   figures follow on their own request. Their slots were reserved at exact
   height in the first paint, so this fade moves nothing -- opacity only,
   like every animation in this file. */
.late-fill { animation: lateFill 400ms ease both; }
@keyframes lateFill {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* --- the Live refresh button says it is working --------------------------- */
/* The tap always refreshed; when the numbers came back unchanged the screen
   simply had nothing to show for it. The swapped label ("Refreshing...") is
   the primary signal; this pulse is reinforcement. The countdown ring is NOT
   spun -- it is a readout, and spinning it would turn it into a lie. */
.refresh-button.is-busy { opacity: 0.8; }
.refresh-button.is-busy .live-countdown svg { animation: busyPulse 900ms ease-in-out infinite; }
@keyframes busyPulse {
  50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  .late-fill { animation: none; }
  .refresh-button.is-busy .live-countdown svg { animation: none; }
}

/* --- Live matrix: hue says which group, strength says which level --------- */
/* The old scheme coloured a cell by the KIND of number in it -- money, share,
   margin -- which is a property the reader can already see in the figure, and
   which changes from row to row inside a single group. The result was
   confetti: nothing told you where Promotion Sales ended and China began, and
   nothing told you that "Share" is derived from the "Revenue" directly above
   it rather than a peer of it.

   Now there are two axes and each carries exactly one thing:
     HUE      which of the four groups a row belongs to
     STRENGTH the level -- band header, then figures, then figures derived
              from those figures

   The label column takes its group's band strength on every row, so the table
   reads as four horizontal blocks AND keeps a vertical spine down the left.
   Hues are the app's own, the same four that identify the KPI tiles. */
:root,
.login-view,
.lp-boot {
  --grp-sales-band: #cfe1ee;  --grp-sales-row: #e9f2f8;  --grp-sales-sub: #f4f9fc;
  --grp-promo-band: #f0e3c4;  --grp-promo-row: #faf2e0;  --grp-promo-sub: #fdf9f0;
  --grp-china-band: #f2cec4;  --grp-china-row: #fbe6e0;  --grp-china-sub: #fdf3f0;
  /* Each band wears the hue its own summary tile wears. BOGO's tile has
     always been rose (--block-pink) while its band was violet — which is the
     colour the Basket Size tile carries. The two were simply swapped, so the
     table was the one place a figure could not be followed up to its card by
     colour alone. */
  --grp-basket-band: #d6c7f7;  --grp-basket-row: #ebe4fb;  --grp-basket-sub: #f6f3fd;
  --grp-bogo-band:  #efd4d4;  --grp-bogo-row:  #f8e9e9;  --grp-bogo-sub:  #fcf5f5;
}

/* Band header: the group's name, and the strongest step of its hue. */
.live-matrix tr.live-band.grp-sales > th,
.live-matrix tr.live-band.grp-sales > td { background: var(--grp-sales-band); }
.live-matrix tr.live-band.grp-promo > th,
.live-matrix tr.live-band.grp-promo > td { background: var(--grp-promo-band); }
.live-matrix tr.live-band.grp-china > th,
.live-matrix tr.live-band.grp-china > td { background: var(--grp-china-band); }
.live-matrix tr.live-band.grp-basket > th,
.live-matrix tr.live-band.grp-basket > td { background: var(--grp-basket-band); }
.live-matrix tr.live-band.grp-bogo > th,
.live-matrix tr.live-band.grp-bogo > td { background: var(--grp-bogo-band); }

/* Figures, then figures derived from them. */
.live-matrix tr.grp-sales > td { background: var(--grp-sales-row); }
.live-matrix tr.grp-sales.is-sub > td { background: var(--grp-sales-sub); }
.live-matrix tr.grp-promo > td { background: var(--grp-promo-row); }
.live-matrix tr.grp-promo.is-sub > td { background: var(--grp-promo-sub); }
.live-matrix tr.grp-china > td { background: var(--grp-china-row); }
.live-matrix tr.grp-china.is-sub > td { background: var(--grp-china-sub); }
.live-matrix tr.grp-basket > td { background: var(--grp-basket-row); }
.live-matrix tr.grp-basket.is-sub > td { background: var(--grp-basket-sub); }
.live-matrix tr.grp-bogo > td { background: var(--grp-bogo-row); }
.live-matrix tr.grp-bogo.is-sub > td { background: var(--grp-bogo-sub); }

/* The label column takes its own ROW's level, not the band's, so every row is
   one colour across its full width. Holding it at band strength drew a darker
   spine down the left that cut across the levels it was sitting beside -- the
   owner spotted it immediately. The group is already named by its band header
   and carried by the hue; the spine was saying it a third time, and saying it
   at the wrong level.

   Keyed on is-money / is-sub rather than the group alone, so the band row --
   which is neither -- keeps its own stronger rule above. */
.live-matrix tr.grp-sales.is-money > th[scope="row"] { background: var(--grp-sales-row); }
.live-matrix tr.grp-sales.is-sub > th[scope="row"] { background: var(--grp-sales-sub); }
.live-matrix tr.grp-promo.is-money > th[scope="row"] { background: var(--grp-promo-row); }
.live-matrix tr.grp-promo.is-sub > th[scope="row"] { background: var(--grp-promo-sub); }
.live-matrix tr.grp-china.is-money > th[scope="row"] { background: var(--grp-china-row); }
.live-matrix tr.grp-china.is-sub > th[scope="row"] { background: var(--grp-china-sub); }
.live-matrix tr.grp-basket.is-money > th[scope="row"] { background: var(--grp-basket-row); }
.live-matrix tr.grp-basket.is-sub > th[scope="row"] { background: var(--grp-basket-sub); }
.live-matrix tr.grp-bogo.is-money > th[scope="row"] { background: var(--grp-bogo-row); }
.live-matrix tr.grp-bogo.is-sub > th[scope="row"] { background: var(--grp-bogo-sub); }

/* The Total column is a column, not a group, so it cannot take a hue of its
   own without breaking the scheme. It needs no rule at all: it already
   carries the only bold weight in the table. An edge was tried and removed --
   table-layout is fixed, so the border took its width out of the column
   beside it and clipped the last branch's figures. */
.live-matrix tr.grp-sales > td.live-total-cell { background: var(--grp-sales-row); }
.live-matrix tr.grp-sales.is-sub > td.live-total-cell { background: var(--grp-sales-sub); }
.live-matrix tr.grp-promo > td.live-total-cell { background: var(--grp-promo-row); }
.live-matrix tr.grp-promo.is-sub > td.live-total-cell { background: var(--grp-promo-sub); }
.live-matrix tr.grp-china > td.live-total-cell { background: var(--grp-china-row); }
.live-matrix tr.grp-china.is-sub > td.live-total-cell { background: var(--grp-china-sub); }
.live-matrix tr.grp-basket > td.live-total-cell { background: var(--grp-basket-row); }
.live-matrix tr.grp-basket.is-sub > td.live-total-cell { background: var(--grp-basket-sub); }
.live-matrix tr.grp-bogo > td.live-total-cell { background: var(--grp-bogo-row); }
.live-matrix tr.grp-bogo.is-sub > td.live-total-cell { background: var(--grp-bogo-sub); }

[data-theme="dark"] {
  /* Same ladder, inverted: near-black tints that still hold their hue. The
     band step is the lightest because it is the group's title bar; the
     derived rows sink closest to the page. */
  --grp-sales-band: #16394c;  --grp-sales-row: #102b3a;  --grp-sales-sub: #0b1f2b;
  --grp-promo-band: #453507;  --grp-promo-row: #332705;  --grp-promo-sub: #231b03;
  --grp-china-band: #5a1e0e;  --grp-china-row: #40150a;  --grp-china-sub: #2c0e06;
  --grp-basket-band: #332055;  --grp-basket-row: #26183f;  --grp-basket-sub: #1a102b;
  --grp-bogo-band:  #4a1236;  --grp-bogo-row:  #360d27;  --grp-bogo-sub:  #24081a;
}

/* --- today's dot pings like a radar sweep --------------------------------- */
/* A ring leaving the dot and fading out. The dot alone said "you are here";
   the ring says the figure under it is still moving. Transform and opacity
   only, so it costs no layout -- and it is the one animation on Home that
   never stops, which is why it is slow and faint rather than insistent. */
.spark-dot { overflow: visible; }
.spark-ping {
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1px solid currentColor;
  animation: sparkPing 2.6s cubic-bezier(0, 0.5, 0.5, 1) infinite;
  pointer-events: none;
}
@keyframes sparkPing {
  0% { transform: scale(1); opacity: 0.7; }
  70% { opacity: 0; }
  100% { transform: scale(5); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .spark-ping { animation: none; opacity: 0.35; }
}

/* --- and the colour of both is where today stands ------------------------- */
/* Four bands against the pace the month needs: under half of it, closing on
   it, past it, and past it by half again. The last one is the only place in
   this app that gets more than one hue at once, which is the point -- it has
   to be unmistakable from the corner of an eye, and a day at 150% of target is
   rare enough to be worth looking up for.
   These are their own tokens rather than --danger and --delta-good: nothing
   here is an error or a comparison, it is a day in progress, and the reds and
   greens used for those are pitched darker than a 6px dot can carry. */
/* Behind is the red the falling triangles in the tiles already use -- the
   same reading in two places on one screen should not be two reds, and this
   one is the app's. It follows --delta-bad rather than copying it, so it
   cannot drift away from those triangles later.
   The other two clear 3:1 on the white card -- the floor for a graphic that
   carries meaning, and this dot is 6px across with colour as its whole
   vocabulary. That floor is what sets how light they can be: the softest
   orange that clears it is #d47c12 at 3.14, and a paler one simply is not
   there on white. The rainbow's yellow is a gold for the same reason. */
:root {
  --ping-behind: var(--delta-bad);
  --ping-near: #d47c12;     /* 3.14:1 */
  --ping-good: #3fa268;     /* 3.19:1 */
  --ping-rainbow: conic-gradient(from 210deg, #e2685c, #d47c12, #a88a0a, #3fa268, #3a7ca5, #8b62c2, #e2685c);
}
:root[data-theme="dark"] {
  /* Lifted, not inverted: the same four readings, held clear of a near-black
     card the way every other colour in this theme is. */
  --ping-behind: var(--delta-bad);
  --ping-near: #efa552;
  --ping-good: #6cc286;
  --ping-rainbow: conic-gradient(from 210deg, #ef8175, #efa552, #e6d05e, #6cc286, #6fb3d8, #a98adb, #ef8175);
}
.spark-dot.is-behind { color: var(--ping-behind); }
.spark-dot.is-near { color: var(--ping-near); }
.spark-dot.is-good { color: var(--ping-good); }
/* A gradient cannot be a border colour, so the ring is a filled disc with its
   middle masked out -- 1px of ring, the same weight the other three draw. */
.spark-dot.is-blowout { background: var(--ping-rainbow); }
.spark-dot.is-blowout .spark-ping {
  border-color: transparent;
  background: var(--ping-rainbow);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 1px), #000 calc(100% - 1px));
  mask: radial-gradient(closest-side, transparent calc(100% - 1px), #000 calc(100% - 1px));
}

/* --- tap a day, read its figure ------------------------------------------- */
/* A guide from the x-axis up to the point, so it measures the value rather
   than just marking the column, and a label beside it. One line and one label
   for the whole chart -- the day being shown moves them, so a 31-day month
   costs exactly what a 5-day one does. */
.spark-probe {
  position: absolute;
  top: 0;
  width: 0;
  height: 44px;
  pointer-events: none;
}
.spark-probe i {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 1px;
  background: var(--accent-ink);
  opacity: 0.55;
}
.spark-probe b {
  position: absolute;
  top: -1px;
  left: 6px;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: -0.1px;
  color: var(--on-accent);
  background: var(--accent-ink);
  border-radius: 4px;
  padding: 2px 5px;
}
/* Past the right of the chart the label would run off the screen -- and a
   label hanging off that edge is what widened the whole document once. */
.spark-probe.is-flipped b { left: auto; right: 6px; }
/* The chart answers taps and drags, so the whole block is the target -- the
   svg is only 44px of a 55px block. */
.spark {
  cursor: pointer;
  /* Dragging across a chart was selecting it: the browser saw a finger moving
     over text and did the one thing it knows. The blue wash covered the very
     line being read. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  /* Sideways is ours, up and down stays the page's. Declaring it here rather
     than calling preventDefault means scrolling never has to fight the chart
     for a gesture -- the browser decides once, by direction. */
  touch-action: pan-y;
}
.spark svg,
.spark-days { pointer-events: none; }

/* --- admin: the same rows, closer together -------------------------------- */
/* Every panel here is a list of short rows, and the space between them was
   sized for a page with fewer of them. The tap targets are NOT touched: the
   32px Remove and Reset stay, because two of them delete things and a target
   you cannot brush past is the point. All of this comes out of padding and
   gaps, which cost nothing to give up. Scoped to .admin-panel, so no other
   screen moves. */
.admin-panel { padding: 12px; }
.admin-panel .wl-row { padding: 4px 8px; }
.admin-panel .wl-list { gap: 3px; }
.admin-panel .admin-row { padding: 7px 10px; gap: 8px; }
.admin-panel .admin-list { gap: 6px; margin-top: 6px; }
.admin-panel .admin-row-main { gap: 3px; }
.admin-panel .admin-bar { height: 4px; }
.admin-panel .activity-row { padding: 5px 8px; }
/* The blurb above each list explains the panel once; it does not need a full
   line of air under it. */
.admin-panel > p { margin-bottom: 8px; }

/* --- Insights & Reports ----------------------------------------------------- */
.insights-fresh {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  color: var(--muted);
  margin-bottom: 12px;
}
.ins-summary {
  display: flex;
  gap: 14px;
  margin-bottom: 12px;
  font-size: 13px;
  font-weight: 650;
  color: var(--muted);
}
.ins-summary b { font-size: 17px; color: var(--text); margin-right: 3px; }
.ins-card {
  border: 1px solid var(--border);
  border-radius: 12px;
  margin-bottom: 10px;
  background: var(--surface);
  overflow: hidden;
}
/* Three levels, and each one has to WIN over the level below it. The first
   cut had the shop name at 14px over 13px rows and a 10px list label under
   them — a header smaller than its own content, which is why it read as one
   grey run. Size, weight and ground all step down together now:
   shop band 16/800 on a tint · list label 11 in its own semantic colour ·
   rows 13 plain. */
.ins-head {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 48px;
  padding: 8px 12px;
  border: 0;
  background: var(--surface-soft);
  color: var(--text);
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.ins-card.is-open .ins-head { border-bottom: 1px solid var(--border); }
.ins-card.is-clean .ins-head { cursor: default; opacity: 0.6; }
.ins-head b { font-size: 16px; font-weight: 800; letter-spacing: -0.2px; }
.ins-head span { flex: 1; font-size: 12px; color: var(--muted); font-weight: 650; }
.ins-chev { flex: 0 0 auto; color: var(--muted); transition: transform 140ms ease; }
.ins-head[aria-expanded="true"] .ins-chev { transform: rotate(180deg); }
.ins-body { display: none; padding: 0 12px 10px; }
.ins-card.is-open .ins-body { display: block; }
/* The list label. Coloured by what it means — the same two tones its rows
   already use — so the eye lands on it before the names underneath, and the
   two lists inside one card cannot run together. */
.ins-sub {
  margin: 12px 0 0;
  padding-bottom: 4px;
  border-bottom: 1px solid currentColor;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--danger);
}
.ins-sub.is-low { color: var(--warn-ink); }
.ins-sub:first-child { margin-top: 4px; }
.ins-list { list-style: none; margin: 4px 0 0; padding: 0; }
/* As a bare `.ins-hidden` (0,1,0) this lost to `.ins-list li` (0,1,1) below
   it, so every row rendered visible and "Show all N" revealed nothing.
   Qualified to (0,2,1) it wins on specificity, order notwithstanding. */
.ins-list li.ins-hidden { display: none; }
.ins-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 0;
  font-size: 13px;
  border-top: 1px solid rgba(var(--ink-rgb), 0.06);
}
.ins-list.is-low li .ins-tail { color: var(--warn-ink); }
.ins-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ins-tail { flex: 0 0 auto; font-size: 12px; font-weight: 700; color: var(--danger); }
.ins-more { border-top: 1px solid rgba(var(--ink-rgb), 0.06); }
.ins-more button {
  min-height: 40px;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--accent-ink);
  font-weight: 700;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
  padding: 0;
}
.ins-badge {
  font-style: normal;
  font-size: 10px;
  font-weight: 700;
  padding: 1px 6px;
  margin-left: 6px;
  border-radius: 999px;
  background: var(--surface-soft);
  color: var(--muted);
  border: 1px solid var(--border);
}
.ins-badge.is-bad { color: var(--danger); border-color: currentColor; }
.ins-gift { margin-bottom: 8px; }
.ins-gift-name { font-weight: 700; font-size: 13px; margin-bottom: 4px; }
.ins-gift-days { display: flex; flex-wrap: wrap; gap: 6px; }
.ins-day {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  padding: 3px 8px;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
}
.ins-day.is-warn { color: var(--warn-ink); }
.ins-day.is-bad { color: var(--danger); border-color: currentColor; }
.ins-branded-now { font-size: 24px; font-weight: 700; margin-bottom: 6px; }
.ins-branded-now i { font-style: normal; font-size: 13px; vertical-align: 3px; }
.ins-report {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 46px;
  padding: 4px 2px;
  text-decoration: none;
  color: var(--text);
  font-weight: 700;
  font-size: 14px;
  border-top: 1px solid rgba(var(--ink-rgb), 0.06);
}
.ins-report:first-of-type { border-top: 0; }
.ins-report .sub { display: block; font-size: 11px; font-weight: 650; color: var(--muted); }
.ins-report svg { flex: 0 0 auto; color: var(--muted); }

/* --- payday chip + stock-risk switch --------------------------------------- */
/* Payday context under the delta line: it exists only inside a window, so it
   reserves nothing when absent. Muted mono — information, not an alarm. */
.payday-chip {
  /* 7, not 2: the inherited-targets line above it is a warning and this is
     context, and at 2px the two mono lines read as one stacked block. */
  margin: 7px 0 0;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--muted);
}

/* Same shape as the payday chip, deliberately NOT the same colour. Payday is
   context you may use; this is a statement that every target figure on the
   screen belongs to a month that has already ended, which is a thing to fix.
   --warn-ink is the token that carries that without shouting, and it is the
   one that stays legible on both grounds (measured, not assumed). */
.target-source {
  display: block;
  width: 100%;
  margin: 4px 0 0;
  padding: 0;
  border: 0;
  background: transparent;
  text-align: left;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.35;
  color: var(--warn-ink);
}

/* Only the owner gets this variant, and only because they can act on it. The
   height is the touch floor, not a design choice — the text stays the same
   size and the extra room is padding. */
.target-source.is-action {
  position: relative;
  min-height: 0;
  padding: 0;
  cursor: pointer;
  border-radius: 6px;
}
/* Grows DOWNWARD only. Centred, the invisible half above would have covered
   the sparkline's date strip — and the chart owns a drag gesture there, so it
   cannot give up a single row of pixels to something it does not know about.
   Below is the payday line and the progress bar, neither of which is
   touchable, so the room is genuinely free. */
.target-source.is-action::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 44px;
}

.target-source.is-action:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- Daily Sales History --------------------------------------------------- */
/* Two pickers, then depth. Everything below them is the existing comparison
   table/chart pair, so this file only has to add the chrome around them. */
.hist-lead {
  margin: 12px 0 8px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--muted);
}
#viewHistory .chip-rail { padding-bottom: 2px; }

/* The month rail is a CHOICE, so the chosen one has to win on fill, not on a
   tint you have to hunt for. Solid accent against an outlined pill: legible at
   a glance and still legible for anyone who cannot separate the two hues. */
#viewHistory .chip {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
  font-weight: 700;
}
#viewHistory .chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

/* The metric switch reads as the month rail because it IS the same kind of
   choice. The 2x2 box it replaces put two controls on one plane and two on
   another, which made the pair below look like a second, lesser row. */
/* The warm tint belongs to the readout card alone. Painting the whole page
   with it also painted the fixed tab bar transparent, so the last rows of the
   table showed straight through the navigation. */
.hist-readout {
  background: #fdf7e3;
  border: 1px solid rgba(var(--ink-rgb), 0.07);
}
[data-theme="dark"] .hist-readout { background: #241f14; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hist-readout { background: #241f14; }
}
#viewHistory .comparison-card,
#viewHistory .comparison-table-wrap { border-radius: 10px; }
/* The chart is the widest thing on the screen and the one that suffers from
   walls. 12px on each side plus the axis gutter was a visible empty band. */
#viewHistory .comparison-card { padding: 10px 6px; }

/* Six tabs now that History has its own. "DASHBOARD" is the long one, so the
   label shrinks a step earlier than it used to. */
@media (max-width: 430px) {
  .tabbtn-label { font-size: 8px; letter-spacing: 0.2px; }
  .tabbar { gap: 1px; padding-inline: 2px; }
}

/* --- History: the controls -------------------------------------------------
   Chips were 44px tall and the two rails ate a third of the screen before a
   number appeared. The BUTTON is 34px now; the FINGER still gets 44, taken
   above and below by a pseudo-element, which costs no layout. */
#viewHistory .chip {
  min-height: 34px;
  padding: 3px 12px;
  position: relative;
}
#viewHistory .chip::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 44px;
}
/* The measure is a choice between three, so it is laid out as three — equal
   thirds of the line, no scrolling rail, nothing hiding past the right edge. */
#viewHistory .hist-metrics {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
}
#viewHistory .hist-metrics .chip {
  min-width: 0;
  width: 100%;
  padding-inline: 4px;
  justify-content: center;
}
.hist-filter-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.hist-filter-head .hist-filter-lbl { margin-bottom: 5px; }
/* Compare is a mode, not a command: off, a month tap is a switch and costs one
   tap; on, taps add and remove. It is drawn as a checkbox because that is what
   it is — a thing that is on or off — where a pill that merely changed colour
   left it ambiguous whether a tap had done something or set something. */
.hist-check {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* A flex item shrinks by default, and "MONTH COMPARISON" is long enough that
     it was being squeezed 6px past its own text. It is a label: it keeps its
     width and the rail beside it can have the rest. */
  flex: 0 0 auto;
  white-space: nowrap;
  gap: 6px;
  margin: 0 0 5px;
  padding: 3px 4px 3px 2px;
  min-height: 26px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.hist-check::after { content: ""; position: absolute; inset: -10px -6px; }
.hist-check-box {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  border: 1.5px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  color: transparent;              /* the tick is drawn always, invisible until on */
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.hist-check-box svg { width: 12px; height: 12px; display: block; }
.hist-check.is-on { color: var(--accent-ink); }
.hist-check.is-on .hist-check-box {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* --- History: the readout --------------------------------------------------
   Four shops and four figures on ONE line: the names lose their space (YS1),
   the row stops wrapping, and the gap between the head line and the shops is
   real space rather than the 4px that made them look like one paragraph. */
/* Equal cells, not a row that ends where the words happen to end: buttons with
   a ragged right edge read as text, and a fluid row was one long name away from
   spilling off the card. The count comes from the renderer (--hist-read-cols,
   capped at four) rather than being fixed at four — Save Care has two shops,
   and a two-button row in a four-column grid left half the card empty and read
   as a screen that had failed to finish loading. Comparing months keeps three
   across, two down. */
.hist-read-shops {
  display: grid !important;
  grid-template-columns: repeat(var(--hist-read-cols, 4), minmax(0, 1fr));
  gap: 5px !important;
  margin-top: 9px !important;
}
/* Same rule, tighter cap: three across is as wide as month names read, and
   comparing two months should not leave a third cell empty either. */
.hist-read-shops.is-multi { grid-template-columns: repeat(var(--hist-read-cols, 3), minmax(0, 1fr)); }
/* A switch has to look pressable before anyone tries it: barely-rounded
   corners, a hairline, and one pixel of shadow underneath — enough to sit off
   the card, not so much that four of them shout. OFF is the same button pressed
   IN: the lift goes, the shadow turns inward, the fill goes flat and the colour
   drains out of the dot. */
.hist-read-shop {
  /* A <button> keeps its native widget look until this is said, and the
     platform paints the face itself — the fill stayed white in dark mode and
     an inline background could not beat it either. */
  appearance: none;
  -webkit-appearance: none;
  min-width: 0;
  gap: 4px !important;
  white-space: nowrap;
  font-size: 11px;
  position: relative;
  padding: 5px 3px;
  border: 1px solid rgba(var(--ink-rgb), 0.13);
  border-radius: 6px;
  background: var(--surface);
  box-shadow: 0 1px 0 rgba(var(--ink-rgb), 0.09), 0 1px 3px rgba(var(--ink-rgb), 0.06);
  font-family: inherit;
  color: var(--muted);
  justify-content: center;
  transition: transform 90ms ease, box-shadow 90ms ease, background-color 120ms ease;
}
.hist-read-shop b { color: var(--text); }
.hist-read-shop:active { transform: translateY(1px); box-shadow: none; }
.hist-read-shop.is-off {
  /* NOT --bg: it is #ffffff in the light theme, the same as --surface, so the
     off state changed nothing there. --surface-soft is a step away from the
     card in both themes, which is what this needs. */
  background: var(--surface-soft);
  box-shadow: inset 0 1px 2px rgba(var(--ink-rgb), 0.10);
  transform: translateY(1px);
}
.hist-read-shop.is-off b { color: var(--muted); text-decoration: line-through; }
.hist-read-shop.is-off i { background: var(--border) !important; }
.hist-read-shop::after { content: ""; position: absolute; inset: -10px -1px; }
.hist-read-shop i { flex: 0 0 auto; }

/* --- History: six columns that fit ----------------------------------------
   Date + four shops + Total is 6, and at the shared 420px minimum the last one
   lived off the right edge where nobody scrolled to find it. */
#viewHistory .comparison-table { min-width: 0; width: 100%; }
#viewHistory .comparison-table th,
#viewHistory .comparison-table td {
  padding: 7px 3px;
  font-size: 11.5px;
  text-align: right;
}
#viewHistory .comparison-table th:first-child,
#viewHistory .comparison-table td:first-child { text-align: left; padding-left: 8px; }
#viewHistory .comparison-table td:last-child,
#viewHistory .comparison-table th:last-child { padding-right: 8px; }
#viewHistory .comparison-table thead th { font-size: 10.5px; }

/* The Months table's Average/Total switch, and the way out to the full sheet. */
.hist-block-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}
.hist-block-head .eyebrow { margin: 0; }
.hist-seg {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface);
}
.hist-seg button {
  position: relative;
  padding: 4px 10px;
  min-height: 26px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.4px;
}
.hist-seg button::after { content: ""; position: absolute; inset: -9px 0; }
.hist-seg button.is-on { background: var(--ink-soft); color: var(--surface); }
.hist-seg-note {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.6px;
  color: var(--muted);
  text-transform: uppercase;
}
/* Show all / show fewer. A full-width bar under the table rather than a link
   beside it: it is the width of the thing it opens, which is the clearest
   possible statement of what it acts on. The count rides along so the reader
   knows what they are asking for before they ask. */
.hist-more {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  min-height: 44px;
  margin-top: 8px;
  padding: 6px 10px;
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--accent);
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}
.hist-more span {
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent-weak);
  color: var(--accent-ink);
  font-size: 10px;
}
.hist-more:active { background: var(--surface-soft); }

.hist-full {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 10px;
  min-height: 44px;
  padding: 4px 2px;
  color: var(--accent);
  font-size: 12.5px;
  font-weight: 700;
  text-decoration: none;
}
.hist-full:hover { text-decoration: underline; }

/* One labelled filter card (mockup A, owner's pick 2026-08-10): the tiny
   caption names each rail, a hairline says where one choice ends and the next
   begins, and the two selections keep different inks — months in the accent,
   the measure in near-black. Landscape support was tried and removed: turning
   the phone did not earn its keep, so this screen is one portrait column and
   pinch-zoom is the way to more detail (see historyChartWidth). */
.hist-filter {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 10px 10px 11px;
}
.hist-filter-lbl {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.8px;
  color: var(--muted);
  margin: 0 0 5px;
  text-transform: uppercase;
}
.hist-filter-div {
  border-top: 1px solid var(--surface-soft);
  margin: 10px -10px 9px;
}
#viewHistory .hist-metrics .chip.is-active {
  background: var(--ink-soft);
  border-color: var(--ink-soft);
  color: var(--surface);
}
#viewHistory .chip { padding: 6px 12px; }

/* The readout for the selected day. Above the chart, never over it: a tooltip
   that follows the finger covers the very lines it describes, and on a phone
   the finger covers the tooltip. Two fixed lines, so nothing below it moves as
   the day changes. */
.hist-readout {
  /* background + border: see the warm-tint rule above — one place decides */
  border-radius: 8px;
  padding: 8px 10px;
  margin-bottom: 6px;
}
.hist-read-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
  color: var(--muted);
}
.hist-read-head strong { font-size: 14px; color: var(--text); }
.hist-read-head b { color: var(--text); font-weight: 700; }
.hist-read-shops {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin-top: 4px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.hist-read-shop { display: inline-flex; align-items: center; gap: 4px; color: var(--muted); }
.hist-read-shop i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.hist-read-shop b { color: var(--text); font-weight: 700; }

/* The rule that says "you are here". Dashed so it never competes with a data
   line, and drawn under the marks. */
.hist-rule {
  stroke: var(--text);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.45;
}
.hist-mark { stroke: var(--surface); stroke-width: 1.5; }
/* Horizontal drags belong to the scrubber; vertical ones must still scroll the
   page, or the chart becomes a trap halfway down a long screen. */
.hist-chart { touch-action: pan-y; cursor: crosshair; }

/* Both tables answer the same selection. */
#viewHistory .hist-day-row.is-selected > *,
#viewHistory .hist-month-row.is-selected > * {
  background: var(--accent-weak);
  color: var(--accent-ink);
  font-weight: 700;
}
#viewHistory .hist-day-row,
#viewHistory .hist-month-row { cursor: pointer; }
/* A grid item's min-width is auto, which means it refuses to shrink below its
   content — so a 6-column table made the whole VIEW 548px wide and
   overflow-x: auto on the wrapper never got the chance to fire. The page
   scrolled sideways instead of the table. Both links in the chain have to
   opt out. (.view.is-active is display:grid — see the view transitions block.) */
#viewHistory,
#historyContent,
#viewHistory .comparison-table-wrap { min-width: 0; }
/* Four metrics do not fit one row of readable type at 390px — two by two does,
   and keeps every target at full size. */
.hist-block { margin-top: 20px; }
/* The month table stacks a shop name over what the column means, so the header
   stays honest without a second header row. */
.hist-months th .hist-sub {
  display: block;
  font-size: 9px;
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--muted);
  text-transform: none;
}
.hist-note {
  margin: 6px 0 0;
  font-size: 11px;
  color: var(--muted);
}

/* --- Home's cold-error card ----------------------------------------------- */
/* Replaces the skeleton when the first load fails with nothing to bridge
   from — a shimmer with no exit was a dead end. */
.home-error {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 40px 16px;
  text-align: center;
}
.home-error p { color: var(--muted); font-weight: 650; }
.home-error button { min-height: 44px; padding: 10px 28px; border-radius: 10px; }

/* --- reply-to: hold a bubble, quote it ------------------------------------ */
/* The browser's own long-press behaviour — text selection, the iOS callout —
   would race the hold gesture, so bubbles opt out the way LINE's do. The
   trade is deliberate: you reply to a message rather than copy from it. */
#chatThread .chat-bubble {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* The quoted block inside a sent bubble. currentColor at low alpha, so the
   same rule reads correctly on the accent bubble and in dark mode, without
   naming any of their colours. */
.chat-quote {
  border-left: 2px solid currentColor;
  padding: 2px 8px;
  margin-bottom: 6px;
  font-size: 12px;
  line-height: 1.4;
  opacity: 0.75;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.chat-quote b {
  display: block;
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

/* The bar above the composer while a reply is armed. */
.chat-reply-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  margin-bottom: 6px;
  border-radius: 8px;
  background: var(--surface-soft);
  border: 1px solid var(--border);
}
.chat-reply-bar.is-hidden { display: none; }
.chat-reply-quote {
  flex: 1;
  min-width: 0;
  border-left: 2px solid var(--accent);
  padding-left: 8px;
  font-size: 12px;
  line-height: 1.35;
}
.chat-reply-quote b {
  display: block;
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.chat-reply-quote span {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--muted);
}
.chat-reply-bar button {
  flex: 0 0 auto;
  min-height: 32px;
  min-width: 32px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  font-size: 18px;
  cursor: pointer;
}

/* "Select and copy" opens the message in its own sheet, where nothing of
   ours — press handlers, user-select rules, thread repaints — exists to fight
   Chrome's selection. The body is the ONLY place in the chat where text
   selects, and there it selects completely. */
.chat-select-sheet {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  background: rgba(0, 0, 0, 0.4);
  animation: chatMenuIn 120ms ease both;
}
.chat-select-card {
  display: flex;
  flex-direction: column;
  width: min(560px, 100%);
  max-height: 78vh;
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
  animation: chatMenuPop 140ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
.chat-select-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 6px 8px 16px;
  border-bottom: 1px solid var(--border);
}
.chat-select-head b {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.chat-select-head button {
  min-height: 40px;
  min-width: 40px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 20px;
  cursor: pointer;
}
.chat-select-body {
  padding: 12px 16px 16px;
  overflow-y: auto;
  font-size: 14px;
  line-height: 1.6;
  white-space: pre-line;
  overflow-wrap: anywhere;
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}
@media (prefers-reduced-motion: reduce) {
  .chat-select-sheet, .chat-select-card { animation: none; }
}

/* --- the long-press menu -------------------------------------------------- */
/* Centred rather than anchored to the bubble: a bubble can be anywhere in a
   scrolling thread, including under the keyboard, and a menu that follows it
   would need to solve flipping, clamping and re-anchoring on scroll for no
   gain. The scrim already says which gesture you are in. */
.chat-menu-scrim {
  position: fixed;
  inset: 0;
  z-index: 60;
  -webkit-user-select: none;
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.32);
  animation: chatMenuIn 120ms ease both;
}
.chat-menu {
  min-width: 220px;
  padding: 6px;
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
  animation: chatMenuPop 140ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
.chat-menu button {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 48px;          /* a thumb target, not a pointer one */
  padding: 0 14px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--text);
  font-size: 15px;
  font-weight: 650;
  text-align: left;
  cursor: pointer;
}
.chat-menu button:hover { background: var(--surface-soft); }
.chat-menu button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.chat-menu button svg { flex: 0 0 auto; color: var(--muted); }
@keyframes chatMenuIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes chatMenuPop {
  from { opacity: 0; transform: scale(0.94); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .chat-menu-scrim, .chat-menu { animation: none; }
  .ins-chev { transition: none; }
}

/* Said once, above the composer, then gone. */
.chat-toast {
  position: absolute;
  left: 50%;
  bottom: 76px;
  transform: translateX(-50%);
  z-index: 61;
  padding: 7px 14px;
  border-radius: 999px;
  background: rgba(var(--ink-rgb), 0.88);
  color: var(--surface);
  font-size: 13px;
  font-weight: 650;
  white-space: nowrap;
  pointer-events: none;
  animation: chatMenuIn 140ms ease both;
}

/* --- the keyboard is open on the chat screen ------------------------------ */
/* See wireChatKeyboard in app.js. While a composer field has focus the tab
   bar steps aside (on iOS it rode the keyboard up and sat exactly on top of
   the composer), and the workspace gives back the room it reserves for the
   bar — so the keyboard-sized chat shell is the whole page and iOS has no
   overflow left to yank around. */
body.kb-open .tabbar { display: none; }
body.kb-open .workspace { padding-bottom: 0; }
/* While typing, the JS owns the shell's height completely: the resting
   min-heights (280/320px, purely cosmetic floors for empty desktop states)
   were silently overruling the keyboard estimate — the JS computed 258px,
   the CSS floored it back to 280, and the composer sat behind the keyboard
   by exactly the difference. */
body.kb-open .chat-shell { min-height: 0; }

/* --- Lucky Egg campaign tables ------------------------------------------- */
/* .comparison-table carries min-width:420px because the SKU and history
   tables genuinely need it — six columns of money do not fit a phone and
   scrolling inside the wrapper is the right answer there. These are four or
   five short columns of counts, so the floor only forced a 32px sideways
   scroll for nothing. Fit the wrapper instead, and let the cells breathe less
   at the narrow end. */
.comparison-table.is-campaign { min-width: 100%; }
.comparison-table.is-campaign th,
.comparison-table.is-campaign td { padding: 8px 6px; }
.comparison-table.is-campaign th:first-child,
.comparison-table.is-campaign td:first-child { padding-left: 10px; }
.comparison-table.is-campaign th:last-child,
.comparison-table.is-campaign td:last-child { padding-right: 10px; }

/* Draws and vouchers are two different populations — a draw wins a voucher OR
   a gift kept at the shop — and one run of numbers invited exactly the division
   nobody should make (redeemed ÷ draws). Two labelled blocks with a rule
   between them, so the eye cannot pair a figure with the wrong denominator. */
.camp-summary {
  display: grid;
  gap: 10px;
  margin-bottom: 14px;
}
.camp-set {
  padding-left: 10px;
  border-left: 2px solid var(--rule);
}
.camp-set-lbl {
  margin: 0 0 3px;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--muted);
}
.camp-set .ins-summary { margin-bottom: 3px; }
.camp-set .cap { margin: 0; }

/* Same trap the history view hit, and for the same reason: .view.is-active is
   display:grid, a grid item's min-width is auto, so the day rail refused to
   shrink below its 31 chips — 1794px wide — and overflow-x: auto never fired.
   The PAGE scrolled sideways instead of the rail. Every link in the chain has
   to opt out, or the widest one wins. */
#viewCampaign,
#campaignContent,
#viewCampaign .chip-rail-wrap,
#viewCampaign .comparison-table-wrap { min-width: 0; }

/* The day rail. `.chip.is-active` was written only under #viewHistory, so the
   campaign's chips all rendered identically and nothing said which day was on
   screen. Same treatment as the month rail it is modelled on — one selected
   chip, filled with the accent. */
#viewCampaign .chip { padding: 6px 12px; }
#viewCampaign .chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}


/* ── KOL Schedule ──────────────────────────────────────────────────────────
   More > KOL Schedule: the marketing team's booking sheet as a calendar.
   Month by default with a List alternative, and a Next up line above both.
   Built at 375px first — the grid must show a whole month without scrolling,
   which is what keeps the header under ~90px. */
#viewKolSchedule, #kolContent { min-width: 0; }

.kol-sync {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--muted);
  opacity: 0.7;
  margin: 0 0 10px;
}

/* The one line that survives every month and every view. Dashed, because it
   is always about something that has not happened yet. */
.kol-nextup {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 11px;
  border: 1px dashed var(--border);
  border-radius: 9px;
  background: var(--surface-soft);
  margin-bottom: 12px;
}
.kol-nextup-label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--muted);
  opacity: 0.75;
  margin: 0 0 2px;
}
.kol-nextup-who { font-size: 13.5px; font-weight: 600; letter-spacing: -0.2px; margin: 0; }
.kol-nextup-when {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--muted);
  margin: 0 0 0 auto;
  text-align: right;
  flex: 0 0 auto;
  line-height: 1.35;
}

/* Month / List. A segmented control rather than two pills: the choice is
   exclusive and permanent, not an action. */
.kol-seg {
  display: flex;
  background: var(--surface-soft);
  border-radius: 8px;
  padding: 3px;
  margin-bottom: 14px;
}
.kol-seg button {
  flex: 1;
  border: 0;
  background: transparent;
  border-radius: 6px;
  padding: 7px 0;
  min-height: 38px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
}
.kol-seg button.is-on {
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 0 0 1px var(--border) !important;
}

.kol-monthbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.kol-monthbar b { font-size: 15px; font-weight: 600; }
.kol-step {
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 17px;
  line-height: 1;
  min-width: 44px;
  min-height: 44px;
  cursor: pointer;
}

.kol-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.kol-grid-head {
  font-family: var(--mono);
  font-size: 9px;
  color: var(--muted);
  opacity: 0.7;
  text-align: center;
  padding: 3px 0;
}
.kol-cell {
  aspect-ratio: 1;
  border: 0;
  background: transparent;
  border-radius: 7px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  font: inherit;
  font-size: 12px;
  color: var(--text);
  cursor: pointer;
  padding: 0;
}
.kol-cell.is-outside { visibility: hidden; }
.kol-cell.has-entries { background: var(--surface-soft); font-weight: 600; }
.kol-cell.is-today { outline: 1.5px solid var(--accent); outline-offset: -1.5px; }
.kol-cell.is-selected { background: var(--accent); color: var(--on-accent); font-weight: 600; }
.kol-cell-num { line-height: 1; }
.kol-dots { display: flex; gap: 2px; height: 5px; }
.kol-dot { width: 5px; height: 5px; border-radius: 50%; display: block; }
.kol-dot.is-tentative { background: transparent; border: 1px solid var(--muted); }
.kol-cell.is-selected .kol-dot.is-tentative { border-color: var(--on-accent); }

.kol-daysheet {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--hairline-soft);
}
.kol-daysheet-head,
.kol-month-head {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 9px;
}
.kol-month-head {
  margin: 16px 0 9px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--hairline-soft);
}
.kol-month-head:first-child { margin-top: 0; }

/* List view: the date lives in its own column so the eye can run down it. */
.kol-row { display: flex; gap: 11px; margin-bottom: 9px; }
.kol-daycol { width: 38px; flex: 0 0 38px; text-align: center; padding-top: 2px; }
.kol-daynum { display: block; font-size: 19px; font-weight: 340; line-height: 1; letter-spacing: -0.5px; }
.kol-dayname { font-family: var(--mono); font-size: 9px; color: var(--muted); opacity: 0.75; text-transform: uppercase; }
.kol-daycol.is-today .kol-daynum { color: var(--accent); font-weight: 600; }
.kol-rowbody { flex: 1; min-width: 0; }

/* One card shape for both views, so a booking looks the same wherever it is
   read. An <a> when there is a clip to open, a <div> when there is not. */
.kol-card {
  display: block;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 11px;
  margin-bottom: 8px;
  color: inherit;
  text-decoration: none;
}
.kol-card.is-tentative { border-style: dashed; background: var(--surface-soft); }
.kol-card.is-empty {
  border-style: dotted;
  text-align: center;
  padding: 13px;
  color: var(--muted);
  font-size: 12px;
}
.kol-card-head { display: flex; align-items: baseline; gap: 7px; }
.kol-kol { font-size: 14px; font-weight: 600; letter-spacing: -0.2px; }
.kol-time { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-left: auto; }
.kol-content { margin: 5px 0 0; font-size: 12px; color: var(--muted); }
.kol-chips { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 7px; align-items: center; }
.kol-chip {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.3px;
  padding: 2px 6px;
  border-radius: 4px;
}
.kol-chip.is-shop { color: #2f2f2f; }
.kol-chip.is-num { background: var(--surface-soft); color: var(--text); }
.kol-chip.is-quiet { color: var(--muted); opacity: 0.8; }
.kol-chip.is-status.is-posted { background: var(--accent-weak); color: var(--accent-ink); }
.kol-chip.is-status.is-tentative { background: transparent; border: 1px dashed var(--muted); color: var(--muted); }
.kol-chip.is-status.is-missing { background: #fbeeee; color: #8a3b3b; }

/* Reach: views / likes / comments / shares on one line, management only.
   Icons rather than words — four spelled-out labels do not fit a 375px card
   beside the shop and status chips. The label rides along for screen readers. */
.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;
}
.kol-reach {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 8px;
  padding-top: 7px;
  border-top: 1px solid var(--hairline-soft);
  color: var(--muted);
}
.kol-metric { display: inline-flex; align-items: center; gap: 4px; }
.kol-metric svg { flex: 0 0 auto; opacity: 0.65; }
.kol-metric b {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.2px;
}
/* A reading taken before day 7 is not settled. One mark at the end of the row,
   not a caveat repeated on every figure. */
.kol-climbing {
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1;
  color: var(--accent-ink);
  margin-left: auto;
}

/* ── Membership (More > Membership, 2026-10-29) ─────────────────────────── */
.member-chart-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 8px 8px;
}
.member-chart { display: block; width: 100%; height: auto; }
.member-grid { stroke: var(--border); stroke-width: 1; }
.member-axis { fill: var(--muted); font-size: 9px; font-variant-numeric: tabular-nums; }
.member-axis.is-cum { fill: var(--text); font-weight: 700; }
.member-line { fill: none; stroke: var(--text); stroke-width: 1.6; stroke-linejoin: round; }
.member-dot { fill: var(--surface); stroke: var(--text); stroke-width: 1.4; }
.member-chart-card .comparison-legend { margin-top: 6px; padding: 0 4px; }
.member-swatch {
  display: inline-block; width: 8px; height: 8px; border-radius: 2px;
  margin-right: 6px; vertical-align: 1px;
}
.member-delta { font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }
.comparison-table .member-change { padding-left: 2px; white-space: nowrap; }

.member-delta.is-up { color: var(--delta-good); }
.member-delta.is-down { color: var(--delta-bad); }
.member-chart-title { font-size: 12px; font-weight: 700; color: var(--text); margin: 0 4px 4px; }
.member-axis.is-total { fill: var(--text); font-weight: 700; font-size: 9.5px; }
.member-area { fill: var(--text); opacity: 0.06; }
.comparison-table.member-hist th, .comparison-table.member-hist td { padding-left: 5px; padding-right: 5px; }
.comparison-table.member-hist th:first-child, .comparison-table.member-hist td:first-child { padding-left: 12px; }
