/**
 * Team Summary Panel — bottom-of-page team aggregate strips
 * ============================================
 * One panel per mode (#team-daynight-panel, #team-availability-panel,
 * #team-overlap-panel).
 * Shape is identical: header with headline number, 24-cell strip aligned
 * to the timeline grid, optional meta line below. Mode-specific cell hues
 * live in availability-mode.css / overlap-mode.css so the team strip reads
 * as a continuation of the per-row strips above.
 *
 * Visibility is owned by JS (panel.hidden = true|false) driven by
 * view-mode-changed. Don't add CSS-only mode hiding here — it would
 * fight with the dispatcher and double-render on switch.
 * ============================================
 */

.ts-panel {
  /* ⚠️THE BOX MATCHES A BOARD ROW (2026-08-08, measured live).
     A member row is `.timezone-section { margin: 0 var(--space-3); padding:
     ... var(--space-3) }`, so its .timeline starts 24px from the board edge.
     This panel was `margin: 8px 16px; padding: 12px 14px` — 30px — so its
     strip could never sit on the board's column track no matter what the rail
     did. Matching the section's own horizontal margin and padding puts the
     summary's 24 cells exactly under the member rows' 24 cells, which is the
     whole premise of a "one member-sized row" summary. Vertical values stay
     the panel's own: it is a footer card, not a row in the list. */
  margin: 8px var(--space-3, 12px) 24px;
  padding: 12px var(--space-3, 12px) 14px;
  background: var(--bg-secondary, #ffffff);
  border: 1px solid var(--border-color, #e2e8f0);
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}

.ts-panel[hidden] { display: none !important; }

/* Header: title on the left, headline number on the right. The headline is
   the answer to the mode's question — keep it terse so it reads at a glance. */
.ts-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
  /* Anchor the expanded-state collapse chevron (::after below). */
  position: relative;
  cursor: pointer;          /* the whole header is the collapse toggle */
}

/* Expanded-state COLLAPSE affordance (user 2026-06-29: "add a downwards collapse
   chevron to this collapsible"). The collapsed state already peeks an up-chevron
   (.ts-collapsed-chip::after / the deskpan ::before); the expanded panel had no
   matching "click to close" hint even though tapping the header collapses it.
   A small centered chevron-DOWN at the header's top edge mirrors the collapsed
   up-chevron and reads as "pull me down to collapse" — added inline on the
   existing header, so NO new row is introduced on any viewport. */
.ts-panel:not([data-summary-collapsed="true"]) .ts-header::after {
  content: "\f078";                 /* fa chevron-down — "collapse" affordance */
  font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
  font-weight: 900;
  font-size: 11px;
  color: var(--text-secondary, #64748b);
  position: absolute;
  left: 50%;
  top: -6px;
  transform: translateX(-50%);
  opacity: 0.7;
  pointer-events: none;
  transition: opacity 0.15s ease;
}
.ts-panel:not([data-summary-collapsed="true"]) .ts-header:hover::after {
  opacity: 1;
}

.ts-title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary, #0f172a);
  letter-spacing: 0.01em;
}
.ts-title i { font-size: 14px; color: var(--text-secondary, #64748b); }

/* Anchor tabs (my time | UTC), right of the header opposite the mode title
   (owner 2026-09-01: "a tab for my time and utc on the right opposite local
   times title"). Same header-resident-control precedent as the fairness
   period toggle; mobile-summary-collapse.js already exempts buttons from the
   header's collapse tap. */
.ts-anchor-tabs {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border-radius: 8px;
  background: var(--bg-secondary, #f1f5f9);
  flex-shrink: 0;
}
.ts-anchor-tab {
  appearance: none;
  border: none;
  background: transparent;
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text-secondary, #64748b);
  padding: 3px 8px;
  border-radius: 6px;
  cursor: pointer;
  line-height: 1.2;
  white-space: nowrap;
}
.ts-anchor-tab:hover { color: var(--text-primary, #0f172a); }
.ts-anchor-tab.is-active {
  background: var(--bg-primary, #ffffff);
  color: var(--text-primary, #0f172a);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12);
}
/* Collapsed panel: the header is a bare tap handle showing the verdict chip;
   an anchor switch would relabel nothing visible, so hide the tabs. */
.ts-panel[data-summary-collapsed="true"] .ts-anchor-tabs { display: none; }

/* Verdict block: the answer the panel leads with. Lead line is the window you'd
   act on ("Most reachable: 8 – 10 AM"); the why line explains the basis ("the
   most of the group at once today") so the count never reads as an unexplained
   shortfall.

   2026-07-30: the verdict MOVED OUT of .ts-header into the panel body. The
   header is the collapse tap-handle and carries the centred chevron, so it now
   holds the title alone — nothing may sit in the chevron's lane. As a body
   block the verdict is full-width, so it reads left-aligned like every other
   line in the panel rather than right-aligned against a title. */
/* 2026-07-31 (user: "team summary the panels [could] be clearer"): the panel was
   four undifferentiated blocks stacked at the same weight — verdict, strip, hour
   axis, legend — so nothing announced which one was the ANSWER. Seat the verdict
   on its own tinted surface with a status-toned leading rule, so the eye lands on
   it first and the strip below reads as the evidence for it. Tone comes from the
   same [data-tone] the lead text already uses, so the two never disagree. */
.ts-verdict {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  gap: 3px;
  min-width: 0;
  margin-bottom: 12px;
  padding: 8px 10px;
  border-radius: 8px;
  border-left: 3px solid var(--status-empty);
  background: rgba(148, 163, 184, 0.08);
}
.ts-verdict[data-tone="all"]  { border-left-color: var(--status-available); }
.ts-verdict[data-tone="most"] { border-left-color: var(--status-available); }
.ts-verdict[data-tone="few"]  { border-left-color: var(--status-flex); }
.ts-verdict[data-tone="none"] { border-left-color: var(--shading-busy, #dc2626); }
.ts-verdict-lead {
  font-size: 15px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text-primary, #0f172a);
}
.ts-verdict-why {
  font-size: 12px;
  line-height: 1.35;
  color: var(--text-secondary, #64748b);
}
/* Tone tints the lead so the verdict's "good / partial / none" reads at a glance,
   sourced from the shared status tokens (no raw hex → palette-ratchet stays green). */
.ts-verdict[data-tone="all"]  .ts-verdict-lead { color: var(--status-available, #0f766e); }
.ts-verdict[data-tone="most"] .ts-verdict-lead { color: var(--text-primary, #0f172a); }
.ts-verdict[data-tone="few"]  .ts-verdict-lead { color: var(--status-flex, #b45309); }
.ts-verdict[data-tone="none"] .ts-verdict-lead { color: var(--text-secondary, #64748b); }

/* ⚠️PEAK CELLS CARRY NO PAINT OF THEIR OWN (2026-08-10). Every attempt to
   mark the window ON the cells — the teal ring, the green ring, then a green
   top-edge tick — was reported as an unexplained mark ("there are green
   dashes at the top of the cells... this is discontinued"). The ★—◆ arc above
   the strip is the ONLY window mark now, exactly like the board's golden-hour
   arc, and data-peak stays as a data/a11y attribute with no visual. The now
   ring below is unaffected: pink on the current hour is app-wide language. */
.ts-cell[data-peak="true"] {
  /* deliberately paint-free; see the note above */
  z-index: 1;
}

/* ── Collapsed state (default, ALL viewports) ────────────────────────────────
   The panel rests as a single centered line — the verdict chip + a chevron —
   and expands on tap. The chip is the click handle (mobile-summary-collapse.js
   listens on the header too; both toggle). Everything but the chip is hidden in
   the collapsed state. Expanded hides the chip and shows the full panel.
   The collapse default + toggle is driven by data-summary-collapsed on the
   panel (set by JS), so this is purely presentational and survives innerHTML
   rebuilds. */
.ts-collapsed-chip {
  display: none;               /* shown only when collapsed (rule below) */
  align-items: center;
  justify-content: center;
  gap: 8px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary, #0f172a);
  text-align: center;
  /* Anchor the centre-top chevron (::after) and leave a lane for it, so the
     verdict text never runs under the handle. */
  position: relative;
  padding: 20px 4px 2px;
  min-height: 22px;
  /* Allow the verdict to wrap to a second line on a narrow bar rather than
     overflow it; the chevron stays at the end of the flow. */
  flex-wrap: wrap;
  line-height: 1.3;
}
/* The chevron sits CENTRE-TOP (2026-08-06, user: "the chevron should be center
   top"). It was previously pinned to the RIGHT edge via margin-left:auto
   (2026-07-31, "can the collapsable area look nicer") — which fixed the older
   problem of a bare glyph floating wherever the verdict text happened to end,
   but left the two collapse affordances disagreeing: the EXPANDED chevron is
   centred on the header's top edge, so collapsing threw the handle to the
   right and expanding threw it back to the middle. One handle, one place. Now
   both are centre-top, so the control stays put across the toggle and the pair
   reads as the same button in two states.
   Absolute, so it is out of the chip's wrap flow entirely — on a narrow phone
   the verdict can wrap to a second line without ever displacing the handle. */
.ts-collapsed-chip::after {
  content: "\f077";                 /* fa chevron-up — "expand" affordance (matches fairness handle) */
  font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
  font-weight: 900;
  font-size: 11px;
  color: var(--text-secondary, #64748b);
  position: absolute;
  left: 50%;
  top: -2px;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--bg-tertiary, rgba(148, 163, 184, 0.14));
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.ts-collapsed-chip:hover::after {
  background: rgba(148, 163, 184, 0.26);
  color: var(--text-primary, #0f172a);
  transform: translateX(-50%) translateY(-1px);
}
/* The whole collapsed bar is the hit target, so give it a real hover surface —
   without one there is no feedback that the row is clickable at all. */
.ts-collapsed-chip {
  border-radius: 8px;
  transition: background-color 0.15s ease;
}
.ts-collapsed-chip:hover {
  background: rgba(148, 163, 184, 0.10);
}
/* NOTE: no :focus-visible rule here on purpose. The chip is rendered
   aria-hidden="true" with no tabindex and the collapse toggle is a delegated
   CLICK handler (js/ui/mobile-summary-collapse.js), so the chip can never take
   focus — a focus style would be dead CSS implying a keyboard path that does
   not exist. Making the collapse keyboard-operable is a JS change (real button
   semantics), not a styling one. */

/* When collapsed: trim the panel to the chip, hide the rest. */
.ts-panel[data-summary-collapsed="true"] {
  padding-top: 10px;
  padding-bottom: 10px;
}
/* COLLAPSED IS THE SENTENCE ALONE (2026-08-06, user: "it shouldn't show the
   diagram unless expanded"). This REVERSES the same-day rule that kept a slim
   18px strip in the resting bar.
   That rule was added to answer "each cell should have a hourly number", on
   the theory that a collapsed footer should still show the day's SHAPE. In a
   rendered pass the tradeoff went the wrong way: an 18px strip is too short to
   carry a legible hour numeral, so the collapsed bar showed a colour ramp with
   unreadable specks — the diagram without the annotations that make a diagram
   mean anything, in the one state where the user asked for less. Collapsed now
   means collapsed: the verdict chip and the chevron, nothing else. The strip
   and its hour numbers are the EXPANDED read, where there is room to render
   them properly (see .ts-cell-hour). */
.ts-panel[data-summary-collapsed="true"] .ts-header,
.ts-panel[data-summary-collapsed="true"] .ts-verdict,
.ts-panel[data-summary-collapsed="true"] .ts-striprail,
.ts-panel[data-summary-collapsed="true"] .ts-strip,
.ts-panel[data-summary-collapsed="true"] .ts-barlane,
.ts-panel[data-summary-collapsed="true"] .ts-stacklane,
.ts-panel[data-summary-collapsed="true"] .ts-rowhead,
.ts-panel[data-summary-collapsed="true"] .ts-lane,
.ts-panel[data-summary-collapsed="true"] .ts-meta {
  display: none !important;
}
.ts-panel[data-summary-collapsed="true"] .ts-collapsed-chip {
  display: flex;
}
/* Expanded (explicit or default-without-attr): hide the chip, show everything. */
.ts-panel:not([data-summary-collapsed="true"]) .ts-collapsed-chip {
  display: none;
}

/* Strip: 24 columns, identical grid to .timeline above, so cells line up
   column-for-column with each member row. min-height keeps it readable on
   wide screens; --cell-width is implicit from grid 1fr like .timeline.
   32px, up from 28: each cell now carries its own hour numeral at the top
   (see .ts-cell-hour below), and the segment fills need headroom under it. */
.ts-strip {
  display: grid;
  grid-template-columns: repeat(24, 1fr);
  gap: 0;
  /* ⚠️HEIGHT TRACKS THE BOARD'S OWN ROW (2026-08-08). This was a flat 32px, a
     number the board uses nowhere: at the default density a member row's
     .timeline is --timeline-row-height (68px base), and in focus mode 36px. A
     summary that claims to be "one member-sized row" must therefore breathe
     with the same token, or it reads as a chart pinned under the board rather
     than the last row of it. Clamped: the summary carries no event chips, so
     it does not need the full 68px, but it must never be shorter than a
     legible numeral + fill (28px) nor taller than a real row. */
  height: clamp(28px, calc(var(--timeline-row-height, 68px) * 0.62), 44px);
  /* --radius-sm is the board timeline's own corner. */
  border-radius: var(--radius-sm, 6px);
  border: 1px solid var(--border-color, #e2e8f0);
  overflow: hidden;
  /* TRANSPARENT, matching .timeline (style.css): the board deliberately lets
     the section's day/night ambient show through an unpainted track rather
     than flashing near-white. A hardcoded #f8fafc here was the summary's
     brightest patch in dark mode until the override below caught it. */
  background-color: transparent;
}

/* A11y-only row wrapper (2026-08-04). The strip is role="grid" so each cell can
   carry its own hour label, and a grid's children must be rows — but the 24
   cells must stay DIRECT grid items of .ts-strip or repeat(24,1fr) would apply
   to the wrapper instead and collapse every cell into one column.
   `display: contents` gives the row to the a11y tree and nothing to layout. */
.ts-strip-row { display: contents; }

.ts-cell {
  position: relative;
  height: 100%;
  border-right: 1px solid rgba(148, 163, 184, 0.20);
  transition: filter 0.12s ease;
  /* Inline-size container so the X/Y count can hide itself against the CELL
     width — the same @container contract .timeline-cell uses for .ovl-count.
     Children that matter are absolutely positioned, so layout is undisturbed. */
  container-type: inline-size;
}

/* ── Per-cell headcount, the board's .ovl-count idiom ───────────────────────
   2026-08-10 (user: "the cells fail to indicate anything but time ...
   consider bars like overlap or counts to show relevant team info"). Every
   mode's cell now prints its own X/Y — awake/total, reachable/total, or
   here/of — in the exact type the board's overlap cells use for theirs:
   ui-monospace, tabular-nums, white double halo. Top-right, diagonal to the
   bottom-left hour numeral, so the two never collide. Hidden by container
   query below ~30px cell width (phones), where the tooltip/aria sentence
   still carries the number. */
/* ⚠️2026-08-30: the X/Y FRACTION became TWO BARE NUMERALS (owner: "just the
   number not the fraction ie 5 not 5/6" → "we have two bars anyway. Just label
   both"). The fraction needed ~55px in a ~14px cell, which is why it shipped
   with a `@container (max-width:30px) { display:none }` hatch — and why the
   owner reported "no indicators" on a phone: it was hidden at exactly the width
   it mattered. That hatch is REMOVED; a bare numeral fits, so the count now
   survives at every width.

   Each numeral labels the bar it counts: --lo on the filled awake/reachable
   region (bottom), --hi on the empty remainder (top). Opposite corners, since
   two numerals on one vertical axis would crowd the cell. */
.ts-cell-count {
  position: absolute;
  z-index: 2;
  padding: 0.5px 2px;
  border-radius: 3px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 8px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  line-height: 1;
  pointer-events: none;
  user-select: none;
  white-space: nowrap;
}
/* AWAKE / REACHABLE — the filled lower bar. Solid chip, full-strength ink:
   this is the quantity the strip is about. The chip (rather than a bare halo)
   lifts it off the bucket wash; the hairline keeps it legible where the fill
   behind it is near-white. */
/* ⚠️THE BAR IS THE INDICATOR; THE NUMERAL ANNOTATES IT (2026-08-30, owner:
   "currently we have both numbers but no bar as before"). The first cut gave
   this chip an opaque white ground, which in a ~14px cell covered most of the
   ratio bar it sits on — the count won and the bar disappeared. A translucent
   ground and no ring let the bar read through, so the cell still answers
   "how much" at a glance and the numeral answers "exactly how many". */
.ts-cell-count--lo {
  bottom: 1px;
  right: 1px;
  background: rgba(255, 255, 255, 0.62);
  color: var(--text-primary, #0f172a);
}
/* ASLEEP / UNREACHABLE — the empty upper region. Same chip vocabulary so both
   read as counts, but quieter, so the awake figure stays the primary read. */
/* ONLY LOCAL TIMES RENDERS THIS ONE (2026-08-30). Awake vs asleep is a real
   contest between two halves of one roster, and the cell's bar already draws
   the split — so a numeral on each side names what the bar shows. The other
   modes pass `hi` as null: their second number would just be
   total-minus-the-count, competing with the quantity the mode actually owns. */
.ts-cell-count--hi {
  top: 1px;
  left: 1px;
  background: rgba(255, 255, 255, 0.62);
  color: var(--text-secondary, #64748b);
  font-weight: 600;
}
/* The chips invert on the dark track — a white pill would glare there and
   invert the figure/ground the light theme sets up. */
body.dark-mode .ts-cell-count--lo {
  background: rgba(15, 23, 42, 0.62);
  color: var(--text-primary, #e2e8f0);
}
body.dark-mode .ts-cell-count--hi {
  background: rgba(15, 23, 42, 0.62);
  color: var(--slate-400, #94a3b8);
}
/* (Local times no longer paints the night sky in this strip — cells are the
   slate track + awake bar since 2026-08-26 — so no night-hour ink flip is
   needed; the halo above covers every hour.) */
/* ⚠️The `@container (max-width: 30px) { .ts-cell-count { display: none } }`
   hatch that lived here is DELETED (2026-08-30). It existed because the X/Y
   fraction could not fit a phone cell — but hiding the indicator at phone width
   is precisely the defect the owner reported ("still on the team summary cells
   and no indicators"). Bare numerals fit, so there is nothing left to hide
   from. Do not reinstate this without first checking that whatever the cell
   prints actually needs more room than a ~14px cell has. */

/* Availability's bottom ratio bar — the board's .ovl-bar, verbatim idiom:
   height is the hour's reachable share, hue is the mode's own token, 0.4
   opacity so the bucket wash stays the primary read (overlap-mode.css ~164).
   Gives the cell a second, glanceable encoding of MORE/FEWER without a new
   colour language. */
body[data-color-mode="availability"] .ts-cell.ts-cell[data-avail-bucket]::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  /* Two segments, one bar (chosen over a dual top/bottom pair, 2026-08-10):
     green reachable from the floor, amber flex stacked directly above it.
     The stop is precomputed in JS (--avail-green-stop is a % of the STACK,
     not of the cell) because CSS calc cannot divide by a variable. Fallbacks
     collapse to the plain green ratio bar if the vars are absent. */
  height: calc(var(--avail-stack-h, var(--avail-ratio, 0)) * 100%);
  background: linear-gradient(
    to top,
    var(--status-available) 0 var(--avail-green-stop, 100%),
    var(--status-flex) var(--avail-green-stop, 100%) 100%
  );
  opacity: 0.4;
  pointer-events: none;
}
.ts-cell:last-child { border-right: none; }
.ts-cell:hover { filter: brightness(1.06); }

/* ── HOUR AXIS + PEAK BAR (2026-07-31) ──────────────────────────────────────
   User: "couldn't the summaries be similar to my design norms — like overlap
   shaded cells with bars" / "currently the format seems foreign".

   WHAT WAS FOREIGN: the strip had the right CELLS (canonical bucket shading,
   same --overlap-* / --avail-* tokens the board cells use) but none of the
   surrounding TIMELINE furniture the board pairs them with. A bare 24-wide
   colour ramp floating with no hour ticks and no bar doesn't read as "a day" —
   it reads as a generic sparkline / heatmap widget, i.e. a different kind of
   object than the board above it. The board's language is: shaded hour cells +
   an hour axis to locate them + a bar spanning the chosen window.

   This adds the two missing halves. .ts-axis puts sparse hour labels under the
   strip on the SAME 24-column grid, so a cell and its hour line up exactly.
   .ts-peak-bar spans the run the verdict names, mirroring the board's avg/scan
   bar sitting over its cells. Both are presentational siblings — the existing
   cell markup and every bucket rule are untouched. */
.ts-striprail {
  display: grid;
  /* bar / strip / axis / stack — implicit rows, since the stack lane is
     conditional and the axis moved inside the rail (2026-08-01). Pinning a
     fixed row count here silently collapsed the extra lanes. */
  grid-auto-rows: auto;
  gap: 3px;
}

/* ── THE SUMMARY AS A MEMBER ROW (2026-08-08) ────────────────────────────────
   User: "make sure the design is similar to my team member cells per view mode
   ... it's effectively a one-team-member-sized summary of the whole team", and
   "unfamiliar indicators like underlining from above and below without
   explanation."

   A board row is an IDENTITY LINE with a full-bleed 24-cell track beneath it
   (.timezone-label is flex:1/min-width:0 and .timeline-row is its SIBLING —
   there is no left column). .ts-rowhead is that identity line for the group:
   the same icon-then-name-then-readout rhythm the member rows use, so the
   footer reads as one more row of the board rather than a chart beneath it. */
.ts-rowhead {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0 4px;
  min-width: 0;
}
/* Mirrors the board's .home-icon / .offset-pill slot: a small tinted square
   that carries the row's tone. Tone comes from the shared bucket vocabulary,
   so no new colour language enters the panel. */
.ts-rowhead-icon {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: var(--text-secondary, #64748b);
  background: rgba(148, 163, 184, 0.16);
}
.ts-rowhead-icon[data-tone="all"] {
  color: var(--status-available);
  background: color-mix(in srgb, var(--status-available) 16%, transparent);
}
.ts-rowhead-icon[data-tone="few"] {
  color: var(--status-flex);
  background: color-mix(in srgb, var(--status-flex) 18%, transparent);
}
.ts-rowhead-icon[data-tone="none"] {
  color: var(--text-secondary, #64748b);
  background: rgba(148, 163, 184, 0.16);
}
.ts-rowhead-ident { min-width: 0; flex: 1; }
/* .city-name's weight/size, so the two lines read as the same kind of label. */
.ts-rowhead-name {
  font-size: 13px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text-primary, #0f172a);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* .city-offset's sub-line: the BASIS this panel counts on, stated once per
   mode so the strip's numbers can never be read against the wrong cohort. */
.ts-rowhead-sub {
  font-size: 11px;
  line-height: 1.3;
  color: var(--text-secondary, #64748b);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The board row's clock slot. Holds the day's PEAK, never a live count — the
   sticky status bar owns "right now" (see the JS note on live duplication). */
.ts-rowhead-readout {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.15;
}
.ts-rowhead-value {
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary, #0f172a);
}
.ts-rowhead-valnote {
  font-size: 10px;
  color: var(--text-secondary, #64748b);
  white-space: nowrap;
}

/* ── NAMED LANES ────────────────────────────────────────────────────────────
   Every lane states its own name in a fixed left gutter. The 2026-08-06 pass
   put those names INSIDE the lanes (riding the bar, floating over the first
   stack cell) and CSS then density-hid them below 900px — so on a phone, and
   on any short window, the marks went back to being the unexplained
   "underlining from above and below" the user reported. A gutter caption is
   legible at every width because it never competes with the lane for space.

   ⚠️THE CAPTION MUST NOT CONSUME TRACK WIDTH (caught in a live probe,
   2026-08-08). The first attempt gave .ts-lane a `52px 1fr` grid, which
   shortened the strip: measured against the board, the member row's .timeline
   ran x=265 w=750 while the summary strip ran x=329 w=680. Every cell was then
   ~3% narrower than the column it was supposed to summarise, so the footer no
   longer lined up with the rows above — the exact alignment contract the rail
   exists to keep, broken by the thing meant to explain it.

   The caption is therefore ABSOLUTELY POSITIONED in the panel's left padding,
   outside the flow. The lane keeps its full width and the 24 columns stay
   pinned to the board's own track. */
.ts-lane {
  position: relative;
  min-width: 0;
}
.ts-lane > .ts-strip,
.ts-lane > .ts-barlane,
.ts-lane > .ts-stacklane,
.ts-lane > .ts-hourlane { min-width: 0; }

/* ── THE HOUR LANE, under the cells (2026-08-30) ────────────────────────────
   Owner: "times should go under". The cell interior is spoken for by the two
   headcount numerals now, so the hour moves to its own lane beneath the strip:
   the cell says WHAT is counted, the lane says WHEN.

   ⚠️Carries the SAME repeat(24, 1fr) track as .ts-strip and sits inside the
   same .ts-striprail, so column N of the lane IS column N of the strip by
   construction. That is what stops it drifting from the cells it names — the
   failure that retired the previous detached ruler.

   Labels are 24-HOUR ("22", not "10p"): 2 glyphs instead of 3, so all 24 fit at
   phone width with NO density thinning. The other failure of the old ruler was
   hiding most labels on a phone; there is nothing to hide here. */
.ts-hourlane {
  display: grid;
  grid-template-columns: repeat(24, 1fr);
  margin-top: 2px;
  font-size: 8px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary, #64748b);
  user-select: none;
  pointer-events: none;
}
.ts-hour-tick {
  text-align: center;
  /* The ruler is the axis, not the subject — it stays quieter than the counts
     on the cells above, the same relationship the board sets between its own
     hour labels and its cell content. */
  opacity: 0.66;
}
/* Midnight and noon are the landmarks the rest of the strip is read against. */
.ts-hour-tick[data-anchor="true"] {
  font-weight: 700;
  color: var(--text-primary, #0f172a);
  opacity: 1;
}
/* THE NOW TICK, in the canonical pink (2026-08-30). The cell above it already
   carries the pink ring; marking the hour underneath means the ring and its
   NAME read as one indicator instead of a ring on an anonymous column — the
   owner's "but that's not the time?". Same hue as .ts-cell[data-now] and the
   board's own now-line, so all three surfaces speak one colour. */
.ts-hour-tick[data-now="true"] {
  font-weight: 700;
  color: #ec4899;
  opacity: 1;
}
/* The frame, stated once under the ruler: "9 AM your time". Without it a bare
   "08" does not say WHOSE 08 — every member row above prints its own local
   clock, so the reader has no reason to assume this ruler is theirs. */
.ts-hourlane-frame {
  display: flex;
  justify-content: flex-end;
  margin-top: 1px;
  padding-right: 2px;
  font-size: 8px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  user-select: none;
  pointer-events: none;
}
.ts-hourlane-now {
  color: #ec4899;
  font-weight: 600;
}
body.dark-mode .ts-hour-tick[data-now="true"],
body.dark-mode .ts-hourlane-now { color: #f472b6; }
body.day-cycle-enabled .ts-hour-tick[data-now="true"],
body.day-cycle-enabled .ts-hourlane-now { color: #db2777; }
body.dark-mode .ts-hourlane { color: var(--slate-400, #94a3b8); }
body.dark-mode .ts-hour-tick[data-anchor="true"] { color: var(--slate-200, #e2e8f0); }
/* ⚠️ALIGNMENT BEATS THE GUTTER — MEASURED TWICE, LIVE (2026-08-08).
   Two attempts at a left gutter for the captions both broke the one contract
   this rail must keep, that its 24 columns line up with the member rows above:
     1. a `56px 1fr` grid on .ts-lane   → strip x=329 w=680 vs timeline x=265 w=750
     2. left padding on the panel/rail  → same 62px push, same misalignment
   The arithmetic makes it unavoidable: a board row's .timeline starts at
   --space-3 margin + --space-3 padding = 24px from the board edge, while the
   panel's own 16px margin + 14px padding is already 30px. Any gutter added on
   top can only push the strip further right.

   So the captions sit ABOVE the rail, on one line, rather than beside the
   lanes. Each still names its lane (and .ts-lane--stack's caption drops out
   with the lane when a day has no bookings), the meaning is still visible at
   every width, and the strip keeps the board's column track untouched. */
/* Lowercase, matching the mode picker's own key (.vm-picker-info-swatch), which
   is the idiom this app already uses for "colour chip + short label". The
   SHOUTING CAPS were this panel's alone and made three short labels read as
   three headings (2026-08-17). */
.ts-lane-captions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin: 2px 0 5px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.01em;
  text-transform: lowercase;
  color: var(--text-secondary, #94a3b8);
  user-select: none;
}
/* THREE KINDS, NOT THREE EQUALS. The keys describe a MARKER (the best-window
   arc), a SCALE (the per-hour colours) and an OVERLAY (booked shading). Flat
   styling made them read as one list of peers, so nothing told the reader that
   the middle one is the strip itself. Rank them, and divide them. */
.ts-lane-key[data-kind="marker"]  { color: var(--text-primary, #0f172a); }
.ts-lane-key[data-kind="overlay"] { opacity: 0.75; }
.ts-lane-key + .ts-lane-key {
  border-left: 1px solid var(--border-color, #e2e8f0);
  padding-left: 10px;
}
/* Each caption carries the same mark its lane draws, so the word and the
   shape are learned together — that pairing is what the bare underlines were
   missing. */
.ts-lane-key { display: inline-flex; align-items: center; gap: 5px; }
.ts-lane-key::before {
  content: "";
  width: 12px;
  height: 4px;
  border-radius: 999px;
  background: currentColor;
  opacity: 0.9;
}
/* Each key DRAWS THE MARK ITS LANE DRAWS (2026-08-08 second pass: the booked
   key used to show a red hatch — the conflict texture — while the lane painted
   solid blocks, so the legend taught a mark that never appeared). */
.ts-lane-key[data-key="window"] { color: var(--status-working); }
.ts-lane-key[data-key="window"]::before {
  /* The arc in miniature: gold star + connector, like the mark itself. */
  content: '\2605';
  width: auto;
  height: auto;
  border-radius: 0;
  background: none;
  font-size: 8px;
  line-height: 1;
  color: var(--status-flex);
}
/* THE CELLS KEY DRAWS ITS STRIP'S OWN SCALE (2026-08-17, owner: "we have white
   box available per hour but we dont use white, same issue for other view
   modes").

   This was `background: transparent` with a 1px outline. On the white panel
   that renders as an EMPTY WHITE BOX beside a strip that is never white, so it
   reads as missing data rather than as a colour scale. The board key already
   records this exact lesson (css/calendar-key.css: five blank swatches, "a key
   that shows nothing is worse than no key") and the repo's never-white
   invariant is guard-pinned.

   So the swatch becomes the ramp in miniature, built from the SAME custom
   properties the cells paint, and the label names both ends of it. Default is
   Local times' sky, matching the mini-gradient the board key already uses for
   the day/night wash. */
.ts-lane-key[data-key="cells"]  { color: var(--text-primary, #475569); }
.ts-lane-key[data-key="cells"]::before {
  width: 18px;
  height: 10px;
  border-radius: 2px;
  border: none;
  /* Hairline ring, the same containment the mode picker's chips use, so a pale
     end of a ramp still reads as a swatch and not as a gap. */
  box-shadow: inset 0 0 0 1px color-mix(in srgb, #000 12%, transparent);
  /* ⚠️THE SWATCH IS THE CELLS' DRAWING IN MINIATURE — AND IT MUST RUN IN THE
     ORDER THE WORDS DO (2026-08-26, owner: "awake fewer to more the visual
     key doesn't make sense", then: "a green for awake vs purple asleep bar
     … a battle of lower and upper bars"). Four columns: the green awake bar
     steps UP from the floor left-to-right (fewer → more, as the label
     reads) while the purple asleep bar fills the remainder from the top —
     the same battle the cells draw, in the same tokens and mix weights. */
  --ts-key-awake: color-mix(in srgb, var(--status-available) 72%, var(--surface-primary, #fff));
  --ts-key-asleep: color-mix(in srgb, var(--status-sleep) 70%, var(--surface-primary, #fff));
  background: linear-gradient(var(--ts-key-awake), var(--ts-key-awake)) 0% 100% / 19% 25% no-repeat,
    linear-gradient(var(--ts-key-awake), var(--ts-key-awake)) 33.3% 100% / 19% 50% no-repeat,
    linear-gradient(var(--ts-key-awake), var(--ts-key-awake)) 66.7% 100% / 19% 75% no-repeat,
    linear-gradient(var(--ts-key-awake), var(--ts-key-awake)) 100% 100% / 19% 100% no-repeat,
    linear-gradient(var(--ts-key-asleep), var(--ts-key-asleep)) 0% 0% / 19% 75% no-repeat,
    linear-gradient(var(--ts-key-asleep), var(--ts-key-asleep)) 33.3% 0% / 19% 50% no-repeat,
    linear-gradient(var(--ts-key-asleep), var(--ts-key-asleep)) 66.7% 0% / 19% 25% no-repeat,
    color-mix(in srgb, var(--status-empty, #94a3b8) 32%, var(--surface-primary, #fff));
}
/* (The daynight-only sleep-band swatch override is GONE, 2026-08-26 — the
   cells no longer paint a sleep band, and a key must never show a mark the
   strip doesn't draw. The default above IS the Local-times swatch.) */
body[data-color-mode="availability"] .ts-lane-key[data-key="cells"]::before {
  /* HARD BANDS, NOT A BLEND (2026-08-26, owner asked if this ramp was in
     reverse). The smooth gradient squeezed "none" — the palest token — into
     the first couple of pixels, so the swatch read amber→green ("few to
     all") and looked reversed against its "none to all" label. The cells
     paint four discrete buckets; the swatch shows those four, equal width,
     in the order the label reads. */
  background: linear-gradient(90deg,
    var(--avail-bar-none) 0 25%,
    var(--avail-bar-few) 25% 50%,
    var(--avail-bar-most) 50% 75%,
    var(--avail-bar-all) 75% 100%);
}
body[data-color-mode="overlap"] .ts-lane-key[data-key="cells"]::before {
  background: linear-gradient(90deg,
    var(--overlap-cell-none) 0 33.4%,
    var(--overlap-cell-med) 33.4% 66.7%,
    var(--overlap-cell-all) 66.7% 100%);
}
body[data-color-mode="fairness"] .ts-lane-key[data-key="cells"]::before {
  background: linear-gradient(90deg, var(--fv-l1), var(--fv-l5), var(--fv-l9));
}
/* The fairness in-cell ratio bar (--fv-ratio): a miniature cell with a bar
   rising from its floor, which is exactly what the cells draw. Added 2026-08-17
   because this key used to borrow data-key="booked" and therefore showed the
   slate BOOKED-LANE swatch while naming something else entirely. */
.ts-lane-key[data-key="hourbar"] { color: var(--text-secondary, #64748b); }
.ts-lane-key[data-key="hourbar"]::before {
  width: 11px;
  height: 11px;
  border-radius: 2px;
  border: none;
  background: linear-gradient(to top, currentColor 0 55%, transparent 55%);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 35%, transparent);
  opacity: 0.85;
}
/* Slate block, exactly the lane's ink at full density. Neutral on purpose:
   an ordinary booking is not a conflict, so the key must not be red either. */
.ts-lane-key[data-key="booked"] { color: rgb(120, 130, 150); }
.ts-lane-key[data-key="booked"]::before {
  height: 7px;
  border-radius: 2px;
  background: color-mix(in srgb, rgb(120, 130, 150) 78%, var(--surface-primary, #fff));
}

/* ── THE BEST-WINDOW ARC (2026-08-08) ────────────────────────────────────────
   The lane over the cells, restyled from a 5px pill into the BOARD'S OWN
   recommended-window idiom: the ★ opener + ◆ terminator + thin connector that
   best-time-indicator.css draws over .golden-hour cells. The pill was a mark
   the board draws nowhere, which is why it read as an unexplained overline.

   Same grammar as the board:
     ★  always gold (--status-flex), the window's opening glyph;
     —  a 2px connector in the window's quality colour;
     ◆  the terminator, carrying the quality colour (green full-team window,
        amber partial, red none) — data-tone on the lane, exactly the ramp
        the board's quality token (--team-window-color) runs.
   A wrapped window is two arcs: ★ rides the first segment's start, ◆ the last
   segment's end, so the pair still brackets the whole window. JS positions
   segments by grid-column on the shared 24-column track, unchanged. */
.ts-barlane {
  display: grid;
  grid-template-columns: repeat(24, 1fr);
  height: 11px;             /* glyph room; the connector centres inside */
  align-items: center;
  color: var(--status-working);   /* quality colour; toned below */
}
.ts-peak-bar {
  height: 2px;
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 50%, transparent);  /* the board's -soft connector mix */
  min-width: 3px;
  position: relative;
  overflow: visible;
}

/* ── ★—◆ WINDOW TERMINATORS ───────────────────────────────────────────────
   The board's ONLY recommended-window idiom (css/best-time-indicator.css) now
   also closes the summary's window bar, so the panel names its window in the
   same language the cells above it use.

   Why glyphs and not just the bar: `.ts-peak-bar-label` ("best window") is
   display:none below 900px, so on every phone the bar was an unexplained
   coloured stripe: the "unfamiliar indicators without explanation" defect
   (user 2026-08-08). The glyphs carry the meaning at EVERY width; the text
   label stays for desktop.

   GRAMMAR (mirrors the board exactly):
     ★ gold  = opener, a fixed brand anchor, never quality-coloured
     ◆ toned = terminator, carries the verdict, like a stopwatch result
     ☆ ◇     = HOLLOW pair for a compromise window ("best you can do", NOT a
               clean recommendation). data-tone is already 'all' only when the
               whole group is reachable, so tone doubles as the quality signal
               the board gets from --team-window-quality.

   ⚠️SCOPED TO .ts-* ON PURPOSE. The board's glyph rules are a documented
   specificity minefield (best-time-indicator.css:134-147, where the ★ silently
   blanked when a window landed on a boundary cell). These never reference
   .golden-hour, so the two systems cannot fight. */
.ts-peak-bar::before,
.ts-peak-bar::after {
  position: absolute;
  top: 50%;
  font-size: 9px;
  line-height: 1;
  z-index: 2;
  pointer-events: none;
  /* Same softened halo as the board's glyphs: legible on any cell colour
     without the glare that got flagged 2026-07-12. */
  text-shadow: 0 0 2px rgba(255, 255, 255, 0.7), 0 0 4px rgba(255, 255, 255, 0.35);
}
/* ★ opens the window. Gold is a FIXED anchor: it does not take the tone, so
   the eye always finds the start in the same colour. */
.ts-peak-bar::before {
  left: 0;
  transform: translate(-50%, -50%);
  /* --status-flex is the board's own ★ colour (best-time-indicator.css ~188):
     amber. There is NO --status-golden token; using one would resolve to its
     fallback forever and drift from the board the moment amber moved. */
  color: var(--status-flex);
}
/* ◆ closes it, carrying the verdict colour (set per-tone below). */
.ts-peak-bar::after {
  right: 0;
  transform: translate(50%, -50%);
  color: var(--status-working);
}
/* Only the LONGEST arc of a wrapped window gets terminators. A past-midnight
   window draws as two arcs, and glyphs on both would claim two windows. The
   label already rides the longest arc; the glyphs follow it. */
.ts-peak-bar[data-window-edge="none"]::before,
.ts-peak-bar[data-window-edge="none"]::after {
  content: none;
}
.ts-peak-bar[data-window-edge="start"]::after { content: none; }
.ts-peak-bar[data-window-edge="end"]::before  { content: none; }

/* COMPROMISE: hollow pair, two independent signals (shape AND colour) that this
   is the best available rather than the answer.

   ⚠️SOLID IS THE EXCEPTION, NOT THE DEFAULT. Written the other way round (list
   the compromise tones, let everything else fall through to solid) this is
   wrong twice over: `data-tone` is a HEADCOUNT BUCKET, not a window verdict, so
   its 'all' band starts at 80% — a 4-of-5 window scored 'all' and drew the
   SOLID pair while one person was still out. And any tone the list forgot
   silently became a clean recommendation. Inverting it makes solid require an
   explicit whole-group signal, so an unknown tone degrades to the honest
   hollow mark instead of overclaiming.

   `data-window-whole` is set by the renderer only when the window truly reaches
   everyone (maxReach >= total), which is the question the glyph answers. It is
   deliberately NOT derived from data-tone. */
.ts-peak-bar::before { content: '\2606'; }   /* ☆ hollow by default */
.ts-peak-bar::after  { content: '\25C7'; }   /* ◇ hollow by default */
.ts-barlane[data-window-whole="true"] .ts-peak-bar::before { content: '\2605'; }
.ts-barlane[data-window-whole="true"] .ts-peak-bar::after  { content: '\25C6'; }

/* ◆ takes the verdict colour, matching the bar it terminates. No rule for
   tone="none": that tone is only ever set when there is NO window, so no bar
   (and no glyph) exists to colour. */
.ts-barlane[data-tone="most"] .ts-peak-bar::after { color: var(--status-available); }
.ts-barlane[data-tone="few"]  .ts-peak-bar::after { color: var(--status-flex); }
body.dark-mode .ts-peak-bar::before,
body.dark-mode .ts-peak-bar::after {
  text-shadow: 0 0 2px rgba(15, 23, 42, 0.8), 0 0 4px rgba(15, 23, 42, 0.5);
}
/* The opening star: always gold, like every golden-hour opener on the board. */
.ts-peak-bar:first-child::before {
  content: '\2605';
  position: absolute;
  left: -1px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 9px;
  line-height: 1;
  color: var(--status-flex);
  text-shadow: 0 0 2px rgba(255, 255, 255, 0.9);
}
/* The terminator diamond: quality-coloured, closing the window. */
.ts-peak-bar:last-child::after {
  content: '\25C6';
  position: absolute;
  right: -1px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 8px;
  line-height: 1;
  color: currentColor;
  text-shadow: 0 0 2px rgba(255, 255, 255, 0.9);
}
/* Tone the arc to the verdict, the same ramp the board's quality token runs —
   a "most" or "few" window must not claim the full-team green. */
.ts-barlane[data-tone="most"] { color: var(--status-available); }
.ts-barlane[data-tone="few"]  { color: var(--status-flex); }
.ts-barlane[data-tone="none"] { color: var(--shading-busy, #dc2626); }
body.dark-mode .ts-peak-bar:first-child::before,
body.dark-mode .ts-peak-bar:last-child::after {
  text-shadow: 0 0 2px rgba(15, 23, 42, 0.9);
}

/* ── EVENT STACK LANE (2026-07-31) ──────────────────────────────────────────
   The strip says how many people are REACHABLE at each hour. It cannot say
   what is already in the way, because its source (getTeamHourBreakdown) is a
   headcount, not intervals. This lane is the missing half: every member's
   actual events for the day, merged onto one row.

   MERGED, not one-row-per-member — a 10-person board would make the panel
   taller than the board it summarises. Simultaneity is carried by DENSITY
   (--stack-density = peak concurrent busy / team size), so two people in
   meetings reads darker than one, and the lane stays one row at any team size.

   Colour: --status-busy at varying alpha. No new hue is introduced, and no
   var(--token, #hex) fallbacks — a literal fallback here would bypass the
   palette ratchet guard the way the teal regression did.

   Sub-hour shape comes from --stack-slots, a hard-stop gradient the JS builds
   at quarter-hour resolution, so a 09:30 start fills half its cell rather than
   the whole hour. Hard stops (not smooth) keep it reading as blocks. */
/* ── LANE KEY ─────────────────────────────────────────────────────────────
   One line of chips, each DRAWING THE MARK IT NAMES rather than showing an
   abstract swatch. This is the fix for the standing complaint that the panel
   carries "unfamiliar indicators ... without explanation" (user 2026-08-08):
   both in-lane captions (.ts-peak-bar-label, .ts-stacklane-caption) are
   display:none below 900px, so on a phone the window bar and the booked lane
   were two unlabelled stripes.

   ⛔NOT a left gutter on the rail. Two earlier attempts to caption the lanes
   inline pushed the strip 62px right and broke column alignment with the member
   rows. This sits BELOW the rail as its own line, so the 24-column track is
   untouched.

   Only three chips, and only the ones a given mode can actually draw. The
   panel's original defect was too many undifferentiated blocks, so the key must
   not become a fourth one. */
.ts-lane-key {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 14px;
  margin-top: 7px;
  font-size: 11px;
  line-height: 1.3;
  color: var(--text-secondary, #64748b);
}
.ts-lane-key-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
}
/* Each mark is drawn at the size it appears in the lane, so the key is a
   literal sample rather than an approximation. */
.ts-lane-key-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
/* ⛔NO window chip here. The bar directly above already draws the ★—◆ pair,
   carries its own label and title, and the verdict states the window in prose.
   A legend for a mark sitting two centimetres away was a seventh surface
   naming one window on a panel whose founding defect was too many blocks at
   equal weight. This key is only for marks with NO other explanation. */
/* BOOKED: --status-busy (slate). Slate, never red: red is reserved for
   confirmed conflicts, so an alarm colour on the board always means something
   is actually wrong (token note, view-mode-strip.css:23). Blue is NOT available
   here either: --status-shift is blue and means "on duty this hour". */
.ts-lane-key-mark[data-mark="booked"] {
  width: 22px;
  height: 10px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--status-busy) 78%, var(--surface-primary, #fff));
}
/* CONFLICT: the red depth hatch, at the same -45° pitch the cells paint. This
   is the one mark that CONTRADICTS the cell under it (an hour reads reachable
   while conflicts stack inside it), so it is the most costly one to leave
   unexplained. */
.ts-lane-key-mark[data-mark="conflict"] {
  width: 22px;
  height: 10px;
  border-radius: 3px;
  background-color: var(--status-empty, #e2e8f0);
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0,
    transparent 4px,
    rgba(220, 38, 38, 0.30) 4px,
    rgba(220, 38, 38, 0.30) 6px
  );
}
.ts-panel[data-summary-collapsed="true"] .ts-lane-key { display: none; }
body.dark-mode .ts-lane-key { color: var(--text-secondary, #94a3b8); }
body.dark-mode .ts-lane-key-mark[data-mark="booked"] {
  background: color-mix(in srgb, var(--status-busy) 78%, var(--surface-primary, #0f172a));
}
body.dark-mode .ts-lane-key-mark[data-mark="conflict"] {
  background-color: rgba(148, 163, 184, 0.16);
}

.ts-stacklane {
  display: grid;
  grid-template-columns: repeat(24, 1fr);
  /* 7px, back down from the 12px the lane's own inline caption needed
     (2026-08-08: that caption moved to the key line above the rail, so the
     height is free again). A live render showed the 12px lane reading as a
     solid red slab that outweighed the cells it annotates — it is the
     SECONDARY read, so it must be visibly slimmer than the hour cells. */
  height: 7px;
  margin-top: 2px;
  border-radius: 3px;
  overflow: hidden;
  background: rgba(148, 163, 184, 0.12);
  position: relative;
}
/* (.ts-stacklane-caption removed 2026-08-08. The lane's inline "booked" label
   moved to the key line above the rail, where it is legible at every width
   instead of being density-hidden below 900px. Removing it also retires the
   prefix-match hazard documented in team-summary-stack-lane-palette.test.js:
   `indexOf('body.dark-mode .ts-stacklane')` used to match the CAPTION rule,
   which sat earlier in the file, so a slice meant for the lane began ~1000
   chars too soon. There is now only one `body.dark-mode .ts-stacklane*` rule
   family, so the prefix is unambiguous again.) */
.ts-stack-cell {
  position: relative;
  height: 100%;
}
/* The gradient paints WHICH quarter-hours are booked. Density (HOW MANY people)
   rides COLOUR-MIX, not opacity: a booked hour is a CONFIRMED fact and must read
   as solid. One person's meeting mixes toward the surface, the whole team lands
   on the full token — same signal, without spending transparency on it.

   Transparency is deliberately NOT used here. It is reserved for the proposed /
   unconfirmed language the board already speaks (dashed + faded = not agreed
   yet, 2026-07-27); if this lane also faded for headcount, a real booking and a
   proposal would be indistinguishable at a glance. */
.ts-stack-cell[data-busy="true"],
.ts-stack-cell[data-away="true"] {
  background-image: var(--stack-slots);
}
.ts-stack-cell[data-busy="true"] {
  /* SLATE, NOT RED (2026-08-08). On the board an ordinary booking is NEUTRAL:
     the masked-busy marker is grey and the compact chip lane is slate; red
     (--status-busy) is reserved for confirmed CONFLICTS. Painting every
     meeting red made a normal 9-to-8 workday read as a continuous alarm
     strip under the cells. rgb(120, 130, 150) is the canonical busy-hatch
     stop (style.css .busy-block); density still rides the colour-mix, never
     opacity. */
  --ts-stack-ink: color-mix(
    in srgb,
    rgb(120, 130, 150) calc(55% + (var(--stack-density, 0) * 45%)),
    var(--surface-primary, #fff)
  );
}
/* Away/PTO is a different KIND of unavailability, not more of the same. It takes
   --status-away-scan (deep-orange), NOT the categorical fuchsia: away on the
   cells and on the scanner is deep-orange, and a magenta block here would read
   as a fourth event type that exists nowhere else on the board. */
.ts-stack-cell[data-away="true"] {
  --ts-stack-ink: color-mix(in srgb, var(--status-away-scan) 78%, var(--surface-primary, #fff));
}
/* An hour with both paints busy (the meeting is the harder constraint) and keeps
   an away underline. Listed last so it wins the specificity tie against the
   away-only rule above. */
.ts-stack-cell[data-away="true"][data-busy="true"] {
  --ts-stack-ink: color-mix(
    in srgb,
    rgb(120, 130, 150) calc(55% + (var(--stack-density, 0) * 45%)),
    var(--surface-primary, #fff)
  );
  box-shadow: inset 0 -2px 0 0 var(--status-away-scan);
}
/* PROPOSED / not-yet-confirmed is the one thing that fades, matching the dashed
   event-border language on the cells. */
.ts-stack-cell[data-proposed="true"] {
  opacity: 0.45;
}
body.dark-mode .ts-stacklane { background: rgba(148, 163, 184, 0.16); }
/* Mix toward the dark panel, not white — otherwise a one-person hour washes out
   to a pale pink instead of darkening toward the surface. */
body.dark-mode .ts-stack-cell[data-busy="true"],
body.dark-mode .ts-stack-cell[data-away="true"][data-busy="true"] {
  --ts-stack-ink: color-mix(
    in srgb,
    rgb(148, 163, 184) calc(55% + (var(--stack-density, 0) * 45%)),
    var(--surface-primary, #1a1a2e)
  );
}
body.dark-mode .ts-stack-cell[data-away="true"] {
  --ts-stack-ink: color-mix(in srgb, var(--status-away-scan-dark) 80%, var(--surface-primary, #1a1a2e));
}

/* ── IN-CELL HOUR NUMERALS (2026-08-05) ─────────────────────────────────────
   User: "the cells and indicators aren't directly relatable and don't provide
   the hourly labels as expected throughout my app; the cells must be
   individually numbered."

   The strip used to print its hours on a separate .ts-axis lane below the
   cells. That is not this app's convention: every member-row cell names its
   own hour with a .cell-hour span (formatSummaryHourLabel, 12a / 1a / ... /
   12p / 1p), anchored at the cell's LEFT SEAM because the seam is the instant
   the hour begins (the boundary-not-centre decision of 2026-08-04). The
   detached lane also thinned its labels by density, so on a phone the now and
   peak rings usually sat on cells with no visible name. The numeral now lives
   inside each cell, top-left at the seam, so a cell and its hour can never
   sit a lane apart.

   Positioned + z-index 1: the segment stack (::before) and the ratio fill
   (::after) are positioned pseudos, which paint above in-flow inline content;
   without the lift the fills would bury the glyph. The white halo keeps it
   legible on saturated fills, the same trick as the scan column's re-drawn
   hour labels (style.css .scan-hour-label). */
/* CENTRED, NOT CORNER-PINNED (2026-08-06, user: "the team summary STILL lacks
   by hour annotations"). The numerals were already in the DOM and all 24 were
   computed-visible at desktop width — but at 7.5px, in --text-secondary grey,
   tucked into the top-LEFT corner and sitting on saturated bucket fills, they
   read as texture rather than as labels. "Missing" was a fair description of
   the result even though the markup was present: a label nobody can resolve
   is not a label. So the glyph now centres in its cell and carries enough
   size/weight/contrast to survive the fill under it. Verified in a rendered
   pass, not by reading the markup — the markup was never the problem. */
.ts-cell-hour {
  position: absolute;
  /* BOTTOM-centre (2026-08-10, user: "perhaps the time label should be at the
     bottom"). The BOARD keeps its hour labels on the cell floor —
     .timeline-cell is display:flex with align-items:flex-end — so the centred
     glyph was one more small foreignness, and it also sat exactly where the
     new X/Y count wants to read. Bottom keeps it clear of the count
     (top-right) and of the now ring; the availability ratio bar rises behind
     it and the white halo keeps it legible on top. */
  bottom: 3px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;      /* claw back a hair at 24-across */
  font-variant-numeric: tabular-nums;
  color: var(--text-primary, #0f172a);
  /* A tight opaque halo, not a soft glow: at 9px on a saturated green fill a
     blurred shadow smears into the fill and costs more legibility than it
     buys. The ring of 1px offsets keeps the stroke crisp. */
  text-shadow:
    0 0 3px rgba(255, 255, 255, 0.95),
    1px 0 0 rgba(255, 255, 255, 0.9),
    -1px 0 0 rgba(255, 255, 255, 0.9),
    0 1px 0 rgba(255, 255, 255, 0.9),
    0 -1px 0 rgba(255, 255, 255, 0.9);
  pointer-events: none;         /* never steal the cell's hover/tooltip target */
  user-select: none;
  white-space: nowrap;
}
/* Midnight and noon are the landmarks the rest of the strip is read against.
   Slightly more ink so the eye lands on them first; weight and text colour
   only, no new hue, so the numerals stay subordinate to the cells they name. */
.ts-cell[data-hour="0"] .ts-cell-hour,
.ts-cell[data-hour="12"] .ts-cell-hour {
  font-weight: 700;
  color: var(--text-primary, #0f172a);
}
/* The current hour names itself in the canonical now-pink, so the pink ring
   and its hour numeral read as ONE indicator instead of a ring on an
   anonymous cell. Same hue as the ring below (#ec4899 / dark #f472b6). */
.ts-cell[data-now="true"] .ts-cell-hour {
  color: #ec4899;
  font-weight: 700;
}

/* ⚠️SHRINK THE NUMERALS, DO NOT DELETE THEM (2026-08-08).
   User: "every cell must have a time label per hour [but] some view modes
   [don't]". The panel already emitted all 24 numerals — CSS then HID them:
   every odd hour below 900px, and everything but 0/6/12/18 below 560px. So on
   a laptop half the cells were unnamed and on a phone twenty of them were,
   which is exactly the reported symptom. It also broke the marks: the now-ring
   and best-window ring frequently landed on a cell with no visible hour.

   The board does not thin. A .timeline-cell prints its hour at
   `max(7px, calc(8px * var(--timeline-zoom-scale)))` at EVERY width and lets
   the glyph shrink. This does the same, so the summary keeps the board's
   promise that a cell always names itself. The 560px block is retained (a
   guard pins the midnight/noon selectors there) but now only re-asserts the
   landmark weighting rather than restoring hidden labels. */
/* The am/pm letter of the 12h glyph. Dropped on narrow boards so "12a"
   degrades to "12" instead of the whole label being hidden. */
.ts-cell-hour-mer { font-weight: inherit; }

@media (max-width: 900px) {
  .ts-cell-hour { font-size: 7px; letter-spacing: -0.04em; }
}

/* MOBILE = NUMERALS ONLY, NO MERIDIEM (2026-08-29, owner: "also for this
   mobile mode. only times should appear, no am or pm").
   The a/p suffix already drops below 560px, but that is a VIEWPORT test while
   the compact board is a LAYOUT mode — so a compact board on a wider screen
   (tablet, landscape phone, desktop with compact selected) kept showing it.
   Gate on the layout as well. Same precedent as the stacked card's clock,
   which already forces 24h when data-layout="compact"
   (stacked-timezone-card.js:136).
   Only the `.ts-cell-hour-mer` span is hidden — the NUMERAL always stays.
   ⚠️Do not extend this to hide whole ticks: the standing rule is SHRINK THE
   NUMERALS, DO NOT DELETE THEM (2026-08-08), and the spoken labels come from
   the cell's aria-label/title, which are untouched. */
body[data-layout="compact"] .ts-cell-hour-mer { display: none; }
@media (max-width: 560px) {
  /* Below this a ~14px cell cannot hold three characters, so the meridiem
     letter goes and the numeral stays. EVERY cell keeps a visible label —
     the landmark selectors below now only add weight; they no longer have to
     restore visibility, because nothing is hidden any more. */
  .ts-cell .ts-cell-hour {
    display: inline;
    font-size: 7px;
    letter-spacing: -0.06em;
  }
  .ts-cell-hour-mer { display: none; }
  .ts-cell[data-hour="0"] .ts-cell-hour,
  .ts-cell[data-hour="6"] .ts-cell-hour,
  .ts-cell[data-hour="12"] .ts-cell-hour,
  .ts-cell[data-hour="18"] .ts-cell-hour { font-weight: 700; }
}

/* "Now" marker: vertical inset on the current base-tz hour cell so users can
   anchor the strip to their current moment without colour confusion.
   2026-07-31 (user: "the format seems foreign" / "why is teal still here").
   Was the brand teal --accent-primary #14b8a6. Everywhere ELSE in this app
   "now" is PINK — the cell now-line and the scanner now-tick are both #ec4899
   (dark #f472b6, day-cycle #db2777; see css/scanner-scope-shading.css
   .scanner-now-tick). A teal "now" in the summary strip was the panel speaking a
   different colour language than the board it summarises, AND it spent the brand
   accent on a meaning the brand accent doesn't carry anywhere else. Repointed to
   the canonical pink so "now" reads identically on the board and in the summary. */
.ts-cell[data-now="true"] {
  box-shadow: inset 0 0 0 2px #ec4899;
  z-index: 1;
}
body.dark-mode .ts-cell[data-now="true"] {
  box-shadow: inset 0 0 0 2px #f472b6;
}
body.day-cycle-enabled .ts-cell[data-now="true"] {
  box-shadow: inset 0 0 0 2px #db2777;
}

/* (The stat-tile CSS is GONE, 2026-08-08 — see "Where the stat tiles went"
   in team-summary-panel.js. Tiles were a dashboard idiom the board never
   uses; their facts now live in the row head, the verdict and the tooltips.) */

.ts-meta {
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-secondary, #64748b);
}
.ts-meta strong {
  color: var(--text-primary, #0f172a);
  font-weight: 600;
}
/* Solo-overlap nudge: overlap needs 2+ people, so instead of a fabricated
   verdict a lone user gets a gentle "add a teammate" prompt (2026-07-10). */
.ts-meta-hint {
  /* Block, NOT flex (2026-08-10). As a flex row, the <strong> inside the
     sentence became its own flex item and the text nodes around it were laid
     out as separate items too — on the live phone panel the sentence rendered
     as three columns with the emphasis floating apart from its own sentence.
     Block flow keeps it a sentence; the icon rides inline before it. */
  display: block;
}
.ts-meta-hint::before {
  content: "\f0c0"; /* fa-users */
  font-family: "Font Awesome 6 Free", "Font Awesome 6 Pro", sans-serif;
  font-weight: 900;
  color: var(--text-secondary, #64748b);
  opacity: 0.7;
  display: inline-block;
  margin-right: 6px;
}

/* Empty state, matched to .fv-empty-state (fairness-view.css) — the two footers
   sit in the SAME slot and had unrelated "nothing here yet" designs: an italic
   line here vs a dashed, tinted card with an icon there. Italic-as-empty-state
   is the weaker of the two (it reads as de-emphasised prose rather than a
   deliberate placeholder), so this adopts the fairness treatment while keeping
   the .ts-* token vocabulary. (Polish pass 2026-08-04.) */
.ts-empty {
  padding: 20px 12px;
  text-align: center;
  font-size: 12px;
  color: var(--text-secondary, #64748b);
  background: var(--slate-50, #f8fafc);
  border: 1px dashed var(--border-subtle, #e2e8f0);
  border-radius: 8px;
}
.ts-empty i {
  display: block;
  font-size: 22px;
  margin-bottom: 8px;
  opacity: 0.5;
}

/* ── MODE CELL PAINT: INHERITED, NEVER RESTATED (2026-08-08) ────────────────
   The board paints every mode as a translucent status veil LAYERED OVER the
   per-hour sky gradient (availability-mode.css:
   `linear-gradient(fill,fill), var(--cell-daynight), var(--cell-daynight-solid)`),
   and view-mode-service keeps body.day-cycle-enabled on in EVERY mode so the
   sky is the universal never-white base. `.ts-cell` is now included in
   style.css's [data-hour] palette selectors, so each summary cell carries the
   sky and its --cell-daynight custom property by construction. Each mode then
   layers ITS OWN canonical aggregate tokens:

     daynight     → NO veil. The sky IS the mode (board Local-times cells carry
                    no analytical overlay). The group's hourly make-up rides
                    over it as the scanner's faint composition washes — see the
                    segment rule below, mirroring .tz-avg-background.
     availability → the segmented team-bar's --avail-bar-* buckets, so the
                    footer strip and the avg bar above the scanner are the same
                    colour for the same answer ("the bar yellow == the cell
                    yellow", view-mode-strip.css).
     overlap      → the board overlap cells' own --overlap-cell-* washes plus
                    the .ovl-bar bottom thermometer, verbatim idioms from
                    overlap-mode.css.

   The 2026-07-31 "bucket separation" remixes are gone with the old flat paint:
   they made the same bucket a DIFFERENT colour here than on the team-bar,
   breaking the one-token-one-meaning promise in this file's own header.

   ⚠️SPECIFICITY, NEVER ORDER (2026-08-10, found on the DEPLOYED beta). The
   first cut left these veils tied with the sky rules at (0,3,1) and relied on
   this file loading after style.css. The raw <link> order does satisfy that —
   but the deployed app serves BUNDLED, content-hashed CSS and the bundle does
   not preserve it, so in production every mode painted pure sky and the
   panels all looked like Local times. The class is doubled (.ts-cell.ts-cell)
   to make the veils (0,4,1): they now beat the sky in ANY load order — the
   same trick availability-mode.css's .timeline-cell.timeline-cell uses for
   the identical fight on board cells. */

/* Availability: team-bar bucket tokens over the sky. */
body[data-color-mode="availability"] .ts-cell.ts-cell[data-avail-bucket="none"] {
  /* ⛔SOLID, NOT HATCHED (2026-08-26, owner: "stripes means tentative in my
     app. revise as solid fill"). This borrowed the team-bar's slate hatch
     ("checked, and empty") — but on CELLS the -45° stripe is the tentative /
     deep-sleep texture everywhere else in the app, so a zero-reachable hour
     read as a tentative hour. The opaque slate token alone carries
     "checked, and empty"; it is never white, so the never-white invariant
     holds without the texture. */
  background: linear-gradient(var(--avail-bar-none), var(--avail-bar-none)),
    var(--cell-daynight, none) !important;
}
body[data-color-mode="availability"] .ts-cell.ts-cell[data-avail-bucket="few"] {
  background: linear-gradient(var(--avail-bar-few), var(--avail-bar-few)),
    var(--cell-daynight, none) !important;
}
body[data-color-mode="availability"] .ts-cell.ts-cell[data-avail-bucket="most"] {
  background: linear-gradient(var(--avail-bar-most), var(--avail-bar-most)),
    var(--cell-daynight, none) !important;
}
body[data-color-mode="availability"] .ts-cell.ts-cell[data-avail-bucket="all"] {
  background: linear-gradient(var(--avail-bar-all), var(--avail-bar-all)),
    var(--cell-daynight, none) !important;
  /* Still no green inset frame — removed twice on purpose (2026-06-29,
     2026-07-02); the fill alone carries the all band. */
}

/* ── Daynight: awake vs asleep, a BATTLE of lower and upper bars ──
   2026-08-26, owner: "perhaps a green for awake vs purple asleep bar with a
   count is best, no need for local time shades in summary a battle of lower
   and upper bars showing percentage awake and asleep". (Supersedes the same
   day's single blue awake bar, which itself replaced the scrim-over-sky.)

   Green (::before) rises from the cell FLOOR, height = share awake.
   Purple (::after) hangs from the cell TOP, height = share asleep.
   awake = total - asleep by the canonical numerator ruling, so the bars
   always meet: the seam IS the day/night split and the X/Y count names it.
   Both hues are the palette's own tokens (--status-available green,
   --status-sleep purple) mixed toward the surface at the availability-token
   weights, so the numerals and counts stay legible on top.

   ⚠️The board rejected a top-descending bar for AVAILABILITY in 2026-08-10
   ("nothing in the app descends from a cell's top edge") — that ruling was
   about a third colour on an already-two-colour cell. Here the owner asked
   for the descent explicitly, and the cell has exactly these two colours.

   ⛔NEVER hatched — the -45° stripe means "tentative" on every other cell
   (owner 2026-08-26: "stripes means tentative in my app").

   ⚠️SPECIFICITY: the style.css sky rules ([data-hour] palette) include
   .ts-cell at (0,3,1)+!important. The doubled class makes the base (0,4,0)
   +!important so it beats the sky in ANY bundle order — never rely on file
   order here (the deployed bundle reorders; see the veil note above). The
   slate base is a backstop only (the two bars sum to 100%); it keeps the
   never-white invariant if a share var ever goes missing. */
.ts-strip[data-mode="daynight"] .ts-cell.ts-cell {
  background: linear-gradient(
      color-mix(in srgb, var(--status-empty, #94a3b8) 32%, transparent),
      color-mix(in srgb, var(--status-empty, #94a3b8) 32%, transparent)),
    var(--surface-primary, #fff) !important;
}
.ts-strip[data-mode="daynight"] .ts-cell::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(var(--awake-share, 0) * 100%);
  pointer-events: none;
  /* AWAKE = green, the same 72% surface-mix weight as --avail-bar-most, so
     the fill sits at cell brightness, not bar-saturation. */
  background: color-mix(in srgb, var(--status-available) 72%, var(--surface-primary, #fff));
}
.ts-strip[data-mode="daynight"] .ts-cell::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: calc(var(--asleep-share, 0) * 100%);
  pointer-events: none;
  /* ASLEEP = the canonical sleep purple. No hex fallback on the token — the
     palette ratchet flags duplicated sleep hexes (the 2026-08-23 band learned
     this); the token is defined on :root in view-mode-strip.css. */
  background: color-mix(in srgb, var(--status-sleep) 70%, var(--surface-primary, #fff));
}

/* ── Overlap: the board overlap cells' own washes ────────────────
   --overlap-cell-* are the exact tokens overlap-mode.css paints member cells
   with, over the solid daynight backstop, so the summary and the rows above
   are the same surface. (The guard-pinned bucket="all" block stays free of
   the retired green border.) */
/* SOLO board: NEUTRAL, never the none-wash (2026-08-10). With the veils
   fixed, a solo overlap strip painted all 24 cells in the pale busy-red
   "none" mix, which reads as "zero overlap all day" - fabricated data on a
   board with nothing to overlap (canonical rule: neutral when alone, do not
   make up data). Solo cells go to the flat slate empty tone instead. Scoped
   over the veil by the same doubled-class weight + data-solo on the strip. */
body[data-color-mode="overlap"] .ts-strip[data-solo="true"] .ts-cell.ts-cell[data-overlap-bucket] {
  background: linear-gradient(
      color-mix(in srgb, var(--status-empty) 18%, transparent),
      color-mix(in srgb, var(--status-empty) 18%, transparent)
    ),
    var(--cell-daynight-solid, var(--surface-primary, #fff)) !important;
}

body[data-color-mode="overlap"] .ts-cell.ts-cell[data-overlap-bucket="none"] {
  background: linear-gradient(var(--overlap-cell-none), var(--overlap-cell-none)),
    var(--cell-daynight-solid, var(--surface-primary, #fff)) !important;
}
body[data-color-mode="overlap"] .ts-cell.ts-cell[data-overlap-bucket="low"] {
  background: linear-gradient(var(--overlap-cell-low), var(--overlap-cell-low)),
    var(--cell-daynight-solid, var(--surface-primary, #fff)) !important;
}
body[data-color-mode="overlap"] .ts-cell.ts-cell[data-overlap-bucket="med"] {
  background: linear-gradient(var(--overlap-cell-med), var(--overlap-cell-med)),
    var(--cell-daynight-solid, var(--surface-primary, #fff)) !important;
}
body[data-color-mode="overlap"] .ts-cell.ts-cell[data-overlap-bucket="high"] {
  background: linear-gradient(var(--overlap-cell-high), var(--overlap-cell-high)),
    var(--cell-daynight-solid, var(--surface-primary, #fff)) !important;
}
body[data-color-mode="overlap"] .ts-cell.ts-cell[data-overlap-bucket="all"] {
  background: linear-gradient(var(--overlap-cell-all), var(--overlap-cell-all)),
    var(--cell-daynight-solid, var(--surface-primary, #fff)) !important;
}

/* The .ovl-bar thermometer, verbatim: bottom fill whose height is the hour's
   here/of ratio, whose hue is the band's OWN status token, at the board's
   0.4 opacity (overlap-mode.css ~164). This is the one double-encoding the
   BOARD itself keeps, so the summary keeps it too. */
.ts-cell[data-overlap-bucket]::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: calc(var(--ovl-ratio, 0) * 100%);
  opacity: 0.4;
  pointer-events: none;
}
.ts-cell[data-overlap-bucket="none"]::after,
.ts-cell[data-overlap-bucket="low"]::after  { background: var(--shading-busy, #dc2626); }
.ts-cell[data-overlap-bucket="med"]::after  { background: var(--status-flex); }
.ts-cell[data-overlap-bucket="high"]::after { background: var(--status-available); }
.ts-cell[data-overlap-bucket="all"]::after  { background: var(--status-working); }

/* Keep each hour countable over the deeper fills. */
.ts-panel .ts-cell {
  border-right-color: rgba(148, 163, 184, 0.38);
}

/* (The red depth hatch is GONE, 2026-08-08. The BOARD retired its overlap
   depth hatch on user request in 2026-06-03 — overlap-mode.css: "data-overlap-
   depth is still stamped by the painter for the hover tooltip, but nothing
   paints it visually" — and this panel was the last surface still painting
   it. Conflicts speak the board's way now: the verdict names the day's
   double-booked hour count, and the cell/lane tooltips carry the who/when.) */

/* (The night-hour white-ink numeral block is GONE, 2026-08-26. It flipped
   hours 0-5/19-23 to white text because those cells used to be the sky's deep
   navy — the summary cells now paint the slate track + awake bar, both light
   in light mode, so white ink there would vanish. The base .ts-cell-hour rule
   with its halo covers every hour again, like the other modes.) */

/* (.ts-legend removed 2026-08-08: only overlap had one, availability had
   none for an equally unexplained ramp, and the app-wide Calendar Key owns
   ramp education. The lane keys + per-cell tooltips carry the meaning.) */

/* ── MOBILE (rev-10): team summary = STICKY BOTTOM BAR, collapsed by default ──
   The user wanted the team summary kept at the bottom but de-emphasised: a thin
   sticky bar showing just the headline + a chevron, expanding upward on tap. Only
   on the deskpan mobile board (the desktop-pan model); desktop and compact are
   untouched. mobile-summary-collapse.js sets data-summary-collapsed on the active
   panel (default "true") and toggles it on header tap. The panel is normally a
   DOM sibling below the board (scroll to it); here we pin it to the viewport
   bottom so it's always one tap away without dominating the board.
   Width note: in deskpan the page is wider than the viewport (the board pans), so
   position:fixed resolves against the viewport and the bar spans the SCREEN, not
   the page width — left:0;right:0 keeps it screen-width regardless of pan. */
@media (max-width: 640px) {
  /* SLIMMER bottom team-summary bar (user 2026-06-21: "the team summary at the
     bottom can also be more minimal with its space"). --ts-collapsed-h is the
     single token driving BOTH the collapsed bar's min-height (lines ~370/373/414)
     AND the #timezone-list bottom reservation (line ~472). Declaring it here once
     (46 → 36px) shrinks the resting bar and its board reservation in lockstep, so
     the last board row stays clear and nothing desyncs. Scoped to the deskpan
     bottom-bar context so the desktop/expanded panel keeps its roomier spacing. */
  body[data-mobile-board="deskpan"] {
    --ts-collapsed-h: 36px;
  }
  body[data-mobile-board="deskpan"] .ts-panel,
  body[data-mobile-board="deskpan"] .fv-panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 90;            /* above the board, below modals/menus (--z-modal) */
    margin: 0;
    border-radius: 14px 14px 0 0;
    border-bottom: none;
    /* Lighter upward shadow (second-pass 2026-06-21): the 16px-blur shadow cast a
       dark gradient onto the last timeline rows and drew a strong horizontal edge,
       making the collapsed 36px bar feel like a major region instead of a
       recessive one-tap handle. Softer shadow keeps the float affordance while
       letting the board own the lower viewport. */
    box-shadow: 0 -2px 8px rgba(15, 23, 42, 0.08);
    /* NEVER RISE INTO THE FIXED CHROME (2026-08-04, user: the panel title was
       "cut in half"). A flat 70dvh puts the panel's top edge at 30dvh, which on
       a phone is INSIDE the band owned by .app-header (position:fixed, z-index
       60) and .team-status-bar (position:fixed, z-index 450). The panel is
       z-index 90 — above the header but far below the status bar — so the bar
       painted straight over the panel's first child, which is its header, and
       sheared the title horizontally.
       Reserving the real chrome height means the panel can never reach the
       occluding band, whatever the content height. --deskpan-chrome-h is the
       LIVE measured header+indicator height (setChromeVars, script.js ~:7392,
       `hH + bH`) — the same measurement the board's own padding-top budget uses,
       so this tracks the chrome instead of guessing at it. The 70dvh cap stays
       as the upper bound for tall viewports. */
    max-height: min(
      70dvh,
      calc(100dvh
        - var(--first-run-banner-offset, 0px)
        - var(--deskpan-chrome-h, 100px)
        - 12px)
    );
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    transition: transform 0.22s ease;
  }
  /* Tighter resting padding on the collapsed bar (was the always-on 10px at
     ~line 125) so the slimmer --ts-collapsed-h isn't re-padded back up. Deskpan
     only — desktop keeps 10px. */
  body[data-mobile-board="deskpan"] .ts-panel[data-summary-collapsed="true"] {
    padding-top: 6px;
    padding-bottom: 6px;
  }
  /* Availability/overlap (.ts-panel) collapse via the centered chip (the
     universal rules above hide the header/strip/meta and show .ts-collapsed-chip).
     No slide needed — the panel shrinks to chip height and sits at the bottom
     edge. Centre the chip and give it the bar's tap height.
     KEEP position:fixed from the base deskpan rule above — an earlier
     `position: relative` here dropped the collapsed bar back into normal flow,
     so on the panned deskpan board it floated at the bottom of the (tall) page
     instead of pinning to the viewport, "drifting" as the user scrolled/panned.
     The centered peek chevron (::before, position:absolute) anchors fine to a
     fixed panel — fixed is a containing block for absolute children — so it does
     not need a relative ancestor. */
  body[data-mobile-board="deskpan"] .ts-panel[data-summary-collapsed="true"] {
    min-height: var(--ts-collapsed-h, 46px);
  }
  body[data-mobile-board="deskpan"] .ts-collapsed-chip {
    min-height: var(--ts-collapsed-h, 46px);
    /* Drop the desktop chevron lane: deskpan hides the chip's own ::after
       (rule below) and draws the panel-level ::before peek chevron instead,
       so there is no handle here to leave room for. Without this reset the
       desktop 20px top padding would push the verdict off-centre in the bar. */
    padding-top: 2px;
  }
  /* Match the fairness panel: a centered peeking chevron at the bar's top edge
     (user 2026-06-20). The chip's own trailing ::after chevron beside the verdict
     text is redundant with this handle, so hide it on deskpan and let the centered
     peek chevron be the single "pull me up" affordance. */
  body[data-mobile-board="deskpan"] .ts-panel[data-summary-collapsed="true"] .ts-collapsed-chip::after {
    display: none;
  }
  body[data-mobile-board="deskpan"] .ts-panel[data-summary-collapsed="true"]::before {
    content: "\f077";                 /* fa chevron-up */
    font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
    font-weight: 900;
    font-size: 13px;
    color: var(--text-secondary, #64748b);
    position: absolute;
    left: 50%;
    top: 4px;
    transform: translateX(-50%);
    pointer-events: none;
    z-index: 1;
  }

  /* EMPTY STATE on the fixed bottom bar. A panel with no group members renders
     only <div class="ts-empty"> — no .ts-header, no .ts-collapsed-chip — so the
     chip-centering rules above don't reach it and the message sat top-anchored,
     taller than the bar and offset from where the populated collapsed verdict
     rests. Center .ts-empty in the SAME ~46px bar so the resting position is
     identical whether the panel is populated or empty (user: "standardised to a
     typical view ... despite my scroll"). Scoped to deskpan + the empty child so
     it never touches the populated/collapsed/expanded panel. */
  body[data-mobile-board="deskpan"] .ts-panel[data-summary-collapsed="true"]:has(.ts-empty) {
    display: flex;
    align-items: center;
    justify-content: center;
    padding-top: 0;
    padding-bottom: 0;
  }
  body[data-mobile-board="deskpan"] .ts-panel[data-summary-collapsed="true"] .ts-empty {
    margin: 0;
    padding: 0 16px;
    min-height: var(--ts-collapsed-h, 46px);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    line-height: 1.3;
  }
  /* No expand target when empty — drop the centered "pull me up" peek chevron so
     the bar doesn't imply a hidden panel a tap can't reveal. */
  body[data-mobile-board="deskpan"] .ts-panel[data-summary-collapsed="true"]:has(.ts-empty)::before {
    display: none;
  }

  /* Fairness (.fv-panel) keeps the slide-down model — it has no chip, so its
     header band stays the handle. Slide the panel down so only the header shows. */
  body[data-mobile-board="deskpan"] .fv-panel[data-summary-collapsed="true"] {
    transform: translateY(calc(100% - var(--ts-collapsed-h, 46px)));
  }
  body[data-mobile-board="deskpan"] .fv-header {
    cursor: pointer;
    margin-bottom: 0;
    min-height: var(--ts-collapsed-h, 46px);
    align-items: center;
    /* The peek chevron is positioned against the header so it can sit centered
       above the title row rather than crowding it. */
    position: relative;
  }
  /* Peeking chevron handle, CENTERED (user 2026-06-20: "a peeking arrow chevron
     thing in the middle is better"). Instead of a right-aligned ^ tucked beside
     the title, the collapsed fairness bar gets a small centered chevron that
     "peeks" from the top edge of the sticky bar — a bottom-sheet grab-handle
     gesture that reads as "pull me up". Absolutely centered (left:50% + translate)
     so it sits in the middle regardless of the title/verdict width, and nudged
     up to the bar's top edge so it peeks. */
  body[data-mobile-board="deskpan"] .fv-header::after {
    content: "\f077";                 /* fa chevron-up — "expand" affordance */
    font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
    font-weight: 900;
    font-size: 13px;
    color: var(--text-secondary, #64748b);
    position: absolute;
    left: 50%;
    top: 4px;
    transform: translateX(-50%);
    transition: transform 0.22s ease;
    pointer-events: none;
  }
  body[data-mobile-board="deskpan"] .fv-panel:not([data-summary-collapsed="true"]) .fv-header::after {
    /* Expanded: flip to chevron-down (points down to collapse), kept centered. */
    transform: translateX(-50%) rotate(180deg);
  }
  /* When collapsed, trim interaction on the fairness body (hidden via translateY). */
  body[data-mobile-board="deskpan"] .fv-panel[data-summary-collapsed="true"] .fv-body {
    pointer-events: none;
  }
  /* Collapsed = a minimal handle bar, NOT a control surface (2026-07-10, user:
     "team summary strip should not have tabs when minimised"). The fairness
     header carries the period tablist (Weekly/Monthly/Quarterly); when the panel
     is collapsed only the header band shows, so those tabs sat live on the
     minimised strip. Hide the tablist while collapsed — the title + peek chevron
     stay as the grab handle; the tabs return the moment it's expanded. */
  body[data-mobile-board="deskpan"] .fv-panel[data-summary-collapsed="true"] .fv-period-toggle {
    display: none !important;
  }
  /* Give the board bottom clearance so the collapsed bar never covers the last
     row's cells — OR the column's bottom drag handle AND its avg bar, which hang
     below the cell band — when the page is scrolled to the bottom.
     2026-06-26 (user: "the bottom handle is hidden by the most-reachable sticky
     team-average strip"): the old reservation (--ts-collapsed-h + 16px) cleared
     only the CELLS. The bottom .time-column-drag-handle is positioned bottom:-8px
     on the column (8px of the 16px grip hangs below the last cell) and its
     touch-area ::before extends another 16px below that — so the grip + its tap
     zone sat ~24px below the cell band and got tucked under the 36px bar. Added a
     --scope-handle-overhang (28px ≈ the 8px visible drop + 16px tap area + a hair)
     so the column bottom floated high enough that the whole handle cleared the bar.
     2026-06-29 (user: "the lowest handles and avg bar is obscured by the team
     collapsible strip even when collapsed, give more white space so they don't get
     overlapped"): the 28px reserve cleared the HANDLE but NOT the bottom avg bar.
     .column-team-strip-bottom sits at top:calc(--handle-bottom + 18px) and is 28px
     tall (style.css ~44478), so its bottom edge lands at --handle-bottom + 46px —
     ~30px BELOW the handle's bottom edge (--handle-bottom + 16px), plus the avg
     bar's label/members-toggle chip hangs a touch lower still. So the reserve must
     cover the avg bar's lower extent, not just the handle. Bump the overhang to
     62px (28px prior handle clearance + ~30px to reach the avg bar's bottom edge +
     ~4px breathing room) so the bottom handle, its tap zone, AND the bottom avg bar
     + its chip all float above the collapsed bar's top edge. Resting states that
     force the bottom avg bar visible (focus-lock, style.css ~44491) are covered too,
     where the overlap was otherwise permanent. */
  /* The :not([data-layout="compact"]):not([data-focus-mode]) qualifiers RAISE this
     rule's specificity to (0,4,1) so it beats the unconditional
     `body:not([data-layout="compact"]):not([data-focus-mode]) #timezone-list`
     padding-bottom:24px (specificity 0,3,1, style.css ~3968) — which otherwise won
     the cascade and pinned padding-bottom at 24px, leaving this whole deskpan
     reservation (handle + team-bar clearance) INERT (verified 2026-06-29: the calc
     resolved but 24px was applied). deskpan is never compact/focus, so the extra
     :not()s only adjust specificity, never which elements match. */
  /* 2026-08-31 (owner: "the handles and team bar are not appearing at the
     bottom", phone): 62px cleared the handle (+16px) and the avg bar's lower
     edge (+46px) but NOT the bottom duration-presets pill, whose inline
     anchor is --handle-bottom + 56px and whose ~32px height puts its bottom
     edge at ~+88px. On a short board the docked sheet tucked exactly that
     chrome. 104px = the pill's +88px extent + the same ~16px breathing room
     the prior bumps kept. */
  body[data-mobile-board="deskpan"]:not([data-layout="compact"]):not([data-focus-mode]) #timezone-list {
    --scope-handle-overhang: 104px;
    padding-bottom: calc(var(--ts-collapsed-h, 46px) + 16px + var(--scope-handle-overhang));
  }
}

/* ── DARK MODE (user 2026-06-29: "team availability footer doesn't match dark
   mode") ──────────────────────────────────────────────────────────────────────
   The panel surface, text and verdict tones already ride theme-aware tokens
   (--bg-secondary / --border-color / --text-* / --status-*), so they flip
   correctly. The one light-island is the strip track, which is a HARDCODED
   #f8fafc — a bright bar that stayed light against the dark panel. Repaint the
   track to a dark slate and soften the cell dividers so the whole footer reads
   as dark-native. Light mode keeps the original values above — every rule here
   is body.dark-mode scoped. */
body.dark-mode .ts-panel {
  /* Lift the panel off the page so it reads as a discrete footer card rather
     than melting into the dark board, and drop the light box-shadow. */
  background: var(--surface-secondary, #1e293b);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
body.dark-mode .ts-strip {
  /* The track itself is transparent in both themes now (see .ts-strip), matching
     the board's .timeline — so dark mode only needs the border, not a fill. A
     fill here would re-create the light-mode island bug in reverse: a slab of
     flat slate under cells that are meant to sit on the panel surface. */
  border-color: var(--border-color, #334155);
}
body.dark-mode .ts-cell {
  /* Lighten the divider hairline a touch so cell edges still read on the darker
     track (the light-mode slate-400 @20% nearly vanished against dark cells). */
  border-right-color: rgba(148, 163, 184, 0.14);
}
/* Hour numerals on the dark track: lighter ink, and the halo flips to a dark
   one so the glyph still separates from a saturated fill (the white halo would
   read as a smudge on the dark cells). Mirrors the scan column's dark-mode
   .scan-hour-label treatment. */
body.dark-mode .ts-cell-hour {
  color: var(--slate-400, #94a3b8);
  text-shadow:
    0 0 2px rgba(15, 23, 42, 0.95),
    0 0 4px rgba(15, 23, 42, 0.85),
    0 1px 1px rgba(15, 23, 42, 0.9);
}
body.dark-mode .ts-cell[data-hour="0"] .ts-cell-hour,
body.dark-mode .ts-cell[data-hour="12"] .ts-cell-hour {
  color: var(--slate-200, #e2e8f0);
}
body.dark-mode .ts-cell[data-now="true"] .ts-cell-hour {
  color: #f472b6;
}
