/* ============================================================================
   Shea's Forest — the App Shell  ·  styles/shell.css  ·  STEP 2 (the tab-strip)
   ----------------------------------------------------------------------------
   The two-tier tab chrome, rendered by shell/tab-strip.js as a pure fold of the
   view-config. Pinned tabs sit small/icon on the LEFT; open tabs sit named on
   the RIGHT; the "+" ends the strip. The ACTIVE tab carries the gold disc — the
   token's meaning: where attention rests. State reads as calm weather, never
   alarm (Theo's rule): no red, no counts.

   Reads every color/type/space/motion from styles/tokens.css. Hard-codes no hex.
   Chrome lives on the dawn ground inside .ring__inner, so text is warm-off-white
   (--ink-on-dark) with gold accents and moss for quiet/absent states.
   ============================================================================ */

/* ---- the strip -------------------------------------------------------------- */
.tabstrip {
  /* ONE HEIGHT FOR EVERY TAB (operator, S24.1620 -- measured, not nudged).
     Before this, `.tab--pinned` carried an explicit `height: 2rem` (32px) and an
     open tab did not, so an open tab's height was INTRINSIC -- set by its tallest
     child, which is `.tab__pin`/`.tab__close` at 1.1rem + line-box = 17.59px, giving
     17.59 + 16 (padding) + 2 (border) = 35.59px. Playwright measurement at 1440x900:
     pinned 32.00 / open 35.59 -- the open tabs were 3.59px TALLER (1.80 proud on top,
     1.79 on the bottom), which is the ~1px-per-edge asymmetry the operator's eye caught
     (he read the direction as pinned-taller; the bytes say open-taller -- same defect,
     inverted attribution, and the per-edge magnitude he estimated was right).
     The fix is FLOOR-drive, not a nudge: ONE declared height token both tiers read, so
     the two can never diverge again under a future child/padding change. */
  --tab-h: 1.875rem;                /* 30px -- every tab, both tiers */
  display: flex;
  align-items: flex-end;            /* every tab bottom lands on ONE line (see border-bottom) */
  flex-wrap: wrap;
  gap: var(--s-2);
  height: calc(var(--tab-h) + 1px); /* tabs + the hairline; DEFEATS the parent's align-items:stretch */
  box-sizing: border-box;
  margin: var(--s-4) 0 0;
  /* The first pinned tab used to sit flush against the left bound while every tab after it
     got --s-2 of air. The eye reads that as a crowding, not a alignment — the row looked
     jammed against the edge. So the strip's leading pad IS the gap: the same --s-2 the tabs
     already give each other, deliberately the SAME TOKEN and not a copy of its value, so the
     two can never drift apart under a future spacing change. (Operator, S13.2200.) */
  padding-left: var(--s-2);
  /* FLUSH (operator, S24.1620). Was `padding-bottom: var(--s-2)`, which pushed the
     hairline 8px clear of the tabs -- and because the strip was ALSO being stretched to
     55px by the parent's align-items:stretch while its tabs overflowed their own content
     box, the measured result was the hairline at y~46-47 with the tab bottoms at 49.8
     (pinned) and 51.59 (open): both tiers hanging BELOW the line, exactly as the operator
     described. With padding-bottom:0 + the declared height above, the tab bottoms and the
     hairline are the SAME y BY CONSTRUCTION, not by arithmetic that drifts. */
  padding-bottom: 0;
  border-bottom: 1px solid var(--line-bark);
  font-family: var(--face-survey);
}
.tabstrip--empty { border-bottom-color: transparent; }

.tabstrip__pinned,
.tabstrip__open {
  display: flex;
  align-items: flex-end;            /* the tiers share the strip's baseline */
  flex-wrap: wrap;
  gap: var(--s-2);
  min-width: 0;
}
/* a quiet seam between the two tiers, only when both are populated */
.tabstrip__pinned:not(:empty) + .tabstrip__open:not(:empty) {
  padding-left: var(--s-3);
  border-left: 1px solid var(--line-bark);
  margin-left: var(--s-1);
}

/* ---- a tab (shared) --------------------------------------------------------- */
.tab {
  --tab-ink: var(--ink-on-dark);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  position: relative;
  margin: 0;
  padding: 0 var(--s-3);            /* vertical padding retired -- `height` owns the box now */
  height: var(--tab-h, 1.875rem);   /* THE declared height (see .tabstrip) -- both tiers */
  box-sizing: border-box;
  border: 1px solid var(--line-bark);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--paper) 8%, transparent);
  color: var(--tab-ink);
  font: inherit;
  font-size: var(--t-small);
  line-height: 1;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}
.tab:hover { background: color-mix(in srgb, var(--paper) 16%, transparent); }

/* pinned = small, icon-forward */
.tab--pinned {
  padding: 0;
  width: var(--tab-h, 1.875rem);    /* square, off the SAME token as the height */
  justify-content: center;
  border-radius: var(--r-m);
}
.tab--pinned .tab__icon { color: var(--sunlight-soft); font-size: 0.95rem; }

/* open = named, roomier */
.tab--open .tab__icon { color: var(--moss); font-size: 0.85rem; }
.tab--open .tab__label {
  font-family: var(--face-path);
  font-size: var(--t-small);
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- item 3 slice B: the label, being edited --------------------------------
   THE LOAD-BEARING PART OF THIS RULE IS WHAT IS ABSENT. No padding, no border, no
   margin, no font-size change, no line-height change — because `.tabstrip` is
   flex-wrap and `align-items: flex-end`, so the flex LINE's cross size is its
   tallest item and any child that grows drags EVERY tab bottom off the hairline
   item 5 made flush. Measured on this line: a 42px child moved tab bottoms
   46.00 -> 58.00, and nothing threw. `.tab` declares `height: var(--tab-h)` with
   border-box so a taller descendant cannot push the button's own box either — that
   is the belt, this is the braces, and the Playwright probe is the proof.

   `outline` is chosen deliberately over `border`: an outline is painted OUTSIDE the
   box and does not participate in layout, so the edit state is plainly visible and
   costs exactly zero reflow. Anything with a border here is a geometry bug wearing
   a focus ring. */
.tab__label--editing {
  outline: 1px solid var(--sunlight-soft);
  outline-offset: 1px;
  background: color-mix(in srgb, var(--paper) 22%, transparent);
  border-radius: 3px;
  cursor: text;
  white-space: nowrap;
  caret-color: var(--sunlight-soft);
}

/* pinned tabs carry the name for assistive tech only */
.tab__label--sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

/* ---- active: the gold disc (where attention rests) -------------------------- */
.tab--active {
  --tab-ink: var(--ink);
  border-color: var(--line-gold);
  background: color-mix(in srgb, var(--sunlight) 14%, transparent);
}
.tab__disc {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--r-pill);
  background: radial-gradient(circle at 40% 35%, var(--sunlight-soft), var(--sunlight) 70%);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sunlight) 22%, transparent);
  flex: 0 0 auto;
}
.tab--pinned.tab--active { box-shadow: var(--shadow-lit); }

/* ---- drag to reorder (operator's note #4) ----------------------------------- *
 * `draggable` is the RENDER's fold of viewConfig.editLocked, so the grab cursor
 * appears only when the tab-set is genuinely editable. That is the whole point of
 * hanging the cursor off the attribute rather than off `.tab`: before this, every
 * tab showed a hand and none of them moved — the cursor was making a promise the
 * shell could not keep. Now the hand means the hand will work.
 * Calm states only: a dimmed source and a gold-lined target. No motion, no alarm. */
.tab[draggable="true"] { cursor: grab; }
.tab--dragging {
  opacity: 0.45;
  cursor: grabbing;
}
.tab--drop-target {
  border-color: var(--line-gold);
  border-style: dashed;
  background: color-mix(in srgb, var(--sunlight) 10%, transparent);
}

/* ---- honest-absent: calm, never alarm --------------------------------------- */
.tab--absent {
  --tab-ink: var(--moss);
  border-style: dashed;
  border-color: var(--line-moss);
  background: transparent;
}
.tab--absent .tab__icon { color: var(--moss); opacity: 0.8; }
.tab__absent {
  font-family: var(--face-survey);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--moss);
  text-transform: lowercase;
}
.tab--pending { opacity: 0.9; }

/* ---- calm weather dot (badges; off by default, fed only at step 6) ---------- */
.tab__weather {
  width: 0.4rem; height: 0.4rem;
  border-radius: var(--r-pill);
  flex: 0 0 auto;
  background: var(--w-quiet);
}
.tab__weather--active { background: var(--w-active); }
.tab__weather--quiet  { background: var(--w-quiet); }
.tab__weather--deep   { background: var(--w-deep); }
.tab__weather--ok     { background: var(--w-ok); }

/* ---- the weather toggle (STEP 6; off by default, a calm preference) --------- */
.forest-badges-toggle-host { display: flex; justify-content: flex-end; padding: 0.15rem 0.1rem 0; }
.forest-badges-toggle {
  font-family: var(--face-survey);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--w-quiet);
  background: transparent;
  border: 0;
  padding: 0.1rem 0.3rem;
  border-radius: var(--r-pill);
  cursor: pointer;
  opacity: 0.7;
}
.forest-badges-toggle:hover { opacity: 1; }
.forest-badges-toggle--on { color: var(--w-active); opacity: 1; }

/* ---- honest-badge grammar (DP-006 §3f; the H3 hollow form is load-bearing) ---
   The declared --badge-* tokens' first true component (shell/honest-badge.js). A
   badge shows a true state or shows it CANNOT reach the truth: every VERIFIED
   state is a SOLID chip, and `unreachable` is the ONLY hollow, dashed ring
   (transparent fill) — so with labels hidden and color stripped, a stale badge
   can never read as a clear one (H3). The structural fill/border are ALSO set
   intrinsically by the component, so the form survives a missing stylesheet;
   this block gives it the finished look. Never red, never a count (Theo). */
.honest-badge {
  display: inline-flex;
  align-items: center;
  font-family: var(--face-survey);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 0.12rem 0.42rem;
  border-radius: var(--r-pill);
  border-width: 1px;               /* the dashed ring needs a width to render */
  border-color: transparent;
  border-style: none;              /* -> `dashed` on the ring, set intrinsically */
  white-space: nowrap;
}
/* verified states — SOLID chips (opaque fill, no ring) */
.honest-badge--known     { background: var(--badge-known);     color: var(--bark); }
.honest-badge--known-due { background: var(--badge-known-due); color: var(--bark); }
.honest-badge--overdue   { background: var(--badge-overdue);   color: var(--badge-overdue-ink); }
/* the honest-absent state — the H3 hollow, dashed ring (NO fill; the FORM says it) */
.honest-badge--unreachable {
  background: var(--badge-unreachable);          /* transparent, by token */
  border-style: var(--badge-unreachable-form);   /* dashed */
  border-color: var(--badge-unreachable-line);
  color: var(--badge-unreachable-ink);
}

/* ---- mail: the honest reachability glance (slice ②, DP-012 §3f) -------------
   The badge is first-class in the mail view as the MAILBOX's reachability state.
   Calm by construction: on a verified read the glance is a quiet `known` chip
   riding the count line ("current · N messages"); it is loud only when it has
   bad news — the `unreachable` pane, where the hollow ring + the SM-1 voice
   (plain fact, no blame, promise of continuity) say the sync couldn't reach the
   truth rather than dressing a stale inbox as current. This is the minimum
   calm styling for slice ②'s new nodes; the full mail skin is "the rest". */
.mail__status {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-2) 0 var(--s-3);
  color: var(--moss-ink);
  font-family: var(--face-survey);
  font-size: var(--t-small);
}
.mail__reach { display: inline-flex; align-items: center; }

/* DP-012 §3f — the OPT-IN unread count. Calm by construction (JT-6): the ON number
   is a quiet, muted Spline-Sans-Mono field (it already inherits --face-survey +
   --moss-ink + --t-small from .mail__status), never a red pill or notification. OFF,
   the slot carries a low-key invite worded to WELCOME (SM-2), met where the number
   would sit — deadpan, never cute (BP-4). Nothing here shouts: no fill, no border,
   no alarm hue; the reach is quiet until you look for it, like the pin/close below. */
.mail__count-slot { display: inline-flex; align-items: baseline; gap: var(--s-2); }
.mail__count-invite {
  color: var(--ink-soft);
  cursor: pointer;
  border-bottom: 1px dotted color-mix(in srgb, var(--ink-soft) 55%, transparent);
  line-height: 1.2;
}
.mail__count-invite:hover,
.mail__count-invite:focus-visible {
  color: var(--moss-ink);
  border-bottom-color: color-mix(in srgb, var(--moss-ink) 60%, transparent);
  outline: none;
}
.mail__unread-count {
  color: var(--moss-ink);
  font-variant-numeric: tabular-nums;   /* the number sits steady, a data-face not an alarm */
  letter-spacing: 0.01em;
}

/* #8 slice ② — the whole-corpus "Search all Gmail" results region. Sits below the local
   list, divided by a quiet hairline (not a loud panel): the local instant filter is the
   resting surface; this is the opt-in reach beneath it. Same calm family as .mail__status —
   --face-survey / --moss-ink, no fill, no alarm hue. The header is a low-key section label;
   the status line (loading / error / nothing-else) is deadpan (BP-4), never a red pill. */
.mail__server-results:not(:empty) {
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid color-mix(in srgb, var(--moss-ink) 14%, transparent);
}
.mail__server-header {
  color: var(--ink-soft);
  font-family: var(--face-survey);
  font-size: var(--t-small);
  letter-spacing: 0.02em;
  margin-bottom: var(--s-2);
}
.mail__server-status {
  color: var(--ink-soft);
  font-family: var(--face-survey);
  font-size: var(--t-small);
  line-height: 1.3;
}
.mail__server-status--error { color: color-mix(in srgb, var(--moss-ink) 78%, var(--ink-soft)); }
.mail-list--server { margin: 0; }

/* #23 — bulk-archive surface. Three new pieces, all in the calm mail family (--face-survey /
   --moss-ink, no alarm hue). CAST-fit: the checkbox is a FIXED leaf riding .row__lead (already
   flex:0 0 auto — no reflow of the row body); the toolbar and toast are FIXED-height bands that
   only exist when populated (:empty -> display:none), so the resting list is byte-unchanged when
   nothing is selected. Overflow policy: the toolbar wraps (flex, gap) rather than clipping. */
.mail-msg__check,
.contacts-row__check {
  box-sizing: border-box;
  width: 16px; height: 16px;
  margin-right: var(--s-2);
  border: 1.5px solid color-mix(in srgb, var(--moss-ink) 45%, transparent);
  border-radius: 4px;
  cursor: pointer;
  display: inline-block;
  position: relative;
}
.mail-msg__check:hover,
.mail-msg__check:focus-visible,
.contacts-row__check:hover,
.contacts-row__check:focus-visible { border-color: var(--moss-ink); outline: none; }
.mail-msg__check.is-checked,
.contacts-row__check.is-checked { background: var(--moss-ink); border-color: var(--moss-ink); }
.mail-msg__check.is-checked::after,
.contacts-row__check.is-checked::after {
  content: "";
  position: absolute; left: 4px; top: 1px;
  width: 4px; height: 8px;
  border: solid var(--paper);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.mail__bulk:empty,
.contacts__bulk:empty { display: none; }
.mail__bulk,
.contacts__bulk { align-items: center; gap: var(--s-2); flex-wrap: wrap; margin-bottom: var(--s-2); }
.mail__bulk-count,
.contacts__bulk-count {
  color: var(--moss-ink);
  font-family: var(--face-survey);
  font-size: var(--t-small);
  letter-spacing: 0.02em;
  margin-right: var(--s-1);
}
/* #26 — the bulk label picker: breaks to its own full-width row below the button
   strip (flex-basis:100% inside the wrapping .mail__bulk), wraps its chips, and
   reads as a hairline sub-panel. The chips themselves ride .strip__action. */
.mail__bulk-picker,
.contacts__bulk-picker {
  flex-basis: 100%;
  display: flex; flex-wrap: wrap; gap: var(--s-2);
  margin-top: var(--s-2); padding-top: var(--s-2);
  border-top: 1px solid var(--line-moss);
}
/* C3 — the import result band. It wears the undo-bar's own clothes (same frame, same tint, same
   type): it IS an undo bar, for the one act in this app most likely to need one. Zero new tokens,
   zero bespoke skin — `contacts__import-dismiss` and the undo control are dual-classed onto
   `.contacts__undo-action` / `.strip__action` and inherit for free. Empty at rest. */
.contacts__import-band:empty { display: none; }
.contacts__import-band {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  margin-bottom: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--line-moss);
  border-radius: 6px;
  background: color-mix(in srgb, var(--moss) 6%, transparent);
}
.contacts__import-msg {
  color: var(--ink); font-family: var(--face-survey); font-size: var(--t-small);
  letter-spacing: 0.02em;
}
.contacts__import-dupes { color: var(--ink-soft); font-family: var(--face-survey); font-size: var(--t-small); }
/* The recovery is ARCHIVE and it is deliberately ORDINARY — no danger tint, no red. It is
   reversible, and dressing a reversible act as a dangerous one teaches the exact hesitation that
   makes people leave a botched import in place. The danger tint belongs on the purge, and the
   purge is behind C2's confirm where it stays. */
.contacts__import-select,
.contacts__import-undo,
.contacts__import-dismiss { margin-left: auto; }
.contacts__import-select ~ .contacts__import-undo,
.contacts__import-undo ~ .contacts__import-dismiss { margin-left: 0; }

.mail__undo:empty,
.contacts__undo:empty { display: none; }
.mail__undo,
.contacts__undo {
  display: flex; align-items: center; gap: var(--s-3);
  margin-bottom: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--line-moss);
  border-radius: 6px;
  background: color-mix(in srgb, var(--moss) 6%, transparent);
}
.mail__undo-label,
.contacts__undo-label { color: var(--ink-soft); font-family: var(--face-survey); font-size: var(--t-small); }
.mail__undo-action,
.contacts__undo-action { color: var(--moss-ink); cursor: pointer; font-weight: 600; }
.mail__undo-action:hover, .mail__undo-action:focus-visible,
.contacts__undo-action:hover, .contacts__undo-action:focus-visible { color: var(--ink); outline: none; }
.mail__undo-status,
.contacts__undo-status { color: var(--ink-soft); font-family: var(--face-survey); font-size: var(--t-small); }
.mail__undo-status--error,
.contacts__undo-status--error { color: color-mix(in srgb, var(--moss-ink) 78%, var(--ink-soft)); }
.mail__count-hide {
  color: color-mix(in srgb, var(--ink-soft) 78%, transparent);  /* quieter than the number — a way back off, never a nag */
  cursor: pointer;
  font-size: 0.92em;
}
.mail__count-hide:hover,
.mail__count-hide:focus-visible { color: var(--ink-soft); outline: none; }
.mail--unreachable {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-4) 0;
}
.mail--unreachable .pane__absent {
  color: var(--ink-soft);
  max-width: 46ch;
  line-height: var(--leading-body);
}

/* ---- the pin + close affordances (quiet until you reach for them) ----------- */
.tab__pin,
.tab__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem; height: 1.1rem;
  border-radius: var(--r-pill);
  color: color-mix(in srgb, var(--ink-on-dark) 55%, transparent);
  font-size: 0.8rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease), color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.tab:hover .tab__pin,
.tab:hover .tab__close,
.tab:focus-within .tab__pin,
.tab:focus-within .tab__close { opacity: 1; }
.tab__pin:hover   { color: var(--sunlight); background: color-mix(in srgb, var(--sunlight) 16%, transparent); }
.tab__close:hover { color: var(--floor);    background: color-mix(in srgb, var(--bark) 40%, transparent); }
/* a pinned tab shows its pin state persistently (it is the "pinned" signal) */
.tab--pinned .tab__pin { opacity: 0.85; color: var(--sunlight-soft); }

/* ---- the "+" ---------------------------------------------------------------- */
.tab--add {
  width: var(--tab-h, 1.875rem);
  justify-content: center;
  padding: 0;
  border-style: dashed;
  border-color: var(--line-bark);
  color: color-mix(in srgb, var(--ink-on-dark) 70%, transparent);
  font-size: 1rem;
  background: transparent;
}
.tab--add:hover {
  border-color: var(--line-gold);
  color: var(--sunlight);
  background: color-mix(in srgb, var(--sunlight) 10%, transparent);
}

/* LOCKED (S13.1945, operator note #3) — tab editing is locked, so the "+" refuses.
   The `disabled` attribute already stops the click; this makes the refusal VISIBLE
   before the click, so the user never spends one to learn it. The hover rule below is
   the load-bearing half: without it the :hover above still fires on a disabled button
   and the control lights up gold as if it were live — an affordance that PROMISES a
   thing it will not do, which is worse than no affordance at all. The title carries the
   remedy ("click the lock"), so a dead end becomes an instruction. */
.tab--add[disabled],
.tab--add-locked {
  opacity: 0.4;
  cursor: not-allowed;
  border-style: dotted;                 /* dashed -> dotted: a second, non-colour channel (Niamh) */
}
.tab--add[disabled]:hover,
.tab--add-locked:hover {
  border-color: var(--line-bark);       /* the resting border — no gold promise */
  color: color-mix(in srgb, var(--ink-on-dark) 70%, transparent);
  background: transparent;
}

/* ---- THE ADD STACK: "+" on top, edit-lock beneath (operator item 4, S24.1656) --
   The lock moved out of the top-bar actions cluster and into the strip, under the "+".
   Two controls where one tab used to stand, so the pair needs its own column -- and the
   column is TALLER than the strip it sits in (42px of buttons in a 31px strip), which is
   deliberate and is why the numbers below are declared rather than left to intrinsics.

   THE BASELINE IS THE CONTRACT: `align-self: flex-end` inside the strip's `flex-end`
   means the stack's BOTTOM lands on the same line every tab bottom lands on -- the line
   the hairline runs along. The stack then grows UPWARD into the bar's dead air (the
   56px bar has ~11px of it above the strip), so nothing below the baseline moves and
   the bar does not grow. That is the same hard constraint item 1 held for the wordmark.

   The buttons are 20px, not the tabs' 30px: two 30px buttons plus a gap is 62px in a
   56px bar -- it does not fit, and shrinking the bar to suit a stack of icons would
   trade the whole reduction for one control. 20 + 2 + 20 = 42, bottom-flush, 4px of
   headroom left at the top of the bar. */
/* THE TRAP, MEASURED AND NAMED (Playwright, 1440x900, this session): the stack CANNOT
   simply be a 42px-tall flex child. `.tabstrip` is `flex-wrap: wrap`, so the flex LINE's
   cross size is the tallest item's -- a 42px stack makes the line 42px, and the strip's
   `align-items: flex-end` then puts EVERY tab bottom at the bottom of that taller line.
   Measured: tab bottoms went 46.00 -> 58.00 and the hairline stayed at 47, i.e. the whole
   tab row dropped 12px BELOW the line. That is item 5's flush hairline destroyed by item
   4 -- silently, in a file item 5 never touched, from a rule that reads correct.
   So the stack's FLOW box is exactly one tab tall (the line never learns about the extra
   height) and its two buttons are taken OUT OF FLOW inside it, growing upward into the
   bar's dead air. Same move item 1 made for the version stamp, same reason: an element
   that must not move something else does not get to participate in its layout. */
.tabstrip { overflow: visible; }         /* the stack overflows the strip BY DESIGN -- never clip it */
.tab-addstack {
  --addstack-btn: 1.25rem;               /* 20px -- BOTH buttons read this; they can never diverge */
  --addstack-gap: 2px;
  flex: 0 0 auto;
  align-self: flex-end;                  /* the stack's bottom == the tab baseline == the hairline */
  position: relative;                    /* the anchor for the two out-of-flow buttons */
  width: var(--addstack-btn);
  height: var(--tab-h, 1.875rem);        /* ONE TAB TALL IN FLOW -- see the trap above */
}
/* the "+" inside the stack: same control, resized to the stack's rhythm, out of flow */
.tab-addstack .tab--add {
  position: absolute;
  left: 0;
  bottom: calc(var(--addstack-btn) + var(--addstack-gap));   /* sits one button + gap above the lock */
  width: var(--addstack-btn);
  height: var(--addstack-btn);
  min-height: 0;                         /* .tab's own height/min-height must not win here */
  font-size: 0.8125rem;                  /* the glyph follows the box down from 1rem */
  line-height: 1;
  border-radius: var(--r-s, 3px);
}
.tab-addstack__lock {
  position: absolute;
  left: 0;
  bottom: 0;                             /* the lock is the baseline-flush half of the pair */
  display: flex;
  line-height: 0;
}
.tab-addstack__lock .tsa-btn {
  width: var(--addstack-btn);
  height: var(--addstack-btn);
  border-radius: var(--r-s, 3px);
  color: color-mix(in srgb, var(--ink-on-dark) 70%, transparent);  /* the strip is DARK: the
       cluster's --ink-soft is tuned for the light bar and reads as a smudge here. Match the
       "+" it now stands under -- the pair is one control group and must look like one. */
}
.tab-addstack__lock .tsa-btn:hover {
  color: var(--sunlight);
  background: color-mix(in srgb, var(--sunlight) 10%, transparent);
}
.tab-addstack__lock .tsa-btn__icon svg { width: 0.875rem; height: 0.875rem; }

/* ---- focus ring: matches the app's gold outline ----------------------------- */
.tab:focus-visible,
.tab__pin:focus-visible,
.tab__close:focus-visible {
  outline: 2px solid var(--sunlight);
  outline-offset: 2px;
  border-radius: var(--r-s);
}

/* ---- narrow screens: the strip wraps, tiers stay grouped -------------------- */
@media (max-width: 40rem) {
  .tab--open .tab__label { max-width: 8rem; }
  .tabstrip__pinned:not(:empty) + .tabstrip__open:not(:empty) {
    padding-left: 0; border-left: 0; margin-left: 0;
  }
}

/* Motion is already switched off under prefers-reduced-motion via tokens.css
   (--dur-1 -> ~0), so these transitions calm down for that reader automatically. */

/* ============================================================================
   The pane (STEP 3) — the tab→pane joint's render surface. Empty pane takes no
   space (the page's own content shows through); absent/pending/error are calm,
   never alarmed (Theo's rule). Renderers own .pane--live's inner DOM.
   ============================================================================ */
.pane { margin: var(--s-4) 0 0; }
.pane--empty { display: none; }               /* no selection -> the page shows through */
.pane--absent,
.pane--pending,
.pane--error,
.pane--live {
  border: 1px solid var(--line-bark);
  border-radius: var(--r-l);
  background: color-mix(in srgb, var(--paper) 6%, transparent);
  padding: var(--s-5) var(--gutter);
}
.pane__title {
  font-family: var(--face-canopy);
  font-size: var(--t-title);
  line-height: var(--leading-tight);
  color: var(--ink);
  margin: 0 0 var(--s-3);
}
/* .pane__version — RETIRED (DP-018). The app's V# is the anchor's, not the body pane's.
   Its declarations moved verbatim to .menu__version (§frame). */
/* Shared skeleton — LAYOUT ONLY, plus the `pending` face. State is carried by each
   word below (L1 self-carriage, design/the-pane-state-words-v1.md §2). `pending` is
   the quiet baseline the other two are read against and is intentionally unstyled here. */
.pane__absent, .pane__pending, .pane__error {
  font-family: var(--face-path);
  font-size: var(--t-body);
  line-height: var(--leading-body);
  color: var(--ink-soft);
  margin: 0 0 var(--s-4);
}
/* absent — a calm fact, not a fault. Promoted OFF .pane--absent so it reads the same
   with or without the block (the modifier is set at 4 sites; the word at 17).
   MEASURED S27.0151: 80% is the MAXIMUM mix that clears AA in BOTH skins (90% = 4.01:1
   dark, FAILS). It buys 1.66:1 dark / 1.27:1 light of separation from `pending` — real
   but sub-perceptual. See the filed finding: --moss resolves to --ink-faint post-DP-017,
   so this reading carries no hue and the token layer has no room left. L1 is met; L2's
   INTENT is not, and cannot be here without a second channel (§3 ruled that out). */
.pane__absent { color: color-mix(in srgb, var(--moss) 80%, var(--ink)); }
/* error — --bark is already the Forest's fault family (.pane--error's border). The
   border-left is the SECOND channel: colour alone is not a channel a person can be
   relied on to read — and MEASURED, it is the ONLY channel here that a person can
   actually read (the mix is 1.31:1 dark / 1.43:1 light from `pending`; the rule carries
   the only hue, via --sunlight). Form reused from .calendar-reschedule-fail (~:1460),
   not minted. */
.pane__error {
  color: color-mix(in srgb, var(--bark) 65%, var(--ink));
  border-left: 3px solid color-mix(in srgb, var(--bark) 50%, var(--sunlight));
  padding-left: var(--s-3);
}
.pane--absent { border-style: dashed; border-color: var(--line-moss); }
/* :638 DELETED S27.0151 — `.pane--absent .pane__absent` was an exact duplicate of the
   element rule above, and it is the SHAPE OF THE BUG: a descendant selector doing the
   element selector's job. Leaving it would teach the next reader that the conditional
   form is prior art. */
.pane--error { border-color: color-mix(in srgb, var(--bark) 50%, var(--line-bark)); }
.pane__connect {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--line-gold);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--sunlight) 12%, transparent);
  color: var(--ink);
  font-family: var(--face-survey); font-size: var(--t-small);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease);
}
.pane__connect:hover { background: color-mix(in srgb, var(--sunlight) 22%, transparent); }
.pane__connect:focus-visible { outline: 2px solid var(--sunlight); outline-offset: 2px; }

/* ---- CONNECTOR PANE — a connector's own ingested items (STEP 5 richer projection) ----
   A connector tab lists its OWN Soil items, GROUPED into category sections. Moss/substrate
   family, all tokens from tokens.css. Content-free by construction (name + category + adapter
   only — the projection PICKs, never spreads). NOT time-ordered: no "recent" claim in the type.
   (The honest-empty line reuses the shared .pane__pending rule above — not restyled here.) */
.pane__census {                               /* at-a-glance total: "3 items · 1 bills, 2 tax" */
  margin: calc(-1 * var(--s-2)) 0 var(--s-3);
  font-family: var(--face-survey); font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--moss-ink);
}
.connector-group { margin-top: var(--s-3); }
.connector-group__head {                      /* section heading: "bills · 2" */
  font-family: var(--face-survey); font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--moss-ink);
  padding-bottom: var(--s-1); border-bottom: 1px solid var(--line-moss);
}
.connector-items {
  list-style: none; margin: var(--s-2) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--s-1);
}
.connector-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-2);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  background: color-mix(in srgb, var(--moss) 8%, transparent);
}
.connector-item__name {                       /* the item name — the main line */
  flex: 1 1 auto;                             /* grow to push the meta (src/time) right; pairs with min-width:0 for ellipsis */
  font-family: var(--face-canopy); color: var(--ink); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.connector-item__src {                        /* adapter badge, umbrella (Files) panes only */
  flex: 0 0 auto; font-family: var(--face-survey); font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--moss-ink);
  border: 1px solid var(--line-moss); border-radius: var(--r-pill); padding: 0.05rem 0.5rem;
  background: color-mix(in srgb, var(--moss) 9%, transparent);   /* a quiet fill so the chip reads as a chip */
}
.connector-item__time {                       /* relative ingestion time — the visible half of STEP 5 recency */
  flex: 0 0 auto; font-family: var(--face-survey); font-size: var(--t-micro);
  color: var(--moss-ink); opacity: 0.9; white-space: nowrap;   /* the quietest line, but still readable */
}

/* ============================ APP-SHELL "+" CATALOG · shell/index-panel.js (STEP 4) ============================ */
/* A transient modal pick-list. The host (#forest-catalog) is an empty overlay mount
   until the panel renders into it; index-panel.js paints .catalog and empties on close. */
.forest-catalog-overlay { position: relative; z-index: 60; }
.catalog {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-5);
}
.catalog__backdrop {
  position: absolute; inset: 0;
  background: color-mix(in srgb, var(--floor) 55%, transparent);
  backdrop-filter: blur(2px);
  cursor: pointer;
}
.catalog__panel {
  position: relative;
  width: min(560px, 92vw);
  max-height: 82vh; overflow: auto;
  background: var(--surface-card);
  border: 1px solid var(--line-gold);
  border-radius: var(--r-l);
  box-shadow: var(--shadow-card);
  padding: var(--s-5) var(--s-5) var(--s-6);
}
.catalog__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3); margin: 0 0 var(--s-4);
}
.catalog__title {
  font-family: var(--face-canopy); font-size: var(--t-body);
  color: var(--ink); letter-spacing: 0.01em;
}
.catalog__close {
  appearance: none; border: 1px solid var(--line-bark);
  background: transparent; color: var(--ink-soft);
  width: 1.9rem; height: 1.9rem; line-height: 1;
  border-radius: var(--r-pill); cursor: pointer; font-size: 1.1rem;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.catalog__close:hover { background: color-mix(in srgb, var(--bark) 12%, transparent); color: var(--ink); }
.catalog__close:focus-visible { outline: 2px solid var(--sunlight); outline-offset: 2px; }
.catalog__group { margin: 0 0 var(--s-4); }
.catalog__group-title {
  font-family: var(--face-survey); font-size: var(--t-small);
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-soft); margin: 0 0 var(--s-2);
}
.catalog__items { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.catalog__item {
  appearance: none; cursor: pointer;
  display: inline-flex; align-items: center;
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--line-gold);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--sunlight) 10%, transparent);
  color: var(--ink);
  font-family: var(--face-survey); font-size: var(--t-small);
  transition: background var(--dur-1) var(--ease);
}
.catalog__item:hover { background: color-mix(in srgb, var(--sunlight) 22%, transparent); }
.catalog__item:focus-visible { outline: 2px solid var(--sunlight); outline-offset: 2px; }
.catalog__item--added {
  cursor: default;
  border-color: var(--line-moss);
  background: transparent;
  color: var(--ink-soft);
  opacity: 0.7;
}
.catalog__empty {
  font-family: var(--face-path); font-size: var(--t-body);
  color: var(--ink-soft); margin: var(--s-2) 0 0;
}

/* ---- §3.8 compose-CREATION surface (Choice A): toggle, picked, action row ---- */
/* The Compose ⊗ toggle lives in the head beside ×; picking two composable units
   pins a grove:a⊗b tab via the shell-boot handler. Same pill idiom as .catalog__item;
   the --on state and --picked highlight read as "selected", not "alarm" (Theo). */
.catalog__compose-toggle {
  appearance: none; cursor: pointer;
  border: 1px solid var(--line-gold);
  background: transparent; color: var(--ink);
  padding: var(--s-2) var(--s-3); line-height: 1;
  border-radius: var(--r-pill);
  font-family: var(--face-survey); font-size: var(--t-small);
  letter-spacing: 0.02em;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.catalog__compose-toggle:hover { background: color-mix(in srgb, var(--sunlight) 16%, transparent); }
.catalog__compose-toggle:focus-visible { outline: 2px solid var(--sunlight); outline-offset: 2px; }
.catalog__compose-toggle--on {
  background: color-mix(in srgb, var(--sunlight) 28%, transparent);
  border-color: var(--sunlight); color: var(--ink);
}
/* a composable item in compose mode is the same pill; --picked is the selected state */
.catalog__item--picked {
  border-color: var(--sunlight);
  background: color-mix(in srgb, var(--sunlight) 34%, transparent);
  color: var(--ink); font-weight: 600;
}
.catalog__compose-actions {
  display: flex; align-items: center; gap: var(--s-3);
  margin: var(--s-4) 0 0; padding: var(--s-3) 0 0;
  border-top: 1px solid var(--line-bark);
}
.catalog__compose-reason {
  font-family: var(--face-path); font-size: var(--t-small);
  color: var(--ink-soft); flex: 1 1 auto;
}
.catalog__compose-go {
  appearance: none; cursor: pointer; margin-left: auto;
  border: 1px solid var(--line-gold);
  background: color-mix(in srgb, var(--sunlight) 22%, transparent);
  color: var(--ink);
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-pill);
  font-family: var(--face-survey); font-size: var(--t-small); letter-spacing: 0.02em;
  transition: background var(--dur-1) var(--ease);
}
.catalog__compose-go:hover { background: color-mix(in srgb, var(--sunlight) 34%, transparent); }
.catalog__compose-go:focus-visible { outline: 2px solid var(--sunlight); outline-offset: 2px; }
.catalog__compose-go--off, .catalog__compose-go[disabled] {
  cursor: default;
  border-color: var(--line-moss);
  background: transparent;
  color: var(--ink-soft);
  opacity: 0.7;
}

/* ---- THE SEARCH ARC, leg 2 (S13.2200) — search inside the catalog panel ------
   The same panel, a second mode. TOKENS ONLY — not one new hue. The DP-017 skin call
   (gold loop vs cool loop) is still the operator's and is still open; a search surface
   that invented a colour would pre-empt a decision that is not mine to make. Every ink
   below is an existing token or a color-mix of one, exactly as the pick surface is. */

/* the query box. Same pill grammar as .catalog__item, sized to sit in the head row and
   take the width the title and the × leave it. */
.catalog__search {
  flex: 1 1 auto; min-width: 0;
  appearance: none;
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--line-gold);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--sunlight) 6%, transparent);
  color: var(--ink);
  font-family: var(--face-survey); font-size: var(--t-small);
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.catalog__search::placeholder { color: var(--ink-soft); opacity: 0.8; }
.catalog__search:focus-visible {
  outline: 2px solid var(--sunlight); outline-offset: 2px;
  background: color-mix(in srgb, var(--sunlight) 12%, transparent);
}

/* the swappable results node. Layout-neutral by design: it exists so paintResults() can
   replace the groups WITHOUT destroying the input above it (see index-panel.js). */
.catalog__groups { display: block; }

/* ---- the three group STATES ------------------------------------------------
   R3 (search-federation.js): a store we could not read SAYS SO — it never renders zero.
   `empty` and `error` are DIFFERENT FACTS, so they get different words AND different ink:
   empty is quiet moss (a true, calm nothing); error is a soft warning that does not shout.
   `pending` is neither — it is a store still being waited on, and it says that rather than
   looking like a store with no results. Theo: state reads as weather, not alarm. */
.catalog__group-pending,
.catalog__group-empty,
.catalog__group-error {
  font-family: var(--face-path); font-size: var(--t-small);
  padding: var(--s-2) 0 0;
}
.catalog__group-pending {
  color: var(--ink-soft);
  animation: catalog-breathe 1.6s var(--ease) infinite;
}
@keyframes catalog-breathe { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.9; } }
/* Motion is switched off under prefers-reduced-motion via tokens.css --dur-*; the shimmer
   is an ANIMATION, so it needs its own retraction — the word still says "Looking…". */
@media (prefers-reduced-motion: reduce) {
  .catalog__group-pending { animation: none; opacity: 0.75; }
}
.catalog__group-empty { color: var(--ink-soft); }
.catalog__group-error {
  color: var(--ink);
  border-left: 2px solid var(--line-bark);
  padding-left: var(--s-3);
}

/* ---- a RESULT (a hit) --------------------------------------------------------
   Two lines, not one: the label is the thing, the sub is how you know WHICH thing (an
   email address, a date, a sender). A one-line hit forces a truncation that throws away
   exactly the disambiguating half. So a hit is a block, and the row is a column of them —
   unlike .catalog__items, which is a wrap of pills, because a capability IS its own label. */
.catalog__group .catalog__items:has(.catalog__item--hit) { flex-direction: column; align-items: stretch; }
.catalog__item--hit {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 0.1rem;
  width: 100%;
  text-align: left;
  border-radius: var(--r-m);
  border-color: var(--line-bark);
  background: transparent;
}
.catalog__item--hit:hover { background: color-mix(in srgb, var(--sunlight) 14%, transparent); }
.catalog__hit-label { color: var(--ink); }
.catalog__hit-sub {
  color: var(--ink-soft); font-size: var(--t-small);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}


/* ---- STEP 5 richer projection: a tree paired with its own items ------------ */
/* .tree-block is the flex item in .grove-floor — it carries the sizing .tree
   used to own, so a lone card (no items / no edge set) looks unchanged; the card
   sits on top, its items nested beneath. app.js's own page renders bare .tree
   cards with NO .tree-block, so the main page is untouched by these rules. */
.tree-block {
  flex: 1 1 240px; min-width: 210px; max-width: 340px;
  display: flex; flex-direction: column; gap: var(--s-2);
}
.tree-block > .tree { flex: initial; width: 100%; max-width: none; min-width: 0; }
.tree-items {
  padding: var(--s-1) var(--s-3);
  margin-left: var(--s-2);
  border-left: 2px solid color-mix(in srgb, var(--sunlight) 45%, transparent);
}
.radar--nested { margin-top: 0; }
.radar--nested .radar-row { font-size: var(--t-micro); }
.tree-items__tag {
  margin-top: var(--s-1); font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-soft);
}

/* STEP 7 — ⊗ groves (two-up compose). A grove:<a>⊗<b> tab shows two units side by
   side; each slot hosts an existing renderer's output. min-width:0 lets slot children
   ellipsize instead of overflowing the column (the classic grid-blowout guard). Graham:
   two-up only — a fixed two-column grid, never a window manager. Desktop-first; the
   narrow-viewport stack is polish, matching .horizon-row's own responsive fold. */
.grove-compose {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4);
  align-items: start;
}
.grove-compose__slot { min-width: 0; }
.grove-compose__slot-head {
  font-size: var(--t-micro); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-soft); margin-bottom: var(--s-2);
  padding-bottom: var(--s-1); border-bottom: 1px solid var(--line-moss);
}
.grove-compose__slot-body { min-width: 0; }
@media (max-width: 40rem) {
  .grove-compose { grid-template-columns: 1fr; }
}

/* ── The Margin (delight #6 of the Seven Delights) ───────────────────────────
   A quiet, content-free shape echo beside the compose draft. Coarse width strata
   (one per line, bucketed short/medium/long/full) + a calm settled/active pulse.
   SM-1: shape / rhythm, never sentiment. JT-1: gestural strata, calm not casino —
   no meter, no score, no numbers. DP-012 tokens only. The raw draft is un-reachable
   by construction (the-margin.js has no draft parameter); this only styles geometry. */
.mail-compose__margin { margin: var(--s-1) 0 var(--s-2); min-width: 0; }
.margin {
  display: flex; flex-direction: column; gap: 3px;
  padding: var(--s-2) var(--s-2) var(--s-2) 0;
  opacity: 0.6; transition: opacity 420ms ease;
}
.margin.is-active  { opacity: 0.82; }   /* present while the hand is moving */
.margin.is-settled { opacity: 0.46; }   /* recedes in the pauses — the settle */
.margin__stratum {
  height: 3px; border-radius: 2px; background: var(--line-moss);
  transition: width 320ms ease, background 320ms ease;
}
.margin.is-settled .margin__stratum { background: var(--line-bark); }
.margin__stratum.stratum--empty  { width: 0; background: transparent; }   /* a blank line = a quiet gap, never a lone mark */
.margin__stratum.stratum--short  { width: 14%; }
.margin__stratum.stratum--medium { width: 34%; }
.margin__stratum.stratum--long   { width: 62%; }
.margin__stratum.stratum--full   { width: 92%; }

/* ── THE WEAVE · E2 — contact autocomplete in compose (To / Cc / Bcc) ─────────
   A calm suggestion dropdown anchored under a recipient field. Each row carries the
   contact NAME (weight) over the ADDRESS (size + --ink-soft) — a Higgins-clean
   hierarchy that reads without leaning on colour. The active/hover row lifts on a
   gold-wash, not a hue swap. DP-017 tokens only; the field row that holds it opts
   into positioning via --ac (nothing else changes). */
.mail-compose__field--ac { position: relative; }
.mail-compose__ac {
  position: absolute; left: 0; right: 0; top: 100%; z-index: 40;
  margin-top: 2px; max-height: 15rem; overflow-y: auto;
  background: var(--paper); border: 1px solid var(--line-moss);
  border-radius: var(--r-m);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--ink) 22%, transparent);
}
.mail-compose__ac[hidden] { display: none; }
.mail-compose__ac-item {
  display: flex; flex-direction: column; gap: 1px;
  padding: var(--s-1) var(--s-2); cursor: pointer;
  border-bottom: 1px solid color-mix(in srgb, var(--line-moss) 50%, transparent);
}
.mail-compose__ac-item:last-child { border-bottom: 0; }
.mail-compose__ac-item.is-active,
.mail-compose__ac-item:hover { background: color-mix(in srgb, var(--sunlight) 12%, var(--paper)); }
.mail-compose__ac-name  { color: var(--ink); font-weight: 600; }
.mail-compose__ac-email { color: var(--ink-soft); font-size: 0.85em; }

/* ============================================================================
   DP-016 Trio · Track CALENDAR — the calendar-* renderer styling (S11.1341).
   The renderer (calendar-renderer.js) was structurally complete but UNSTYLED —
   it emitted .calendar-* classes with no CSS backing, so the month grid rendered
   as a bare stack of divs ("skeletal, miles from usable"). This is the fold-in
   of the loop-dashboard CalendarPanel's LOOK into the shell's own Grove language:
   month grid + category-colored chips + agenda + record + create/edit form.
   Every value is a token from tokens.css (Five Rules: zero new palette). The
   category color is data-driven: the renderer sets --cat inline per chip/row from
   the tool's event.category (getCategoryColor port); the CSS only consumes it.
   TC-1 untouched — this is paint, not event logic.
   ========================================================================== */

/* view toggle: Month / Agenda — a quiet segmented control (mail-status calm) */
.calendar-viewtoggle {
  display: inline-flex; gap: 2px; margin: var(--s-2) 0 var(--s-4);
  padding: 3px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--moss) 12%, transparent);
}
.calendar-viewtoggle__btn {
  appearance: none; border: 0; cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-small);
  letter-spacing: 0.02em; color: var(--ink-soft);
  padding: var(--s-1) var(--s-4); border-radius: var(--r-pill);
  background: transparent; transition: background 180ms var(--ease), color 180ms var(--ease);
}
.calendar-viewtoggle__btn:hover { color: var(--moss-ink); }
.calendar-viewtoggle__btn.is-on {
  background: var(--paper); color: var(--ink);
  box-shadow: 0 1px 3px color-mix(in srgb, var(--shadow) 12%, transparent);
}

/* The calendar box sits FLUSH with the content top — no blue gap above the grid.
   The band was the calendar pane's OWN TOP PADDING — .pane--live carries
   `padding: var(--s-5) var(--gutter)` (~line 503), and --s-5 (1.5rem) is the band.
   The 19.2138 fix zeroed only the pane MARGIN, so the padding band survived; this
   zeroes padding-top (the real lever). Scoped to [data-kind="calendar"] because that
   padding is SHARED across mail/contacts/etc. — keep the pane's left/right/bottom
   padding + border/radius; only the top band goes. */
.pane[data-kind="calendar"] { margin-top: 0; padding-top: 0; }
.calendar-host { margin-top: 0; }
.calendar-loading { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); padding: var(--s-4) 0; }

/* the grid header: ‹ Month Year › + actions */
/* S3 — three-zone header: [Today] ···· [‹ month·year ›] ···· [actions].
 * left + actions are equal flex tracks (1fr each) so the nav group sits DEAD-CENTER
 * of the head; align-items:center centers everything vertically. */
/* S4/A5 — the header is the top tier of the three-tone box: chrome --surface, contiguous
   with the DOW (--surface-2) and grid (--ground) below (one bordered box, zero gradient). */
.calendar-grid__head {
  display: flex; align-items: center; gap: var(--s-2);
  margin-bottom: 0;
  padding: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--line-moss); border-bottom: 0;
  border-radius: var(--r-m) var(--r-m) 0 0;
}
/* S3(b)+ — Today no longer GROWS (was flex:1 1 0, which shoved the nav to center). It is
 * fixed-width now, so the ‹ month·year › group sits immediately to its right; the actions
 * zone (flex:1 1 0) takes the remaining width and holds the far right. Operator ask 15.2325:
 * month/year + prev/next moved left, next to Today. */
.calendar-grid__left { flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--s-2); min-width: 0; }
.calendar-grid__nav {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--s-2);
  justify-content: center;
}
/* S3/A3 — month/year label (blessed Bucket-1 chrome, 15.1912 mockup).
 * Fraunces (--face-canopy) 600; optical (not geometric) vertical centering — Fraunces
 * sets cap-height high in the em-box, so naive centering reads a hair LOW: nudge -1.5px.
 * The label is centered within its own bracket (the ‹ … › group), which now sits LEFT beside
 * Today rather than in the head's center. min-width keeps the arrows from jumping as the month
 * name changes length; tightened 178->152 (still holds the longest, "September 2026") and
 * nowrap guards a long label against wrapping. Year drops to --ink-soft 500. */
.calendar-grid__title {
  font-family: var(--face-canopy); font-weight: 600;
  font-size: 1.35rem; letter-spacing: 0.01em;
  color: var(--ink); min-width: 152px; text-align: center; white-space: nowrap;
  transform: translateY(-1.5px);
}
.calendar-grid__title .calendar-grid__yr { color: var(--ink-soft); font-weight: 500; }
.calendar-nav {
  appearance: none; cursor: pointer;
  width: 2rem; height: 2rem; border-radius: var(--r-pill);
  border: 1px solid var(--line-moss); background: var(--paper);
  color: var(--ink-soft); font-size: 1.1rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color 160ms var(--ease), color 160ms var(--ease);
}
.calendar-nav:hover { border-color: var(--line-gold); color: var(--ink); }

.calendar-grid__actions { flex: 1 1 0; display: inline-flex; gap: var(--s-2); justify-content: flex-end; flex-wrap: wrap; min-width: 0; }
.calendar-action {
  appearance: none; cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-small); letter-spacing: 0.01em;
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
  text-decoration: none; line-height: 1.6;
  transition: border-color 160ms var(--ease), background 160ms var(--ease);
}
.calendar-action:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.calendar-action--deferred { opacity: 0.5; cursor: not-allowed; border-style: dashed; }
.calendar-action--danger { color: color-mix(in srgb, var(--bark) 70%, var(--shadow)); border-color: color-mix(in srgb, var(--bark) 30%, transparent); }
.calendar-action--danger:hover { border-color: var(--bark); background: color-mix(in srgb, var(--bark) 8%, var(--paper)); }

/* S3/A4 — Today: a quiet OUTLINED pill (blessed Bucket-1 chrome, 15.1912 mockup).
 * NOT accent-filled — accent stays reserved for +New (the create act). --mono at label
 * size, --line-strong hairline, transparent fill, --ink-soft. Fitts: whitespace-split
 * from the chevron triad makes it a stable home target. Hover lifts border to --gold and
 * ink to full (background stays transparent — override the base .calendar-action wash).
 * A4 the-one-gold-tell: Today lifts to a --gold hairline ONLY when the shown month is not
 * the current month (a quiet "you've navigated away" signal); reserved-gold otherwise. */
.calendar-action--today {
  font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: 0.04em;
  color: var(--ink-soft); background: transparent;
  border: 1px solid var(--line-strong); border-radius: var(--r-pill);
  padding: 7px 15px; line-height: 1;
}
.calendar-action--today:hover { color: var(--ink); border-color: var(--gold); background: transparent; }
.calendar-action--today.calendar-action--today-off { border-color: var(--line-gold); color: var(--ink); }
.calendar-import-file { display: none; }

/* DP-021 C — the rail's calendar-scoped action group ("Keep": the acts that are about
   the calendar as a WHOLE, not about where you are standing in it). It sits last in the
   column, under My calendars, and it is the reason week/day/agenda have iCal at all: the
   controls moved OUT of the view head, so every view gets them for the same reason
   .rail__compose already reaches every view — it lives in the rail, which no view clears.
   Zero new hue and zero new token: .calendar-action already carries the whole visual, and
   this only stacks them to the column's width. (DP-017 is still open; this does not
   pre-empt the skin.) */
.calendar-railactions { display: flex; flex-direction: column; gap: var(--s-1); }
.calendar-railactions .calendar-action { width: 100%; text-align: center; }

/* the month grid proper */
.calendar-grid {
  border: 1px solid var(--line-moss); border-top: 0;
  border-radius: 0 0 var(--r-m) var(--r-m); overflow: hidden; background: var(--ground);
  /* DP-021 A1 — THE DECLARED FRAME. A cell's height is an INPUT to the render, never
   * an output of it. calendar-renderer.js's GEOM is the single source of truth and
   * writes these onto .calendar-grid__body inline at every paint; the values below are
   * the DECLARED FALLBACK and MUST equal GEOM (calendar-renderer.test.js asserts it).
   * Tokens only — zero new hue (F4; DP-017 is open). */
  --cal-row-h:  7rem;      /* the week-row / cell height */
  --cal-num-h:  19px;      /* the day-number row */
  --cal-chip-h: 18px;      /* one chip, and the "+N more" line */
  --cal-head-h: 2.125rem;  /* the weekday header row */
}
.calendar-grid__weekdays { display: grid; grid-template-columns: repeat(7, 1fr); background: var(--surface-2); }
.calendar-grid__weekday {
  height: var(--cal-head-h); display: flex; align-items: center; justify-content: center;
  padding: 0; text-align: center;
  font-family: var(--face-survey); font-size: var(--t-label);
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-faint);
  border-bottom: 1px solid var(--line-moss);
}
/* S4/A5 — weekend rhythm: Sat/Sun read a step warmer in ink + a deep column wash (pre-attentive) */
.calendar-grid__weekday.is-weekend { color: var(--ink-soft); background: var(--surface-deep); }
.calendar-grid__body { display: flex; flex-direction: column; }
.calendar-grid__week { display: grid; grid-template-columns: repeat(7, 1fr); }
.calendar-grid__cell {
  /* DP-021 A1 — FIXED, not min-height. Inside a `grid` week row a min-height made every
   * cell match the tallest, so the grid was ragged and the frame yielded to the content.
   * height + overflow:hidden is the clip-intentional policy; `k` (chips painted) is
   * DERIVED from these same numbers in JS, so nothing is ever silently cut. */
  height: var(--cal-row-h); overflow: hidden;
  padding: var(--s-1) var(--s-1) var(--s-2);
  border-right: 1px solid color-mix(in srgb, var(--line-moss) 60%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--line-moss) 60%, transparent);
  display: flex; flex-direction: column; gap: 2px;
}
.calendar-grid__week > .calendar-grid__cell:last-child { border-right: 0; }
/* S4/A5 — weekend body cells continue the DOW column wash (an in-month weekend stays lit-but-cooler) */
.calendar-grid__cell.is-weekend:not(.calendar-grid__cell--adjacent) { background: color-mix(in srgb, var(--surface-deep) 55%, transparent); }
/* DP-021 A2 — an adjacent-month day. It is DIMMED, not DEAD: the day number renders and
 * so do its events (Google paints them; the old grid could not, because the query window
 * was cut from the month while the geometry was cut from the grid). The class was named
 * --blank, which is what it used to mean and is now a lie. */
.calendar-grid__cell--adjacent { background: var(--surface-deep); }
.calendar-grid__cell--adjacent .calendar-day__num { color: color-mix(in srgb, var(--ink-soft) 55%, transparent); }
.calendar-grid__cell--adjacent .calendar-chip { opacity: 0.6; }
.calendar-grid__cell.is-today { background: color-mix(in srgb, var(--sunlight) 10%, var(--paper)); }
.calendar-day__num {
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--ink-soft);
  align-self: flex-start; line-height: 1.2; padding: 1px 3px;
  height: var(--cal-num-h); flex: 0 0 auto;          /* declared, not measured (A1) */
}
.calendar-grid__cell.is-today .calendar-day__num {
  color: var(--paper); background: var(--sunlight); border-radius: var(--r-pill);
  min-width: 1.4rem; text-align: center; font-weight: 500;
}
.calendar-day__chips { display: flex; flex-direction: column; gap: 2px; min-height: 0; overflow: hidden; }
/* S4/A6 — parallax the clipping: the last chip above a "+N more" soft-fades into --ground */
.calendar-chip:has(+ .calendar-day__more) { position: relative; }
.calendar-chip:has(+ .calendar-day__more)::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 8px;
  background: linear-gradient(to bottom, transparent, var(--ground)); pointer-events: none;
}

/* the event chip — category color rides the left edge via --cat (data-driven) */
.calendar-chip {
  appearance: none; cursor: grab; text-align: left;
  display: flex; align-items: baseline; gap: var(--s-1);
  padding: 1px var(--s-1) 1px var(--s-2);
  border: 0; border-radius: var(--r-s);
  border-left: 3px solid var(--cat, var(--sunlight));
  background: color-mix(in srgb, var(--cat, var(--accent)) 15%, var(--surface));
  font-size: 0.75rem; line-height: 1.3; color: var(--ink);
  height: var(--cal-chip-h); flex: 0 0 auto;         /* one declared chip-row (A1) */
  overflow: hidden; transition: background 140ms var(--ease);
}
.calendar-chip:hover { background: color-mix(in srgb, var(--cat, var(--accent)) 24%, var(--surface)); }
.calendar-chip.is-allday { border-left-style: double; border-left-width: 4px; }
.calendar-chip__time { font-family: var(--face-survey); font-size: 0.65rem; color: var(--ink-faint); flex: 0 0 auto; }
/* DP-021 A1 — min-width:0 is the whole fix. A flex item's default min-width:auto refuses
 * to shrink below its content, so `text-overflow: ellipsis` below could NEVER fire: the
 * title ran long and the chip's overflow:hidden guillotined it mid-word, no ellipsis. */
.calendar-chip__title { min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── The manipulation pass (grab · carry · place) ──────────────────────────────
 * Direct-manipulation feel for drag-to-reschedule. The renderer toggles these
 * classes across all three drag surfaces (month grid, agenda, week/day time-grid)
 * through shared helpers; the CSS carries the look. GRAB = source dims + cursor;
 * CARRY = the follow-ghost is the thing in your hand + the drop target highlights;
 * PLACE = the settle (a short FLIP slide, JS-driven, honoured only when motion is on). */

/* GRAB — the month grid emitted `is-dragging` on its body but had no cursor rule
 * (the agenda one at .calendar-agenda__body.is-dragging was the only one). Mirror it. */
.calendar-grid__body.is-dragging { cursor: grabbing; user-select: none; }
.calendar-grid__body.is-dragging .calendar-chip { cursor: grabbing; }
.calendar-agenda__open:active,
.calendar-chip:active,
.calendar-tevent:active { cursor: grabbing; }

/* GRAB — the source, once picked up, dims in place so it reads as "lifted out"
 * (the ghost below is the copy now in your hand). Atlassian's source-dim pattern. */
.calendar-chip.is-grabbed,
.calendar-agenda__open.is-grabbed,
.calendar-tevent.is-grabbed { opacity: 0.4; }

/* CARRY — the follow-ghost: a translucent, lifted clone that tracks the cursor.
 * position:fixed so a viewport-space translate() (client coords) lands true; the
 * lift (scale + tilt) is baked into the JS transform, so only the chrome is here. */
.calendar-drag-ghost {
  position: fixed; top: 0; left: 0; z-index: 60;
  pointer-events: none; opacity: 0.9;
  box-shadow: 0 6px 18px color-mix(in srgb, var(--bark, #3a2f20) 35%, transparent);
  will-change: transform;
}

/* CARRY — the cell/day/column under the cursor answers "yes, I'll take that." */
.calendar-grid__cell.is-drop-target,
.calendar-agenda__day.is-drop-target,
.calendar-timegrid__col.is-drop-target {
  background: color-mix(in srgb, var(--sunlight, #e8b34a) 22%, var(--paper));
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--sunlight, #e8b34a) 60%, transparent);
}

/* HOME — the day the grabbed event was picked up from. Cool (--accent) against the
 * warm gold drop-target above, so "where it came from" reads distinctly from "move it
 * here." Kept quiet while you're hovering elsewhere: a faint wash + a thin ring. */
.calendar-grid__cell.is-drag-source {
  background: color-mix(in srgb, var(--accent, #3B9EFF) 9%, var(--paper));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3B9EFF) 32%, transparent);
}
/* HOME, held over — the grabbed event is back on its origin day. Two classes beat the
 * one-class drop-target above (specificity), so the cool wash WINS over the gold: this
 * is the "drop here = put it back, no change" signal (rescheduleTo no-ops same-day). */
.calendar-grid__cell.is-drag-source.is-drop-target {
  background: color-mix(in srgb, var(--accent, #3B9EFF) 20%, var(--paper));
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent, #3B9EFF) 62%, transparent);
}

/* PLACE — the settling chip rides above its neighbours while the FLIP slide plays.
 * The transform + transition are set inline by the renderer (and skipped entirely
 * under prefers-reduced-motion); this only lifts it out of the flow of clipping. */
.calendar-chip.is-settling,
.calendar-tevent.is-settling,
.calendar-agenda__open.is-settling { z-index: 4; will-change: transform; }

/* ARIA — the polite live region the keyboard-grab / reschedule path announces into
 * ("Grabbed…", "Moved to Thursday the 14th"). Visually hidden, present to AT. */
.calendar-live {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); border: 0; white-space: nowrap;
}
.calendar-day__more { font-family: var(--face-survey); font-size: 0.65rem; color: var(--ink-faint); padding-left: var(--s-2);
  height: var(--cal-chip-h); line-height: var(--cal-chip-h); flex: 0 0 auto; }

/* DP-021 A3 — THE SPAN BAR. A multi-day event is ONE bar laid across its days, and a bar
 * crossing a week boundary is TWO bars, ONE event (Google's model, and the only one a
 * 7-column grid can express).
 *
 * THE BARS CANNOT LIVE INSIDE THE CELLS. A cell has horizontal padding and a right border,
 * so a per-cell bar segment would read as a row of separated blocks, not one bar. They live
 * in a per-week OVERLAY whose seven tracks align with the seven cells by construction (the
 * same repeat(7, 1fr) over the same box), and each cell reserves the vertical space with
 * .calendar-day__spanpad — whose height is the SAME L*chipH + (L-1)*gap arithmetic that
 * chipRowsAvailable() subtracts in JS. So the chips below a bar always begin where the bar
 * ends, and the frame is still declared, never measured.
 * Tokens only — ZERO new hue (F4; DP-017 is open). */
.calendar-grid__week { position: relative; }
.calendar-day__spanpad { flex: 0 0 auto; }   /* height is DECLARED inline, from GEOM */
.calendar-week__spans {
  position: absolute; left: 0; right: 0;
  /* the bars start below the day-number row — the same three numbers the cell itself uses */
  top: calc(var(--s-1) + var(--cal-num-h) + 2px);
  display: grid; grid-template-columns: repeat(7, 1fr);
  grid-auto-rows: var(--cal-chip-h); row-gap: 2px;
  pointer-events: none;                      /* the layer is glass; only the bars take a click */
}
.calendar-span {
  pointer-events: auto; appearance: none; cursor: pointer; text-align: left;
  display: flex; align-items: center; gap: var(--s-1);
  min-width: 0; overflow: hidden;
  padding: 0 var(--s-1);
  border: 0; border-radius: 0;               /* SQUARE by default — see --start / --end below */
  background: color-mix(in srgb, var(--gold) 20%, var(--surface));
  /* height stays the declared 18px frame; line-height drops from a flush 18px line-box to
   * 1.3 (matching .calendar-chip, which never clipped) so a tall glyph-top is no longer
   * guillotined by overflow:hidden. Flex align-items:center still does the vertical centering. */
  height: var(--cal-chip-h); line-height: 1.3;
  font-size: 0.75rem; color: var(--gold);
  transition: background 140ms var(--ease);
}
.calendar-span:hover { background: color-mix(in srgb, var(--gold) 30%, var(--surface)); }
/* The TRUE start and the TRUE end are rounded and inset; a CONTINUATION is square and runs
 * flush to the edge of the row. That square edge is the entire signal that the event carries
 * on off the end of the week — it is the load-bearing half of "two bars, one event". */
.calendar-span--start {
  border-top-left-radius: var(--r-s); border-bottom-left-radius: var(--r-s);
  border-left: 3px solid var(--gold); margin-left: 2px;
}
.calendar-span--end {
  border-top-right-radius: var(--r-s); border-bottom-right-radius: var(--r-s); margin-right: 2px;
}
.calendar-span.is-allday.calendar-span--start { border-left-style: double; border-left-width: 4px; }
.calendar-span__time { font-family: var(--face-survey); font-size: 0.65rem; color: color-mix(in srgb, var(--gold) 80%, var(--ink)); flex: 0 0 auto; }
/* min-width:0 — the A1 lesson, and it applies here for exactly the same reason. A flex item
 * will not shrink below its content without it, so `ellipsis` never fires and the title is
 * guillotined mid-word instead of ellipsed. Watch for this anywhere ellipsis sits in a flex row. */
.calendar-span__title { min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* the agenda (1-D forward) */
.calendar-agenda__head { font-family: var(--face-canopy); font-size: var(--t-h); color: var(--ink); margin-bottom: var(--s-3); }
.calendar-agenda__day { margin-bottom: var(--s-4); }
.calendar-agenda__daylabel {
  font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: var(--tracking-caps);
  text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-2);
  padding-bottom: var(--s-1); border-bottom: 1px solid var(--line-moss);
}
.calendar-agenda__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.calendar-agenda__open {
  appearance: none; cursor: grab; width: 100%; text-align: left;
  display: flex; align-items: baseline; gap: var(--s-3);
  padding: var(--s-2) var(--s-2) var(--s-2) var(--s-3);
  border: 0; border-left: 3px solid var(--cat, var(--line-moss)); border-radius: var(--r-s);
  background: transparent; color: var(--ink); font-size: var(--t-body);
  transition: background 140ms var(--ease);
}
.calendar-agenda__open:hover { background: color-mix(in srgb, var(--cat, var(--moss)) 10%, var(--paper)); }
/* dragging a row to reschedule: grabbing cursor across the list + no text-selection */
.calendar-agenda__body.is-dragging { cursor: grabbing; user-select: none; }
.calendar-agenda__body.is-dragging .calendar-agenda__open { cursor: grabbing; }
.calendar-agenda__time { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); flex: 0 0 3.5rem; }
.calendar-agenda__title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* the event record + edit form */
.calendar-record__back, .calendar-newform + * .calendar-record__back {
  appearance: none; cursor: pointer; border: 0; background: transparent;
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink);
  padding: var(--s-1) 0; margin-bottom: var(--s-3);
}
.calendar-record__back:hover { color: var(--ink); }
.calendar-record__head { display: flex; align-items: baseline; gap: var(--s-3); flex-wrap: wrap; margin-bottom: var(--s-4); }
.calendar-record__title { font-family: var(--face-canopy); font-size: var(--t-title); color: var(--ink); margin: 0; }
.calendar-record__repeats {
  font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--moss-ink); padding: 2px var(--s-2); border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--moss) 16%, transparent);
}
.calendar-record__fields { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-4); }
.calendar-field { display: flex; gap: var(--s-3); align-items: baseline; }
.calendar-field__label {
  flex: 0 0 5rem; font-family: var(--face-survey); font-size: var(--t-label);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--moss-ink);
}
.calendar-field__value { color: var(--ink); }
.calendar-record__notes { margin-bottom: var(--s-4); }
.calendar-record__notes-label { font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: 0.04em; text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-1); }
.calendar-record__notes-body { color: var(--ink); line-height: var(--leading-body); margin: 0; white-space: pre-wrap; }
.calendar-record__guests { margin-bottom: var(--s-4); }
.calendar-record__guests-label { font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: 0.04em; text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-1); }
.calendar-record__guests-empty { font-family: var(--face-survey); font-size: var(--t-body); color: var(--moss-ink); margin: 0; }
.calendar-record__guest-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-1); }
.calendar-record__guest { display: flex; align-items: baseline; gap: var(--s-2); }
.calendar-record__guest-name { font-family: var(--face-survey); font-size: var(--t-body); color: var(--ink); }
.calendar-record__guest-meta { font-family: var(--face-survey); font-size: var(--t-label); color: var(--moss-ink); }
/* P6 — un-invite. Trailing edge of the row, and DELIBERATELY QUIET: it is a destructive
   control sitting on every single guest, so it must not read as a call to action. It is
   moss until you are on the row, and only then does it take the danger colour — the row
   is the hover surface, not the glyph, because a 1-em × is a cruel hit target. It reuses
   the shared .is-saving / .is-saved / .is-unsaved write-flash, so a failed un-invite marks
   THE ROW'S OWN control rather than a banner somewhere else on the pane. */
.calendar-record__guest-remove {
  margin-left: auto; flex: 0 0 auto;
  appearance: none; background: none; border: 0; cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-body); line-height: 1;
  color: transparent; padding: 0 var(--s-1); border-radius: var(--r-s);
  transition: color 120ms ease, background 120ms ease;
}
.calendar-record__guest:hover .calendar-record__guest-remove,
.calendar-record__guest-remove:focus-visible { color: var(--moss-ink); }
.calendar-record__guest-remove:hover { color: var(--w-warn, var(--bark)); background: color-mix(in srgb, var(--bark) 8%, transparent); }
.calendar-record__guest-remove:focus-visible { outline: 2px solid var(--figure); outline-offset: 1px; }
.calendar-record__guest-remove[disabled] { cursor: default; }
.calendar-record__actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* E4 — the guest picker for "Invite a contact". The panel opens below the actions
   strip; the contact-autocomplete dropdown mirrors the compose autocomplete's LOOK
   (name-over-email hierarchy, tokens only) but is pick-one, not recipient-list. */
.calendar-invite { position: relative; margin-top: var(--s-2); max-width: 22rem; }
.calendar-invite__input { width: 100%; }
.contact-autocomplete { position: relative; }
.contact-autocomplete__menu {
  position: absolute; left: 0; right: 0; top: 100%; z-index: 40;
  margin-top: 2px; max-height: 15rem; overflow-y: auto;
  background: var(--paper); border: 1px solid var(--line-moss);
  border-radius: var(--r-m);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--ink) 22%, transparent);
}
.contact-autocomplete__menu[hidden] { display: none; }
.contact-autocomplete__item {
  display: flex; flex-direction: column; gap: 1px;
  padding: var(--s-1) var(--s-2); cursor: pointer;
  border-bottom: 1px solid color-mix(in srgb, var(--line-moss) 50%, transparent);
}
.contact-autocomplete__item:last-child { border-bottom: 0; }
.contact-autocomplete__item.is-active,
.contact-autocomplete__item:hover { background: color-mix(in srgb, var(--sunlight) 12%, var(--paper)); }
.contact-autocomplete__name  { font-family: var(--face-survey); font-size: var(--t-body); color: var(--ink); font-weight: 600; }
.contact-autocomplete__email { font-family: var(--face-survey); font-size: var(--t-label); color: var(--moss-ink); }

/* the create/edit form (Item 2, S08.0127 — the design pass) --------------------
   Compose-in-pane, built from the shared Block Alphabet `field` row (block.css:
   .field / .field__label / .field__control), the same language mail's composeView
   wears. The overlay (block.css .calendar-form-overlay) carries the floating card
   chrome; the form flows the labeled rows inside it, everything in its place and form. */
.calendar-newform { display: flex; flex-direction: column; }
/* the controls fill their row and wear the shared field__control skin (paper, bark
   hairline, focus ring) — the calendar-local rule only sizes them to the row. */
.calendar-newform .field__control { width: 100%; box-sizing: border-box; }
.calendar-newform select.field__control { cursor: pointer; }
.calendar-newform textarea.field__control { resize: vertical; min-height: 4.5rem; line-height: var(--leading-body); }
/* Title leads — quiet prominence, not a shout: a hair larger, the canopy voice. */
.calendar-newform__title.field__control { font-family: var(--face-canopy); font-size: var(--t-body-lg, var(--t-body)); }
/* When — the datetime input and its "all day" qualifier are one decision, one field. */
.calendar-newform__field--when .calendar-newform__start { margin-bottom: var(--s-2); }
.calendar-newform__allday-row { display: flex; align-items: center; gap: var(--s-1); }
.calendar-newform__allday { accent-color: var(--gold, var(--line-gold)); cursor: pointer; }
.calendar-newform__allday-wrap { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); cursor: pointer; }
/* the last field carries no trailing margin — the actions row provides the gap + rule. */
.calendar-newform > .field:last-of-type { margin-bottom: var(--s-2); }
/* the save command row wears the shared record__actions band (top rule); the failure
   notice trails Save as a quiet, honest status (red only when it has bad news). */
.calendar-newform__actions .calendar-newform__err {
  margin: 0 0 0 auto; align-self: center;
  color: color-mix(in srgb, var(--bark) 76%, var(--shadow)); font-size: var(--t-small);
}
.calendar-newform__actions .calendar-newform__err:empty { display: none; }

/* the honest states (F3 read axis) + write-axis feedback */
.calendar-unreachable { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2); padding: var(--s-5) 0; }
.calendar-unreachable__msg { color: var(--badge-unreachable-ink); font-family: var(--face-survey); font-size: var(--t-small); margin: 0; }
.calendar-empty { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); padding: var(--s-4); text-align: center; }
.is-saving { opacity: 0.6; }
.is-saved { box-shadow: inset 0 0 0 2px var(--w-ok); }
.is-unsaved { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--bark) 50%, var(--sunlight)); }
/* transient honest-failure notice for a rolled-back drag reschedule (optimistic move that the box refused) */
.calendar-reschedule-fail {
  position: fixed; left: 50%; bottom: var(--s-4); transform: translateX(-50%);
  z-index: 60; max-width: 90vw;
  background: var(--surface); color: var(--ink);
  border-left: 3px solid color-mix(in srgb, var(--bark) 50%, var(--sunlight));
  border-radius: var(--r-m); box-shadow: var(--shadow-card);
  font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-2) var(--s-3);
}

/* ---- WEEK / DAY time-grid (C2, S11.1449 — view geometry only, TC-1) --------- */
.calendar-timegrid { border: 1px solid var(--line-moss); border-radius: var(--r-m); overflow: hidden; background: var(--paper); margin-top: var(--s-2); }
.calendar-timegrid__colhead { display: flex; background: color-mix(in srgb, var(--moss) 10%, var(--paper)); border-bottom: 1px solid var(--line-moss); }
.calendar-timegrid__gutter-corner { flex: 0 0 3.2rem; }
.calendar-timegrid__daylabel {
  flex: 1 1 0; text-align: center; padding: var(--s-2) 0;
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink);
  border-left: 1px solid var(--line-moss);
}
.calendar-timegrid__daylabel.is-today { color: var(--ink); background: color-mix(in srgb, var(--sunlight) 12%, var(--paper)); font-weight: 600; }
.calendar-timegrid__dayname { display: block; }

/* THE FULLNESS READ (P8, DP-020 Slice 2) — one number, in the day header.
   It is a COUNT and deliberately NOT a heat ramp. Cleveland-McGill put position and
   length at the top of the decoding-accuracy hierarchy and colour saturation/density
   at the BOTTOM — so a red-as-it-fills column would be the least accurate channel
   available. It would also be an OPINION ABOUT YOUR WEEK, and the app does not have
   one. The honest encoding and the opinion-free encoding are the same encoding.

   No red. No ramp. No scale. NO --alarm, NO --danger, and none exists to reach for.
   It wears the shared .badge alphabet in the calm register and it says a true thing
   quietly. An empty text node collapses it — a day with nothing overlapping renders
   NOTHING, because it has no claim to make (absence is a shipped state). */
.calendar-fullness {
  margin-top: 2px;
  font-size: var(--t-label);
  color: var(--moss-ink);
  background: color-mix(in srgb, var(--sage) 45%, transparent);
  border-color: var(--line-moss);
  padding: 0 var(--s-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;   /* the 200px column is fixed; the badge never pushes it */
  max-width: 100%;
}
.calendar-fullness:empty { display: none; }
.calendar-timegrid__allday { display: flex; flex-wrap: wrap; gap: 2px; min-height: 0; padding: 2px; border-bottom: 1px solid var(--line-moss); }
.calendar-timegrid__allday:empty { display: none; }
/* seq=526 — the week/day time-grid FILLS a tall pane instead of stubbing at a fixed
   26rem. A viewport-relative cap, calendar-scoped: it touches ONLY this class, so the
   SHARED #forest-pane / .app-content scroll model (mail + contacts depend on it) and the
   month/year/agenda views (which never use .calendar-timegrid__scroll) are untouched. The
   min-height floor keeps a usable grid on short screens. Proven by verify-calendar-timegrid-fill.cjs
   (Tier-1 render proof: fills near the viewport bottom, owns its own scroll, zero regression to the
   pane box or agenda). A true single-scroll flex-fill needs the shared pane/#forest-pane height
   bounded — that is the deploy-gated follow-on, not this safe render-half. APP bump + deploy owed
   (deploy HELD — Loam Slice 1 live). */
.calendar-timegrid__scroll { display: flex; max-height: calc(100vh - 14rem); min-height: 18rem; overflow-y: auto; position: relative; }
.calendar-timegrid__gutter { flex: 0 0 3.2rem; }
.calendar-timegrid__hour { position: relative; border-top: 1px solid color-mix(in srgb, var(--line-moss) 55%, transparent); }
.calendar-timegrid__hourlabel {
  position: absolute; top: -0.55em; right: var(--s-2);
  font-family: var(--face-survey); font-size: 0.6rem; color: var(--moss-ink);
}
.calendar-timegrid__cols { flex: 1 1 auto; display: flex; }
.calendar-timegrid__col {
  flex: 1 1 0; position: relative; border-left: 1px solid var(--line-moss);
  background-image: repeating-linear-gradient(to bottom, transparent, transparent calc(54px - 1px), color-mix(in srgb, var(--line-moss) 45%, transparent) 54px);
  cursor: pointer;
}
.calendar-timegrid__col.is-today { background-color: color-mix(in srgb, var(--sunlight) 6%, var(--paper)); }
.calendar-timegrid--day .calendar-timegrid__col { min-width: 0; }
.calendar-tevent {
  position: absolute; left: 0; right: 0; overflow: hidden; text-align: left;
  border: 1px solid color-mix(in srgb, var(--cat, var(--moss)) 45%, var(--line-moss));
  border-left: 3px solid var(--cat, var(--moss));
  border-radius: var(--r-s, 4px); background: color-mix(in srgb, var(--cat, var(--moss)) 12%, var(--paper));
  padding: 1px 4px; cursor: grab; font-size: 0.68rem; line-height: 1.25;
  transition: background 140ms var(--ease), box-shadow 140ms var(--ease);
}
.calendar-tevent:hover { background: color-mix(in srgb, var(--cat, var(--moss)) 20%, var(--paper)); box-shadow: 0 1px 4px color-mix(in srgb, var(--shadow) 18%, transparent); z-index: 2; }
/* while a block is being dragged to reschedule, the whole grid shows the grabbing cursor and
 * text-selection is suppressed so a drag never paints a selection across the columns. */
.calendar-timegrid.is-dragging { cursor: grabbing; user-select: none; }
.calendar-timegrid.is-dragging .calendar-tevent { cursor: grabbing; }
.calendar-tevent.is-allday { border-left-style: double; border-left-width: 4px; }
.calendar-tevent__time { font-family: var(--face-survey); font-size: 0.58rem; color: var(--moss-ink); display: block; }
.calendar-tevent__title { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
/* A too-short block (a 15-min event is 14px) shows title-only (the renderer drops
 * the time, seq=356). Tighten line-height + trim vertical padding so the single
 * title line clears the ~12px inner box instead of shaving ascenders under the clip. */
.calendar-tevent.is-compact { padding-top: 0; padding-bottom: 0; }
.calendar-tevent.is-compact .calendar-tevent__title { line-height: 1.1; }
.calendar-timegrid__now { position: absolute; left: 0; right: 0; height: 0; border-top: 2px solid color-mix(in srgb, var(--bark) 70%, var(--sunlight)); z-index: 3; pointer-events: none; }
.calendar-timegrid__now::before { content: ""; position: absolute; left: -3px; top: -3px; width: 6px; height: 6px; border-radius: 50%; background: color-mix(in srgb, var(--bark) 70%, var(--sunlight)); }

/* ---- the event popover (C3, S11.1449 — quick actions off a time-grid block) - */
.calendar-popover {
  position: fixed; z-index: 40; width: min(20rem, 90vw);
  background: var(--paper); border: 1px solid var(--line-moss);
  border-left: 3px solid var(--cat, var(--line-gold)); border-radius: var(--r-m);
  box-shadow: 0 6px 20px color-mix(in srgb, var(--shadow) 22%, transparent);
  padding: var(--s-3); display: flex; flex-direction: column; gap: var(--s-1);
}
.calendar-popover__head { display: flex; align-items: baseline; gap: var(--s-2); }
.calendar-popover__title { flex: 1 1 auto; margin: 0; font-family: var(--face-canopy); font-size: var(--t-h); color: var(--ink); }
.calendar-popover__close {
  flex: 0 0 auto; border: 0; background: transparent; cursor: pointer;
  font-size: 1.1rem; line-height: 1; color: var(--moss-ink); padding: 0 2px;
}
.calendar-popover__close:hover { color: var(--ink); }
.calendar-popover__when, .calendar-popover__where, .calendar-popover__cat { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); }
.calendar-popover__repeats {
  align-self: flex-start; margin-top: 2px;
  font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--moss-ink); border: 1px solid var(--line-moss); border-radius: 999px; padding: 1px var(--s-2);
}
.calendar-popover__actions { display: flex; gap: var(--s-2); margin-top: var(--s-2); flex-wrap: wrap; }

/* The rail-inline recolor picker (color seam step 2): the dot IS the trigger, the
   popover is 12 Grove swatches + a Default revert. Same paper/line/shadow language
   as the event popover; zero new palette (the swatches ARE the Grove palette). */
.rail__slot-dot {
  cursor: pointer; padding: 0; border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  border-radius: 999px;
}
.rail__slot-dot:hover { border-color: color-mix(in srgb, var(--ink) 40%, transparent); }
.rail__slot-dot:focus-visible { outline: 2px solid var(--line-gold); outline-offset: 1px; }

/* verb 2 (create): the + on the "My calendars" head mints a new calendar-type in the
   rail (no modal, no Settings room). Scoped to the calendars group via its data-attr,
   so mail/contacts group-labels are untouched; zero new palette (moss-ink + the rail's
   own line-gold focus). The blank create input reuses .rail__slot-rename. */
.rail__group[data-rail-group="calendars"] .rail__group-label,
.rail__group[data-rail-group="contact-groups"] .rail__group-label { display: flex; align-items: center; gap: var(--s-2); }
.rail__group-add {
  margin-left: auto; flex: 0 0 auto; cursor: pointer;
  width: 1.25rem; height: 1.25rem; padding: 0; line-height: 1;
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent); border-radius: 999px;
  background: transparent; color: var(--moss-ink); font-size: 1rem;
}
.rail__group-add:hover { border-color: color-mix(in srgb, var(--ink) 40%, transparent); color: var(--ink); }
.rail__group-add:focus-visible { outline: 2px solid var(--line-gold); outline-offset: 1px; }
.rail__slot--creating .rail__slot-rename { width: 100%; }
.calendar-recolor {
  position: fixed; z-index: 41; width: max-content; max-width: 90vw;
  background: var(--paper); border: 1px solid var(--line-moss); border-radius: var(--r-m);
  box-shadow: 0 6px 20px color-mix(in srgb, var(--shadow) 22%, transparent);
  padding: var(--s-2); display: flex; flex-direction: column; gap: var(--s-2);
}
.calendar-recolor__grid {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--s-1);
}
.calendar-recolor__swatch {
  width: 1.25rem; height: 1.25rem; padding: 0; cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent); border-radius: var(--r-s);
}
.calendar-recolor__swatch:hover { border-color: color-mix(in srgb, var(--ink) 45%, transparent); }
.calendar-recolor__swatch:focus-visible { outline: 2px solid var(--line-gold); outline-offset: 1px; }
.calendar-recolor__swatch.is-current { box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--ink); }
.calendar-recolor__default {
  border: 1px solid var(--line-moss); background: transparent; cursor: pointer; border-radius: var(--r-s);
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink);
  padding: var(--s-1) var(--s-2);
}
.calendar-recolor__default:hover { color: var(--ink); border-color: color-mix(in srgb, var(--ink) 40%, transparent); }
.calendar-recolor__default:focus-visible { outline: 2px solid var(--line-gold); outline-offset: 1px; }

/* ----------------------------------------------------------------------------
   DP-021 · B1 — THE MINI CALENDAR (the rail block). Operator pick C: Google's
   arrows, on his screenshot's geometry — month-year left, ‹ › right, six rows,
   adjacent months greyed, today ringed solid.

   ZERO NEW HUE. Every colour here is an existing token: the today ring is
   --accent (the shell's one accent, whatever DP-017 resolves it to), the
   selected ring is a --sunlight wash, adjacent days are --ink-faint. When the
   skin call lands, this block moves with the rest of the shell and needs no edit.

   THE TWO RINGS ARE TWO DIFFERENT FACTS and are drawn as two different marks:
   TODAY is a filled disc (it is a fact about the world). SELECTED is an outline
   (it is a fact about you). They are never the same ink, because a mini-cal that
   drew them alike would be unable to say "you are looking at a day that is not
   today" — which is most days.
   ------------------------------------------------------------------------- */
.calendar-mini {
  padding: var(--s-2) var(--s-2) var(--s-1);
  border-top: 1px solid var(--line);
  margin-top: var(--s-2);
}
.calendar-mini__head {
  display: flex; align-items: center; gap: var(--s-1);
  margin-bottom: var(--s-2);
}
.calendar-mini__title {
  flex: 1 1 auto;
  font-family: var(--face-canopy);
  font-size: var(--t-body);
  color: var(--ink);
  white-space: nowrap;
}
.calendar-mini__nav {
  flex: 0 0 auto;
  width: 1.5rem; height: 1.5rem;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-s);
  background: transparent; cursor: pointer;
  font-size: 1rem; line-height: 1; color: var(--moss-ink);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.calendar-mini__nav:hover, .calendar-mini__nav:focus-visible {
  background: color-mix(in srgb, var(--sunlight) 8%, transparent);
  color: var(--ink);
}
.calendar-mini__weekdays,
.calendar-mini__body {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
}
.calendar-mini__weekday {
  text-align: center;
  font-family: var(--face-survey);
  font-size: var(--t-label);
  color: var(--ink-soft);
  padding-bottom: 2px;
}
.calendar-mini__day {
  aspect-ratio: 1 / 1;
  display: flex; align-items: center; justify-content: center;
  border: 0; padding: 0;
  background: transparent; cursor: pointer;
  border-radius: 999px;                      /* his shot: a disc, not a square */
  font-family: var(--face-survey);
  font-size: var(--t-small);
  color: var(--ink);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.calendar-mini__day:hover,
.calendar-mini__day:focus-visible {
  background: color-mix(in srgb, var(--sunlight) 10%, transparent);
}
/* An adjacent-month day is REAL and CLICKABLE (it navigates to that month) — it is
   merely quieter. Greyed, never disabled: the leading 28-30 and trailing 1-8 are the
   fastest way to step a month, and a mini-cal that made them dead would waste them. */
.calendar-mini__day--adjacent { color: var(--ink-faint); }
/* TODAY — the filled disc. A fact about the world. */
/* S4/mini — today = GOLD disc (gold reserved for today, matching the main grid + mockup);
   selected stays --accent inset ring below (today = a fact about the world, selected = about you) */
.calendar-mini__day--today {
  background: var(--gold);
  color: var(--ground);
  font-weight: 600;
}
.calendar-mini__day--today:hover,
.calendar-mini__day--today:focus-visible { background: var(--gold-soft); }
/* SELECTED — the outline. A fact about you. Never today's ink (see the note above). */
.calendar-mini__day--selected {
  box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--ink);
  font-weight: 600;
}

/* ============================================================================
   THE YEAR VIEW (Item 4) — twelve month thumbnails, the whole year at a glance.
   Fit-by-construction (CAST-for-space): the root FILLS the pane and SCROLLS when
   short; the month grid WRAPS (auto-fill columns, no fixed count to overflow);
   each day is an aspect-ratio disc that can never spill its column. Density is a
   translucent --accent tint ramp (the app's own interactive hue); today is the
   gold disc, matching the mini + the main grid. Every value is a token.
   ========================================================================== */
.calendar-year {
  /* FILL + SCROLL: the view owns the pane; a short pane scrolls, never clips. */
  overflow-y: auto;
  padding-top: var(--s-2);
}
/* The honest banner shown ONLY when the year's read hit the event cap (approx). */
.calendar-year__approx {
  font-family: var(--face-survey);
  font-size: var(--t-small);
  color: var(--ink-soft);
  background: var(--surface-deep);
  border: 1px solid var(--line);
  border-radius: var(--r-s);
  padding: var(--s-2) var(--s-3);
  margin: 0 0 var(--s-3);
}
/* WRAP: auto-fill columns — the grid reflows to the pane width (4-up wide, 2-up
   narrow), so there is no fixed column count that could overflow. */
.calendar-year__months {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--s-3);
}
/* Each month is a quiet card — the same surface/line vocabulary as the mini. */
.calendar-year__month {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-m, var(--r-s));
  padding: var(--s-2);
}
.calendar-year__month-title {
  display: block; width: 100%;
  border: 0; background: transparent; cursor: pointer;
  text-align: left;
  font-family: var(--face-canopy);
  font-size: var(--t-body);
  color: var(--ink);
  padding: 0 0 var(--s-1);
  transition: color var(--dur-1) var(--ease);
}
.calendar-year__month-title:hover,
.calendar-year__month-title:focus-visible { color: var(--accent); }
.calendar-year__weekdays,
.calendar-year__days {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
}
.calendar-year__weekday {
  text-align: center;
  font-family: var(--face-survey);
  font-size: var(--t-label);
  color: var(--ink-faint);
  padding-bottom: 1px;
}
/* A day disc — smaller than the mini's (twelve months on screen), same shape. */
.calendar-year__day {
  aspect-ratio: 1 / 1;
  display: flex; align-items: center; justify-content: center;
  border: 0; padding: 0;
  background: transparent; cursor: pointer;
  border-radius: 999px;
  font-family: var(--face-survey);
  font-size: var(--t-label);
  line-height: 1;
  color: var(--ink);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.calendar-year__day:hover,
.calendar-year__day:focus-visible {
  background: color-mix(in srgb, var(--sunlight) 12%, transparent);
}
/* Adjacent-month days are REAL + clickable (fastest month-step) — just quieter,
   and NEVER shaded busy (their density belongs to the month you're not looking at). */
.calendar-year__day--adjacent { color: var(--ink-faint); }
/* THE DENSITY RAMP — a translucent --accent tint, three coarse tiers. Coarse on
   purpose: a year thumbnail reads "busier / calmer," never a precise number. */
.calendar-year__day--d1 { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.calendar-year__day--d2 { background: color-mix(in srgb, var(--accent) 34%, transparent); }
.calendar-year__day--d3 {
  background: color-mix(in srgb, var(--accent) 58%, transparent);
  color: var(--ink); font-weight: 600;
}
/* TODAY — the gold disc, a fact about the world. Wins over any density tint
   (later rule + the class order in the renderer appends --today last). */
.calendar-year__day--today {
  background: var(--gold);
  color: var(--ground);
  font-weight: 600;
}
.calendar-year__day--today:hover,
.calendar-year__day--today:focus-visible { background: var(--gold-soft); }

/* ============================================================================
   Calendar Item 3 · REPORTS + SETTINGS — the two summary/prefs panes (S08.1149).
   By-hand, Grove tokens only (tokens.css), zero new hue. CAST-for-space: every
   block declares its SPACE and its OVERFLOW POLICY.
     - .calendar-reports / .calendar-settings : FILL the pane, SCROLL when short.
     - the by-calendar rows                    : each name ELLIPSIS-CLIPS (never
                                                 pushes the count off the row).
     - the settings rows                       : WRAP label/control on a narrow pane.
   Higgins: the figures read without a caption — a big number over a quiet label,
   a colored dot leading each calendar (identity before text). Real-or-Made: the
   approx banner is the same honest banner the Year view wears; the read-only
   week-start value is visibly non-interactive (it states a truth, it isn't a control).
   ========================================================================== */

/* ---- REPORTS ---- FILL + SCROLL: the pane owns the height; a short pane scrolls. */
.calendar-reports { overflow-y: auto; padding-top: var(--s-2); }
/* The honest banner — ONLY when the month's read hit the cap (mirrors the Year view). */
.calendar-reports__approx {
  font-family: var(--face-survey); font-size: var(--t-small);
  color: var(--ink-soft); background: var(--surface-deep);
  border: 1px solid var(--line); border-radius: var(--r-s);
  padding: var(--s-2) var(--s-3); margin: 0 0 var(--s-3);
}
/* The window label — what makes the per-calendar count honest ("in July 2026"). */
.calendar-reports__span {
  font-family: var(--face-survey); font-size: var(--t-small);
  color: var(--ink-faint); margin: 0 0 var(--s-3);
  text-transform: lowercase; letter-spacing: 0.02em;
}
/* The headline figure — a big number over a quiet label (Higgins: reads captionless). */
.calendar-reports__figure { display: flex; align-items: baseline; gap: var(--s-2); margin-bottom: var(--s-4); }
.calendar-reports__figure-value { font-family: var(--face-canopy); font-size: var(--t-hero, var(--t-h)); color: var(--ink); line-height: 1; }
.calendar-reports__figure-label { font-family: var(--face-survey); font-size: var(--t-small); color: var(--ink-soft); }
/* Timed vs all-day — two quiet parts on one row; WRAP if the pane is tight. */
.calendar-reports__split { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); margin-bottom: var(--s-4); }
.calendar-reports__split-part { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); }
/* Busiest day — a labeled fact, quiet label + emphatic value. */
.calendar-reports__busiest { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2) var(--s-3); margin-bottom: var(--s-4); }
.calendar-reports__busiest-label { font-family: var(--face-survey); font-size: var(--t-label); color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.04em; }
.calendar-reports__busiest-value { font-family: var(--face-survey); font-size: var(--t-body); color: var(--ink); }
/* By calendar — a list; each row: dot (identity) · name (ELLIPSIS-CLIPS) · count. */
.calendar-reports__bycal { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-1); }
.calendar-reports__cal { display: flex; align-items: center; gap: var(--s-2); }
.calendar-reports__cal-dot { flex: 0 0 auto; width: 0.66rem; height: 0.66rem; border-radius: 999px; background: var(--cat, var(--moss)); }
.calendar-reports__cal-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--face-survey); font-size: var(--t-small); color: var(--ink); }
.calendar-reports__cal-count { flex: 0 0 auto; font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); }

/* ==== Reports v2 (deferred figures) ==========================================
   Not your grandfather's reports. The HOURS figure reads like the total (big
   number, quiet label). The RECLAIM rows name the negative space — the count
   can't show these, so they get their own soft-inked pair. The RIDGE is the year
   as growth-rings: 12 month-bands, each rising from --h (JS sets the ratio, CSS
   paints the height — DP-021 §7), this month lit in --sap. The EXPORT is a Grove
   affordance, not a corporate download chip. Higgins: the ridge reads captionless.
   ============================================================================ */

/* HOURS — the second headline figure; reuses the figure rhythm, sap-tinted value. */
.calendar-reports__figure--hours .calendar-reports__figure-value { color: var(--sap-ink, var(--ink)); }

/* RECLAIM — the hours that are yours. A quiet labelled pair, indented from the
   booked figure so the eye reads it as its counterpart, not another headline. */
.calendar-reports__reclaim { display: flex; flex-direction: column; gap: var(--s-1);
  margin: calc(-1 * var(--s-2)) 0 var(--s-4); padding-left: var(--s-3);
  border-left: 2px solid var(--moss); }
.calendar-reports__reclaim-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2) var(--s-3); }
.calendar-reports__reclaim-label { font-family: var(--face-survey); font-size: var(--t-label);
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.04em; }
.calendar-reports__reclaim-value { font-family: var(--face-survey); font-size: var(--t-body); color: var(--moss-ink); }

/* RIDGE — the year in rings. The wrap gives it a caption + breathing room; the
   ridge is a baseline-aligned row of bands, each band a full-height track holding
   a fill that rises from the floor by --h. calc keeps a hairline of fill even at
   --h:0 so an empty month reads as "a month with nothing", not "a gap". */
.calendar-reports__ridge-wrap { margin: var(--s-4) 0 var(--s-4); }
.calendar-reports__ridge-cap { font-family: var(--face-survey); font-size: var(--t-label);
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.04em; margin: 0 0 var(--s-2); }
.calendar-reports__ridge { display: flex; align-items: flex-end; gap: var(--s-1);
  height: 3.25rem; padding-bottom: 1.1rem; position: relative; }
.calendar-reports__ridge::after { content: ""; position: absolute; left: 0; right: 0; bottom: 1.1rem;
  border-bottom: 1px solid var(--line); }
.calendar-reports__ridge-band { flex: 1 1 0; min-width: 0; height: 100%;
  display: flex; flex-direction: column; justify-content: flex-end; align-items: center; position: relative; }
.calendar-reports__ridge-fill { width: 68%; border-radius: var(--r-s) var(--r-s) 0 0;
  height: calc(2px + (100% - 2px) * var(--h, 0));
  background: var(--moss); transition: height var(--dur-1) var(--ease); }
.calendar-reports__ridge-band.is-current .calendar-reports__ridge-fill { background: var(--sap, var(--accent)); }
.calendar-reports__ridge-tick { position: absolute; bottom: -1.05rem; font-family: var(--face-survey);
  font-size: var(--t-label); color: var(--ink-faint); }
.calendar-reports__ridge-band.is-current .calendar-reports__ridge-tick { color: var(--sap-ink, var(--ink)); font-weight: 600; }

/* EXPORT — take it with you. A Grove text-button, sap on hover; sovereignty, not a
   corporate download chip. Sits at the foot of the pane after the figures. */
.calendar-reports__export { display: inline-flex; align-items: center; gap: var(--s-2);
  margin-top: var(--s-4); border: 1px solid var(--line); background: var(--surface);
  border-radius: var(--r-s); padding: var(--s-2) var(--s-3); cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--accent);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease); }
.calendar-reports__export:hover,
.calendar-reports__export:focus-visible { color: var(--sap-ink, var(--ink)); border-color: var(--sap, var(--accent)); }
.calendar-reports__export:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- SETTINGS ---- FILL + SCROLL. A quiet index, not a modal maze. */
.calendar-settings { overflow-y: auto; padding-top: var(--s-2); }
.calendar-settings__head { margin-bottom: var(--s-3); }
.calendar-settings__title { font-family: var(--face-canopy); font-size: var(--t-h); color: var(--ink); }
/* Each pref is one row: label left, control right; WRAP on a narrow pane so the
   control drops under its label rather than clipping (CAST overflow policy). */
.calendar-settings__row {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s-2) var(--s-3);
  padding: var(--s-3) 0; border-bottom: 1px solid var(--line);
}
.calendar-settings__label { font-family: var(--face-survey); font-size: var(--t-body); color: var(--ink-soft); }
.calendar-settings__control {
  font-family: var(--face-survey); font-size: var(--t-body); color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-s);
  padding: var(--s-1) var(--s-2);
}
.calendar-settings__control:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Read-only value — visibly a STATEMENT, not a control (Real-or-Made). */
.calendar-settings__value { font-family: var(--face-survey); font-size: var(--t-body); color: var(--ink); }
.calendar-settings__value--readonly { color: var(--ink-soft); font-style: italic; }
/* The My-Calendars pointer — a text button; accent on hover, the Grove affordance. */
.calendar-settings__link {
  border: 0; background: transparent; cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--accent);
  padding: 0; transition: color var(--dur-1) var(--ease);
}
.calendar-settings__link:hover,
.calendar-settings__link:focus-visible { color: var(--accent-deep, var(--ink)); text-decoration: underline; }

/* ============================================================================
   DP-016 Trio · Track CONTACT — the contacts-* renderer styling (S11.1341).
   Same fold-in as the calendar section above: the contacts-renderer emitted its
   .contacts-* classes UNSTYLED. This gives the person directory its Grove face —
   initials-avatar rows, record with sub-records (emails/phones/relationships),
   the honest relationship-context weave, and the ask-first merge — all tokens
   from tokens.css. Real-or-Made honored: the avatar is always initials, never a
   fabricated face; the CSS paints the chip, it invents nothing. TC-1 untouched.
   ========================================================================== */

.contacts-host { margin-top: var(--s-2); }
.contacts-loading { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); padding: var(--s-4) 0; }
.contacts-empty { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); padding: var(--s-5) var(--s-4); text-align: center; }

/* the directory list */
.contacts-list__head { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-3); flex-wrap: wrap; }
.contacts-list__search {
  font-family: var(--face-path); font-size: var(--t-body); color: var(--ink);
  padding: var(--s-2) var(--s-3); border-radius: var(--r-pill);
  border: 1px solid var(--line-moss); background: var(--paper); flex: 1 1 12rem; min-width: 8rem;
}
.contacts-list__search:focus { outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring); }
.contacts-list__census { font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--moss-ink); }
.contacts-list__starred { display: inline-flex; align-items: center; gap: var(--s-1); font-family: var(--face-survey); font-size: var(--t-small); color: var(--ink-soft); cursor: pointer; }
.contacts-list__body { display: flex; flex-direction: column; }

/* a person row */
.contacts-row { border-bottom: 1px solid color-mix(in srgb, var(--line-moss) 55%, transparent); }
.contacts-row__open {
  appearance: none; cursor: pointer; width: 100%; text-align: left;
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-2); border: 0; background: transparent; color: var(--ink);
  transition: background 140ms var(--ease);
}
.contacts-row__open:hover { background: color-mix(in srgb, var(--moss) 8%, var(--paper)); }
.contacts-row__avatar, .contacts-record__avatar {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--surface) 82%, var(--sunlight));
  color: var(--ink-on-dark); font-family: var(--face-survey); font-size: var(--t-small); letter-spacing: 0.02em;
}
.contacts-record__avatar { width: 3rem; height: 3rem; font-size: var(--t-body); }
/* DP-020: a real photo (owner or contact) fills the round avatar frame without distortion —
   the source is a square 512² from photo-pipeline.js, object-fit keeps it clean if a legacy
   non-square blob ever appears. Shared by ownerAvatar and the contact record avatar. */
.contacts-record__avatar--photo { object-fit: cover; }
/* DP-020 Phase 2: the record's photo control (mirrors the owner card's — relies on base tokens,
   not bespoke chrome). Keep it a quiet row under the head. */
.contacts-record__photo-edit { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: var(--sp-2, 0.5rem) 0; }
/* Event photo (mirrors the contact/owner photo): an event carries a banner image, set by
   hand here or via root.eventPhoto.set. Bounded rectangle (not a round avatar — an event is
   an occasion, not a face); object-fit keeps the square 512² pipeline output clean. Tokens
   only, quiet row under the head — same discipline as the contact photo control above. */
.calendar-record__photo { margin: var(--sp-2, 0.5rem) 0; }
.calendar-record__photo-slot:empty { display: none; }             /* honest empty: no blob -> no frame */
.calendar-record__photo-img {
  display: block; width: 100%; max-height: 14rem; object-fit: cover;
  border-radius: var(--radius, 0.5rem);
}
.calendar-record__photo-edit { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: var(--sp-2, 0.5rem) 0; }
.contacts-row__main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.contacts-row__name { color: var(--ink); font-size: var(--t-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.contacts-row__sub { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.contacts-row__star {
  appearance: none; cursor: pointer; border: 0; background: transparent;
  color: var(--line-moss); font-size: 1.15rem; line-height: 1; padding: var(--s-1); flex: 0 0 auto;
  transition: color 140ms var(--ease);
}
.contacts-row__star:hover { color: var(--sunlight); }
.contacts-row__star.is-starred { color: var(--sunlight); }

/* the person record */
.contacts-record__back {
  appearance: none; cursor: pointer; border: 0; background: transparent;
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink);
  padding: var(--s-1) 0; margin-bottom: var(--s-3);
}
.contacts-record__back:hover { color: var(--ink); }
.contacts-record__head { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-4); }
.contacts-record__headtext { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.contacts-record__name { font-family: var(--face-canopy); font-size: var(--t-title); color: var(--ink); margin: 0; }
.contacts-record__fields { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-4); }
.contacts-field { display: flex; gap: var(--s-3); align-items: baseline; }
.contacts-field__label {
  flex: 0 0 5rem; font-family: var(--face-survey); font-size: var(--t-label);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--moss-ink);
}
.contacts-field__value { color: var(--ink); overflow-wrap: anywhere; }
.contacts-field__primary { color: var(--sunlight); font-family: var(--face-survey); font-size: 0.6rem; letter-spacing: 0.04em; text-transform: uppercase; margin-left: var(--s-2); }
.contacts-record__sub {
  padding: var(--s-1) 0 var(--s-1) var(--s-3);
  border-left: 2px solid var(--line-moss); margin-bottom: var(--s-1); color: var(--ink);
}
.contacts-record__notes { margin-bottom: var(--s-4); }
.contacts-record__notes-label { font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: 0.04em; text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-1); }
.contacts-record__notes-body { color: var(--ink); line-height: var(--leading-body); margin: 0; white-space: pre-wrap; }
.contacts-record__actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* the relationship-context weave (contact tool's own proof-chained graph) */
.contacts-context { margin-top: var(--s-4); padding-top: var(--s-3); border-top: 1px solid var(--line-moss); }
.contacts-context__label { font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-2); }
.contacts-context__rels { display: flex; flex-direction: column; gap: var(--s-1); }
.contacts-context__rel { display: flex; align-items: baseline; gap: var(--s-2); }
.contacts-context__reltype { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); flex: 0 0 6rem; }
.contacts-context__relname { color: var(--ink); }
.contacts-context__empty, .contacts-context__deferred { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); font-style: italic; }

/* the action strip (deferred J3 surfaces stay honestly disabled) */
.contacts-action {
  appearance: none; cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-small); letter-spacing: 0.01em;
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink); line-height: 1.6;
  transition: border-color 160ms var(--ease), background 160ms var(--ease);
}
.contacts-action:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.contacts-action--deferred { opacity: 0.5; cursor: not-allowed; border-style: dashed; }

/* the ask-first merge (the confirm gate is UI, the merge judgment is the TOOL's) */
.contacts-merge {
  margin-top: var(--s-3); padding: var(--s-3); border-radius: var(--r-m);
  border: 1px solid var(--line-gold); background: color-mix(in srgb, var(--sunlight) 6%, var(--paper));
}
.contacts-merge__head { font-family: var(--face-survey); font-size: var(--t-small); color: var(--ink); margin-bottom: var(--s-2); }
.contacts-merge__list { display: flex; flex-direction: column; gap: var(--s-1); margin-bottom: var(--s-2); }
.contacts-merge__row { display: flex; align-items: baseline; gap: var(--s-2); }
.contacts-merge__name { color: var(--ink); }
.contacts-merge__conf { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); }

/* honest read-fail (F3 read axis) — reuses .is-saving/.is-saved/.is-unsaved for writes */
.contacts-unreachable { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2); padding: var(--s-5) 0; }
.contacts-unreachable__msg { color: var(--badge-unreachable-ink); font-family: var(--face-survey); font-size: var(--t-small); margin: 0; }

/* ── K1 (Track CONTACTS enrich): search highlight + labels (S11.1522) ───────── */
/* highlight — marks the operator's typed query in list rows (display only) */
.contacts-hl { background: color-mix(in srgb, var(--sunlight) 34%, transparent); color: inherit; border-radius: 2px; padding: 0 1px; }

/* label chips — read-only strip (rows + record); color rides the tool's swatch */
.contacts-labels { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: 2px; }
.contacts-label {
  display: inline-flex; align-items: center; gap: var(--s-1);
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--ink);
  padding: 1px var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); border-left: 3px solid var(--teal);
  background: color-mix(in srgb, var(--moss) 6%, var(--paper));
}
.contacts-labels__empty { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); font-style: italic; }

/* label editor (record interior, WRITE axis) */
.contacts-record__labels { margin-bottom: var(--s-4); }
.contacts-record__labels-label { font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-2); }
.contacts-labels--editable { margin-bottom: var(--s-2); }
.contacts-label--removable { padding-right: var(--s-1); }
.contacts-label__x {
  appearance: none; border: none; background: none; cursor: pointer; line-height: 1;
  color: var(--moss-ink); font-size: var(--t-body); padding: 0 2px; border-radius: 2px;
}
.contacts-label__x:hover { color: var(--ink); }
.contacts-label__x:disabled { opacity: 0.4; cursor: default; }
.contacts-label-add { display: flex; gap: var(--s-2); align-items: center; }
.contacts-label-add__input {
  flex: 1 1 auto; min-width: 0; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-label-add__input:focus { outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring); }
.contacts-label-add__go {
  appearance: none; cursor: pointer; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-label-add__go:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.contacts-label-add__go:disabled { opacity: 0.4; cursor: default; }

/* ── K2 (Track CONTACTS enrich): merge history (S11.1522) ───────────────────── */
.contacts-record__history { margin-bottom: var(--s-4); }
.contacts-record__history-label { font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-2); }
.contacts-history__loading, .contacts-history__empty { color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); font-style: italic; }
.contacts-history__list { display: flex; flex-direction: column; gap: var(--s-1); }
.contacts-history__row { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.contacts-history__verb { color: var(--ink); font-size: var(--t-small); }
.contacts-history__reason { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); }
.contacts-history__when { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); margin-left: auto; }

/* ── K3 (Track CONTACTS enrich): relationship panel (S11.1618) ──────────────── */
.contacts-context__rels--editable .contacts-context__rel { padding-right: var(--s-1); }
.contacts-context__relx {
  appearance: none; border: none; background: none; cursor: pointer; line-height: 1;
  color: var(--moss-ink); font-size: var(--t-body); padding: 0 2px; border-radius: 2px; margin-left: auto;
}
.contacts-context__relx:hover { color: var(--ink); }
.contacts-context__relx:disabled { opacity: 0.4; cursor: default; }
.contacts-rel-add { display: flex; gap: var(--s-2); align-items: center; margin-top: var(--s-2); }
.contacts-rel-add__target, .contacts-rel-add__rel {
  min-width: 0; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-rel-add__target { flex: 1 1 auto; }
.contacts-rel-add__rel { flex: 0 0 7rem; }
.contacts-rel-add__target:focus, .contacts-rel-add__rel:focus { outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring); }
.contacts-rel-add__go {
  appearance: none; cursor: pointer; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-rel-add__go:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.contacts-rel-add__go:disabled { opacity: 0.4; cursor: default; }

/* ===== E6 — THE PERSON CANOPY (S13.1115) ======================================= *
 * The cross-app card: this person's Moments (calendar) + the mail mentioning them. *
 * Existing tokens only — ZERO new hue. The renderer emitted these classes with no  *
 * CSS anywhere in the tree, which is the S11.1341 fault verbatim (calendar and     *
 * contacts were both "structurally complete" and rendered as unstyled div-stacks   *
 * because nobody had written the backing region). Written here, with the renderer. *
 *                                                                                  *
 * ONE DELIBERATE TYPOGRAPHIC ACT: the fuzzy read is styled QUIETER than the precise *
 * one. Moments is a structured join on the attendee link; the mail section is a     *
 * keyword search that can hit a signature or a cc. They are not worth the same, so  *
 * they must not LOOK the same — the caveat sits in --moss-ink italic beneath its    *
 * heading, and the eye is told which answer to trust before it reads either.        */
.contacts-canopy { margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--line); }
.contacts-canopy__label {
  font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: var(--tracking-caps);
  text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-2);
}
.contacts-canopy__sec { margin-bottom: var(--s-3); }
.contacts-canopy__sec:last-child { margin-bottom: 0; }
.contacts-canopy__head {
  font-family: var(--face-survey); font-size: var(--t-small); font-weight: 500;
  color: var(--ink); margin: 0 0 var(--s-2) 0;
}
/* the honesty seam, made visible: quieter, italic, and it sits UNDER the heading it qualifies */
.contacts-canopy__caveat {
  color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small);
  font-style: italic; margin: calc(-1 * var(--s-2)) 0 var(--s-2) 0;
}
.contacts-canopy__body { display: flex; flex-direction: column; gap: var(--s-1); }
.contacts-canopy__list { display: flex; flex-direction: column; gap: var(--s-1); margin: 0; padding: 0; list-style: none; }
.contacts-canopy__row { display: flex; align-items: baseline; gap: var(--s-2); }
.contacts-canopy__when {
  font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink);
  flex: 0 0 5.5rem; font-variant-numeric: tabular-nums;
}
.contacts-canopy__title { color: var(--ink); }
.contacts-canopy__loading,
.contacts-canopy__empty,
.contacts-canopy__deferred,
.contacts-canopy__more {
  color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); font-style: italic;
}
/* unreachable is NOT empty, and it does not get the empty state's quiet italic shrug —
   it carries the honest badge, because a failed read is a fact about the system, not
   a fact about the person. */
.contacts-canopy__unreachable { display: flex; align-items: baseline; gap: var(--s-2); }
.contacts-canopy__unreachable-msg { color: var(--bark); font-size: var(--t-small); margin: 0; }

/* ── K4 (Track CONTACTS enrich): notes log · follow-ups · timeline (S11.1618) ─ */
.contacts-record__noteslog, .contacts-record__followups, .contacts-record__timeline { margin-bottom: var(--s-4); }
.contacts-record__noteslog-label, .contacts-record__followups-label, .contacts-record__timeline-label {
  font-family: var(--face-survey); font-size: var(--t-label); letter-spacing: var(--tracking-caps);
  text-transform: uppercase; color: var(--moss-ink); margin-bottom: var(--s-2);
}
/* notes log */
.contacts-noteslog { display: flex; flex-direction: column; gap: var(--s-1); }
.contacts-noteslog__row { display: flex; align-items: baseline; gap: var(--s-2); }
.contacts-noteslog__text { color: var(--ink); flex: 1 1 auto; margin: 0; }
.contacts-noteslog__when { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); }
.contacts-noteslog__x, .contacts-followups__x {
  appearance: none; border: none; background: none; cursor: pointer; line-height: 1;
  color: var(--moss-ink); font-size: var(--t-body); padding: 0 2px; border-radius: 2px;
}
.contacts-noteslog__x:hover, .contacts-followups__x:hover { color: var(--ink); }
.contacts-noteslog__x:disabled, .contacts-followups__x:disabled { opacity: 0.4; cursor: default; }
.contacts-noteslog__empty, .contacts-followups__empty, .contacts-followups__loading,
.contacts-timeline__empty, .contacts-timeline__loading {
  color: var(--moss-ink); font-family: var(--face-survey); font-size: var(--t-small); font-style: italic;
}
.contacts-note-add, .contacts-followup-add { display: flex; gap: var(--s-2); align-items: center; margin-top: var(--s-2); }
.contacts-note-add__input, .contacts-followup-add__note, .contacts-followup-add__due {
  min-width: 0; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-note-add__input, .contacts-followup-add__note { flex: 1 1 auto; }
.contacts-followup-add__due { flex: 0 0 auto; }
.contacts-note-add__input:focus, .contacts-followup-add__note:focus, .contacts-followup-add__due:focus {
  outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring);
}
.contacts-note-add__go, .contacts-followup-add__go {
  appearance: none; cursor: pointer; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-note-add__go:hover, .contacts-followup-add__go:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.contacts-note-add__go:disabled, .contacts-followup-add__go:disabled { opacity: 0.4; cursor: default; }
/* follow-ups list */
.contacts-followups__list { display: flex; flex-direction: column; gap: var(--s-1); }
.contacts-followups__row { display: flex; align-items: baseline; gap: var(--s-2); }
.contacts-followups__row.is-done .contacts-followups__note { text-decoration: line-through; color: var(--moss-ink); }
.contacts-followups__check {
  appearance: none; border: none; background: none; cursor: pointer; line-height: 1;
  color: var(--moss-ink); font-size: var(--t-body); padding: 0 2px;
}
.contacts-followups__check.is-done { color: var(--moss); }
.contacts-followups__check:disabled { opacity: 0.4; cursor: default; }
.contacts-followups__note { color: var(--ink); flex: 1 1 auto; }
.contacts-followups__due { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); }
/* timeline */
.contacts-timeline__list { display: flex; flex-direction: column; gap: var(--s-1); }
.contacts-timeline__row { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.contacts-timeline__kind { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); flex: 0 0 8rem; }
.contacts-timeline__label { color: var(--ink); }
.contacts-timeline__when { font-family: var(--face-survey); font-size: var(--t-small); color: var(--moss-ink); margin-left: auto; }
/* list due-glow: an honest dot when a person has an open follow-up due */
.contacts-row__due { width: 6px; height: 6px; border-radius: 50%; background: var(--badge-overdue-ink); flex: 0 0 auto; align-self: center; margin-right: var(--s-1); }

/* ── K5 (Track CONTACTS enrich): import / export bar (S11.1618) ─────────────── */
.contacts-io { display: flex; flex-direction: column; gap: var(--s-2); margin: var(--s-2) 0 var(--s-3); padding: var(--s-2); border: 1px solid var(--line-moss); border-radius: var(--r-s); background: color-mix(in srgb, var(--sunlight) 4%, var(--paper)); }
.contacts-io__export { display: flex; gap: var(--s-2); flex-wrap: wrap; }
.contacts-io__exp, .contacts-io__go {
  appearance: none; cursor: pointer; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-io__exp:hover, .contacts-io__go:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.contacts-io__exp:disabled, .contacts-io__go:disabled { opacity: 0.4; cursor: default; }
.contacts-io__import { display: flex; gap: var(--s-2); align-items: flex-start; flex-wrap: wrap; }
.contacts-io__text {
  flex: 1 1 12rem; min-width: 0; font-family: var(--face-survey); font-size: var(--t-small); resize: vertical;
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-io__fmt {
  font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-io__text:focus, .contacts-io__fmt:focus { outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring); }

/* ── W1 (DP-019 §5): THE RECORD WRITE PATH ──────────────────────────────────
   Create · edit-person · emails / phones / addresses / custom fields.
   No new vocabulary: every rule below is built from the SAME tokens the label
   editor and the import/export bar already use (--s-*, --face-survey, --t-*,
   --line-moss, --line-gold, --paper, --ink, --moss-ink, --ring, --r-s), and the
   write-flash states reuse the existing .is-saving / .is-saved / .is-unsaved.
   The write axis should look like the app, not like a form bolted onto it. */

/* the groups (one per multi-value relation) */
.contacts-group { margin-bottom: var(--s-3); }
.contacts-group__label {
  font-family: var(--face-survey); font-size: var(--t-label);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
  color: var(--moss-ink); margin-bottom: var(--s-1);
}
.contacts-group__empty { color: var(--moss-ink); margin-bottom: var(--s-1); }

/* a live row: the value, plus the × that removes it. The × stays quiet until
   the row is hovered/focused — a destructive control should not shout at rest. */
.contacts-field--editable { align-items: center; }
.contacts-field__x {
  appearance: none; border: none; background: none; cursor: pointer; line-height: 1;
  color: var(--moss-ink); font-size: var(--t-body); padding: 0 var(--s-1);
  border-radius: 2px; margin-left: auto; opacity: 0.35;
  transition: opacity 120ms ease, color 120ms ease;
}
.contacts-field--editable:hover .contacts-field__x,
.contacts-field__x:focus { opacity: 1; }
.contacts-field__x:hover { color: var(--bark); }
.contacts-field__x:focus { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.contacts-field__x:disabled { opacity: 0.3; cursor: default; }

/* the add-form under each group — the label editor's form, generalized to N inputs */
.contacts-add { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; margin-top: var(--s-1); }
.contacts-add__input {
  flex: 1 1 8rem; min-width: 0; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-add__input:focus { outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring); }
.contacts-add__go {
  appearance: none; cursor: pointer; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-add__go:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.contacts-add__go:disabled { opacity: 0.4; cursor: default; }

/* edit-person — collapsed by default: the record is a record first, a form second */
.contacts-record__edit { margin-bottom: var(--s-4); }
.contacts-record__edit-toggle {
  appearance: none; cursor: pointer; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
  margin-bottom: var(--s-2);
}
.contacts-record__edit-toggle:hover { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 8%, var(--paper)); }
.contacts-edit {
  display: flex; flex-direction: column; gap: var(--s-2);
  padding: var(--s-3); border: 1px solid var(--line-moss); border-radius: var(--r-s);
  background: color-mix(in srgb, var(--sunlight) 4%, var(--paper));
}
.contacts-edit.is-collapsed { display: none; }
.contacts-edit__row { display: flex; gap: var(--s-2); align-items: center; }
.contacts-edit__label {
  flex: 0 0 5rem; font-family: var(--face-survey); font-size: var(--t-label);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--moss-ink);
}
.contacts-edit__input {
  flex: 1 1 auto; min-width: 0; font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-edit__input:focus { outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring); }
.contacts-edit__save {
  align-self: flex-start; appearance: none; cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-4); border-radius: var(--r-s);
  border: 1px solid var(--line-gold); background: color-mix(in srgb, var(--sunlight) 10%, var(--paper)); color: var(--ink);
}
.contacts-edit__save:hover { background: color-mix(in srgb, var(--sunlight) 18%, var(--paper)); }
.contacts-edit__save:disabled { opacity: 0.4; cursor: default; }

/* create — opened from the rail's primary slot (.rail__compose), which contacts
   finally earns: it has a create verb, and always did (DP-019 §2b v3). */
.contacts-create {
  display: flex; flex-direction: column; gap: var(--s-2);
  margin: var(--s-2) 0 var(--s-3); padding: var(--s-3);
  border: 1px solid var(--line-gold); border-radius: var(--r-s);
  background: color-mix(in srgb, var(--sunlight) 6%, var(--paper));
}
.contacts-create__head {
  font-family: var(--face-survey); font-size: var(--t-label);
  letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--moss-ink);
}
.contacts-create__input {
  font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-create__input:focus { outline: none; border-color: var(--line-gold); box-shadow: 0 0 0 3px var(--ring); }
.contacts-create__go, .contacts-create__cancel {
  align-self: flex-start; appearance: none; cursor: pointer;
  font-family: var(--face-survey); font-size: var(--t-small);
  padding: var(--s-1) var(--s-4); border-radius: var(--r-s);
  border: 1px solid var(--line-moss); background: var(--paper); color: var(--ink);
}
.contacts-create__go { border-color: var(--line-gold); background: color-mix(in srgb, var(--sunlight) 10%, var(--paper)); }
.contacts-create__go:hover { background: color-mix(in srgb, var(--sunlight) 18%, var(--paper)); }
.contacts-create__go:disabled { opacity: 0.4; cursor: default; }
.contacts-create__cancel:hover { border-color: var(--line-gold); }


/* ═══════════════════════════════════════════════════════════════════════════
   THE FRAME (Track F · DP-017) — the conventional 3-region app shell.
   Region-partition: F owns .app-*  ·  B owns tabstrip-*  ·  P owns pane-*  ·  M owns wordmark-*
   CAST-for-space: every block declares its space + its overflow policy (plan §2).
   The old grove-homepage (masthead/strata/bands) is gone; every pixel is the tool (R1).
   ─────────────────────────────────────────────────────────────────────────── */

/* App-mode base — kill page scroll; the frame owns the viewport, not the page.
   (shell.css cascades last, so these win over app.css's scrolling-page body.) */
html { height: 100%; }
body {
  height: 100%;
  overflow: hidden;          /* no page scroll — the frame CLIPs to the viewport */
  padding-bottom: 0;         /* was --s-8 for the scrolling homepage; gone */
  min-height: 0;
}

/* The ring stays in the DOM as Nyx's structural boundary, but in app-mode its
   floating-card treatment (margin / max-width / border / shadow) is retired —
   the frame carries the visible never-default edge itself (a gold hairline). */
.ring        { position: static; margin: 0; max-width: none; border: 0; border-radius: 0; box-shadow: none; }
.ring__inner { padding: 0; }
.ring__tag   { display: none; }   /* the Front Door is the visible security boundary now */

/* root ......... viewport · FIXED 100vh · overflow: CLIP (no page scroll) */
.app-frame {
  position: fixed; inset: 0;
  display: flex; flex-direction: column;
  overflow: clip;
  background: var(--ground);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-gold);   /* Nyx's never-default edge, drawn on the frame */
  z-index: 1;                                      /* the #door dialog (z-index 20+) overlays above */
}

/* top-bar ...... height FIXED (~56px) · overflow-x: SCROLL on the tab region only */
.app-topbar {
  flex: 0 0 auto; height: 56px;
  display: flex; align-items: stretch;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  overflow: hidden;                 /* the row itself never scrolls; only .app-tabs does */
}

/* wordmark ..... FIXED (left) — the Loop World lockup: shared mark + "Loop World"
   ──────────────────────────────────────────────────────────────────────────────────
   S13.1945, operator note #9 — THE BRAND BOX AND THE LEFT COLUMN WERE NEVER THE SAME
   WIDTH, and could not be. `.app-brand` was `flex: 0 0 auto` (CONTENT-sized: it ended
   wherever the wordmark happened to end) while `.app-menu` is `flex: 0 0 200px` (a hard
   200). Two vertical rules, one under the other, whose x-positions were derived from two
   unrelated quantities — so they lined up only by accident, and they didn't.
   The fix is not a nudge, it is a SHARED RULE: both regions now read one token,
   `--rail-w`, so the two borders are the same number BY CONSTRUCTION and cannot drift
   apart again when the wordmark or the menu is next touched. (`box-sizing: border-box`
   is already global — app.css:17 — so the 1px border is inside the 200, on both.) */
.app-brand {
  flex: 0 0 var(--rail-w);
  position: relative;               /* anchor for .app-ver (absolute -> the wordmark CANNOT move) */
  display: flex; align-items: center; justify-content: center;   /* note #8 — see below */
  gap: var(--s-3);
  padding: 0 var(--s-4);            /* SYMMETRIC — the asymmetric 1.5rem/1rem was itself
                                       an off-centre by rule, before any pixel-nudging */
  border-right: 1px solid var(--line);
  min-width: 0;                     /* M's slot; reserved but self-contained */
}

/* tab-strip .... FILL · SCROLL-X — mounts #forest-tab-strip (B's territory) */
.app-tabs {
  flex: 1 1 auto; min-width: 0;     /* min-width:0 is load-bearing — lets scroll-x engage instead of blowing the row out (Priya) */
  display: flex; align-items: stretch;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: thin;
}
.app-tabs > #forest-tab-strip { display: flex; align-items: stretch; min-width: 0; }

/* actions ...... FIXED (right) — edit-lock · search · settings (B fills in .app-actions) */
.app-actions {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-2);
  padding: 0 var(--s-4);
  border-left: 1px solid var(--line);
}

/* body ......... FILL (remaining height) · row split */
.app-body {
  flex: 1 1 auto; min-height: 0;    /* min-height:0 lets the children scroll instead of growing the frame */
  display: flex; align-items: stretch;
}

/* left-menu .... width FIXED (~200px) · overflow-y: SCROLL — the active app's own nav.
   DP-018: padding moved OFF the region and onto its children (.menu__anchor /
   .menu__body). A sticky child inside a padded scroll container needs a negative-offset
   hack to sit flush -- fragile, and it drifts when the token moves. With padding:0 here,
   the anchor's `top: 0` is EXACT and the body scrolls cleanly under it. Same pixels, no hack. */
.app-menu {
  flex: 0 0 var(--rail-w); min-width: 0;   /* the SAME token the brand box reads (note #9) */
  overflow-y: auto; overflow-x: hidden;
  background: var(--surface);
  border-right: 1px solid var(--line);
  padding: 0;                       /* DP-018 — was var(--s-4) var(--s-3); see .menu__* below */
  scrollbar-width: thin;
}

/* ===================== DP-018 · the app-menu's VIEW half =====================
   P owns .menu. THE JOINT WRITES .menu__anchor -- always, for every app, before the
   app's renderer runs. THE APP owns .menu__body, and ONLY .menu__body (via ctx.menuBody).
   The anchor is STICKY: .app-menu scrolls-y, and an anchor that scrolls away is not an
   anchor. Its tokens are .pane__title's and .pane__version's, moved VERBATIM -- the
   nameplate was always right, it was in the wrong room. */
.menu {
  display: flex; flex-direction: column;
  min-height: 100%;
}
/* DP-018 fix (S12.1339): .menu's author `display: flex` OVERRIDES the UA
   [hidden]{display:none}, so pane.js toggling `menu.hidden` on the inactive apps
   had NO visual effect — every app's menu stayed painted and stacked, and the left
   column never appeared to swap on a tab switch (the JS was correct all along). This
   is the menu's twin of the pane's `.pane[hidden]` reset below; the menu was missing it. */
.menu[hidden] { display: none; }
.menu__anchor {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface);                 /* opaque — the body scrolls UNDER it */
  padding: var(--s-4) var(--s-3) var(--s-3);
  border-bottom: 1px solid var(--line);
}
.menu__name {                                  /* THE SIGN-PAINTER — never clip an app title */
  font-family: var(--face-canopy);
  font-size: var(--t-title);
  line-height: var(--leading-tight);
  color: var(--ink);
  margin: 0;
  /* The title is a hand-lettered sign, not a data cell: it is NEVER ellipsized. shell/title-fit.js
     (root.titleFit.arm, wired at pane.js buildMenu) scales the font DOWN so the whole word fits on
     one line, down to a floor; below the floor it wraps. The base here is the never-clip FLOOR for
     the module-absent path: `overflow-wrap: anywhere` lets a too-long word WRAP (whole word, two
     lines) instead of clipping — no overflow:hidden, no text-overflow:ellipsis anywhere. */
  min-width: 0; overflow-wrap: anywhere;
}
.menu__version {                               /* ← .pane__version's declarations, verbatim */
  font-family: var(--face-path);
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  margin: var(--s-1) 0 0;
  white-space: nowrap;                         /* the V# never wraps */
}
.menu__body { flex: 1 1 auto; min-height: 0; padding: var(--s-3) var(--s-3) var(--s-4); }

/* THE CREDIT FOOTER (operator, S08.0008) — joint-owned by pane.js buildMenuFooter, written into
   EVERY app's column. `.menu__body` above it flex-grows, so this sits at the very bottom of the
   column: pinned to the viewport bottom on a short body, below the content on a tall/scrolling one.
   The quiet register (10px, --ink-faint) — a credit, not a headline; the mail is a real link. */
.menu__footer {
  flex: 0 0 auto;
  padding: var(--s-3);
  border-top: 1px solid var(--line);
  font-family: var(--face-survey);
  font-size: 0.625rem;              /* 10px — same quiet step as the version stamp */
  line-height: 1.5;
  letter-spacing: 0.02em;
  color: var(--ink-faint);
}
.menu__footer-line { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu__footer-mail,
.menu__footer-link { color: var(--accent); text-decoration: none; }
.menu__footer-mail:hover,
.menu__footer-mail:focus-visible,
.menu__footer-link:hover,
.menu__footer-link:focus-visible { color: var(--accent-lit); text-decoration: underline; }

/* content ...... FILL · overflow-y: SCROLL — mounts #forest-pane (P's keep-alive pool) */
.app-content {
  flex: 1 1 auto; min-width: 0; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  background: var(--ground);
  scrollbar-width: thin;
}
.app-content > #forest-pane { min-width: 0; }

/* Reduced-motion + small-viewport: the left menu collapses under a narrow app;
   fit-by-construction holds — the menu shrinks, the content keeps the residual. */
@media (max-width: 640px) {
  .app-menu { flex-basis: 148px; }
  .app-brand { padding: 0 var(--s-3); }
}

/* ============================================================================
   Shea's Forest — DP-017 Track B  ·  the tab-strip actions cluster + edit-lock
   ----------------------------------------------------------------------------
   Fills .app-actions (Track F's FIXED right slot) with three icon-buttons and
   themes the edit-lock signal. Two-tone, straight from the palette: GOLD is
   rest, ELECTRIC (--accent) is alive/editing — the wordmark's "gold bit alive
   only in motion" logic applied to a control. Block Principle: this styles
   CONTENT in F's slot; it never touches the .app-* frame layout above.
   New CSS -> DP-017 primaries only (--ink/--accent/--gold/--line), no aliases.
   ============================================================================ */

/* ---- the cluster ------------------------------------------------------------ */
.tsa { display: flex; align-items: center; gap: var(--s-1); }

/* ---- the WEATHER element (operator S13.2038) --------------------------------- *
 * Sits FIRST in the cluster, so it reads immediately LEFT of the version stamp:
 *     [ ☀ 83° ] │ v1.11 │ 🔒 🔍 ☾ ⚙
 * Quiet by design — it is a READING, not an action, and it must not compete with the
 * four controls beside it. But it IS a link, so it earns a hover: the ink warms and
 * the underline appears, which is the only cue that says "this goes somewhere."
 * The empty slot (no reading) collapses to zero width and zero gap — an absent
 * reading leaves NO trace in the layout, which is what "renders nothing" has to mean
 * if it is going to be true (weather.js L1). */
.tsa-wx-slot { display: flex; align-items: center; }
.tsa-wx-slot:empty { display: none; }

.tsa-wx {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-2);
  margin-right: var(--s-1);
  border-radius: var(--r-pill);
  color: var(--ink-faint);
  text-decoration: none;
  line-height: 1;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.tsa-wx:hover,
.tsa-wx:focus-visible {
  color: var(--ink-on-dark);
  background: color-mix(in srgb, var(--paper) 10%, transparent);
}
.tsa-wx__icon {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}
.tsa-wx__icon svg { display: block; }
.tsa-wx__temp {
  font-family: var(--face-survey);
  font-size: var(--t-label);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* ---- viewer skin-version stamp (left of the lock; operator S12.11xx) --------- */
/* Quiet by design: it's a reference number, not an action. Mono face (matches the  */
/* app data/label voice), muted ink, a hairline divider sets it off from the icons. */
.tsa-ver {
  flex: 0 0 auto;
  font-family: var(--face-survey);
  font-size: var(--t-label);
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  padding-right: var(--s-2);
  margin-right: var(--s-1);
  border-right: 1px solid var(--line);
  user-select: none;
  white-space: nowrap;
}

/* ---- an icon-button (Higgins: reads without a label) ------------------------ */
.tsa-btn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r-m);
  background: transparent;
  color: var(--ink-soft);                 /* quiet at rest */
  cursor: pointer;
  transition: color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease);
}
.tsa-btn:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
.tsa-btn:focus-visible {
  outline: none;
  border-color: var(--line-gold);
  box-shadow: 0 0 0 3px var(--ring);      /* the app's never-default gold ring */
}
.tsa-btn__icon { display: inline-flex; line-height: 0; }
.tsa-btn__icon svg { display: block; }

/* ---- edit-lock: the two-tone state (LOCKED gold-quiet · UNLOCKED electric) --- */
/* locked (default): the resting quiet button — nothing to shout                 */
/* unlocked = editing live: the button lights ELECTRIC so locked != unlocked      */
/* THIS RULE WAS DEAD UNTIL S24.1656, and nothing caught it for eleven sessions.  */
/* tabstrip-actions.js built the cluster's classes as "tsa-btn--" + action.id,    */
/* and the id is `edit-lock` — so the element was `.tsa-btn--edit-lock` and this  */
/* selector matched NOTHING. The unlocked/electric state has never once painted;  */
/* the padlock swapped its glyph and stayed the same colour, which reads as a     */
/* deliberately quiet design rather than a miss, which is exactly why the eye     */
/* never flagged it. The reseated button (renderLock) is named `.tsa-btn--lock`   */
/* directly, so the rule below is finally the rule it was always written to be.   */
.tsa-btn--lock.is-unlocked {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
  background: var(--accent-dim);
}
.tsa-btn--lock.is-unlocked:hover {
  color: var(--accent-lit);
  background: color-mix(in srgb, var(--accent) 24%, transparent);
}

/* ---- the strip's own locked/unlocked distinction (host toggles .is-locked) --- */
/* LOCKED (default): the per-tab edit affordances retract — the strip is a calm    */
/* switcher, not an editor. The host ALSO gates the mutations; this is the visual   */
/* half of the same truth (belt + suspenders, Niamh's "must not look the same").    */
#forest-tab-strip.is-locked .tab__pin,
#forest-tab-strip.is-locked .tab__close { display: none; }

/* UNLOCKED = edit mode live: a restrained electric underline says "editable now"  */
#forest-tab-strip:not(.is-locked) .tabstrip {
  border-bottom-color: color-mix(in srgb, var(--accent) 55%, var(--line-bark));
}
#forest-tab-strip:not(.is-locked) .tab { cursor: grab; }

@media (prefers-reduced-motion: reduce) {
  .tsa-btn { transition: none; }
}

/* ---- settings: the honest-deferred surface (real button, honest destination) - */
.tsa-settings-pop {
  z-index: 40; max-width: 15rem;
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border: 1px solid var(--line-gold);
  border-radius: var(--r-m);
  box-shadow: var(--shadow-lit, 0 8px 24px rgba(0,0,0,0.35));
}
.tsa-settings-pop__t {
  margin: 0 0 var(--s-1); font-family: var(--face-survey);
  font-size: var(--t-small); color: var(--ink); letter-spacing: 0.02em;
}
.tsa-settings-pop__d { margin: 0; font-size: var(--t-small); color: var(--ink-soft); line-height: 1.4; }

/* ============================================================================
   Shea's Forest — DP-017 Track P  ·  the pane keep-alive pool          (§pane)
   One live pane PER capability stays mounted in #forest-pane; a tab switch is
   show/hide (the [hidden] attribute), never destroy/rebuild, so pane state
   (form inputs, expanded sections, rendered DOM) SURVIVES a switch. Only the
   active pane is visible; the rest are display:none — no layout, no paint — but
   keep their DOM + state. Pool bounded most-recent-N=8 in shell/pane.js; older
   panes are released and re-mount cleanly on re-select (Web-App/Product-UI
   module Step 7 edge/recovery). Prefix-disjoint from frame-* / Track-B / .pane--*.
   ============================================================================ */
#forest-pane > .pane[hidden] {
  display: none;            /* pooled but inactive: costs no space, keeps DOM+state */
}
#forest-pane > .pane:not([hidden]) {
  margin-top: 0;            /* the active pane sits flush at the content top */
}

/* ============================================================================
   Shea's Forest — the Loop World wordmark (product brand)           (§wordmark)
   The PRIMARY LOCKUP (the shared mark + "Loop World") sits in .app-brand (FIXED-left top-
   bar slot). Its geometry is VERBATIM from the SHARED locked mark
   design/brand/loop-mmt-wordmark-v1.svg (blob 4a248fbb3c, LOCKED §B9 — the same
   mark Loop World inherits) — this section only RE-SKINS it for the dark top-bar by re-
   binding the three skin tokens the mark was designed to swap
   (--id-accent / --id-ink / --id-quiet) — "identity ≠ skin". Fonts (Fraunces,
   Spline Sans Mono) load in <head>. Prefix-disjoint from frame-* / Track-B / §pane.
   ============================================================================ */
.id-wordmark {
  display: block;
  height: 27px;            /* 90% of 30px (operator S12.11xx: "the logo is a touch too big"); width auto from the viewBox */
  width: auto;
  overflow: visible;       /* the gold bit rides above the loop's top run */

  /* ═══ THE SKIN CALL IS MADE — operator B, S13.0109 (DP-017 R5) ═══
     The loop STEPS COOL and the bit KEEPS THE GOLD, so the one gold bit is the
     mark's — and the app's — LONE WARM POINT. This is what tokens.css has said
     in prose since DP-017 ("the ONE warm point is the mark's gold bit"); the mark
     had been silently disagreeing. They now agree.

     --id-loop takes the INK family, not --accent: the loop is STRUCTURE, and
     --accent (#3B9EFF) means "active / selected / focus" in this app — spending it
     on a static logo would make the brand read as an interactive, selected thing.
     The structure colour is the honest one.

     The WORD goes to ink, not gold. If the word stayed gold the bit would NOT be
     the lone warm point — a large gold word would be — and B would contradict its
     own reason. The canonical SVG already sets .word in --id-ink; this brings the
     app INTO AGREEMENT with the mark rather than inventing anything.
     To revert to all-gold: set --id-loop and --id-ink back to var(--gold). One line. */
  --id-accent: var(--gold);       /* THE BIT — the lone warm point   */
  --id-loop:   var(--ink-soft);   /* the loop — cool structure       */
  --id-ink:    var(--ink);        /* "Loop World" (one unified word) */
}
/* BOLD mark (operator S12.11xx: "bold the loop mark — thicker, all around; the little
   circle/bit is part of the SAME discrete unit"). A SKIN weight set in the re-skin layer —
   the canonical SVG geometry (rx 27, rect dims, cx/cy) is untouched; only the loop stroke
   weight (5.5 -> 12.35) and the bit radius (8.5 -> 13.31, in the markup) are lifted, so the
   mark reads clearly bold at the 90% size without a redraw. The canonical wordmark SVG
   carries the same weight so the two never drift. "identity != skin." */
/* ---- the version stamp, UNDER the wordmark (operator, S24.1620) --------------
 * It used to live in the actions cluster on the far right (`.tsa-ver`). It now sits
 * beneath the Loop World lockup, flush-right to the wordmark's own right edge.
 *
 * TWO CONSTRAINTS, BOTH HARD (operator): the wordmark does not move vertically, and
 * the 56px bar does not grow. A normal column stack breaks BOTH -- stacking two boxes
 * in the centred brand slot re-centres the pair and lifts the mark ~7px.
 *
 * So the stamp is taken OUT OF FLOW: `.app-brand__mark` is a zero-cost wrapper sized
 * exactly to the SVG, and `.app-ver` hangs off its bottom edge absolutely. The mark's
 * layout is byte-identical to before (measured: y 14.00 -> 41.00, unchanged), and the
 * stamp lands in the 14px of dead air that was already sitting under it inside the
 * 55px brand box. Nothing grew; the space was always there.
 *
 * `right: 0` on the wrapper == the wordmark's right edge, so the two are flush by
 * construction rather than by a hand-set offset that drifts when the lockup changes. */
.app-brand__mark { position: relative; display: inline-flex; align-items: center; min-width: 0; }
.app-ver {
  position: absolute;
  top: 100%;                        /* up 2px from calc(100% + 2px) — operator, S08.0008 */
  right: 2.67px;                    /* FLUSH with the "Loop World" TEXT, not the SVG box edge.
                                       The viewBox (516u) overruns the text by 8.51u; at the mark's
                                       rendered 162px (0.314px/u) that is 2.67px of dead space to the
                                       right of the final "d". Measured in a headless render (getBBox
                                       of .word vs the viewBox right edge), S08.0008 — not eyeballed.
                                       Was right: 0 (flush to the SVG box, which overshot). */
  font-family: var(--face-survey);
  font-size: 0.625rem;              /* 10px -- a step down from the old 11px --t-label */
  letter-spacing: 0.04em;
  line-height: 1;
  color: var(--ink-faint);
  white-space: nowrap;
  user-select: none;
  pointer-events: auto;             /* it still carries the runtime-commit tooltip */
}
.app-ver:empty { display: none; }   /* no stamp -> no element, never an empty box */

.id-wordmark .loop  { fill: none; stroke: var(--id-loop); stroke-linejoin: round; stroke-width: 12.35; }
.id-wordmark .bit   { fill: var(--id-accent); }
.id-wordmark .word  { font-family: "Fraunces", Georgia, "Times New Roman", serif; fill: var(--id-ink); }

/* LIGHT-GROUND legibility: on the light skin the top bar is white, so the all-gold
   "Loop World" word washes out — set the WORD in ink and keep the loop + gold bit as the
   warm mark. (Legibility call, operator-overridable — the loop stays gold.) */
:root[data-theme="light"] .id-wordmark { --id-ink: var(--ink); }   /* now redundant — the word is ink on BOTH themes since the S13.0109 skin call; kept as a no-op guard */

/* narrow viewports: shrink the mark a touch so the FIXED brand leaves room for tabs */
@media (max-width: 640px) {
  .id-wordmark { height: 23px; }   /* 90% of 26px, matching the desktop shrink */
}

/* ── §3 SKIN CALL — DECIDED. Operator picked B (S13.0109): cool loop, lone gold
   bit. Executed in the re-skin layer above (--id-loop). The 'pending the operator's
   pick' placeholder that stood here for four sessions is retired — a stale 'NOT
   decided here' comment on a decided question is worse than no comment.
   Canonical marks: design/brand/loop-mmt-wordmark-v2.svg + loop-world-wordmark-v2.svg.
   ───────────────────────────────────────────────────────── */

/* ============================================================================
   §settings — the Settings pane · Sources (the connect-Google surface, DP-017)
   ----------------------------------------------------------------------------
   Re-seats the connector/link surface DP-017 Track F dropped (it replaced the
   grove-homepage that mounted it; app.js still ships the code). An in-frame
   pane over .app-body, toggled by the top-bar Settings action (shell-boot
   repoints forest:actions-settings → #settings-pane). Dark DP-017 tokens; the
   inner connector/link controls REUSE the gate__*/conn-* classes from app.css,
   already dark under the Track T token compat layer — so the surface reads
   coherent with the frame with no re-style. The Door (z-index 1000) still
   covers this (z-index 40): the app gate always wins.
   ========================================================================== */
.app-body { position: relative; }                 /* anchor for the settings overlay */

.settings-pane {
  position: absolute; inset: 0; z-index: 40;
  display: flex; flex-direction: column;
  background: var(--ground);
  animation: settings-in 140ms ease-out;
}
.settings-pane[hidden] { display: none; }
@keyframes settings-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .settings-pane { animation: none; } }

/* bar — title + close; mirrors the top-bar chrome */
.settings-pane__bar {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s-4) var(--s-5);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.settings-pane__title {
  margin: 0; font-family: var(--face-canopy); font-weight: 500;
  font-size: var(--t-title); line-height: 1; letter-spacing: -0.01em;
  color: var(--ink);
}
.settings-pane__close {
  flex: 0 0 auto; cursor: pointer;
  width: 2rem; height: 2rem; line-height: 1;
  display: grid; place-items: center; font-size: 1.5rem;
  color: var(--ink-soft); background: transparent;
  border: 1px solid transparent; border-radius: var(--r-s);
  transition: color 120ms, background 120ms, border-color 120ms;
}
.settings-pane__close:hover { color: var(--ink); background: var(--surface-2); border-color: var(--line); }
.settings-pane__close:focus-visible { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px var(--line-gold); }

/* body — left settings-nav rail + the scrolling section column */
.settings-pane__body {
  flex: 1 1 auto; min-height: 0;
  display: flex; align-items: stretch; overflow: hidden;
}
.settings-nav {
  flex: 0 0 200px; min-width: 0;
  padding: var(--s-5) var(--s-3);
  background: var(--surface);
  border-right: 1px solid var(--line);
  overflow-y: auto; scrollbar-width: thin;
}
@media (max-width: 720px) { .settings-nav { display: none; } }
.settings-nav__item {
  display: block; padding: var(--s-2) var(--s-3);
  font-family: var(--face-survey); font-size: var(--t-small);
  letter-spacing: 0.01em; color: var(--ink-soft); border-radius: var(--r-s);
}
.settings-nav__item.is-active {
  color: var(--ink); background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--gold);
}

.settings-sections {
  flex: 1 1 auto; min-width: 0; min-height: 0;
  overflow-y: auto; scrollbar-width: thin;
  padding: var(--s-6) var(--s-6) var(--s-8);
}

/* a section — header + blocks, centered reading column */
.settings-section { max-width: 640px; margin: 0 auto; }
.settings-section__head { margin-bottom: var(--s-5); }
.settings-section__title {
  margin: 0 0 var(--s-2); font-family: var(--face-canopy); font-weight: 500;
  font-size: var(--t-h); color: var(--ink);
}
.settings-section__gloss {
  margin: 0; font-family: var(--face-path); font-size: var(--t-small);
  line-height: 1.5; color: var(--ink-soft); max-width: 52ch;
}

/* a block within a section (connected sources / connect a source) */
.settings-block {
  margin-top: var(--s-5); padding: var(--s-5);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-m);
}
.settings-block[hidden] { display: none; }
.settings-block__head { margin-bottom: var(--s-4); }
.settings-block__label {
  display: block; font-family: var(--face-survey);
  font-size: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--gold);
}
.settings-block__gloss {
  display: block; margin-top: var(--s-1);
  font-family: var(--face-path); font-size: var(--t-small);
  line-height: 1.4; color: var(--ink-faint);
}

/* the reused gate__panel form flattens inside the block (drop its own frame so
   there's no double border), and the connector list sits flush */
.settings-block .gate__panel { margin: 0; padding: 0; background: transparent; border: 0; }
.settings-block .conn-list { margin: 0; }


/* ===== DP-019 C2 — the contacts bulk surface ==================================== *
 * The SKIN is inherited, not forked: `.contacts-row__check` / `.contacts__bulk` /
 * `.contacts__undo` join mail's selectors above (DP-012 — one alphabet, zero marginal
 * CSS). Only the two things contacts genuinely has and mail does NOT get new rules:
 *
 *   --danger    mail's bulk actions are archive + mark-read, both REVERSIBLE, so mail has
 *               no destructive chip and needs no danger tone. Contacts' `delete` is a HARD
 *               PURGE (loopcontact.js:1164 — cascade FK delete, graph node and edges gone,
 *               no trash). A purge that LOOKS like an archive is the lie the tone exists to
 *               prevent, and a bulk purge that looks like an archive fails the Higgins Test
 *               outright: strip the words and it is indistinguishable from the button beside
 *               it that can be taken back.
 *
 *               ⚠ OPEN DESIGN CALL (carded to the operator, DP-019 U3). The Forest palette
 *               has NO danger tone — moss, gold, ink, bark, by design; the Forest is calm and
 *               there is no red in it anywhere. So this rule does NOT invent one. It reaches
 *               for `--gold`, the palette's single high-attention accent (the wordmark's one
 *               warm bit), and lets the SECOND ACT carry the rest of the weight. That is a
 *               real design decision with a real cost — gold currently reads "accent," not
 *               "careful," and overloading it is a semantic collision. Whether the Forest
 *               grows a true danger token belongs to the design language (DP-006), not to a
 *               renderer. Existing tokens only until the operator rules.
 *   -confirm    the blocking second act. mail's safety net is the undo toast; a purge has
 *               nothing to undo TO, so the friction moves BEFORE the write, not after it. The
 *               count and the cascade are named in the sentence — the user reads how much they
 *               are about to lose, never a bare "are you sure?".
 * Existing tokens only. No new custom properties. No hex. */
.strip__action--danger {
  color: var(--gold);
  border-color: var(--line-gold);
  font-weight: 600;
}
.strip__action--danger:hover,
.strip__action--danger:focus-visible {
  color: var(--ink);
  background: color-mix(in srgb, var(--gold) 18%, transparent);
  border-color: var(--gold);
  outline: none;
}
.contacts__bulk-confirm {
  flex-basis: 100%;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
  margin-top: var(--s-2); padding-top: var(--s-2);
  border-top: 1px solid var(--line-gold);
}
.contacts__bulk-confirm-text {
  flex-basis: 100%;
  color: var(--ink-soft);
  font-family: var(--face-survey);
  font-size: var(--t-small);
  line-height: 1.45;
}

/* T5 / seq=469 — THE CEILING, on the control he reads (Screening view only).
   The Cannot's DEPENDENCY tier rendered: this app cannot verify who actually sent a
   message (no DKIM, no SPF in a browser), so the approve control is labelled with what
   it CANNOT do — never dressed as a guard it cannot keep (Graham's law).

   Deliberately QUIET, not a warning: --ink-soft on the surface, no red, no icon, no
   alarm. It is a standing truth about the instrument, not an incident. Dressing an
   honest ceiling as a danger state would teach him to dismiss it, and the one thing
   this line cannot afford is to be dismissed. It rides existing tokens only — the
   Forest's own design system, hand-authored (no new token, no new scale). */
.mail__screen-ceiling {
  color: var(--ink-soft);
  font-family: var(--face-survey);
  font-size: var(--t-small);
  line-height: var(--leading-body);
  max-width: var(--measure);
  padding: var(--s-1) 0 var(--s-1) var(--s-1);
  border-left: var(--line);
}

/* ── contacts · the merge CompareDialog (DP-019 slot 07) ───────────────────────
   Tokens only, per the standing rule — no hex invented, no new hue, and no token
   invented either: the first draft of this block reached for `--space-2`/`--radius-1`/
   `--ink-dim`, none of which exist in tokens.css. An undefined custom property does not
   error — it silently drops the whole declaration — so it would have shipped as a
   half-styled dialog that every test still called green. The real scale is --s-1..--s-8,
   --r-s/m/l, --ink/--ink-soft/--ink-faint. Read the vocabulary; do not guess it.

   The only new semantics: (a) `is-picked` — the value that will survive, on the SAME
   accent contacts already uses; (b) `is-conflict` — a real disagreement, said with
   WEIGHT rather than colour, because the Forest has no danger token (DP-019 §7 U3 is
   still the operator's call and this dialog does not pre-empt it); (c) `is-blank` — an
   em-dash at --ink-faint. An honest empty, never a hidden row. */
.contacts-compare { margin-top: var(--s-3); padding: var(--s-3);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-m); }
.contacts-compare__head { font-family: var(--face-survey); font-size: var(--t-small);
  color: var(--ink); margin-bottom: var(--s-2); }
.contacts-compare__reason { color: var(--ink-soft); font-size: var(--t-small); margin-bottom: var(--s-2); }
.contacts-compare__survivor { display: flex; align-items: center; gap: var(--s-2);
  margin-bottom: var(--s-3); flex-wrap: wrap; }
.contacts-compare__survivor-label { color: var(--ink-soft); font-size: var(--t-small); }
.contacts-compare__keep { padding: var(--s-1) var(--s-2); border: 1px solid var(--line);
  border-radius: var(--r-s); background: transparent; color: var(--ink);
  font: inherit; cursor: pointer; }
.contacts-compare__keep.is-on { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.contacts-compare__grid { display: flex; flex-direction: column; gap: var(--s-1); }
.contacts-compare__row { display: grid; grid-template-columns: 7rem 1fr 1fr;
  gap: var(--s-2); align-items: stretch; }
.contacts-compare__row.is-conflict .contacts-compare__field { color: var(--ink); font-weight: 600; }
.contacts-compare__field { color: var(--ink-soft); font-size: var(--t-small); align-self: center; }
.contacts-compare__val { text-align: left; padding: var(--s-1) var(--s-2);
  border: 1px solid var(--line); border-radius: var(--r-s); background: transparent;
  color: var(--ink); font: inherit; cursor: pointer; overflow-wrap: anywhere; }
.contacts-compare__val.is-picked { border-color: var(--accent); background: var(--surface); }
.contacts-compare__val.is-blank { color: var(--ink-faint); }
.contacts-compare__empty { color: var(--ink-soft); font-size: var(--t-small); }
.contacts-compare__foot { display: flex; gap: var(--s-2); margin-top: var(--s-3); }
.contacts-merge__reason { color: var(--ink-soft); font-size: var(--t-small); }

/* ============================================================================
   AUTHORITY — the B4 Gate console interiors (S13.1359, owed 331)
   The .gate__* frame survived DP-017 Track F; these — the classes app.js
   actually RENDERS INTO (renderActiveAuthority / renderCompositionBanner /
   renderHaltSurface / renderDenied / renderResolved) — did not, so the console
   would have lit up as unstyled div-stacks. Tokens only; zero new palette.
   ========================================================================= */

/* --- S4: standing authority ---------------------------------------------- */
.active__list { display: flex; flex-direction: column; gap: var(--s-2); }
.active__empty {
  margin: 0; padding: var(--s-3);
  color: var(--ink-soft); font-size: var(--t-small); line-height: var(--leading-body);
  border: 1px dashed var(--line); border-radius: var(--r-m); background: var(--surface-2);
}
.active__row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-3);
  border: 1px solid var(--line); border-left: 3px solid var(--moss);
  border-radius: var(--r-m); background: var(--surface-card);
}
.active__what { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.active__key { color: var(--ink); font-size: var(--t-body); font-weight: 600; }
.active__scope {
  color: var(--ink-soft); font-size: var(--t-micro);
  font-family: var(--face-mono); letter-spacing: var(--tracking-caps);
}

/* --- S6: the composition banner (aggregate blast radius over the grants) --- */
.active__composition {
  margin-bottom: var(--s-3); padding: var(--s-3);
  border: 1px solid var(--line); border-radius: var(--r-m); background: var(--surface-2);
}
.active__composition--safe { border-left: 3px solid var(--moss); }
.active__composition--halt { border-left: 3px solid var(--gold); background: var(--gold-soft); }
.active__composition-title {
  display: block; margin-bottom: 2px;
  color: var(--ink); font-size: var(--t-label); font-weight: 600;
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
}
.active__composition-body { color: var(--ink-soft); font-size: var(--t-small); line-height: var(--leading-body); }
.active__composition-grants { color: var(--ink-faint); font-size: var(--t-micro); font-family: var(--face-mono); }

/* --- S3: the HALT surface -------------------------------------------------
   Three states, one card. The mark carries the state at a glance; the kicker
   names it in words. Never a colour-only signal (Higgins). ------------------ */
.gate__surface { margin: var(--s-4) 0; }
.surface__card {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-2) var(--s-3);
  padding: var(--s-4);
  border: 1px solid var(--line-strong); border-radius: var(--r-l);
  background: var(--surface-card); box-shadow: var(--shadow-card);
}
.surface__card--denied   { border-left: 4px solid var(--bark); }
.surface__card--done     { border-left: 4px solid var(--moss); }
.surface__card--declined { border-left: 4px solid var(--line-strong); }
.surface__mark {
  grid-row: 1 / span 2; align-self: start;
  width: 2rem; height: 2rem; display: grid; place-items: center;
  border-radius: var(--r-pill); background: var(--surface-2);
  color: var(--ink); font-size: var(--t-title); line-height: 1;
}
.surface__card--done .surface__mark { background: var(--moss); color: var(--ink-on-dark); }
.surface__head { min-width: 0; }
.surface__kicker {
  color: var(--ink-soft); font-size: var(--t-label);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
}
.surface__claim {
  margin-top: 2px;
  color: var(--ink); font-size: var(--t-h); line-height: var(--leading-tight);
  font-family: var(--face-canopy);
}
.surface__fields { grid-column: 2; margin: 0; display: flex; flex-direction: column; gap: var(--s-2); }
.surface__row { display: grid; grid-template-columns: 9rem 1fr; gap: var(--s-3); align-items: baseline; }
.surface__row dt {
  color: var(--ink-faint); font-size: var(--t-micro);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
}
.surface__row dd {
  margin: 0; color: var(--ink); font-size: var(--t-small);
  font-family: var(--face-mono); overflow-wrap: anywhere;
}
.surface__note {
  grid-column: 2; margin: 0;
  color: var(--ink-soft); font-size: var(--t-small); line-height: var(--leading-body);
}

/* ===================== LEG 4 (S13.1604) — THE STALENESS ALARM (frame region) =========
   The loud half of the connector alarm. It sits in the FRAME, above every app, because
   an alarm behind a gear click is still silence — and silence is the defect this whole
   campaign exists to close (248 contacts frozen thirteen days, rendered in confident
   quiet, for five sessions).

   It is HIDDEN unless there is something TRUE to say. All sources fresh -> nothing on
   screen. That restraint is what buys it the right to be loud when it does speak.

   THEO'S RULE HOLDS: never red, and never a bare count. The line NAMES the sources
   ("Contacts and Calendar stopped answering") because a name is actionable and a
   numeral just sends him hunting. Urgency is carried by WEIGHT and a gold rule, not by
   an alarm colour the rest of this app has never used.

   H3, at frame scale: the `unknown` tone (we could not reach the freshness read) wears
   the DASHED rule — the same hollow form the honest-badge uses — so "I could not look"
   is visibly a different KIND of statement from "something is wrong."             */
.app-alarm {
  flex: 0 0 auto;                                   /* FIXED height in the frame column */
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-4);
  border-bottom: 1px solid var(--line-moss);
  border-left: 3px solid var(--badge-known-due);
  background: color-mix(in srgb, var(--gold) 7%, var(--surface));
  font-family: var(--face-path); font-size: var(--t-small); color: var(--ink);
}
.app-alarm[hidden] { display: none; }               /* the UA rule, restated — .app-* sets display */

.app-alarm__text { flex: 1 1 auto; min-width: 0; }
.app-alarm__act {
  flex: 0 0 auto;
  font-family: var(--face-survey); font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.06em;
  padding: 5px 14px; border-radius: var(--r-s);
  border: 1px solid var(--line-gold);
  background: transparent; color: var(--gold); cursor: pointer;
}
.app-alarm__act:hover { background: color-mix(in srgb, var(--gold) 14%, transparent); }

/* THE DISMISS × (S24.2221) — reveal-on-need, the .rail__slot-actions pattern (PX: reuse
   the shape, don't re-solve it). Hidden until the owner is actually AT the message, so
   the alarm's resting state is the sentence and its action, not a row of chrome — an
   escape hatch you have to reach toward, never an invitation to silence it.

   `visibility` + `opacity`, NOT `display:none`: the button keeps its box, so revealing
   it cannot reflow the sentence sideways under the cursor. It is reachable by KEYBOARD
   at all times — :focus-within reveals it, and focus is never trapped behind a hover a
   keyboard user cannot produce. This is a snooze until local midnight, not a silence;
   see paintAlarm()'s header for why that distinction is the whole permission. */
.app-alarm__dismiss {
  flex: 0 0 auto;
  width: 22px; height: 22px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-s);
  background: transparent; color: var(--ink-soft);
  font-size: 15px; line-height: 1; cursor: pointer;
  opacity: 0; visibility: hidden;
  transition: opacity 120ms ease;
}
.app-alarm:hover .app-alarm__dismiss,
.app-alarm:focus-within .app-alarm__dismiss {
  opacity: 1; visibility: visible;
}
.app-alarm__dismiss:hover { background: color-mix(in srgb, var(--bark) 10%, transparent); color: var(--ink); }
.app-alarm__dismiss:focus-visible { opacity: 1; visibility: visible; outline: 2px solid var(--line-gold); outline-offset: 1px; }

@media (prefers-reduced-motion: reduce) { .app-alarm__dismiss { transition: none; } }

/* FAILING — the source is REFUSING. Heavier rule, full-strength ink, bolder text. The
   one state where clicking Sync actively wastes the owner's time, so it earns weight. */
.app-alarm--failing {
  border-left-color: var(--badge-overdue);
  background: color-mix(in srgb, var(--gold) 12%, var(--surface));
}
.app-alarm--failing .app-alarm__text { font-weight: 600; }
.app-alarm--failing .app-alarm__act {
  background: var(--badge-overdue); color: var(--badge-overdue-ink); border-color: transparent;
}

/* UNLINKED — THE SOURCE IS NOT THERE. (S13.1718.) Same weight as `failing`, because the owner's
   mistake is the same one and it is the expensive one: reaching for Sync at a source that has no
   grant to sync. Thirteen days of frozen contacts were spent on exactly that click. The difference
   from `failing` is the WORD, not the volume — "not connected" sends him to Link; "stopped
   answering" sends him to re-link. Both are Settings; neither is Sync. Zero new hue (THEO: the
   alarm never reaches for red — this is the overdue token, the same one `failing` wears). */
.app-alarm--unlinked {
  border-left-color: var(--badge-overdue);
  background: color-mix(in srgb, var(--gold) 12%, var(--surface));
}
.app-alarm--unlinked .app-alarm__text { font-weight: 600; }
.app-alarm--unlinked .app-alarm__act {
  background: var(--badge-overdue); color: var(--badge-overdue-ink); border-color: transparent;
}

/* STALE / NEVER — sync will help. Calm. (Leg 2 auto-syncs these at the next sign-in, so
   in practice this line is transient; the ones that PERSIST are `failing` and `unlinked`.) */
.app-alarm--stale { border-left-color: var(--badge-known-due); }

/* UNKNOWN — H3 at frame scale. We could not reach the read. The DASHED rule says the
   app is reporting IGNORANCE, not a diagnosis. Deliberately not silent: an alarm that
   goes quiet precisely when the system is broken is not an alarm. */
.app-alarm--unknown {
  border-left-style: dashed;
  border-left-color: var(--badge-unreachable-line);
  background: color-mix(in srgb, var(--ink-faint) 8%, var(--surface));
  color: var(--ink-soft); font-style: italic;
}
.app-alarm--unknown .app-alarm__act { color: var(--ink-soft); border-color: var(--line-moss); }

@media (max-width: 560px) {
  .app-alarm { flex-wrap: wrap; gap: var(--s-2); }
}

/* ── T1.2 · THE TYPED PILL (search arc, S14.0216) ────────────────────────────
   F3: "Calendar" appeared three times in one undifferentiated wall — the app,
   the Horizon, and the connector — with no control to tell them apart. The type
   that WOULD have disambiguated them is exactly the thing soilItems flattened
   away (F2). T1.1 restores the groups; this gives each kind a visual grammar so
   the four categories stop being one chip.

   TOKENS ONLY — DP-017 (gold-vs-cool) is UNRESOLVED and this must not pre-empt
   the skin. So the grammar is carried by SHAPE, WEIGHT and BORDER TREATMENT, not
   by a new hue: every value below is an existing token reference, and the whole
   set re-skins correctly whichever way DP-017 lands.

   The grammar, and it is not arbitrary:
     app        the thing you OPEN            — filled, most present
     grove      a CLUSTER of trees            — gold hairline, squared off
     tree       a thing that HOLDS your stuff — a left rule, like a file tab
     horizon    a COMPUTED view               — dashed: derived, not stored
     connector  plumbing you're MIGRATING OFF — dotted + faint: it is on its way out,
                and the surface should quietly say so (the charter, in a border-style) */

.catalog__group--soil-app .catalog__item {
  background: color-mix(in srgb, var(--sunlight) 18%, transparent);
  border-color: var(--line-gold);
}
.catalog__group--soil-grove .catalog__item {
  border-radius: var(--r-2, 6px);
  border-color: var(--line-gold);
  background: transparent;
}
.catalog__group--soil-tree .catalog__item {
  border-radius: var(--r-2, 6px);
  border-left-width: 3px;
  border-left-color: var(--sunlight);
  background: color-mix(in srgb, var(--sunlight) 6%, transparent);
}
.catalog__group--soil-horizon .catalog__item {
  border-style: dashed;
  border-color: var(--line-strong);
  background: transparent;
}
.catalog__group--soil-connector .catalog__item {
  border-style: dotted;
  border-color: var(--line);
  background: transparent;
  color: var(--ink-soft);
}
.catalog__group--soil-connector .catalog__item:hover { color: var(--ink); }

/* the `added` state still wins over the kind grammar — a pill already on the strip
   is inert and greyed regardless of what kind it is. Specificity, stated on purpose. */
.catalog__group[class*="catalog__group--soil-"] .catalog__item--added {
  border-style: solid;
  border-color: var(--line-moss);
  border-left-width: 1px;
  background: transparent;
  color: var(--ink-soft);
}

/* ===================== email-app undo-send · the DOCK + the SENT flash =====================
   The reconsideration window moved OUT of the compose overlay. On a queued send: compose closes,
   a quiet "Sent" flash reassures, and a small dock arrives at the BOTTOM-LEFT carrying Undo + a
   dual countdown (numeric seconds + a shrinking ring). Calm by rule — fade, never flash; no motion
   louder than the rest of the Grove. Shell-level (position: fixed), so it survives the compose
   overlay closing and app-switches. Tokens only; no new palette. ======================= */

/* the quiet "Sent" flash — self-dismisses; reassures that the send went + compose closed */
.mail-sent-flash {
  position: fixed; left: 50%; top: var(--s-6, 24px);
  transform: translate(-50%, -6px);
  display: inline-flex; align-items: center; gap: var(--s-2, 8px);
  padding: var(--s-2, 8px) var(--s-3, 12px);
  background: var(--surface-card, #fff); color: var(--ink-soft, #556);
  border: 1px solid var(--border, rgba(0,0,0,.08)); border-radius: 999px;
  box-shadow: var(--shadow-card, 0 2px 10px rgba(0,0,0,.08));
  font-size: .82rem; z-index: 60; pointer-events: none;
  opacity: 0; transition: opacity .28s ease, transform .28s ease;
}
.mail-sent-flash[data-shown="1"] { opacity: 1; transform: translate(-50%, 0); }
.mail-sent-flash[data-leaving="1"] { opacity: 0; transform: translate(-50%, -6px); }
.mail-sent-flash__mark { color: var(--accent, #2f7d5b); font-weight: 600; }

/* the dock — seats into the BOTTOM-LEFT of the left pane. It is technically a body-level
   singleton (survives app-switches), but it is sized to the rail (--rail-w) and flush to the
   corner so it reads as part of the pane rising from its floor (operator note #1). Surface, hairline
   and shadow are the pane's own tokens; only the inner (top-right) corner rounds, so the two outer
   edges sit clean against the viewport corner. */
.mail-undo-dock {
  position: fixed; left: 0; bottom: 0;
  width: var(--rail-w, 200px); box-sizing: border-box;
  display: flex; flex-direction: column; gap: var(--s-2, 8px);
  padding: var(--s-3, 12px) var(--s-3, 12px) var(--s-4, 16px);
  background: var(--surface, #fff); color: var(--ink, #223);
  border-top: 1px solid var(--line, rgba(0,0,0,.10));
  border-right: 1px solid var(--line, rgba(0,0,0,.10));
  border-radius: 0 var(--radius, 12px) 0 0;
  box-shadow: 0 -10px 30px color-mix(in srgb, var(--shadow, #000) 22%, transparent);
  z-index: 55;
  opacity: 0; transform: translateY(10px);
  transition: opacity .3s ease, transform .3s ease;
}
.mail-undo-dock[data-shown="1"] { opacity: 1; transform: translateY(0); }
.mail-undo-dock[data-leaving="1"] { opacity: 0; transform: translateY(10px); }
.mail-undo-dock[data-landed="1"] { opacity: .9; }

/* the "message sent" heading — a small confident line above the action */
.mail-undo-dock__head {
  display: flex; align-items: center; gap: var(--s-1, 4px);
  font-size: .78rem; color: var(--ink-soft, #556);
}
.mail-undo-dock__label { font-weight: 600; letter-spacing: .01em; }

.mail-undo-dock__row { display: flex; align-items: center; gap: var(--s-3, 12px); }

/* the gauge: the ring with the numeric count centered inside it */
.mail-undo-dock__gauge { position: relative; width: 24px; height: 24px; flex: 0 0 auto; }
.mail-undo-dock__ring { width: 24px; height: 24px; transform: rotate(-90deg); display: block; }
.mail-undo-dock__ring-track { fill: none; stroke: var(--surface-2, #eef0ee); stroke-width: 2.5; }
.mail-undo-dock__ring-arc {
  fill: none; stroke: var(--accent, #2f7d5b); stroke-width: 2.5; stroke-linecap: round;
  transition: stroke-dashoffset .95s linear;   /* smooth, calm sweep between ticks */
}
.mail-undo-dock__num {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: .62rem; font-variant-numeric: tabular-nums; color: var(--ink-soft, #556);
}

/* the Undo action — a REAL primary button (note #2), the same buttony language as .rail__compose:
   accent fill, on-accent label, pill radius, hover-lift / active-press / gold focus ring. It fills
   the row so it reads as the thing to press, not a text link. */
.mail-undo-dock__undo {
  appearance: none; -webkit-appearance: none;
  flex: 1 1 auto; min-width: 0;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-1, 4px);
  padding: var(--s-2, 8px) var(--s-3, 12px);
  font: inherit; font-size: .86rem; font-weight: 600;
  color: var(--ink-on-accent, #fff);
  background: var(--accent, #2f7d5b);
  border: 1px solid var(--accent, #2f7d5b);
  border-radius: var(--r-pill, 999px);
  cursor: pointer; white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.mail-undo-dock__undo:hover { background: var(--accent-strong, var(--accent)); border-color: var(--accent-strong, var(--accent)); }
.mail-undo-dock__undo:active { background: color-mix(in srgb, var(--accent) 88%, var(--shadow, #000)); }
.mail-undo-dock__undo:focus-visible {
  background: var(--accent-strong, var(--accent)); border-color: var(--accent-strong, var(--accent));
  outline: none; box-shadow: 0 0 0 3px var(--ring, rgba(0,0,0,.2));
}

/* the "Edit Undo Settings" link — a small, quiet text button UNDER the action (note #4). The place a
   person decides the window was too short/long, so the way to change it sits right there — discreet,
   never competing with the primary button. */
.mail-undo-dock__settings {
  appearance: none; -webkit-appearance: none; border: 0; background: transparent;
  align-self: center;
  margin-top: calc(-1 * var(--s-1, 4px));
  padding: var(--s-1, 4px) var(--s-2, 8px);
  font: inherit; font-size: .72rem; color: var(--ink-faint, #889);
  cursor: pointer; border-radius: 6px;
  transition: color .15s ease, background .15s ease;
}
.mail-undo-dock__settings:hover { color: var(--ink-soft, #556); text-decoration: underline; text-underline-offset: 2px; }
.mail-undo-dock__settings:focus-visible { outline: none; color: var(--ink-soft, #556); box-shadow: 0 0 0 2px var(--ring, rgba(0,0,0,.2)); }
.mail-undo-dock__settings[hidden] { display: none; }

/* the done-state (note #3): after the window expires the dock shows a single calm "on its way" line
   with a soft accent checkmark, then fades. The row + settings link are hidden by the JS. */
.mail-undo-dock[data-done="1"] { opacity: .96; }
.mail-undo-dock[data-done="1"] .mail-undo-dock__head { color: var(--ink, #223); }
.mail-undo-dock__done-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; flex: 0 0 auto;
  font-size: .7rem; font-weight: 700; line-height: 1;
  color: var(--ink-on-accent, #fff); background: var(--accent, #2f7d5b);
  border-radius: 999px;
}

/* honor reduced-motion: still fade, but no transform slide, no ring sweep animation */
@media (prefers-reduced-motion: reduce) {
  .mail-sent-flash, .mail-undo-dock { transition: opacity .2s ease; transform: none; }
  .mail-sent-flash[data-shown="1"], .mail-undo-dock[data-shown="1"] { transform: none; }
  .mail-undo-dock__ring-arc { transition: none; }
}

/* ── the app-shell MINIMIZE → DOCK (compose-dock.js, L3/C1-c) ──────────────────────────────
   A bottom-RIGHT strip of minimized-draft tabs — where the compose overlay sits, opposite the
   bottom-LEFT undo-dock so the two never collide. A dumb shell-level view (document.body); it
   survives compose-close and app-switches. Calm: fade in/out, no flash. */
.compose-dock {
  position: fixed; right: var(--s-3, 12px); bottom: 0;
  display: flex; flex-direction: row; align-items: flex-end; gap: var(--s-2, 8px);
  max-width: calc(100vw - 2 * var(--s-3, 12px));
  flex-wrap: wrap-reverse;
  padding-bottom: var(--s-3, 12px);
  z-index: 54;
  opacity: 0; transform: translateY(10px);
  transition: opacity .3s ease, transform .3s ease;
  pointer-events: none;
}
.compose-dock[data-shown="1"] { opacity: 1; transform: translateY(0); }
.compose-dock__tab {
  pointer-events: auto;
  display: flex; align-items: center; gap: var(--s-2, 8px);
  max-width: 220px; box-sizing: border-box;
  padding: var(--s-2, 8px) var(--s-2, 8px) var(--s-2, 8px) var(--s-3, 12px);
  background: var(--surface, #fff); color: var(--ink, #223);
  border: 1px solid var(--line-bark, var(--line, rgba(0,0,0,.10)));
  border-bottom: none;
  border-radius: var(--radius, 12px) var(--radius, 12px) 0 0;
  box-shadow: 0 -8px 24px color-mix(in srgb, var(--shadow, #000) 18%, transparent);
}
.compose-dock__title {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--face-survey, inherit); font-size: var(--t-small, 0.85rem);
  color: var(--ink, #223); cursor: pointer;
}
.compose-dock__title:hover { color: color-mix(in srgb, var(--ink, #223) 100%, transparent); }
.compose-dock__title:focus-visible { outline: 2px solid var(--line-gold, #b8860b); outline-offset: 2px; border-radius: 3px; }
.compose-dock__x {
  flex: 0 0 auto;
  background: none; border: none; cursor: pointer; line-height: 1;
  font-size: 1.05rem; padding: 0 2px;
  color: color-mix(in srgb, var(--ink-soft, #667) 78%, transparent);
}
.compose-dock__x:hover, .compose-dock__x:focus-visible { color: var(--ink, #223); outline: none; }

/* ============================================================================ *
 * Contacts · the Design Language finish plane (DP-034 §5, S09.1855)            *
 * The two surfaces (roll + card) wear the shared .row/.record skin from        *
 * block.css; these rules add only the contacts-specific CENTERS that the       *
 * shared alphabet does not carry — the disc (C4), the face fill (C5), and the  *
 * folded expand/collapse motion (§5 play). Scoped to [data-kind="contacts"]    *
 * so mail's shared .row__monogram is never touched. Falsifiable bar:           *
 * wow ∧ colorblind-reads-every-state ∧ zero-ornament.                          *
 * ============================================================================ */

/* C4 · the disc — the gold-haloed avatar frame (SL·disc-halo). Quiet DP-017    *
 * surface fill, the LONE warm gold as the ring; the halo is the only accent.   */
[data-kind="contacts"] .row__monogram {
  background: color-mix(in srgb, var(--gold) 8%, var(--surface));
  border: 1px solid var(--line-gold);
  color: var(--ink-soft);
  box-shadow: var(--shadow-disc);          /* the gold ring — SL·disc-halo */
}

/* C5 · the face — a REAL photo fills the disc (Real-or-Made); initials otherwise.
 * `--photo` is a contacts-only class (mail never emits it), so it needs no scope. */
.row__monogram--photo {
  object-fit: cover;
  padding: 0;
  border: 1px solid var(--line-gold);
}

/* the record masthead disc — prominent (DP-034 C4: one size in the row, one in
 * the record). The row disc stays 1.75rem; the masthead reads as the identity. */
[data-kind="contacts"] .record__head .row__monogram {
  width: 3rem;
  height: 3rem;
  font-size: var(--t-body);
}

/* §5 play, FOLDED — delight-by-restraint. Opening a record is the fold's
 * `expand`: the card grows in on --ease. No bounce, no flourish; the motion IS
 * the fold made visible. Honors prefers-reduced-motion (the motion was never
 * there for those who ask for stillness). */
@media (prefers-reduced-motion: no-preference) {
  [data-kind="contacts"] .record {
    animation: contacts-record-expand 180ms var(--ease) both;
  }
}
@keyframes contacts-record-expand {
  from { opacity: 0; transform: translateY(4px) scale(0.994); }
  to   { opacity: 1; transform: none; }
}

/* C3 · SL-primary-marked — the primary reach-field is the one thing marked in a
 * quiet record: a small gold tag, the lone warm accent (DP-017). Scoped so the
 * shared .line stays neutral everywhere else. */
[data-kind="contacts"] .line__primary {
  flex: 0 0 auto;
  align-self: center;
  font-family: var(--face-survey);
  font-size: var(--t-label);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--gold);
  border: 1px solid var(--line-gold);
  border-radius: 999px;
  padding: 0 var(--s-2);
  line-height: 1.5;
}
[data-kind="contacts"] .line--primary .line__value { color: var(--ink); font-weight: 600; }

/* ============================================================================
 * DP-034 · Contacts full-width columnar row  (09.2058 redesign, wired S09.2142)
 * The list wastes horizontal space when the row rides mail's flex `.row` skin:
 * `button.row__open` is content-width, so name+reach cluster at the far left and
 * the pane's right two-thirds sit dead. This scopes a GRID to the contacts LIST
 * row only ([data-kind="contacts"] .contacts-list .row) — mail's shared `.row`
 * and the contacts RECORD masthead are never touched — laying the fields across
 * their own tracks:        [check]  disc   NAME | EMAIL | PHONE   [★]
 * The data already rides each row object (emails[]/phones[]); the renderer only
 * splits the one collapsed reachLine into two mono cells. `display:contents` on
 * the wrappers lets the existing DOM children fall straight into the tracks —
 * no DOM rewrite. Cell placement is EXPLICIT (grid-column) so the conditional
 * K4 due-dot and K1 label badges can never shove a column out of true.
 * The column header (.contacts-list__head) is a SIBLING above the <ul>, an
 * independent grid with the same template + width so its columns align with the
 * rows while the <ul> keeps holding exactly one child per contact.
 * ============================================================================ */
[data-kind="contacts"] .contacts-list { display: flex; flex-direction: column; }

[data-kind="contacts"] .contacts-list .row,
[data-kind="contacts"] .contacts-list__head {
  display: grid;
  /* S09.2142 turn-3 (block^3 — row ⊃ lead-cluster ⊃ {check,star,disc}): the three lead
     items are a CLUSTER, not three peer tracks sprayed apart by a full gap. Their tracks are
     sized to the disc (1.75rem) and held together by a snug column-gap; the content columns
     (name|email|phone) carry their own width. Every cell is pinned to row 1 below, so the star
     can never fall to a second row (the turn-2 auto-placement drop). */
  grid-template-columns:
    1.75rem 1.75rem 1.75rem minmax(180px, 1.3fr) minmax(170px, 1.6fr) minmax(130px, 0.9fr);
  align-items: center;
  column-gap: var(--s-3);
  row-gap: 2px;
  padding: var(--s-3) var(--s-4);
  position: relative;                       /* anchor for the absolute due-dot */
}
[data-kind="contacts"] .contacts-list .row:hover {
  background: color-mix(in srgb, var(--gold) 6%, transparent);
}

/* the row wrappers dissolve so their children become direct grid items of `.row` */
[data-kind="contacts"] .contacts-list .row__open,
[data-kind="contacts"] .contacts-list .row__body,
[data-kind="contacts"] .contacts-list .row__lead { display: contents; }

/* explicit tracks — EVERY primary cell is pinned to row 1, so auto-placement can never drop
   the star (DOM-last, column 2) onto a new row. The lead cluster is check · star · disc. */
[data-kind="contacts"] .contacts-list .contacts-row__check { grid-column: 1; grid-row: 1; justify-self: center; }
[data-kind="contacts"] .contacts-list .row__trail          { grid-column: 2; grid-row: 1; justify-self: center; }  /* star, left of the disc */
[data-kind="contacts"] .contacts-list .row__monogram       { grid-column: 3; grid-row: 1; justify-self: center; margin-top: 0; }
/* name flush-left: it also carries .field (a form-field flex skin) — pin it to a plain left cell */
[data-kind="contacts"] .contacts-list .row__title          { grid-column: 4; grid-row: 1; justify-self: start; text-align: left; margin: 0; }
[data-kind="contacts"] .contacts-list .row__title.field    { display: block; margin: 0; }
[data-kind="contacts"] .contacts-list .row__email          { grid-column: 5; grid-row: 1; }
[data-kind="contacts"] .contacts-list .row__phone          { grid-column: 6; grid-row: 1; }
/* the star: a BARE glyph (the app's one quiet warm accent), ~body +25%. Reset the UA button
   chrome (that stray white box) so it reads as a mark, not a control-with-background. */
[data-kind="contacts"] .contacts-list .row__trail .row__star {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  font-size: 1.25rem; line-height: 1; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
[data-kind="contacts"] .contacts-list .row__trail .row__star:focus-visible {
  outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 3px;
}

/* the two reach cells: mono, quiet, single-line with ellipsis (the "not Google" move) */
[data-kind="contacts"] .contacts-list .row__email,
[data-kind="contacts"] .contacts-list .row__phone {
  font-family: var(--face-survey);
  font-size: var(--t-small);
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  text-align: left;   /* email + phone flush-left within their columns (operator S09.2142 turn-2) */
}
/* honest-empty: a blank cell still holds its column; no fabricated dash */
[data-kind="contacts"] .contacts-list .row__cell--empty { color: transparent; }

/* the column header — the "arrayed to the side" affordance, mono label face */
[data-kind="contacts"] .contacts-list__head {
  cursor: default;
  border-bottom: 1px solid var(--line-strong);
  padding-top: var(--s-1);
  padding-bottom: var(--s-2);
}
/* the head cells are SORT BUTTONS (S09.2142 turn-2): they look like the mono label
   at rest and reveal themselves as clickable on hover/focus. The active column is
   inked up and carries a caret. This is the first instance of a sortable-column
   header; it is written to be liftable into a shared list pattern later (DP-034c). */
[data-kind="contacts"] .contacts-list__head .contacts-row__head {
  font-family: var(--face-survey);
  font-size: var(--t-label);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  cursor: pointer; text-align: left; justify-self: start;
  display: inline-flex; align-items: center; gap: 4px;
  transition: color 120ms ease;
}
[data-kind="contacts"] .contacts-list__head .contacts-row__head:hover      { color: var(--ink-soft); }
[data-kind="contacts"] .contacts-list__head .contacts-row__head.is-sorted  { color: var(--ink); }
[data-kind="contacts"] .contacts-list__head .contacts-row__head:focus-visible {
  outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 2px;
}
[data-kind="contacts"] .contacts-list__head .contacts-row__head-caret { font-size: 0.8em; line-height: 1; }
[data-kind="contacts"] .contacts-list__head .contacts-row__head--name  { grid-column: 4; }
[data-kind="contacts"] .contacts-list__head .contacts-row__head--email { grid-column: 5; }
[data-kind="contacts"] .contacts-list__head .contacts-row__head--phone { grid-column: 6; }

/* K4 due-glow: absolute so it never consumes a grid track (it rode the old flex row inline) */
[data-kind="contacts"] .contacts-list .contacts-row__due {
  position: absolute;
  left: 0.35rem;
  top: 50%;
  transform: translateY(-50%);
  margin: 0;
}

/* K1 label badges: tuck under the name on a second grid row; never a reach track */
[data-kind="contacts"] .contacts-list .contacts-labels {
  grid-column: 4 / -1;
  grid-row: 2;
  margin-top: 2px;
}

/* responsive fold <720: the reach columns stack under the name (graceful) */
@media (max-width: 720px) {
  [data-kind="contacts"] .contacts-list .row {
    grid-template-columns: 1.75rem 1.75rem 1.75rem 1fr;
    grid-template-areas:
      "sel star disc name"
      "sel star disc email"
      "sel star disc phone";
    row-gap: 2px;
  }
  [data-kind="contacts"] .contacts-list__head { display: none; }
  [data-kind="contacts"] .contacts-list .contacts-row__check { grid-area: sel; align-self: center; }
  [data-kind="contacts"] .contacts-list .row__monogram       { grid-area: disc; align-self: center; }
  [data-kind="contacts"] .contacts-list .row__title          { grid-area: name; }
  [data-kind="contacts"] .contacts-list .row__email          { grid-area: email; }
  [data-kind="contacts"] .contacts-list .row__phone          { grid-area: phone; }
  [data-kind="contacts"] .contacts-list .row__trail          { grid-area: star; align-self: center; }
  [data-kind="contacts"] .contacts-list .contacts-labels     { grid-column: 4; grid-row: auto; }
}
