:root,
[data-theme="dark"] {
  --bg: #15171c;
  --panel: #1d2026;
  --border: #2f333c;
  --fg: #e6e6e6;
  --dim: #7e828a;
  --accent: #6cc6ff;
  --hl: #ffd479;
  --warn: #ff7e7e;
  --ok: #8be09a;
  --json-string: #98c379;
  --json-number: #d19a66;
  --json-boolean: #c678dd;
  --json-null: #6a737d;
  --json-file: #61afef;
  --fault-bg: #3a1f1f;
  --overlay: rgba(0, 0, 0, 0.65);
  --shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}

/* Solarized-light-inspired: warm parchment base for low eye strain plus six
   distinct desaturated hues for chromatic variety without harsh contrast. */
[data-theme="light"] {
  --bg: #fdf6e3;            /* base3  — warm parchment */
  --panel: #eee8d5;         /* base2  — slightly darker panel */
  --border: #c8c0a8;         /* derived — softer than base1 for less visual noise */
  --fg: #073642;            /* base02 — deep teal-black for body text */
  --dim: #93a1a1;           /* base1  — secondary text */
  --accent: #268bd2;        /* blue   — links, hovers, focus */
  --hl: #b58900;            /* yellow — code, highlights, headings */
  --warn: #dc322f;          /* red    — errors, danger */
  --ok: #859900;            /* green  — success */
  --json-string: #859900;   /* green */
  --json-number: #cb4b16;   /* orange */
  --json-boolean: #6c71c4;  /* violet */
  --json-null: #93a1a1;     /* gray */
  --json-file: #2aa198;     /* cyan */
  --fault-bg: #fde2dd;      /* very pale red wash */
  --overlay: rgba(7, 54, 66, 0.35);
  --shadow: 0 8px 32px rgba(7, 54, 66, 0.15);
}

/* Catppuccin Frappé-inspired: warm dark base (avoids the harsh #15… of
   the standard dark theme), plus richer chromatic variety — peach, mauve,
   blue, green where the standard dark only has off-white + amber. Same
   contrast ratio against text but the eye fatigues much slower because
   the base isn't crushed-black. Counts as "dark" for Shoelace purposes
   (sl-theme-dark class is mirrored when this theme is active). */
[data-theme="warm"] {
  --bg: #303446;            /* base — warm slate */
  --panel: #414559;         /* surface0 */
  --border: #51576d;        /* surface2 */
  --fg: #c6d0f5;            /* text — soft cool white */
  --dim: #838ba7;           /* overlay1 */
  --accent: #ca9ee6;        /* mauve — links, focus, primary */
  --hl: #ef9f76;            /* peach — code, headings */
  --warn: #e78284;          /* red */
  --ok: #a6d189;            /* green */
  --json-string: #a6d189;   /* green */
  --json-number: #ef9f76;   /* peach */
  --json-boolean: #ca9ee6;  /* mauve */
  --json-null: #949cbb;     /* overlay2 */
  --json-file: #8caaee;     /* blue */
  --fault-bg: #4a3a3a;
  --overlay: rgba(48, 52, 70, 0.7);
  --shadow: 0 8px 40px rgba(0, 0, 0, 0.45);
}

/* High-contrast outdoor theme: in bright sun the limiting factor is reflected
   glare off the glass, which adds a near-constant amount of light to every
   pixel. A pure-white field driven at full brightness emits the most light and
   best overcomes that glare (the way ink-on-paper stays readable in sun), while
   a dark field would just mirror the surroundings. So: pure white, near-black
   text, deeply saturated accents (all ~4.5:1+ on white), no mid-grays — faint
   grays are the first thing to wash out. Pairs with Shoelace's LIGHT theme:
   kept OUT of the sl-theme-dark sets in app.js / settings.js / index.html. */
[data-theme="outdoor"] {
  --bg: #ffffff;            /* pure white — max emission vs. glare */
  --panel: #ffffff;         /* same as bg; panels delineated by strong borders */
  --border: #4a4a4a;        /* heavy border — does the work shadows can't outdoors */
  --fg: #000000;            /* pure black body text */
  --dim: #404040;           /* "dim" is still dark — faint gray vanishes in sun */
  --accent: #0050b3;        /* strong blue, ~5.9:1 on white */
  --hl: #8a5a00;            /* dark amber — yellow is unreadable on white */
  --warn: #c01010;          /* strong red */
  --ok: #007000;            /* strong green */
  --json-string: #006b00;
  --json-number: #b34700;
  --json-boolean: #6a1b9a;
  --json-null: #4a4a4a;
  --json-file: #006b75;
  --fault-bg: #ffd6d6;      /* pale red wash */
  --overlay: rgba(0, 0, 0, 0.6);
  --shadow: 0 4px 16px rgba(0, 0, 0, 0.35);  /* shadows barely read in sun — borders carry it */
}
/* Slightly heavier body text outdoors: thin strokes wash out under glare. */
[data-theme="outdoor"] body { font-weight: 500; }
/* Kanban cards hardcode a faint rgba(127,127,127,0.25) border (fine on a dark
   panel, near-invisible on pure white). Outdoor uses the same white for card
   and page bg, so the border is the ONLY separation — promote it to the heavy
   --border token. Scoped to outdoor so the other three themes are unchanged. */
[data-theme="outdoor"] .kanban-card { border-color: var(--border); }

* { box-sizing: border-box; }

/* ============================================================
   Global UI scale. The app is px-based, so the cleanest way to
   make everything bigger uniformly — text, controls, spacing,
   chrome — is a page-level `zoom`, exactly like setting the
   browser to 150%. Single knob: --ui-scale.

   `zoom` scales the whole render including viewport-locked boxes,
   so anything sized in vh/vw must be divided by --ui-scale to keep
   its intended fraction of the REAL viewport (see body height below
   and the palette/help overlay caps). New vh/vw rules on fixed
   overlays should follow the same `calc(<n>vh / var(--ui-scale))`
   pattern, or they'll render --ui-scale× too large and overflow.
   ============================================================ */
:root { --ui-scale: 1.5; }
/* Mobile (≤ 760px) skips the 150% zoom entirely. Phone viewports
 * already render at the right size via their own meta-viewport
 * scaling; layering an additional zoom on top made the UI ~25%
 * too large and — because every vh-based height was divided by
 * --ui-scale — clipped the kanban + calendar at ~65% of the
 * screen. Resetting --ui-scale to 1 at the mobile breakpoint
 * makes zoom a no-op AND restores the body height to a full 100vh.
 * Desktop / wide screens keep the 150% baseline. */
@media (max-width: 760px) {
  :root { --ui-scale: 1; }
}
html { zoom: var(--ui-scale); }

/* Mobile scroll-vs-select hygiene. Without this, dragging a finger
 * to scroll through a list of cards (kanban project cards, card-list
 * rows, activity feed, calendar event chips) starts an OS text
 * selection on whatever row the touch first landed on, AND the
 * default tap-highlight flashes the row gray — so a scroll reads
 * visually as "I selected those items."
 *
 * Earlier passes scoped this to forms only (see the .form-fields
 * block farther down) and to the legacy .kanban-card class. As more
 * scroll surfaces appeared (card-list, FullCalendar, activity feed,
 * Profile screens) the patch-by-patch approach kept missing one.
 * The rule below covers the whole #view-root in one shot and opts
 * editable inputs back in.
 *
 * Desktop is unaffected — mouse drag-to-select on cards is a power-
 * user feature there (copy text out of a card title for search),
 * and the tap-highlight only fires on touch. */
@media (max-width: 760px) {
  #view-root, #view-root * {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }
  /* Re-enable selection where the user actually needs it: typing
     into inputs, multi-tap to select a word, copying SQL/code
     output. .user-selectable is an explicit opt-in escape hatch
     for any future block that wants triple-tap-to-copy. */
  #view-root input,
  #view-root textarea,
  #view-root [contenteditable="true"],
  #view-root .user-selectable,
  #view-root .user-selectable * {
    -webkit-user-select: text;
    user-select: text;
    -webkit-touch-callout: default;
  }
}

body {
  margin: 0;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 13px;
  background: var(--bg);
  color: var(--fg);
  display: flex;
  flex-direction: column;
  /* 100vh would be scaled by `zoom` to 100vh×--ui-scale and overflow
     the screen vertically; divide it back so the shell fills exactly
     one viewport. */
  height: calc(100vh / var(--ui-scale));
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}

h1 {
  font-size: 12px;
  margin: 0;
  letter-spacing: 0.08em;
  color: var(--dim);
  text-transform: uppercase;
}

h2 {
  font-size: 11px;
  margin: 0 0 8px;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.dim { color: var(--dim); font-weight: normal; }

.controls { display: flex; gap: 6px; }

input, button {
  font: inherit;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 5px 9px;
  border-radius: 2px;
}

#push-input { min-width: 320px; }

#view-root input[type="checkbox"] {
  width: auto;
  min-width: 0;
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
}

button { cursor: pointer; }
button:hover { border-color: var(--accent); color: var(--accent); }
button:active { background: var(--panel); }

main {
  display: grid;
  grid-template-columns: 1fr 360px;
  flex: 1;
  min-height: 0;
}

#view-pane {
  padding: 16px 18px;
  overflow: auto;
  border-right: 1px solid var(--border);
}

.frame-header {
  font-size: 11px;
  color: var(--dim);
  margin-bottom: 14px;
  padding-bottom: 8px;
  border-bottom: 1px dashed var(--border);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  gap: 10px;
}

.frame-header code { color: var(--hl); }
.frame-header #frame-back-btn { font-size: 11px; padding: 2px 10px; letter-spacing: 0.06em; }
.frame-header #frame-back-btn:disabled { opacity: 0.4; cursor: not-allowed; }

#view-root .box-col { display: flex; flex-direction: column; gap: 8px; }
#view-root .box-row { display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px; align-items: center; }

#view-root { display: flex; flex-direction: column; gap: 12px; }
#view-root label { color: var(--dim); display: block; margin-bottom: 4px; }
#view-root p { margin: 0; line-height: 1.5; }
#view-root code { color: var(--hl); }
#view-root h4 {
  font-size: 11px;
  margin: 8px 0 -4px;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
#view-root .actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
#view-root input { min-width: 280px; }
/* Restore checkbox + radio to their natural compact width — the
   global min-width above sets a comfortable floor for text inputs
   but balloons checkbox controls so the label gets pushed off to
   the far edge of the form row. This sits after the global rule
   on purpose so source-order wins (specificity is identical). */
#view-root input[type="checkbox"],
#view-root input[type="radio"] { min-width: 0; }
#view-root pre {
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 8px 10px;
  overflow-x: auto;
  font-size: 12px;
  margin: 0;
  color: var(--fg);
}
#view-root .ok { color: var(--ok); }
#view-root .failed { color: var(--warn); }
#view-root .dim { color: var(--dim); }

#view-root .form-fields {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 520px;
}
#view-root .field { display: flex; flex-direction: column; gap: 3px; }
#view-root .field label { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
#view-root .field input[type="text"],
#view-root .field input[type="number"],
#view-root .field input[type="email"],
#view-root .field input[type="password"],
#view-root .field select,
#view-root .field textarea {
  font: inherit;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 5px 8px;
  border-radius: 2px;
  min-width: 0;
  width: 100%;
}
#view-root .field textarea { resize: vertical; }
#view-root .select-with-create { display: flex; gap: 6px; align-items: stretch; }
#view-root .select-with-create select { flex: 1 1 auto; }
#view-root .select-with-create .ref-create { flex: 0 0 auto; white-space: nowrap; }
/* Phase changer on the project detail page: option labels are short
   ("Final Measure" is the widest at ~13 chars). Letting the select
   span the form width makes the changer feel like a primary field
   instead of the quick action it is. Cap at a comfortable width
   that fits the longest option + the native chevron.   */
#view-root .field select[name="phase"] {
  width: auto;
  min-width: 180px;
  max-width: 260px;
}
#view-root .field-checkbox {
  flex-direction: row;
  align-items: center;
  gap: 8px;
  position: relative;
}
#view-root .field-checkbox label {
  text-transform: none;
  letter-spacing: 0;
  font-size: 13px;
  color: var(--fg);
}
/* Mobile scroll fix: dragging a finger up/down anywhere on a form
 * row used to highlight the row text via OS text-select, which read
 * as "scrolling is selecting items." First pass scoped this to labels
 * + legends only, but iOS happily starts selection on the row's div
 * container (.field, .field-checkbox) when the touch lands between
 * label and input. Widen the no-select region to the whole form
 * surface, then carve out the editable inputs where typing /
 * copy-paste actually need text selection.
 *
 * touch-action: manipulation also tells the browser to skip the
 * click-delay heuristics that let borderline-stationary touches still
 * fire a click after a scroll. Inputs keep auto so multi-tap to
 * select a word still works inside them. */
#view-root .form-fields,
#view-root .form-section,
#view-root .form-section legend,
#view-root .field,
#view-root .field label {
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}
#view-root .form-fields input[type="text"],
#view-root .form-fields input[type="email"],
#view-root .form-fields input[type="tel"],
#view-root .form-fields input[type="number"],
#view-root .form-fields input[type="password"],
#view-root .form-fields input[type="search"],
#view-root .form-fields textarea {
  -webkit-user-select: text;
  user-select: text;
  touch-action: auto;
}
#view-root .field.has-error input,
#view-root .field.has-error select,
#view-root .field.has-error textarea {
  border-color: var(--warn);
}
#view-root .field-error {
  color: var(--warn);
  font-size: 11px;
}
#view-root .req { color: var(--warn); }

/* Eyebrow badge — tiny accent label rendered above a form field's
   label. Optional companion to .field-highlighted; the badge gives
   the field a name ("● Current phase"), the highlight ring gives
   it visual emphasis. Either can be used without the other. */
#view-root .field-eyebrow {
  color: var(--accent);
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1;
}
/* Quiet accent border on a "this is the active one" input. No
   inner glow, no background tint — just enough hue shift on the
   border that the eye lands on it among neighbors. Pairs naturally
   with a .field-eyebrow above, but stands on its own when the
   surrounding label already names the field clearly. */
#view-root .field.field-highlighted input[type="text"],
#view-root .field.field-highlighted input[type="date"],
#view-root .field.field-highlighted input[type="datetime-local"],
#view-root .field.field-highlighted input[type="time"],
#view-root .field.field-highlighted input[type="number"],
#view-root .field.field-highlighted select,
#view-root .field.field-highlighted textarea,
#view-root .field.field-highlighted .dt15-group {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
/* And nudge the label color slightly so the whole row reads as
   "lit up" rather than just "input border is a bit blue." */
#view-root .field.field-highlighted > label {
  color: color-mix(in srgb, var(--accent) 70%, var(--dim));
}
/* Viewport-gated form fields. Authors mark a field MobileOnly=true
   in the entity-def when the desktop UI already provides a better
   affordance for the same data (e.g. kanban drag for a status
   field). Hidden by default; the media query at the mobile
   breakpoint flips it back on. */
#view-root .field.mobile-only { display: none; }
@media (max-width: 760px) {
  #view-root .field.mobile-only { display: flex; }
}
/* Author-declared "hide on mobile" hook for any tree element.
   Pairs with the field-level MobileOnly entity hint, but works
   at the screen-tree level for paragraphs / headings / boxes —
   useful to trim long intro copy on phones where vertical space
   matters more than the explanatory text. Authors opt in by
   adding class="hide-on-mobile" to the element. */
@media (max-width: 760px) {
  #view-root .hide-on-mobile { display: none !important; }
}
/* Form section — adjacent fields sharing a Section value get
   wrapped in a fieldset by renderFieldsSectioned. The legend
   sits on the top border to read as a category label rather than
   a heading, and the inner gap matches the normal between-field
   gap so the section looks like "a piece of the form" rather than
   "a popup inside the form." */
#view-root fieldset.form-section {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 12px 14px 14px;
  margin: 4px 0 6px;
  background: color-mix(in srgb, var(--panel) 40%, transparent);
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}
#view-root fieldset.form-section > legend {
  padding: 0 6px;
  color: var(--dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
}

/* phase-notes section widget — segmented selector + collapsibles.
   The widget renders inside .form-section so it inherits the
   fieldset chrome; everything below overlays the navigation and
   collapsibles on top of that base.

   Desktop (≥761px): row of phase buttons.  Mobile: a <select>
   dropdown takes its place — the buttons get cramped under ~5
   labels at narrow widths.  Both sit in .phase-notes-nav, which
   stays on a single row so the user never loses the navigator
   while scrolling through long notes. */
#view-root .phase-notes-section { padding-top: 10px; }
#view-root .phase-notes-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 4px;
  padding: 6px 0;
}
#view-root .phase-notes-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  flex: 1;
}
#view-root .phase-notes-button {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px;
  font-size: 12px;
  color: var(--dim);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
#view-root .phase-notes-button:hover {
  color: var(--text);
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}
#view-root .phase-notes-button.is-current {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: var(--accent);
  color: var(--text);
  font-weight: 600;
}
#view-root .phase-notes-select-wrap { display: none; flex: 1; }
#view-root .phase-notes-select {
  width: 100%;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
}
@media (max-width: 760px) {
  /* Buttons → dropdown swap at the same breakpoint the rest of
     the substrate uses (.hide-on-mobile / .mobile-only). Keeps
     spacing predictable on phones where 5 pill buttons stop
     fitting on one line. */
  #view-root .phase-notes-buttons     { display: none; }
  #view-root .phase-notes-select-wrap { display: block; }
}

/* Collapsible rows — one per phase. <details> handles open/close
   natively; the styles just make it match the rest of the form.
   When closed, the preview shows the first ~15 words of that
   phase's note so scrolling the list reads as a stack of summaries
   rather than five blank chevrons. */
#view-root .phase-notes-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
#view-root .phase-note-row {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: color-mix(in srgb, var(--panel) 60%, transparent);
  overflow: hidden;
}
#view-root .phase-note-row[open] {
  background: var(--panel);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}
#view-root .phase-note-summary {
  cursor: pointer;
  padding: 8px 10px;
  display: flex;
  align-items: baseline;
  gap: 8px;
  list-style: none;
  user-select: none;
}
/* Hide the default ▶ marker so we can render our own chevron
   via the row's open state. Webkit needs both selectors. */
#view-root .phase-note-summary::-webkit-details-marker { display: none; }
#view-root .phase-note-summary::marker { display: none; }
#view-root .phase-note-summary::before {
  content: '▸';
  color: var(--dim);
  font-size: 11px;
  transition: transform 0.15s;
  display: inline-block;
  width: 12px;
}
#view-root .phase-note-row[open] .phase-note-summary::before {
  transform: rotate(90deg);
}
#view-root .phase-note-label {
  font-weight: 600;
  font-size: 13px;
  color: var(--text);
  flex-shrink: 0;
}
#view-root .phase-note-preview {
  color: var(--dim);
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1;
}
#view-root .phase-note-preview.dim { opacity: 0.6; font-style: italic; }
/* When open, hide the preview — the textarea below shows the
   full text, so duplicating it in the summary is noisy. */
#view-root .phase-note-row[open] .phase-note-preview { display: none; }
#view-root .phase-note-body {
  padding: 0 10px 10px;
}
#view-root .phase-note-body textarea {
  width: 100%;
  resize: vertical;
  min-height: 80px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 8px;
  font: inherit;
}

/* Generic collapsible block (src/blocks/collapsible.js) borrows the
   phase-note row styling above; this just relaxes the body for prose —
   a little headroom under the summary and even spacing between
   paragraphs, since a guide panel holds text rather than a form. */
#view-root .collapsible-body { padding-top: 4px; }
#view-root .collapsible-body > p { margin: 0 0 8px; line-height: 1.5; }
#view-root .collapsible-body > p:last-child { margin-bottom: 0; }

#view-root .array-field .array-items {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 4px;
}
#view-root .array-item {
  display: flex;
  gap: 4px;
}
#view-root .array-item input {
  flex: 1;
  font: inherit;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 4px 8px;
  border-radius: 2px;
}
#view-root .array-remove {
  width: 28px;
  padding: 0;
  color: var(--warn);
}
#view-root .array-remove:hover { border-color: var(--warn); }
#view-root .array-add {
  align-self: flex-start;
  font-size: 11px;
  padding: 3px 8px;
}
#view-root .array-items .empty {
  color: var(--dim);
  font-style: italic;
  font-size: 11px;
  padding: 2px 0;
}

#view-root .file-current {
  font-size: 11px;
  color: var(--dim);
  margin-top: 3px;
}

#view-root .session-bar {
  padding: 4px 8px;
  border: 1px solid var(--border);
  background: var(--panel);
  font-size: 11px;
  color: var(--dim);
  align-self: flex-start;
  flex-wrap: wrap;
}

#view-root .screen-params {
  padding: 6px 8px;
  border: 1px solid var(--border);
  background: var(--panel);
  font-size: 12px;
}
#view-root .param-row {
  align-items: center;
}
#view-root .param-row input[type="text"] { width: 140px; }
#view-root .param-row select { font: inherit; padding: 3px 6px; min-width: 90px; }
#view-root .param-row .remove-param { padding: 1px 6px; color: var(--warn); }
#view-root .param-required { align-items: center; }
#view-root .add-param { font-size: 11px; padding: 2px 8px; }

#view-root .nav-hint {
  border-left: 2px solid var(--accent);
  padding: 4px 0 4px 8px;
  margin-top: 4px;
  font-size: 11px;
}
#view-root .nav-hint code { font-size: 11px; }

#view-root .screens-row {
  flex-wrap: wrap;
  align-items: center;
  padding: 6px 8px;
  border: 1px solid var(--border);
  background: var(--panel);
}
#view-root .screen-tab {
  font-size: 11px;
  padding: 3px 8px;
  background: var(--bg);
  border: 1px solid var(--border);
}
#view-root .screen-tab.active {
  border-color: var(--accent);
  color: var(--accent);
}

#view-root .editor-row {
  align-items: stretch;
}
#view-root .tree-pane,
#view-root .props-pane {
  flex: 1 1 0;
  min-width: 0;
  border: 1px solid var(--border);
  padding: 8px;
  background: var(--panel);
}
#view-root .versions-panel {
  border: 1px solid var(--border); background: var(--panel); padding: 8px;
}
#view-root .versions-panel .version-row {
  align-items: center; padding: 4px 0;
  border-bottom: 1px dashed var(--border);
}
#view-root .versions-panel .version-row:last-of-type { border-bottom: none; }
#view-root .versions-panel .version-row code { font-size: 11px; color: var(--hl); }
#view-root .versions-panel .version-row button { padding: 2px 8px; font-size: 11px; }
#view-root .versions-panel .version-preview textarea {
  width: 100%; box-sizing: border-box;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px; padding: 6px;
  background: var(--bg); color: var(--fg); border: 1px solid var(--border);
}
#view-root .tree-node-btn {
  font: inherit;
  text-align: left;
  background: transparent;
  border: 1px solid transparent;
  padding: 2px 6px;
  color: var(--fg);
  cursor: pointer;
  white-space: nowrap;
  width: auto;
}
#view-root .tree-node-btn:hover {
  border-color: var(--border);
  color: var(--accent);
}
#view-root .tree-node-btn.selected {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--bg);
}
/* Drag-and-drop reorder feedback. drag-source is added on dragstart and
   removed on dragend; drag-over is added/removed as the cursor crosses
   each potential drop target. The visual cue is "you're picking this up"
   (faded source) plus "you'd drop after this row" (highlighted target). */
#view-root .tree-node-btn.drag-source {
  opacity: 0.4;
}
#view-root .tree-node-btn.drag-over {
  border-color: var(--hl);
  background: var(--panel);
}
/* Versions tab line-level diff. One row per line; the marker column is
   a fixed-width gutter so the text columns align between adds, dels,
   and unchanged context. Themes inherit via --ok / --warn / --dim so
   the diff stays readable across light/dark/warm without per-theme
   overrides. */
#view-root .diff-view {
  font: 12px ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  border: 1px solid var(--border);
  background: var(--bg);
  padding: 4px 0;
  max-height: 480px;
  overflow: auto;
}
#view-root .diff-line {
  align-items: stretch;
  padding: 0 8px;
  white-space: pre;
}
#view-root .diff-marker {
  display: inline-block;
  width: 14px;
  flex: 0 0 14px;
  text-align: center;
  color: var(--dim);
}
#view-root .diff-text {
  flex: 1 1 auto;
  white-space: pre;
}
#view-root .diff-add { background: rgba(139, 224, 154, 0.10); color: var(--ok); }
#view-root .diff-del { background: rgba(255, 126, 126, 0.10); color: var(--warn); }
#view-root .diff-eq  { color: var(--dim); }
#view-root .preview-pane {
  border: 1px solid var(--border);
  padding: 8px;
  background: var(--panel);
}
#view-root .preview-area {
  border: 1px dashed var(--border);
  padding: 12px;
  background: var(--bg);
  pointer-events: none;
  user-select: none;
  opacity: 0.95;
}
#view-root .session-bar code { color: var(--hl); }
/* Make the BACK button in the chrome bar prominent — non-tech users
   were missing it at the original 11px / 2px padding. Bigger label,
   accent color, easy to spot from any screen. */
#view-root .session-bar button {
  padding: 6px 14px; font-size: 14px; font-weight: 600;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 4px;
}
#view-root .session-bar button:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); }
/* Hide the dev breadcrumb text ("app: foo · screen: bar") in the
   chrome bar for end users — useful for devs, noise for everyone
   else. Only the BACK button needs to be visible. Gated on the
   document role so admin keeps the dev tooling. */
body[data-role="user"] #view-root .session-bar > span,
body[data-role="user"] #view-root .session-bar > code { display: none; }

/* Dashboard primary action row. Primary "+ Add" sits leftmost,
   alternate-view buttons in the middle, Profile button anchored to
   the far right via margin-left: auto so it reads as the "user
   menu" the rest of the page hangs off of. */
.dashboard-primary  { margin-top: 4px; align-items: center; }
.dashboard-primary button.primary { padding: 12px 22px; font-size: 16px; font-weight: 600; }
.dashboard-profile { margin-left: auto; padding: 8px 16px; font-size: 14px; }

/* Dashboard titlebar: heading on the left, action toolbar pinned
   to the right via margin-left:auto on the .dashboard-actions
   group. Sits at the top of the dashboard so the user reads
   "Projects ← [+ New project] [📅 Calendar] [✅ Archive]" in one
   horizontal sweep, with PROFILE just above in the chrome. */
/* #view-root prefix bumps specificity over the base
 * #view-root .box-row { align-items: center } in style.css head —
 * without it the more-specific box-row rule wins and the dashboard
 * heading re-centers when the row wraps, shifting its Y position
 * relative to the (single-row) calendar titlebar. */
#view-root .dashboard-titlebar {
  align-items: flex-start;
  flex-wrap: wrap;
  row-gap: 8px;
}
.dashboard-titlebar > h3,
.calendar-titlebar > h3 {
  margin: 0;
  flex: 0 0 auto;
  /* Bigger, weighted, with a subtle accent underline so the page
   * title reads as a real heading instead of a small caption.
   * 4px gap to the accent bar keeps the bar visually attached.
   * Shared between the kanban dashboard and the calendar so both
   * pages anchor with the same visual weight. */
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--fg);
  padding-bottom: 4px;
  border-bottom: 3px solid var(--accent);
  line-height: 1.1;
}
.dashboard-actions {
  margin-left: auto;
  align-items: center;
  flex-wrap: wrap;
}
.dashboard-actions button {
  padding: 8px 14px;
  font-size: 14px;
  font-weight: 500;
  border-radius: 4px;
}
.dashboard-actions button.primary {
  padding: 9px 16px;
  font-weight: 600;
}
/* Compact at narrow widths: keep all three actions on one row at
   iPhone SE width. Specificity bumped via #view-root so this beats
   the generic "44px iOS tap target" rule lower in the file that
   otherwise pushes every #view-root button to 10px 16px / 15px /
   44px and overflows the toolbar to a second row. */
@media (max-width: 540px) {
  #view-root .dashboard-actions { gap: 4px; }
  #view-root .dashboard-actions button {
    padding: 4px 8px;
    font-size: 12px;
    min-height: 30px;
    flex: 0 0 auto;
    line-height: 1.2;
  }
  #view-root .dashboard-actions button.primary { padding: 5px 10px; font-size: 13px; }
}

/* Profile screen: vertical stack of large clickable buttons.
   Roughly matches iOS settings layout — wide enough to read
   labels at a glance, tall enough for thumb taps. */
.profile-actions button {
  padding: 12px 16px; font-size: 15px; text-align: left;
  min-width: 280px; max-width: 420px;
}
.profile-actions .profile-logout {
  color: var(--warn, #d97757);
  border-color: color-mix(in srgb, var(--warn, #d97757) 35%, transparent);
}
.profile-actions .profile-logout:hover {
  background: color-mix(in srgb, var(--warn, #d97757) 12%, transparent);
}
/* Form top nav: prominent Back at the top of every edit/new form so
   users don't have to scroll past every field to find Cancel. */
.form-top-nav { margin-bottom: 12px; }
.form-top-nav .form-back {
  padding: 8px 14px; font-size: 14px; font-weight: 600;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 4px;
}
.form-top-nav .form-back:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); }

/* Mobile: Save / Cancel sit inline at the bottom of the form.
   We used to pin them with position:sticky so the user could save
   from anywhere mid-scroll, but on the project edit form the
   floating bar obscured the per-field validation errors (a red
   "pick a date for the time you selected" under Installation
   would be hidden behind the Save button on a 568px viewport).
   Inline keeps them out of the way and forces the user past
   every field on the way to Save, which surfaces any errors. */
@media (max-width: 760px) {
  #view-root .actions { display: flex; gap: 8px; margin-top: 12px; }
  #view-root .actions button { flex: 1; min-height: 44px; }
  /* Compact the chrome on mobile to ~36px total — brand on the
     left, PROFILE on the right, both vertically centered on the
     same baseline as the 20px armillary logo. Conventional mobile
     chrome height (Safari/Chrome's auto-hide bars are 50pt); going
     much shorter risks losing the 'this is the nav bar' read and
     pushes PROFILE below the comfortable thumb-tap floor.
     line-height: 1 on the button + h1 prevents the row from
     stretching to the emoji line-box height. */
  header { padding: 4px 10px; gap: 8px; flex-wrap: nowrap; align-items: center; }
  .header-brand { display: flex; align-items: center; gap: 6px; }
  .header-icon { width: 20px; height: 20px; }
  h1 { font-size: 11px; line-height: 1; }
  #profile-btn {
    min-height: 28px; padding: 5px 10px; font-size: 12px; line-height: 1;
    white-space: nowrap;
  }
  .controls { display: flex; align-items: center; }
  /* Session-bar BACK as a compact "← Back" chip rather than the
     full-width accent button it grew into. Saves a whole row of
     vertical space on every non-entry screen. */
  #view-root .session-bar { padding: 4px 6px; }
  #view-root .session-bar button {
    padding: 3px 10px; font-size: 12px; font-weight: 500;
    min-height: 0;
  }
  /* The form view's own top-of-screen Back link (renders on the
     auto-CRUD New / Edit forms) follows the session-bar chip size
     so the user gets the same affordance whether they came in via
     a screen nav or an api.call('form'). Margin shrunk too so the
     first form field starts close to the top. #view-root prefix
     wins the specificity fight with the global
     #view-root button { min-height: 44px } mobile rule lower in
     the file. */
  #view-root .form-top-nav { margin-bottom: 6px; }
  #view-root .form-top-nav .form-back {
    padding: 3px 10px;
    font-size: 12px;
    font-weight: 500;
    min-height: 0;
  }
}

/* Activity feed — chronological list grouped by day. */
.activity-feed { display: flex; flex-direction: column; gap: 4px; }
.activity-feed .activity-day {
  margin: 18px 0 6px; font-size: 12px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--dim);
  border-bottom: 1px solid var(--border); padding-bottom: 4px;
}
.activity-feed .activity-day:first-child { margin-top: 0; }
.activity-feed .activity-item {
  display: flex; gap: 12px; padding: 6px 0; align-items: baseline;
  font-size: 14px; line-height: 1.4;
}
.activity-feed .activity-item.clickable { cursor: pointer; }
.activity-feed .activity-item.clickable:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.activity-feed .activity-time {
  flex: 0 0 70px; font-size: 12px; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.activity-feed .activity-text { flex: 1 1 auto; }
.activity-feed .activity-entity {
  font-size: 11px; padding: 1px 6px; border-radius: 3px;
  background: color-mix(in srgb, var(--hl) 18%, transparent);
  color: var(--hl); margin-right: 4px;
}
.activity-feed .activity-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
.activity-feed .activity-changes {
  margin-top: 2px; padding-left: 12px;
  border-left: 2px solid color-mix(in srgb, var(--accent) 30%, transparent);
  display: flex; flex-direction: column; gap: 2px;
}
.activity-feed .activity-change {
  font-size: 13px; color: var(--dim);
}
.activity-feed .activity-change em { font-style: normal; color: var(--fg); }
.activity-feed .activity-change strong { color: var(--fg); }
.activity-load-more {
  margin-top: 14px; text-align: center;
}
.activity-load-more button {
  padding: 8px 18px; font-size: 13px;
  background: transparent; color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 4px;
}
.activity-load-more button:disabled { opacity: 0.6; }
.activity-load-more button:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

#view-root .todo-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  align-items: flex-end;
  padding: 6px 0 8px;
}
#view-root .todo-control {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
#view-root .todo-control-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--dim);
}
#view-root .todo-control input,
#view-root .todo-control select {
  font: inherit;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 4px 8px;
  border-radius: 2px;
  min-width: 160px;
}

#view-root .todo-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  background: var(--panel);
}
#view-root .todo-list-row {
  display: grid;
  grid-template-columns: repeat(var(--cols, 4), minmax(0, 1fr));
  grid-auto-flow: column;
  gap: 8px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
#view-root .todo-list-row:last-child { border-bottom: 0; }
#view-root .todo-list-row.todo-list-header {
  background: var(--bg);
  color: var(--dim);
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: 0.06em;
}
#view-root .todo-list-row.clickable { cursor: pointer; }
#view-root .todo-list-row.clickable:hover { background: var(--bg); color: var(--accent); }

/* Quota usage cells (Settings → Quotas & usage). Color picks up
 * the row when usage approaches or exceeds the configured cap so
 * a scanning admin can see at a glance which tenant is hot. */
#view-root .quota-cell { font-variant-numeric: tabular-nums; }
#view-root .quota-cell.quota-soft { color: #b8860b; }     /* amber: 80%+ */
#view-root .quota-cell.quota-over { color: #c0392b; font-weight: 600; }
#view-root .quota-cell.quota-unlimited { color: var(--dim); }

/* Apps editor → nav.params editor → suggestion chips. Click a
 * chip to drop the $-path into the row's input. Small + flat so a
 * long list (many params + many $row fields) wraps gracefully. */
#view-root .nav-param-chips button.chip {
  font-size: 11px;
  padding: 1px 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  cursor: pointer;
}
#view-root .nav-param-chips button.chip:hover {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}

/* Apps editor → props panel → context hints. Surfaces $params /
 * $row paths the author can reach from the selected node. Soft
 * background + thin border distinguishes it from the editable
 * prop rows below. */
#view-root .context-hints {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 8px 10px;
  margin: 4px 0 6px;
  font-size: 12px;
}
#view-root .context-hints code {
  font-size: 12px;
  background: transparent;
  padding: 0;
}
#view-root .todo-list-actions {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}
#view-root .todo-list-actions button {
  font-size: 11px;
  padding: 2px 8px;
}
#view-root .todo-list-actions button.danger {
  border-color: var(--warn);
  color: var(--warn);
}
#view-root .todo-list-actions button.danger:hover {
  background: var(--warn);
  color: var(--bg);
}

/* Danger zone for settings — section-level warning treatment. The
 * button inside also picks up the .danger reds. Border + bg tint
 * mark the whole block as "intentionally irreversible" so it stops
 * mid-scroll instead of blending with the other settings sections. */
#view-root .danger-zone {
  border: 1px solid var(--warn);
  background: rgba(255, 126, 126, 0.06);
  padding: 10px;
  border-radius: 4px;
}
#view-root .danger-zone button.danger {
  border: 1px solid var(--warn);
  color: var(--warn);
  background: var(--bg);
}
#view-root .danger-zone button.danger:hover {
  background: var(--warn);
  color: var(--bg);
}
/* Auto-CRUD form extra-actions row (e.g. soft-delete). Sits below
   the standard Save / Cancel row so destructive intents read as
   a separate ladder instead of competing for the eye next to the
   primary path. The .danger styling matches the danger-zone
   button: outlined in warn color until hovered. */
#view-root .actions-extra {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
}
#view-root .actions-extra button.danger {
  border: 1px solid var(--warn);
  color: var(--warn);
  background: var(--bg);
}
#view-root .actions-extra button.danger:hover {
  background: var(--warn);
  color: var(--bg);
}
#view-root .todo-list-cell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#view-root .todo-list-pager {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 6px 10px;
  border-top: 1px solid var(--border);
  background: var(--bg);
  font-size: 11px;
}
#view-root .todo-list-pager .todo-list-summary {
  color: var(--dim);
  margin-right: auto;
}
#view-root .todo-list-pager button {
  font-size: 11px;
  padding: 2px 8px;
}
#view-root .todo-list-pager button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

#view-root .entity-row {
  align-items: center;
  padding: 4px 6px;
  border: 1px solid var(--border);
  background: var(--panel);
}

#view-root .upload-preview {
  max-width: 320px;
  max-height: 240px;
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 4px;
  display: block;
}

#view-root .wizard-progress {
  border-bottom: 1px dashed var(--border);
  padding-bottom: 8px;
  margin-bottom: 4px;
}
#view-root .wizard-dots {
  display: flex;
  gap: 6px;
  margin-bottom: 6px;
}
#view-root .step-dot {
  width: 24px;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
}
#view-root .step-dot.done { background: var(--accent); opacity: 0.5; }
#view-root .step-dot.active { background: var(--accent); }
#view-root .wizard-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--hl);
}

#debug-pane {
  display: flex;
  flex-direction: column;
  background: var(--panel);
  overflow: auto;
  position: relative;
}

#debug-pane > section {
  padding: 12px;
  border-bottom: 1px solid var(--border);
}

#debug-toggle {
  position: sticky;
  top: 0;
  align-self: flex-end;
  z-index: 1;
  width: 22px;
  height: 22px;
  padding: 0;
  margin: 6px 6px 0 0;
  font-size: 12px;
  line-height: 1;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--dim);
}
#debug-toggle:hover { color: var(--accent); border-color: var(--accent); }
#debug-toggle::before { content: "›"; }

body.debug-collapsed main { grid-template-columns: 1fr 34px; }
body.debug-collapsed #debug-body { display: none; }
body.debug-collapsed #debug-pane { overflow: hidden; align-items: center; padding-top: 6px; }
body.debug-collapsed #debug-toggle { margin: 0; align-self: center; }
body.debug-collapsed #debug-toggle::before { content: "‹"; }
/* When the machine internals are hidden, the stack-manipulation controls
   in the header (PUSH input, PUSH button, RESET) and the "TOP FRAME"
   indicator are noise too. THEME / HELP stay visible. */
body.debug-collapsed #push-input,
body.debug-collapsed #push-btn,
body.debug-collapsed #reset-btn,
body.debug-collapsed #view-pane > .frame-header { display: none; }

ol { list-style: none; padding: 0; margin: 0; }

#call-stack li {
  font-family: inherit;
  padding: 6px 8px;
  border: 1px solid var(--border);
  margin-bottom: 4px;
  background: var(--bg);
  word-break: normal;
  overflow-wrap: anywhere;
}

#call-stack li.top {
  border-color: var(--accent);
  box-shadow: inset 2px 0 0 var(--accent);
}

#call-stack .view-name { color: var(--hl); font-weight: 600; }

/* Frame chrome */
.frame-header {
  display: flex;
  gap: 6px;
  align-items: center;
}
.frame-depth {
  font-size: 10px;
  color: var(--dim);
  background: var(--panel);
  padding: 1px 4px;
  border: 1px solid var(--border);
  border-radius: 2px;
}
.stack-badge {
  font-size: 9px;
  background: var(--accent);
  color: var(--bg);
  padding: 1px 5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: 2px;
  font-weight: 600;
}
.frame-section {
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.45;
}
.frame-label {
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 9px;
  margin-right: 4px;
}

/* JSON tree (collapsible, color-coded) */
.json-collapsible { display: inline-block; max-width: 100%; }
.json-toggle {
  display: inline-block;
  width: 12px;
  cursor: pointer;
  color: var(--dim);
  user-select: none;
  text-align: center;
}
.json-toggle:hover { color: var(--accent); }
.json-summary { color: var(--dim); }
.json-children {
  border-left: 1px dashed var(--border);
  margin: 2px 0 2px 5px;
  padding-left: 6px;
}
.json-row {
  margin: 1px 0;
  white-space: pre-wrap;
}
.json-key   { color: var(--accent); }
.json-index { color: var(--dim); }
.json-string  { color: var(--json-string); }
.json-number  { color: var(--json-number); }
.json-boolean { color: var(--json-boolean); }
.json-null,
.json-undefined { color: var(--json-null); font-style: italic; }
.json-fn { color: var(--dim); font-style: italic; }
.json-file { color: var(--json-file); }
.json-other { color: var(--fg); }

.empty {
  color: var(--dim);
  font-style: italic;
  padding: 4px 0;
}

#fault {
  background: var(--fault-bg);
  color: var(--warn);
  border-bottom: 1px solid var(--warn);
  padding: 6px 12px;
  font-size: 12px;
}
#fault.hidden { display: none; }
#fault::before { content: "FAULT  "; color: var(--warn); opacity: 0.7; letter-spacing: 0.08em; }

#help-overlay {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
}
#help-overlay.hidden { display: none; }


/* ============================================================
   Command palette (Cmd+K / Ctrl+K). Floats top-center, fuzzy-
   searches across apps / entities / built-in views, dispatches the
   selected entry through the same api.call the rest of the
   framework uses. Mirrors the help-overlay backdrop pattern so a
   single z-index regime covers both modals.
   ============================================================ */
#palette-overlay {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: calc(10vh / var(--ui-scale));
  z-index: 110;          /* above help-overlay so palette wins if both open */
}
#palette-overlay.hidden { display: none; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
#palette-panel {
  background: var(--panel);
  border: 1px solid var(--border);
  width: min(620px, 92vw);
  max-height: calc(70vh / var(--ui-scale));
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow);
}
#palette-input {
  font-family: inherit;
  font-size: 14px;
  background: var(--bg);
  color: var(--fg);
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 12px 14px;
  outline: none;
  width: 100%;
  box-sizing: border-box;
}
#palette-input::placeholder { color: var(--dim); }
#palette-results {
  overflow: auto;
  padding: 4px 0;
  max-height: calc((70vh - 56px) / var(--ui-scale));   /* viewport minus input row */
}
#palette-empty {
  padding: 16px;
  color: var(--dim);
  text-align: center;
  font-size: 12px;
}
.palette-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 14px;
  font-size: 13px;
  cursor: pointer;
  border-left: 2px solid transparent;
}
.palette-row.selected {
  background: var(--bg);
  border-left-color: var(--accent);
  color: var(--hl);
}
.palette-row-label {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.palette-row-hint {
  flex: 0 0 auto;
  margin-left: 12px;
  font-size: 11px;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

#help-panel {
  background: var(--panel);
  border: 1px solid var(--border);
  width: min(880px, 92vw);
  max-height: calc(88vh / var(--ui-scale));
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow);
}

#help-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

#help-header h2 {
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.08em;
  color: var(--hl);
  text-transform: uppercase;
}

#help-body {
  padding: 16px 20px;
  overflow: auto;
  line-height: 1.55;
}

#help-body h3 {
  font-size: 12px;
  margin: 22px 0 8px;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-bottom: 1px dashed var(--border);
  padding-bottom: 4px;
}
#help-body h3:first-child { margin-top: 0; }

#help-body h4 {
  font-size: 12px;
  margin: 14px 0 6px;
  color: var(--hl);
}

#help-body p, #help-body ul, #help-body ol {
  margin: 6px 0;
}

#help-body ul, #help-body ol {
  padding-left: 22px;
}

#help-body li { margin-bottom: 3px; }

#help-body code {
  color: var(--hl);
  background: var(--bg);
  padding: 1px 4px;
  border: 1px solid var(--border);
  border-radius: 2px;
}

#help-body pre {
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 10px 12px;
  overflow-x: auto;
  font-size: 12px;
  line-height: 1.5;
  color: var(--fg);
  margin: 8px 0;
}

#help-body table {
  border-collapse: collapse;
  margin: 8px 0;
  width: 100%;
}
#help-body th, #help-body td {
  border: 1px solid var(--border);
  padding: 6px 10px;
  text-align: left;
  vertical-align: top;
}
#help-body th {
  background: var(--bg);
  color: var(--dim);
  font-weight: normal;
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: 0.06em;
}

/* <kbd> keycaps. Reuses --bg / --border / --fg so each theme styles
   the keys consistently with the rest of the help overlay. The
   inline-block + min-width keeps single-character keys (`?`, `]`)
   from rendering as a thin sliver. */
#help-body kbd {
  display: inline-block;
  min-width: 22px;
  padding: 1px 6px;
  font-family: ui-monospace, monospace;
  font-size: 12px;
  text-align: center;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 3px;
}
/* The shortcut table is keycap → description; first column is narrow
   so the kbds align right-edge against the descriptions on the left. */
#help-body table.shortcut-table td:first-child {
  width: 110px;
  text-align: center;
  vertical-align: middle;
}

.anim-pane .anim-row { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 6px; align-items: center; }
.anim-pane .anim-row button { font-size: 12px; padding: 3px 8px; }
.anim-pane .anim-speed input[type="range"] { flex: 1 1 auto; min-width: 0; }
.anim-pane .anim-speed label { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.06em; }
.anim-pane .anim-pos { font-size: 11px; }

/* Playback mode: freeze view-root so clicks against snapshot-rendered widgets
   don't fire actions against live state. A subtle outline + striped overlay
   make the read-only state obvious without obscuring the snapshot. */
body.playback-mode #view-root {
  pointer-events: none;
  outline: 1px dashed var(--hl);
  outline-offset: -2px;
  position: relative;
}
body.playback-mode #view-root::before {
  content: 'PLAYBACK · click LIVE to interact';
  position: absolute; top: 4px; right: 8px;
  background: var(--hl); color: var(--bg);
  padding: 2px 8px; font-size: 10px;
  letter-spacing: 0.08em; font-weight: bold;
  border-radius: 2px;
  pointer-events: none;
  z-index: 10;
}
.anim-pane .anim-bp { margin-top: 8px; }
.anim-pane .anim-bp label { font-size: 11px; color: var(--dim); display: block; margin-bottom: 4px; }
.anim-pane .anim-bp input[type="text"] { flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 3px 6px; }
.anim-pane .anim-bp-list { display: flex; flex-wrap: wrap; gap: 4px; }
.anim-pane .anim-bp-pill {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 10px; padding: 1px 4px 1px 8px; font-size: 11px;
}
.anim-pane .anim-bp-pill button {
  background: none; border: none; color: var(--dim);
  cursor: pointer; padding: 0 4px; font-size: 14px; line-height: 1;
}
.anim-pane .anim-bp-pill button:hover { color: var(--warn); }
.anim-pane button:disabled { opacity: 0.4; cursor: not-allowed; }
.anim-pane .anim-bp-pill code { cursor: pointer; }
.anim-pane .anim-bp-pill code:hover { color: var(--hl); }
.anim-pane .anim-snaps { margin-top: 8px; }
.anim-pane .anim-snaps label { font-size: 11px; color: var(--dim); display: block; margin-bottom: 4px; }
.anim-pane .anim-snaps input[type="text"] { flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 3px 6px; }
.anim-pane .anim-snap-list { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.anim-pane .anim-snap-item {
  display: flex; align-items: flex-start; gap: 4px;
  background: var(--bg); border: 1px solid var(--border);
  padding: 4px 6px; font-size: 11px;
}
.anim-pane .anim-snap-meta { flex: 1 1 auto; min-width: 0; overflow: hidden; }
.anim-pane .anim-snap-meta code {
  cursor: pointer; color: var(--accent); font-size: 12px;
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.anim-pane .anim-snap-meta code:hover { color: var(--hl); }
.anim-pane .anim-snap-item button {
  background: none; border: none; color: var(--dim);
  cursor: pointer; padding: 0 4px; font-size: 14px; line-height: 1;
}
.anim-pane .anim-snap-item button:hover { color: var(--warn); }
.anim-pane .anim-recorder { margin-top: 8px; }
.anim-pane .anim-recorder label { font-size: 11px; color: var(--dim); display: block; margin-bottom: 4px; }
.anim-pane .anim-recorder input[type="text"] { flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 3px 6px; }
.anim-pane .anim-rec-count { padding: 2px 6px; }
.anim-pane .anim-rec-list { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.anim-pane .anim-rec-item {
  display: flex; align-items: flex-start; gap: 4px;
  background: var(--bg); border: 1px solid var(--border);
  padding: 4px 6px; font-size: 11px;
}
.anim-pane .anim-rec-meta { flex: 1 1 auto; min-width: 0; overflow: hidden; }
.anim-pane .anim-rec-meta code {
  cursor: pointer; color: var(--accent); font-size: 12px;
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.anim-pane .anim-rec-meta code:hover { color: var(--hl); }
.anim-pane .anim-rec-item button {
  background: none; border: none; color: var(--dim);
  cursor: pointer; padding: 0 4px; font-size: 14px; line-height: 1;
}
.anim-pane .anim-rec-item button:hover { color: var(--warn); }

.anim-pane .anim-rec-picker {
  margin: 2px 0 6px 16px;
  padding: 6px 8px;
  background: var(--bg);
  border-left: 2px solid var(--accent);
  display: flex; flex-direction: column; gap: 4px;
}
.anim-pane .anim-rec-picker-row {
  display: flex; align-items: center; gap: 6px;
  padding: 2px 4px;
  border: 1px solid var(--border);
  background: var(--bg);
}
.anim-pane .anim-rec-picker-row:hover { border-color: var(--accent); }
.anim-pane .anim-rec-picker-meta { flex: 1 1 auto; min-width: 0; overflow: hidden; }
.anim-pane .anim-rec-picker-meta code {
  font-size: 11px; color: var(--accent);
  display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}
.anim-pane .anim-rec-picker button {
  background: none; border: 1px solid var(--border); color: var(--fg);
  cursor: pointer; padding: 2px 8px; font-size: 11px;
}
.anim-pane .anim-rec-picker button:hover { border-color: var(--accent); color: var(--hl); }
.anim-pane .anim-rec-picker-err {
  color: var(--warn); font-size: 11px;
}

.anim-pane .anim-rep-assertion { margin-top: 8px; }
.anim-pane .anim-rep-assertion-pass {
  font-size: 11px; padding: 4px 6px; border-radius: 3px;
  background: var(--bg); border: 1px solid var(--ok); color: var(--ok);
}
.anim-pane .anim-rep-assertion-fail {
  font-size: 11px; padding: 4px 6px; border-radius: 3px;
  background: var(--bg); border: 1px solid var(--warn); color: var(--fg);
  display: flex; flex-direction: column; gap: 6px;
}
.anim-pane .anim-diff-table { width: 100%; border-collapse: collapse; font-size: 10px; }
.anim-pane .anim-diff-table th {
  text-align: left; padding: 2px 4px; color: var(--dim); font-weight: normal;
  border-bottom: 1px solid var(--border);
}
.anim-pane .anim-diff-table td { padding: 2px 4px; vertical-align: top; word-break: break-word; }
.anim-pane .anim-diff-path { color: var(--accent); }
.anim-pane .anim-diff-expected { color: var(--dim); }
.anim-pane .anim-diff-actual { color: var(--warn); }
.anim-pane .anim-fork-editor {
  margin-top: 8px; border: 1px solid var(--border);
  background: var(--bg); padding: 6px;
}
.anim-pane .anim-fork-editor label { font-size: 11px; color: var(--dim); display: block; margin-bottom: 4px; }
.anim-pane .anim-fork-editor textarea {
  width: 100%; min-width: 0; box-sizing: border-box;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px; padding: 4px 6px;
  background: var(--panel); color: var(--fg);
  border: 1px solid var(--border);
}
.anim-pane .anim-fork-err { font-size: 11px; color: var(--warn); margin: 4px 0; min-height: 14px; }
.anim-pane .anim-fork-editor input[type="text"] { flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 3px 6px; }
.anim-pane .anim-scrub-row { padding: 0; }
.anim-pane #anim-scrub { width: 100%; min-width: 0; }
.anim-pane #anim-scrub:disabled { opacity: 0.4; cursor: not-allowed; }
#flow-diagram-pane h2 { margin-top: 16px; }
#flow-diagram-pane .fd-acc { font-size: 11px; color: var(--dim); display: inline-flex; align-items: center; gap: 4px; }
#flow-diagram-pane .fd-host {
  border: 1px solid var(--border);
  background: var(--bg);
  padding: 6px;
  overflow: auto;
  max-height: 360px;
}
#flow-diagram-pane .fd-host svg { max-width: none; }
#flow-diagram-pane .flow-empty { font-size: 11px; padding: 6px; }
#flow-diagram-pane .flow-error { font-size: 10px; color: var(--warn); padding: 6px; white-space: pre-wrap; }

#home-link { cursor: pointer; user-select: none; }
#home-link:hover { color: var(--hl); }
#home-link:focus { outline: 1px dashed var(--accent); outline-offset: 2px; }
#breadcrumbs { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 11px; }
#breadcrumbs .crumb { color: var(--dim); text-transform: none; letter-spacing: 0; }
#breadcrumbs .crumb-current { color: var(--hl); text-transform: none; letter-spacing: 0; font-weight: bold; }
#breadcrumbs .crumb-sep { color: var(--border); }

#debug-body section.collapsible h2 { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; }
#debug-body section.collapsible h2::before {
  content: '▾'; font-size: 9px; color: var(--dim); width: 10px; display: inline-block;
}
#debug-body section.collapsible.collapsed h2::before { content: '▸'; }
#debug-body section.collapsible.collapsed > *:not(h2) { display: none; }

#faults-pane .fault-row { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 6px; align-items: center; }
#faults-pane .fault-row button { font-size: 12px; padding: 3px 8px; }
#faults-pane .fault-help { font-size: 10px; }
#faults-pane .fault-list { display: flex; flex-direction: column; gap: 4px; }
#faults-pane .fault-empty { font-size: 11px; padding: 6px 0; }
#faults-pane .fault-entry { font-size: 11px; border: 1px solid var(--border); border-radius: 3px; padding: 4px 6px; }
#faults-pane .fault-entry > summary { cursor: pointer; display: flex; align-items: center; gap: 6px; list-style: none; }
#faults-pane .fault-entry > summary::-webkit-details-marker { display: none; }
#faults-pane .fault-entry > summary::before { content: '▸'; color: var(--dim); font-size: 9px; }
#faults-pane .fault-entry[open] > summary::before { content: '▾'; }
#faults-pane .fault-ts { flex: 0 0 auto; }
#faults-pane .fault-msg { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
#faults-pane .fault-badge {
  font-size: 9px; text-transform: uppercase; padding: 1px 5px; border-radius: 2px;
  background: var(--border); color: var(--dim);
}
#faults-pane .fault-badge.fault-http { background: var(--warn); color: var(--bg); }
#faults-pane .fault-badge.fault-action { background: var(--hl); color: var(--bg); }
#faults-pane .fault-body { margin-top: 6px; display: flex; flex-direction: column; gap: 6px; }
#faults-pane .fault-body b { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; }
#faults-pane .fault-pre {
  background: var(--bg); border: 1px solid var(--border); border-radius: 2px;
  font-size: 10px; padding: 4px 6px; margin: 2px 0 0; overflow-x: auto; max-height: 240px;
  font-family: monospace; white-space: pre;
}

#flow-popout {
  position: fixed; inset: 0;
  background: var(--bg);
  z-index: 1000;
  display: flex; flex-direction: column;
  box-shadow: var(--shadow);
}
#flow-popout .popout-bar {
  display: flex; justify-content: space-between; align-items: center;
  padding: 8px 14px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  letter-spacing: 0.04em;
}
#flow-popout .popout-host {
  flex: 1; overflow: auto; padding: 20px;
  background: var(--bg);
}
#flow-popout .popout-host svg { max-width: none; }

#view-root .chart-block {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 8px;
  position: relative;
  display: flex;
  flex-direction: column;
}
#view-root .chart-block .chart-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 6px;
}
#view-root .chart-block canvas { flex: 1 1 auto; min-height: 0; }

/* ============================================================
 * pivot-block: tabular aggregate. Same chrome treatment as chart-
 * block above so dashboards of mixed pivots + charts read as one
 * cohesive grid.
 * ============================================================ */
#view-root .pivot-block {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 8px;
}
#view-root .pivot-block .pivot-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 6px;
}
#view-root .pivot-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}
#view-root .pivot-table th,
#view-root .pivot-table td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--border);
  text-align: left;
}
#view-root .pivot-table th {
  font-weight: normal;
  color: var(--dim);
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: 0.06em;
  border-bottom: 1px solid var(--border);
}
#view-root .pivot-table .pivot-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
#view-root .pivot-table tfoot td {
  border-top: 1px solid var(--accent);
  border-bottom: none;
  font-weight: bold;
}
#view-root .pivot-table .pivot-total-label { color: var(--dim); }
#view-root .pivot-table .pivot-total-value { color: var(--accent); }

/* 2-D pivot extras: caption row sits above the column headers as a
 * small axis label; cells with no underlying data render empty (not
 * "0") so the eye picks up which row × col combinations are populated.
 * The grand total in the bottom-right corner gets an extra accent
 * border to break the column-totals row from the row-totals column. */
#view-root .pivot-table .pivot-axis-caption {
  text-align: center;
  font-style: italic;
  color: var(--dim);
  font-size: 9px;
  letter-spacing: 0.1em;
  border-bottom: none;
  padding-bottom: 0;
}
#view-root .pivot-table .pivot-axis-blank { border-bottom: none; }
#view-root .pivot-table .pivot-axis-row { color: var(--dim); }
#view-root .pivot-table .pivot-cell-empty { color: var(--dim); opacity: 0.4; }
#view-root .pivot-table .pivot-grand-total {
  border-left: 1px solid var(--accent);
  color: var(--accent);
}

/* ============================================================
 * kpi-tile: single-number readout. Big value on top of a small
 * label — same chrome edges as the other report blocks so they
 * line up in a tile grid.
 * ============================================================ */
#view-root .kpi-tile {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 140px;
}
#view-root .kpi-tile .kpi-label {
  font-size: 11px;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
#view-root .kpi-tile .kpi-value {
  font-size: 28px;
  font-weight: bold;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
  line-height: 1.1;
}
#view-root .kpi-tile .kpi-value.kpi-loading { color: var(--dim); font-weight: normal; }
#view-root .kpi-tile .kpi-value.kpi-error   { color: var(--warn); }

#view-root .hook-row {
  border-left: 2px solid var(--border);
  padding: 4px 8px;
}
#view-root .hook-row code { color: var(--accent); font-size: 11px; }

/* ============================================================
 * Role-based chrome gating. Set by views.js when /api/auth/me
 * resolves (data-role="admin" | "user" on <body>). Non-admin
 * users see only the apps surface — no debug pane, no
 * breadcrumbs, no help/visual-guide, no machine internals.
 * Admin (default fallback if data-role isn't set yet) sees
 * everything. Login screen has no role applied so it shows
 * the bare minimum chrome.
 * ============================================================ */
body[data-role="user"] #debug-pane,
body[data-role="user"] #breadcrumbs,
body[data-role="user"] #frame-back-btn,
body[data-role="user"] #help-btn,
body[data-role="user"] #reset-btn {
  display: none !important;
}
/* Center the lone view-pane when there's no debug pane to balance it. */
body[data-role="user"] main { grid-template-columns: 1fr; }
body[data-role="user"] #view-pane > .frame-header { display: none; }

/* ============================================================
 * App content max-width — user + public modes only.
 *
 * Without this cap, a 27" monitor sees the dashboard buttons
 * (PROFILE on the top-right; + New / Calendar / Archive on the
 * titlebar) flung to the far right edge of the screen, kilometers
 * away from the kanban cards they relate to. Constraining the
 * inner content band to a more readable max-width and centering
 * it puts the controls back in line with the content under them.
 *
 * Header technique: the existing layout flexes the brand to the
 * left edge and the controls to the right of *its padding box*.
 * We expand the padding on wide screens so the inner box is at
 * most 1200px — the brand still hugs the left of that band and
 * the controls still hug the right. No HTML changes, no new
 * wrapper element, no breakage on narrow screens (the max(...)
 * clamps padding to the original 12px floor).
 *
 * Admin mode is intentionally untouched — the 360px debug pane
 * already constrains the view-pane visually; an extra cap would
 * leave awkward dead space on the left.
 * ============================================================ */
/* Header stays full-width edge-to-edge — brand pins to the left,
 * controls pin to the right via the existing flex space-between.
 * The previous "centered band" calc pushed the brand inward on
 * wide screens (1920 viewport → padding-left jumped to 360px,
 * dragging the logo ~360px right of where it belongs). Only the
 * view-root content below honours the 1200px cap; the chrome bar
 * stays at the window edges, which is the standard SaaS pattern. */
body[data-role="user"] #view-pane > #view-root,
body[data-role="public"] #view-pane > #view-root,
body.debug-collapsed #view-pane > #view-root {
  max-width: 1200px;
  margin-left:  auto;
  margin-right: auto;
}

/* Usage strip on the home view. Single horizontal row of "X / Y" pairs.
 * usage-hot adds an amber accent when the user is near any cap. */
.usage-strip {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 6px 10px;
  font-size: 12px;
  flex-wrap: wrap;
}
.usage-strip.usage-hot { border-color: #c89a3b; }
.usage-strip.usage-hot::before { content: '⚠ '; color: #c89a3b; margin-right: 4px; }

/* ============================================================
 * Shoelace token bridge. Shoelace v2 components style themselves
 * via CSS custom properties prefixed with --sl-*. Mapping our
 * palette onto theirs lets every <sl-*> blend with the rest of
 * the UI without per-component overrides. The dark theme just
 * inherits these (sl-theme-dark adds its own neutrals which we
 * don't override).
 * ============================================================ */
:root {
  --sl-color-primary-50:  color-mix(in oklch, var(--accent) 10%, var(--bg));
  --sl-color-primary-100: color-mix(in oklch, var(--accent) 20%, var(--bg));
  --sl-color-primary-200: color-mix(in oklch, var(--accent) 35%, var(--bg));
  --sl-color-primary-300: color-mix(in oklch, var(--accent) 50%, var(--bg));
  --sl-color-primary-400: color-mix(in oklch, var(--accent) 70%, var(--bg));
  --sl-color-primary-500: var(--accent);
  --sl-color-primary-600: var(--accent);
  --sl-color-primary-700: color-mix(in oklch, var(--accent) 80%, black);
  --sl-color-primary-800: color-mix(in oklch, var(--accent) 60%, black);
  --sl-color-primary-900: color-mix(in oklch, var(--accent) 40%, black);
  --sl-color-primary-950: color-mix(in oklch, var(--accent) 25%, black);

  --sl-color-success-500: var(--ok);
  --sl-color-success-600: var(--ok);
  --sl-color-warning-500: var(--hl);
  --sl-color-warning-600: var(--hl);
  --sl-color-danger-500:  var(--warn);
  --sl-color-danger-600:  var(--warn);

  --sl-input-color: var(--fg);
  --sl-input-color-hover: var(--fg);
  --sl-input-color-focus: var(--fg);
  --sl-input-background-color: var(--panel);
  --sl-input-background-color-hover: var(--panel);
  --sl-input-background-color-focus: var(--panel);
  --sl-input-border-color: var(--border);
  --sl-input-border-color-hover: var(--accent);
  --sl-input-border-color-focus: var(--accent);
  --sl-input-placeholder-color: var(--dim, #7e828a);

  --sl-panel-background-color: var(--panel);
  --sl-panel-border-color: var(--border);
  --sl-tooltip-background-color: var(--panel);
  --sl-tooltip-color: var(--fg);
  --sl-tooltip-border-radius: 4px;

  --sl-font-sans: var(--font-stack, system-ui, -apple-system, sans-serif);
}

/* API token UI. Plaintext is shown ONCE; the box is meant to be visually
 * loud so the user actually copies it before navigating away. */
.token-card {
  border: 1px solid var(--accent);
  background: var(--panel);
  padding: 10px 12px;
}
.token-plain {
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 8px 10px;
  font-family: var(--font-mono, ui-monospace, monospace);
  word-break: break-all;
  white-space: pre-wrap;
  user-select: all;       /* triple-click selects the whole token */
}
.warn { color: var(--warn); font-weight: bold; }

/* <inbox-list> block — one row per inbox item with a JSON payload pre
 * and inline action buttons. Status colors borrow from existing tokens. */
.inbox-list { display: flex; flex-direction: column; gap: 8px; }
.inbox-row {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 8px 10px;
  display: flex; flex-direction: column; gap: 6px;
}
.inbox-row-head {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline;
  font-size: 12px;
}
.inbox-row-head .inbox-source { color: var(--accent); }
.inbox-row.inbox-status-pending  { border-left: 3px solid var(--hl); }
.inbox-row.inbox-status-done     { border-left: 3px solid var(--ok);   opacity: 0.7; }
.inbox-row.inbox-status-error    { border-left: 3px solid var(--warn); }
.inbox-row.inbox-status-archived { border-left: 3px solid var(--dim);  opacity: 0.5; }
.inbox-payload {
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 6px 8px;
  margin: 0;
  font-size: 11px;
  max-height: 240px;
  overflow: auto;
  white-space: pre;
}

/* Labeled-field render for inbox payloads. Used when the screen
 * author passes payloadFields to <inbox-list>. */
.inbox-payload-fields {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 12px;
  row-gap: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 8px 10px;
  margin: 0;
  font-size: 13px;
}
.inbox-payload-field {
  display: contents;
}
.inbox-payload-label {
  color: var(--dim, #888);
  font-weight: 600;
  white-space: nowrap;
}
.inbox-payload-value {
  color: var(--fg);
  word-break: break-word;
}
.inbox-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.inbox-actions button { font-size: 11px; padding: 2px 8px; }

/* Inbox failure log. Rows collapse to one line with a colored stripe by
 * reason; expanding reveals the captured headers + payload preview. */
.inbox-failure-row {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 6px 8px;
  font-size: 12px;
}
.inbox-failure-row.inbox-failure-invalid-secret    { border-left: 3px solid var(--warn); }
.inbox-failure-row.inbox-failure-missing-secret    { border-left: 3px solid var(--warn); }
.inbox-failure-row.inbox-failure-disabled          { border-left: 3px solid var(--dim);  opacity: 0.85; }
.inbox-failure-row.inbox-failure-payload-too-large { border-left: 3px solid var(--hl); }
.inbox-failure-headers,
.inbox-failure-payload {
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 6px 8px;
  margin: 0;
  font-size: 11px;
  max-height: 200px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-all;
}
.inbox-failures-empty .empty { font-size: 12px; }

/* Clickable inbox rows show a hover affordance + cursor. The status-stripe
 * border-left styling on .inbox-row already differentiates rows; we just
 * brighten the panel + thicken the stripe on hover. */
.inbox-row-clickable { cursor: pointer; transition: background 80ms ease; }
.inbox-row-clickable:hover { background: var(--bg); }
.inbox-row-clickable:hover .inbox-row-head .inbox-source { text-decoration: underline; }
.inbox-notes {
  font-size: 12px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 4px 8px;
}
.inbox-bulk {
  display: flex; gap: 6px; flex-wrap: wrap;
  padding: 6px 8px;
  border: 1px dashed var(--border);
  background: var(--panel);
  margin-bottom: 8px;
}
.inbox-bulk button { font-size: 12px; }

/* <inbox-submit-form> styles. Match the visual language of the form-view
 * fields (label above input, error in warn red below) so authors moving
 * between the two don't get visual whiplash. */
.isf {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 12px 14px;
  display: flex; flex-direction: column; gap: 10px;
  max-width: 520px;
}
.isf-title { font-size: 14px; font-weight: bold; color: var(--fg); }
/* .fs-* classes come from <form-section>, which is composed into
 * <inbox-submit-form> and used standalone in multi-section flows.
 * The rules below cover both. */
.fs-fields { display: flex; flex-direction: column; gap: 8px; }
.fs-field { display: flex; flex-direction: column; gap: 3px; }
.fs-field label {
  color: var(--dim); font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.fs-field input[type="text"],
.fs-field input[type="email"],
.fs-field input[type="password"],
.fs-field input[type="number"],
.fs-field input[type="date"],
.fs-field input[type="datetime-local"],
.fs-field input[type="url"],
.fs-field input[type="tel"],
.fs-field select,
.fs-field textarea {
  font: inherit;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 5px 8px;
  width: 100%;
}
.fs-field textarea { resize: vertical; }
.fs-error input,
.fs-error select,
.fs-error textarea { border-color: var(--warn); }
.fs-field-error { color: var(--warn); font-size: 11px; }
.fs-disabled { opacity: 0.55; pointer-events: none; }
.fs-loading select { font-style: italic; color: var(--dim); }
.fs-children { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
/* Row-builder editor for fields[] arrays in <form-section> / <inbox-submit-form>
 * propsEditor panels. Each .fs-field-row stacks a header row (type, name,
 * required, reorder, remove) above a meta row (label, placeholder, default)
 * and an optional type-specific tail (select options textarea, textarea rows). */
.fs-fields-editor { border: 1px solid var(--border); border-radius: 6px; padding: 8px; background: var(--panel); }
.fs-field-row { border: 1px solid var(--border); border-radius: 4px; padding: 6px; background: var(--bg); }
.fs-field-row-head, .fs-field-row-meta { align-items: center; flex-wrap: wrap; }
.fs-field-row-head select { min-width: 110px; }
.fs-field-row-head input[type="text"],
.fs-field-row-meta input[type="text"] { flex: 1 1 120px; min-width: 80px; }
.fs-field-required { align-items: center; }
.fs-field-required label { font-size: 11px; color: var(--dim); }
.fs-field-remove { color: var(--warn); font-weight: bold; }
.fs-fields-add { align-self: flex-start; }
/* Inline row builder for a select field's options[]. Sits inside the
 * field row's type-specific tail; tighter than .fs-field-row to keep
 * the visual nesting obvious. */
.fs-options-editor,
.fs-object-editor { border: 1px dashed var(--border); border-radius: 4px; padding: 6px; }
.fs-option-row,
.fs-object-row { align-items: center; }
.fs-option-row input[type="text"],
.fs-object-row input[type="text"] { flex: 1 1 100px; min-width: 80px; }
.isf-buttons { display: flex; gap: 6px; }
.isf-banner-error {
  color: var(--warn);
  border: 1px solid var(--warn);
  background: var(--fault-bg, var(--bg));
  padding: 6px 8px;
  font-size: 12px;
}
.isf-success {
  color: var(--ok);
  border: 1px solid var(--ok);
  padding: 6px 8px;
  font-size: 12px;
}

/* Public-app role: anonymous visitors get the rendered screen with zero
 * framework chrome — no header, no debug pane, no breadcrumbs, no frame
 * back button. The page should look like an ordinary embedded form. */
body[data-role="public"] header,
body[data-role="public"] #debug-pane,
body[data-role="public"] #breadcrumbs,
body[data-role="public"] #frame-back-btn,
body[data-role="public"] #view-pane > .frame-header {
  display: none !important;
}
body[data-role="public"] main { grid-template-columns: 1fr; }
body[data-role="public"] #view-pane { padding: 24px; }

/* ---- kanban-block ----------------------------------------------------
   Horizontal scrollable board; each column flexes to a fixed-min width
   so a 4-column board doesn't squeeze each column to nothing on narrow
   screens. Cards inherit panel + fg colors so dark/light/warm themes
   pick them up automatically.
*/
.kanban-block .kanban-title { margin-bottom: 6px; }
.kanban-board {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  padding-bottom: 8px;
  align-items: flex-start;
}
/* Wide-screen: let the board use the full window width instead of being
   trapped in the 1200px content band. That cap (on #view-root) forced a
   horizontal scroll even on a big monitor, because 6 columns + gaps run
   past 1200px. Break the board out to the viewport edges (minus a small
   gutter) so the columns above can grow and fill — no scroll, no dead
   space. Scoped to the modes where #view-root is capped AND #view-pane
   spans the whole viewport (user / public / debug-collapsed); admin keeps
   its left debug pane, so a viewport-width breakout there would overlap
   it. The rest of the screen (titlebar, toggles) stays inside the 1200px
   band. vw is divided by --ui-scale because the page-level zoom scales vw
   too — see the global UI-scale note at the top of this file. */
@media (min-width: 900px) {
  body[data-role="user"]   .kanban-board,
  body[data-role="public"] .kanban-board,
  body.debug-collapsed     .kanban-board {
    width: calc(100vw / var(--ui-scale) - 40px);
    max-width: none;
    margin-left:  calc(50% - 50vw / var(--ui-scale) + 20px);
    margin-right: calc(50% - 50vw / var(--ui-scale) + 20px);
  }
}
.kanban-column {
  /* Grow to share whatever width the board has (so a wide screen fills
     instead of leaving dead space), but never shrink below a readable
     240px — past that the board scrolls rather than crushing cards. */
  flex: 1 1 240px;
  min-width: 240px;
  background: var(--panel);
  border-radius: 6px;
  display: flex;
  flex-direction: column;
}
.kanban-column-orphans { opacity: 0.7; }
.kanban-column-head {
  padding: 8px 10px;
  font-weight: 600;
  text-transform: capitalize;
  border-bottom: 1px solid rgba(127,127,127,0.2);
  display: flex;
  justify-content: space-between;
  gap: 6px;
}
.kanban-column-cards {
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 60px;     /* keep an empty column droppable */
}
.kanban-card {
  background: var(--bg);
  color: var(--fg);
  border: 1px solid rgba(127,127,127,0.25);
  border-radius: 4px;
  padding: 8px 10px;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  /* Mobile: the only two verbs on a card are tap (open) and
     long-press → drag. Both Safari and Chrome's mobile callout
     menus + the blue text-selection highlight are noise. */
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  font-size: 0.95em;
  line-height: 1.3;
  word-break: break-word;
}
/* Same suppression for everything inside the card — title pill,
   address text, phase date line. Otherwise iOS will still pop
   a text selection on a long press even if the wrapper opts
   out. */
.kanban-card,
.kanban-card * {
  -webkit-user-select: none;
  user-select: none;
}
.kanban-card.clickable { cursor: pointer; }
/* Mobile browsers simulate :hover on touch, so a finger brushing
   the card during a scroll would leave the card accent-bordered
   until the user tapped elsewhere — looking like the card stayed
   selected. Scope the hover affordance to genuinely hover-capable
   pointers (desktop mouse / trackpad) via the hover media query. */
@media (hover: hover) and (pointer: fine) {
  .kanban-card.clickable:hover { border-color: var(--accent); }
}
.kanban-card.kanban-card-pending { opacity: 0.5; }
/* Whole-card staleness tint — driven by the kanban-block's
   `cardDataValueBind` prop (which stamps data-value on the card
   root). Subtle border-and-wash so a late or warn job pops at a
   glance across the whole board, not just via the small chip the
   cardTemplate renders. ok is intentionally a no-op: a board full
   of healthy cards stays quiet.
   The :empty :where guard via attribute-presence keeps cards
   without a cardDataValueBind binding completely unaffected. */
.kanban-card[data-value="warn"] {
  border-color: color-mix(in srgb, #d49a3a 55%, var(--border));
  background: color-mix(in srgb, #d49a3a 6%, transparent);
}
.kanban-card[data-value="late"] {
  border-color: color-mix(in srgb, #e36161 60%, var(--border));
  background: color-mix(in srgb, #e36161 8%, transparent);
}
/* Swipe-gesture release transition. Applied at touchend so the
   commit-slide / snap-back rides a 200ms curve, then removed
   after the animation so the next swipe's translateX tracks the
   finger immediately (no lag from a lingering transition). */
.kanban-card.kanban-card-swipe-release {
  transition: transform 200ms ease-out, opacity 200ms ease-out;
}

/* SortableJS state hooks. Match the class names we pass into
   `new Sortable(...)` — keep these in sync with kanban-block.js. */
.sortable-ghost { opacity: 0.4; background: rgba(127,127,127,0.15); }
/* SortableJS marks the touched card with .sortable-chosen the
   moment the long-press completes. Make this visually loud on
   mobile so the user knows the card is grabbed and they can now
   move it — previously the bare accent border looked the same as
   the hover affordance and they had to test-drag to find out
   whether it stuck. Accent ring + slight lift + opacity dip read
   as "picked up." */
.sortable-chosen {
  border-color: var(--accent);
  border-width: 2px;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent),
              0 8px 20px rgba(0,0,0,0.35);
  transform: translateY(-2px) scale(1.02);
  transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
  z-index: 5;
}
.sortable-drag { cursor: grabbing; box-shadow: 0 6px 18px rgba(0,0,0,0.25); }

/* ---- calendar-block --------------------------------------------------
   FullCalendar ships its own structural CSS via the global bundle;
   these rules just integrate it with our theme tokens and color events
   by status (.fc-color-<slug>). Authors can add more `.fc-color-*`
   rules in their own CSS without touching this block.
*/
.calendar-block .calendar-title { margin-bottom: 6px; }
/* Tight titlebar: 📅 Calendar on the left, phase filter pinned
   right via margin-left: auto. Mirrors the dashboard's title +
   action toolbar pattern so the two screens feel of a piece. */
/* #view-root prefix beats the base #view-root .box-row rule, see
 * the dashboard-titlebar comment above for why flex-start. */
#view-root .calendar-titlebar { align-items: flex-start; flex-wrap: wrap; row-gap: 6px; }
.calendar-titlebar > h3 { margin: 0; flex: 0 0 auto; }
.calendar-filter {
  margin-left: auto;
  padding: 4px 8px !important;
  font-size: 13px !important;
  min-width: 0 !important;
  width: auto !important;
}
@media (max-width: 540px) {
  .calendar-filter {
    padding: 4px 6px !important;
    font-size: 12px !important;
  }
}
/* Legend row for multi-color calendars. Each item pulls its
   background from the same fc-color-<slug> class as the events
   it labels, so changing the palette in one place updates both. */
.calendar-legend {
  display: flex;
  flex-wrap: wrap;
  /* Right-aligned to match the Mine/All toggle above — the filter
   * controls all cluster on the right edge so the eye scans the
   * calendar grid from left, not the chrome. */
  justify-content: flex-end;
  gap: 6px 12px;
  margin: 0 0 10px;
  font-size: 12px;
}
.calendar-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px 2px 4px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--border);
}
.calendar-legend-swatch {
  width: 10px; height: 10px; border-radius: 50%;
  /* Inherit background-color from the fc-color-<slug> rule on the
     parent .calendar-legend-item so swatch = event color. */
  background: currentColor;
}
.calendar-legend-label { color: var(--fg); }
/* Click-to-filter mode: legend items become interactive when the
 * calendar-block's filterPhase prop is bound to a local. Inactive
 * chips stay outlined; active chip fills with its phase colour to
 * make the current filter obvious. Hover lifts the inactive chips
 * subtly so the affordance is discoverable without an extra hint. */
.calendar-legend-item-clickable {
  cursor: pointer;
  user-select: none;
  transition: opacity 0.12s ease, background 0.12s ease, transform 0.06s ease;
}
.calendar-legend-item-clickable:hover { opacity: 0.85; transform: translateY(-1px); }
.calendar-legend-item-clickable:active { transform: translateY(0); }
/* Active chip — fills with currentColor (its phase tint) and inverts
 * the label/clock to dark text so the filled background reads. The
 * !important undoes the per-phase color rule that paints the OUTLINE
 * variant's foreground; we want the active state to feel like a
 * pressed pill. */
.calendar-legend-item.calendar-legend-item-active {
  background: currentColor !important;
  border-color: currentColor !important;
}
.calendar-legend-item.calendar-legend-item-active .calendar-legend-label {
  color: #222;
}
.calendar-legend-item.calendar-legend-item-active .calendar-legend-swatch {
  background: #222;
}
/* "all" chip — neutral colour, sits at the end. Active state mirrors
 * the per-phase chips: filled with the accent so the filter posture
 * reads "showing everything" without ambiguity. */
.calendar-legend-item-all {
  color: var(--dim);
  border-color: var(--border);
}
.calendar-legend-item-all .calendar-legend-swatch { display: none; }
.calendar-legend-item-all.calendar-legend-item-active {
  background: var(--dim) !important;
  border-color: var(--dim) !important;
}
.calendar-legend-item-all.calendar-legend-item-active .calendar-legend-label { color: var(--bg); }

/* End-user screen chrome — a reserved-height strip at the top of
 * every screen for non-admin viewers. On non-entry screens it holds
 * a small ← Back chip; on the entry screen the strip is empty but
 * still occupies the same height, so the screen-tree title row sits
 * at a constant y position across pages. Switching Projects ↔ Calendar
 * no longer "jumps" because the chip presence used to make the
 * box-with-min-height bump a few pixels.
 *
 * Locked to a FIXED height (not min-height) so even a slightly
 * taller chip can't grow the strip. Chip is shrunk and centered
 * so it sits in the middle of the band on inner screens. */
#view-root .screen-chrome {
  height: 28px;
  display: flex;
  align-items: center;
  /* Beat the global mobile-tap-target rule that pushes
   * #view-root button to min-height:44px — the chip is purely
   * decorative chrome here, not a tap target users hunt for. */
  min-height: 0;
}
#view-root .screen-back-chip {
  font-size: 11px;
  padding: 1px 8px;
  min-height: 0;
  line-height: 1.4;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--dim);
  font-weight: 600;
  cursor: pointer;
}
#view-root .screen-back-chip:hover {
  color: var(--accent);
  border-color: var(--accent);
}
@media (max-width: 760px) {
  #view-root .screen-chrome { height: 24px; }
  #view-root .screen-back-chip { font-size: 10px; padding: 1px 7px; }
}
/* fc-color-* rules paint background on .fc-event; reuse the same
   color for the swatch via color (so currentColor lights it up)
   without leaking the chunky background onto the pill itself. */
.calendar-legend-item.fc-color-measure  { background: transparent; color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.calendar-legend-item.fc-color-order    { background: transparent; color: var(--hl);     border-color: color-mix(in srgb, var(--hl) 45%, var(--border)); }
.calendar-legend-item.fc-color-deliver  { background: transparent; color: rgb(176, 132, 220); border-color: rgba(176, 132, 220, 0.5); }
.calendar-legend-item.fc-color-install  { background: transparent; color: var(--ok);     border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); }
.calendar-legend-item.fc-color-inspect  { background: transparent; color: color-mix(in srgb, var(--ok) 60%, var(--border)); border-color: color-mix(in srgb, var(--ok) 30%, var(--border)); }
.calendar-host { color: var(--fg); }
/* Under the global page `zoom: var(--ui-scale)`, FullCalendar mis-measures
   its container and sets an explicit pixel width on the day-grid <table>
   that is ~--ui-scale× too wide, so the last weekday columns (Fri/Sat)
   overflow the host and clip — leaving Sun–Thu visible. Clamp every
   scrollgrid table to the host width so the 7 columns are forced to share
   the real available width instead of honoring FullCalendar's inflated
   measurement. Mirrors the long-standing mobile rule below, applied to
   all viewports. */
.calendar-host .fc-scrollgrid,
.calendar-host .fc-scrollgrid table,
.calendar-host .fc-col-header,
.calendar-host .fc-daygrid-body,
.calendar-host .fc-daygrid-body table,
.calendar-host .fc-timegrid-body,
.calendar-host .fc-timegrid-body table {
  width: 100% !important;
}
.calendar-host .fc { font-size: 0.95em; }
.calendar-host .fc-button { background: var(--panel); border-color: rgba(127,127,127,0.3); color: var(--fg); }
.calendar-host .fc-button-primary:not(:disabled).fc-button-active,
.calendar-host .fc-button-primary:not(:disabled):hover {
  background: var(--accent); border-color: var(--accent); color: var(--bg);
}
.calendar-host .fc-toolbar-title { color: var(--fg); }
/* FullCalendar ships a light-theme default for cell + grid borders
   (rgb(221,221,221)) which is invisible against our dark surface.
   Theme the grid through to the substrate's --border token so the
   month/week layout reads on both light and dark themes. */
.calendar-host .fc-theme-standard td,
.calendar-host .fc-theme-standard th,
.calendar-host .fc-theme-standard .fc-scrollgrid,
.calendar-host .fc-theme-standard .fc-scrollgrid-section > * {
  border-color: var(--border) !important;
}
.calendar-host .fc-col-header-cell { color: var(--dim); }
.calendar-host .fc-daygrid-day-number { color: var(--fg); padding: 4px 6px; font-size: 12px; }
.calendar-host .fc-day-today { background: color-mix(in srgb, var(--accent) 14%, transparent) !important; }
.calendar-host .fc-event { cursor: pointer; }
/* List view (auto-selected on mobile): day-grouped chronological list.
   FullCalendar's default styling is light-theme; bring it through to
   the substrate's tokens so rows are readable on both themes. */
.calendar-host .fc-list,
.calendar-host .fc-list-table {
  background: var(--panel);
  color: var(--fg);
}
.calendar-host .fc-list-day-cushion {
  background: color-mix(in srgb, var(--accent) 8%, var(--panel)) !important;
  color: var(--fg);
}
.calendar-host .fc-list-event:hover td { background: color-mix(in srgb, var(--accent) 12%, transparent) !important; }
.calendar-host .fc-list-event-time,
.calendar-host .fc-list-event-title { color: var(--fg); }
/* The dot is FullCalendar's default 'an event lives here' marker;
   our phase-colored row background + the legend at the top already
   carry that signal. Hiding it removes a noisy column of blue
   stamps next to every event title. */
.calendar-host .fc-list-event-dot { display: none; }
.calendar-host .fc-list-event-graphic { display: none; }
.calendar-host .fc-list-empty {
  background: var(--panel); color: var(--dim);
}

/* Floating tooltip for calendar events. Appended to document.body
   by calendar-block.js so it escapes FullCalendar's per-event
   stacking context (where neighbouring events would otherwise
   paint over it) and so the JS can clamp x/y to the viewport
   without fighting overflow clipping on day cells. position:fixed
   because the JS sets viewport coords directly. pointer-events:
   none so the bubble never blocks the click that opens the
   project — the cursor still hits the underlying event element. */
.fc-event-tooltip {
  position: fixed;
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 4px 8px;
  font-size: 12px; font-weight: 500;
  white-space: nowrap;
  z-index: 10000;
  pointer-events: none;
  box-shadow: 0 4px 12px rgba(0,0,0,0.25);
}

/* Article-status palette. Editorial uses status as colorBy, so these
   rules light up automatically. Other apps using calendar-block can
   add their own `.fc-color-*` rules without touching the block. */
/* Patio-tracker phase palette. Each project surfaces five events
   (one per phase date); coloring them lets the eye scan a month
   for "what kind of work is loading up next" at a glance. Chosen
   to walk warm → cool as the workflow progresses: blue early
   funnel → orange in-shop → purple-ish in-transit → green active
   build → soft-green sign-off. */
/* Solid (opaque) phase colors so events read clearly both in the
 * grid AND inside FullCalendar's "+N more" popover, which has a
 * different surface background than the grid cells. All five phase
 * backgrounds (amber, light blue, medium purple, green, pale green)
 * are light enough that black text hits WCAG AA contrast on every
 * one. Picking a single text color across the lot removes the
 * "Singh has black text but others white" inconsistency the user
 * surfaced, and makes the chips easier to scan as a group. */
.fc-event.fc-color-measure  { background: var(--accent);             border-color: var(--accent);             color: #222; }
.fc-event.fc-color-order    { background: var(--hl);                 border-color: var(--hl);                 color: #222; }
.fc-event.fc-color-deliver  { background: rgb(176, 132, 220);        border-color: rgb(176, 132, 220);        color: #222; }
.fc-event.fc-color-install  { background: var(--ok);                 border-color: var(--ok);                 color: #222; }
.fc-event.fc-color-inspect  { background: color-mix(in srgb, var(--ok) 65%, #fff); border-color: var(--ok);   color: #222; }
/* Force text dark across FullCalendar's inner event spans
 * (.fc-event-title, .fc-event-time, .fc-event-title-container) so
 * the chip text doesn't get repainted with FullCalendar's default
 * --fc-event-text-color when its internal CSS wins specificity.
 * Mirrored under .fc-popover so the overflow surface follows.
 *
 * Mobile's listMonth view renders events as <tr class="fc-list-event">
 * with .fc-list-event-title cells instead of the dayGrid pills.
 * Without the list-event selectors below, mobile event text stayed
 * white against the dark theme — readable in dark mode but visually
 * mismatched with the dayGrid view's dark-text pills, and unreadable
 * if a theme switch brings a light background. */
.fc-event[class*="fc-color-"],
.fc-event[class*="fc-color-"] .fc-event-title,
.fc-event[class*="fc-color-"] .fc-event-time,
.fc-event[class*="fc-color-"] .fc-event-title-container,
.fc-popover .fc-event[class*="fc-color-"],
.fc-popover .fc-event[class*="fc-color-"] .fc-event-title,
.fc-popover .fc-event[class*="fc-color-"] .fc-event-time,
.fc-popover .fc-event[class*="fc-color-"] .fc-event-title-container,
.fc-list-event[class*="fc-color-"] .fc-list-event-title,
.fc-list-event[class*="fc-color-"] .fc-list-event-time,
.fc-list-event[class*="fc-color-"] .fc-list-event-title a {
  color: #222 !important;
}
/* List-view rows in mobile pick up the phase background too — gives
 * each row the same chip vibe the dayGrid view has, and keeps the
 * #222 text rule above legible. */
.fc-list-event[class*="fc-color-measure"] td { background: color-mix(in srgb, var(--accent) 35%, var(--panel)) !important; }
.fc-list-event[class*="fc-color-order"]   td { background: color-mix(in srgb, var(--hl) 35%, var(--panel))     !important; }
.fc-list-event[class*="fc-color-deliver"] td { background: rgba(176, 132, 220, 0.30) !important; }
.fc-list-event[class*="fc-color-install"] td { background: color-mix(in srgb, var(--ok) 35%, var(--panel))     !important; }
.fc-list-event[class*="fc-color-inspect"] td { background: color-mix(in srgb, var(--ok) 22%, var(--panel))     !important; }
/* Popover surface itself — FullCalendar's default white doesn't
 * fit the dark theme. Give it a real panel background + border so
 * the event chips inside read against a consistent backdrop. */
.fc-popover {
  background: var(--panel) !important;
  border: 1px solid var(--border) !important;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35) !important;
}
.fc-popover .fc-popover-header {
  background: var(--panel) !important;
  color: var(--fg) !important;
  border-bottom: 1px solid var(--border) !important;
  padding: 4px 8px !important;
}
.fc-popover .fc-popover-body { background: var(--panel) !important; padding: 6px !important; }
.fc-event.fc-color-draft      { background: rgba(127,127,127,0.4); border-color: rgba(127,127,127,0.6); }
.fc-event.fc-color-in_review  { background: var(--hl, #ffd479); border-color: var(--hl, #ffd479); color: #222; }
.fc-event.fc-color-scheduled  { background: var(--accent); border-color: var(--accent); }
.fc-event.fc-color-published  { background: var(--ok, #8be09a); border-color: var(--ok, #8be09a); color: #222; }

/* ---- revision-diff-block --------------------------------------------
   Two-pane field-level diff. Unchanged rows dim; changed rows
   highlight. Long values get truncated; full text on hover via the
   title attribute the block sets.
*/
.revision-diff-block .revision-diff-title { margin-bottom: 6px; }
.revdiff-controls {
  display: flex; gap: 16px; flex-wrap: wrap;
  margin-bottom: 12px;
}
.revdiff-pick { display: flex; align-items: center; gap: 6px; }
.revdiff-pick select { background: var(--bg); color: var(--fg); }
.revdiff-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.92em;
  table-layout: fixed;
}
.revdiff-table th, .revdiff-table td {
  border: 1px solid rgba(127,127,127,0.2);
  padding: 6px 8px;
  vertical-align: top;
  word-break: break-word;
}
.revdiff-table thead th {
  background: var(--panel);
  text-align: left;
}
.revdiff-table .revdiff-field {
  width: 18%;
  font-weight: 600;
  background: var(--panel);
}
.revdiff-row.same td { color: rgba(127,127,127,0.6); }
.revdiff-row.diff td { background: rgba(108,198,255,0.06); }   /* faint accent tint */
.revdiff-row.diff td.dim { color: rgba(127,127,127,0.7); }
.revdiff-summary { margin-top: 8px; }

/* ---- data-list (generic) ---------------------------------------------
   Default card-style rendering used by the public blog reader; the
   editorial app uses per-entity list-blocks instead.
*/
.data-list { display: flex; flex-direction: column; gap: 12px; }
.data-list-row { padding: 14px 16px; border: 1px solid rgba(127,127,127,0.2); border-radius: 6px; background: var(--panel); }
.data-list-row.clickable { cursor: pointer; transition: border-color 0.15s ease, transform 0.15s ease; }
.data-list-row.clickable:hover { border-color: var(--accent); }
.data-list-title    { font-size: 1.15em; font-weight: 600; margin-bottom: 4px; }
.data-list-excerpt  { color: var(--fg); opacity: 0.85; line-height: 1.5; }
.data-list-meta     { font-size: 0.85em; margin-top: 6px; }

/* ---- public blog reader ----------------------------------------------
   Public-mode-only typography so the reader feels like a real blog
   instead of an admin list. Scoped via body[data-role="public"] so
   the same data-list block renders as a plain list elsewhere.
*/
body[data-role="public"] .blog-home { max-width: 720px; margin: 0 auto; }
body[data-role="public"] .blog-tagline { opacity: 0.8; font-style: italic; margin-bottom: 8px; }
body[data-role="public"] .blog-home .data-list-row { padding: 18px 20px; }
body[data-role="public"] .blog-home .data-list-title { font-size: 1.4em; }

/* Article single-row view: data-list with `columns`, but we restyle
   the cells so they read like prose. data-list stamps data-field on
   each cell so we can target by field name. */
body[data-role="public"] .blog-article { max-width: 720px; margin: 0 auto; }
body[data-role="public"] .blog-article-nav { margin-bottom: 8px; }
body[data-role="public"] .blog-article .data-list-row {
  background: transparent;
  border: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
body[data-role="public"] .blog-article .data-list-cell { display: block; }
body[data-role="public"] .blog-article .data-list-cell[data-field="title"] {
  font-size: 2em;
  font-weight: 700;
  line-height: 1.2;
}
body[data-role="public"] .blog-article .data-list-cell[data-field="slug"] {
  font-family: var(--mono, monospace);
  font-size: 0.85em;
  opacity: 0.6;
}
body[data-role="public"] .blog-article .data-list-cell[data-field="publishedAt"] {
  font-size: 0.9em;
  opacity: 0.7;
}
body[data-role="public"] .blog-article .data-list-cell[data-field="body"] {
  font-size: 1.05em;
  line-height: 1.7;
  white-space: pre-wrap;     /* preserve paragraph breaks in longtext */
}

/* ---- header brand mark: armillary-sphere icon -----------------------
   Small CSS-only 3D sphere icon next to the wordmark. Pure CSS, no JS,
   aria-hidden because decorative. Ring color follows --hl so dark /
   light / warm all look right. Header itself is hidden on /public/*
   and /share/* via existing rules, so the icon doesn't leak onto
   reader / share surfaces.
*/
.header-brand {
  display: flex;
  align-items: center;
  gap: 12px;
}
.header-icon {
  position: relative;
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  transform-style: preserve-3d;
  perspective: 200px;
  animation: armillary-rotate 15s linear infinite;
}
/* Customer-uploaded brand logo, swapped in for the armillary by
   the boot-time brand fetch. The 3D rotation animation is removed
   (a flat raster/SVG looks weird spinning); we keep the same box
   size so the header layout doesn't reflow when the logo lands. */
.header-icon-custom {
  position: relative;
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.header-icon-custom img {
  max-width: 100%;
  max-height: 100%;
  display: block;
}
.header-icon .ring {
  position: absolute;
  inset: 0;
  border: 1px solid var(--hl, #ffd479);
  border-radius: 50%;
  box-shadow: 0 0 6px rgba(255, 212, 121, 0.30);
}
.header-icon .r1 { transform: rotateX(45deg); }
.header-icon .r2 { transform: rotateX(-45deg); }
.header-icon .r3 { transform: rotateY(90deg); }
.header-icon .core {
  position: absolute;
  top: 50%; left: 50%;
  width: 3px; height: 3px;
  background: var(--fg);
  border-radius: 50%;
  box-shadow: 0 0 8px 2px var(--hl, #ffd479);
  transform: translate(-50%, -50%);
}
@keyframes armillary-rotate {
  from { transform: rotateY(0deg)   rotateX(20deg); }
  to   { transform: rotateY(360deg) rotateX(20deg); }
}
/* Respect prefers-reduced-motion: pause the rotation but keep the rings
   visible at their resting tilt. */
@media (prefers-reduced-motion: reduce) {
  .header-icon { animation: none; transform: rotateY(0deg) rotateX(20deg); }
}

/* ---- apps editor: toolbox grouping ----------------------------------
   The "add child" panel groups blocks by category so authors can find
   them faster. Each group is a labeled row of buttons. Empty groups
   are omitted at render time, so users with no entities don't see an
   empty "Forms" or "Data — Lists" section. */
.toolbox-group { padding-top: 4px; }
.toolbox-category {
  margin: 0;
  font-size: 0.78em;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.65;
}
.toolbox-buttons { flex-wrap: wrap; }
.toolbox-buttons button {
  font-size: 0.85em;
  padding: 3px 8px;
}
/* Toolbox search input. Width spans the toolbox column so it's clearly
   a discoverable affordance; styling keeps it muted (lighter border
   than the buttons) so it doesn't fight the actual block buttons for
   attention. */
input.toolbox-search {
  width: 100%;
  padding: 4px 8px;
  font-size: 0.85em;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid rgba(127,127,127,0.3);
  border-radius: 3px;
}
input.toolbox-search:focus { outline: none; border-color: var(--accent); }
.toolbox-search-results { padding-top: 2px; }

/* Snippets — visually distinct from individual block buttons so the
   "this drops in a multi-block pattern" affordance is obvious. Slightly
   larger padding and a subtle accent-tinted border. The category label
   above already says "Snippets"; the button styling reinforces it. */
.toolbox-snippets .toolbox-buttons button.snippet-btn {
  font-size: 0.88em;
  padding: 4px 10px;
  border-color: var(--accent);
  opacity: 0.85;
}
.toolbox-snippets .toolbox-buttons button.snippet-btn:hover {
  opacity: 1;
  background: var(--panel);
}

/* Validation: tree-node-btn-warn flags nodes with bind-path errors.
   Subtle left border + warn-tinted color so the warning is visible
   without competing with the .selected highlight. The "⚠ " prefix on
   the label (set in JS) carries the primary signal; this is the
   peripheral cue. */
.tree-node-btn-warn { border-left: 3px solid var(--warn, #c00); }
.tree-node-btn-warn.selected { border-left-color: var(--warn, #c00); }
.props-validation-hint {
  padding: 6px 8px;
  border: 1px solid var(--warn, #c00);
  border-radius: 3px;
  background: rgba(204, 0, 0, 0.08);
  margin-bottom: 4px;
}
.props-validation-hint .failed { margin: 0; font-size: 0.9em; }

/* Public booking app (/public/book) — keeps the visual ladder readable
 * on a phone-first surface where the visitor is committing money +
 * time. The rest of the visual style comes from the inbox-submit-form
 * (.isf) styles above. */
.book-home, .book-pick {
  max-width: 880px;
  margin: 0 auto;
  padding: 24px 16px 80px;
}

/* Spa palette overrides — warmer tones to read as "wellness" rather
 * than "admin app". Applied to .book-spa so the public booking flow
 * picks up the new palette without disturbing the rest of the app.
 * The values lean into muted clay / sage / cream rather than the
 * default cobalt accent. */
.book-spa {
  --accent: #8a6f5c;
  --panel:  #faf7f3;
  --border: #e6dfd6;
  --dim:    #8a7e6f;
}
@media (prefers-color-scheme: dark) {
  .book-spa {
    --accent: #c9b29c;
    --panel:  #2c2622;
    --border: #3f3631;
    --dim:    #b8a899;
  }
}

.book-nav button {
  background: transparent;
  border: 0;
  color: var(--accent, #08c);
  cursor: pointer;
  padding: 4px 0;
  font-size: 0.95em;
}
.book-nav button:hover { text-decoration: underline; }

.book-lede { color: var(--dim); font-size: 1.05em; }
.book-form-intro { color: var(--dim); margin-top: 8px; }

/* Progress strip — three pill segments across the top of every
 * screen. Current step gets the accent border, completed steps get a
 * muted fill, future steps stay neutral. */
.book-progress {
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.book-progress-step {
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
  color: var(--dim);
  font-size: 0.9em;
}
.book-progress-step.book-progress-done {
  color: var(--fg);
  opacity: 0.7;
}
.book-progress-step.book-progress-current {
  color: var(--fg);
  border-color: var(--accent);
  border-width: 2px;
  padding: 5px 11px;
  font-weight: 600;
}

/* Summary card — service + therapist + duration + price. On the time
 * picker screen it can stick to the top of the viewport as the
 * visitor scrolls through slots. */
.book-summary {
  border: 1px solid var(--border, #ddd);
  border-radius: 8px;
  padding: 14px 16px;
  background: var(--panel, #fafafa);
}
.book-summary h2 { margin: 0 0 4px 0; }
.book-summary-meta { margin: 0; color: var(--dim, #666); font-size: 0.95em; }
.book-summary-meta code {
  background: transparent; padding: 0; font-family: inherit; font-weight: 600;
}
.book-summary-sticky {
  position: sticky;
  top: 16px;
  z-index: 1;
}

/* Service + therapist card grids. CSS turns generic .data-list-row
 * children into actual cards: tile layout, padding, hover lift. The
 * data-list block renders one cell per column inside .data-list-row;
 * we let those flow vertically inside the card with column 1 used as
 * the title (bigger type) and the rest as meta. */
.book-services-grid,
.book-therapists-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 14px;
}
.book-services-grid .data-list-row,
.book-therapists-grid .data-list-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  padding: 18px;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
}
.book-services-grid .data-list-row:hover,
.book-therapists-grid .data-list-row:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
  box-shadow: 0 4px 14px rgba(0,0,0,0.08);
}
.book-services-grid .data-list-cell,
.book-therapists-grid .data-list-cell {
  display: block;
  white-space: normal;
  text-overflow: clip;
  overflow: visible;
}
/* Full-width "no preference" CTA above the therapist grid. Picks up
 * the same border/hover treatment as the grid cards so it reads as
 * a sibling option instead of a generic button. */
.book-therapist-anyone {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 1.05em;
  color: var(--fg);
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--panel);
  padding: 16px 18px;
  margin-bottom: 4px;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
}
.book-therapist-anyone:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
  border-style: solid;
  box-shadow: 0 4px 14px rgba(0,0,0,0.08);
}
/* First column = title. Bigger, bolder. */
.book-services-grid   .data-list-cell:nth-child(1),
.book-therapists-grid .data-list-cell:nth-child(1) {
  font-size: 1.15em;
  font-weight: 600;
  color: var(--fg);
}
/* Duration (col 2) + price (col 3) get a meta-row look on service
 * cards. The data-list separates cells with no visible joiner, so
 * the surrounding parent picks up styling from the cell rules. */
.book-services-grid .data-list-cell:nth-child(2)::before { content: ""; }
.book-services-grid .data-list-cell:nth-child(2)::after  { content: " min"; }
.book-services-grid .data-list-cell:nth-child(3)::before { content: "$"; }
.book-services-grid .data-list-cell:nth-child(2),
.book-services-grid .data-list-cell:nth-child(3) {
  display: inline-block;
  color: var(--dim);
  font-size: 0.95em;
  margin-right: 10px;
}
.book-services-grid .data-list-cell:nth-child(4) {
  color: var(--fg);
  opacity: 0.85;
  margin-top: 4px;
}

/* Therapist cards: avatar circle generated from initials in CSS
 * via ::before. The data-list-row doesn't expose record data to a
 * pseudo-element directly, so the avatar is a fixed seal that pulls
 * its color from the row's title text by inheriting the accent
 * shade. The real "personality" comes from the displayName which is
 * already the first cell. */
.book-therapists-grid .data-list-row::before {
  content: "";
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.85;
  display: block;
  margin-bottom: 4px;
}
.book-therapists-grid .data-list-cell:nth-child(2),
.book-therapists-grid .data-list-cell:nth-child(3) {
  color: var(--dim);
  font-size: 0.93em;
}

/* ============================================================
 * <date-time-picker> — Calendly-style picker. Framework primitive.
 * Two-column on wide screens, stacked on mobile.
 * ============================================================ */
.date-time-picker {
  display: grid;
  grid-template-columns: 1fr 280px;
  gap: 24px;
  margin: 8px 0;
  align-items: start;
}
@media (max-width: 720px) {
  .date-time-picker {
    grid-template-columns: 1fr;
  }
}

.dtp-calendar {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  background: var(--panel);
}
.dtp-cal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.dtp-cal-nav {
  background: transparent;
  border: 0;
  font-size: 1.4em;
  color: var(--accent);
  cursor: pointer;
  width: 32px;
  height: 32px;
  border-radius: 50%;
}
.dtp-cal-nav:hover { background: var(--bg); }
.dtp-cal-month { font-weight: 600; font-size: 1.05em; }
.dtp-cal-dow {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  text-align: center;
  color: var(--dim);
  font-size: 0.8em;
  margin-bottom: 4px;
}
.dtp-cal-dow-cell { padding: 4px 0; }
.dtp-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.dtp-cal-day {
  background: transparent;
  border: 0;
  aspect-ratio: 1;
  border-radius: 50%;
  font: inherit;
  font-size: 0.95em;
  cursor: pointer;
  color: var(--fg);
  transition: background 120ms ease, color 120ms ease;
}
.dtp-cal-day:hover:not(:disabled) {
  background: var(--accent);
  color: white;
  opacity: 0.85;
}
.dtp-cal-day.dtp-day-other { color: var(--dim); opacity: 0.4; }
.dtp-cal-day.dtp-day-past,
.dtp-cal-day.dtp-day-closed {
  color: var(--dim);
  opacity: 0.3;
  cursor: not-allowed;
  text-decoration: line-through;
}
.dtp-cal-day.dtp-day-selected {
  background: var(--accent);
  color: white;
  font-weight: 600;
}

.dtp-slots {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  background: var(--panel);
  max-height: 480px;
  overflow-y: auto;
}
.dtp-slots-head {
  font-weight: 600;
  margin-bottom: 4px;
  font-size: 0.95em;
}
.dtp-slots-tz {
  color: var(--dim);
  font-size: 0.78em;
  margin-bottom: 10px;
}
.dtp-slots-empty {
  color: var(--dim);
  font-size: 0.9em;
  padding: 16px 0;
  text-align: center;
}
.dtp-slots-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.dtp-slot {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  cursor: pointer;
  font: inherit;
  text-align: center;
  color: var(--fg);
  transition: border-color 120ms ease, transform 80ms ease, background 120ms ease;
}
.dtp-slot:hover { border-color: var(--accent); transform: translateY(-1px); }
.dtp-slot.dtp-slot-selected {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
  font-weight: 600;
}

/* Settings screen — booking hours editor row layout. */
.settings-hours { margin: 4px 0 12px; }
.settings-hours-row {
  align-items: center;
  padding: 4px 0;
  border-bottom: 1px dashed var(--border);
}
.settings-hours-row code {
  min-width: 96px;
  background: transparent;
  font-family: inherit;
  font-weight: 600;
}
.settings-hours-row input[type="time"] {
  font: inherit;
  padding: 4px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--fg);
}


/* ============================================================
   First-run onboarding card. Renders at the top of main for fresh
   admins (zero entities, not dismissed). Subtle accent border so it
   reads as guidance rather than an error, themed via --accent.
   ============================================================ */
.onboarding-card {
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: 6px;
  padding: 16px 18px;
  background: var(--bg);
}
.onboarding-card h3 { margin: 0; }
.onboarding-steps { margin-top: 4px; }
.onboarding-step {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 4px;
}
.onboarding-step .step-title {
  font-weight: bold;
  font-size: 14px;
}
.onboarding-step .step-desc {
  color: var(--dim);
  font-size: 12px;
  line-height: 1.45;
}
.onboarding-footer { justify-content: flex-end; }


/* ============================================================
   Button loading state. The runtime sets data-loading="true" on a
   button that fired an op:'http' action and removes it on settle.
   Visual: a small spinning ring before the label, plus dimmed opacity
   and the wait cursor. Combined with `disabled = true` (also set
   by the runtime) the button is unclickable while the request is in
   flight. Theme-aware via --border + --accent.
   ============================================================ */
button[data-loading="true"] {
  cursor: wait;
  opacity: 0.7;
  position: relative;
}
button[data-loading="true"]::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 6px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: btn-spin 0.7s linear infinite;
  vertical-align: middle;
}
@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

/* ============================================================
   View-root busy state. The runtime sets data-busy="true" on
   #view-root whenever one or more op:'http' actions are in flight.
   Companion to the per-button data-loading above: that disables the
   triggering button, this freezes every other interactive element on
   the view so the user can't race-click a sibling button, edit a
   form field mid-save, or follow a link before the chain settles.
   pointer-events:none on the parent covers descendants without
   touching their own enabled/disabled state. Opacity is a hair off
   full so the freeze is perceptible without being heavy-handed.
   Same primitive the animate pane uses for playback-mode freeze,
   scoped to async I/O instead of time-travel.
   ============================================================ */
#view-root[data-busy="true"] {
  pointer-events: none;
  cursor: wait;
  opacity: 0.96;
}


/* ============================================================
   Toast notifications. Stacks bottom-right, fixed position so the
   toasts float above whatever view is rendered. Each kind gets a
   left-edge accent bar that picks up the theme palette (--ok /
   --accent / --warn for success / info / warn — error reuses
   --warn). Fade-in / fade-out via the `toast-in` / `toast-out`
   classes the JS toggles; absent both, the toast sits at opacity 0
   so the initial DOM insert is invisible until the next frame.
   ============================================================ */
#toast-host {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
  max-width: min(380px, calc(100vw - 32px));
}
.toast {
  pointer-events: auto;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  padding: 10px 14px;
  border-radius: 6px;
  font-size: 13px;
  line-height: 1.4;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 180ms ease-out, transform 180ms ease-out;
  word-break: break-word;
}
.toast.toast-in {
  opacity: 1;
  transform: translateY(0);
}
.toast.toast-out {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 200ms ease-in, transform 200ms ease-in;
}
.toast-success { border-left-color: var(--ok); }
.toast-info    { border-left-color: var(--accent); }
.toast-warn    { border-left-color: var(--warn); }
.toast-error   { border-left-color: var(--warn); }


/* ============================================================
   Mobile / narrow-viewport layout. Activates at ≤ 760px — covers
   every phone in portrait + small Android tablets. The desktop
   layout (grid: 1fr 360px) doesn't degrade gracefully at that width
   because the debug rail eats half the screen; this block stacks
   it under the view pane and hides it by default (the toggle stays
   accessible). Tap targets are bumped to 44px / 16px font on inputs
   so iOS doesn't auto-zoom on focus.
   ============================================================ */
@media (max-width: 760px) {
  /* One column. Debug pane lives below the view pane in the DOM
     order, hidden by default — the user reveals it via the same
     #debug-toggle that exists on desktop. */
  main {
    display: flex;
    flex-direction: column;
  }
  #view-pane {
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding: 12px;
  }
  #debug-pane {
    /* Always visible in the layout, but the body collapses to a
       thin strip when debug-collapsed — keeps the #debug-toggle
       button reachable since it lives inside the pane (display:none
       on the whole pane would orphan the toggle on a phone). */
    flex: 0 0 auto;
    max-height: 50vh;
    overflow: auto;
  }
  body.debug-collapsed #debug-pane {
    max-height: none;
    padding: 4px;
  }
  body.debug-collapsed #debug-body { display: none; }
  body.debug-collapsed #debug-toggle { margin: 0; }
  /* Desktop's sliver-on-collapse rule (main { grid-template-columns:
     1fr 34px }) doesn't apply here — the flex-column main above
     already overrides it. Also un-hide the header buttons that the
     desktop debug-collapsed rule hides — on mobile, debug-collapsed
     is the DEFAULT, so hiding RESET / THEME / HELP behind it would
     leave the user with no controls. */
  body.debug-collapsed #reset-btn,
  body.debug-collapsed #push-input,
  body.debug-collapsed #push-btn,
  body.debug-collapsed #view-pane > .frame-header { display: revert; }

  /* Header stays single-line on phones now that non-admin chrome
     hides RESET / HELP and we have only the brand + PROFILE button
     to fit. Earlier rule wrapped controls to a second row on the
     assumption that all four buttons would otherwise crowd the
     title; that's no longer true after the chrome diet. */
  header {
    flex-wrap: nowrap;
    padding: 4px 10px;
    gap: 8px;
    align-items: center;
  }
  header .controls {
    width: auto;
    justify-content: flex-end;
  }

  /* Tap targets. 44×44 is the iOS HIG minimum; we land at 36 — large
     enough to land thumbs reliably without dominating the layout.
     Initial mobile pass only covered header + .actions buttons, but
     the audit (2026-05-20) showed nearly every nav / form / app-row
     button outside those two containers was 19-27px tall and
     un-tappable. Scope is now "everything in #view-root that isn't
     inside a <table>" — dense CRUD-list row controls stay compact;
     everything else gets a real tap target. */
  header button,
  .frame-header button,
  #view-root button:not(table button) {
    min-height: 36px;
    padding: 6px 12px;
    font-size: 13px;
  }

  /* The debug-toggle is the ONLY way to reveal the debug pane on a
     phone (the pane starts collapsed). At 22×22 it's smaller than
     anything else on the chrome and a real ergonomic problem. */
  #debug-toggle {
    min-height: 36px;
    min-width: 36px;
    padding: 4px 10px;
  }

  /* Native checkbox / radio at the platform default is ~13-16px —
     painful on touch, especially in auto-generated boolean fields
     inside CRUD forms. Bumps them to a thumb-friendly hit area
     without rebuilding the controls. */
  #view-root input[type="checkbox"],
  #view-root input[type="radio"] {
    min-width: 24px;
    min-height: 24px;
  }

  /* 16px font on text-class inputs prevents iOS Safari from auto-
     zooming when the user focuses one. Below 16px Safari assumes
     the input is too small to read and triggers the zoom. The
     :not([type=…]) catches bare <input> (default type = text) plus
     the explicit text-class types; checkboxes / radios stay at
     their default density. */
  #view-root input:not([type]),
  #view-root input[type="text"],
  #view-root input[type="number"],
  #view-root input[type="email"],
  #view-root input[type="date"],
  #view-root input[type="search"],
  #view-root input[type="password"],
  #view-root input[type="tel"],
  #view-root input[type="url"],
  #view-root textarea,
  #view-root select {
    font-size: 16px;
    min-height: 36px;
    box-sizing: border-box;
  }

  /* Forms: full-width so labels + inputs stack predictably instead
     of squeezing onto one line. */
  #view-root .field {
    width: 100%;
    box-sizing: border-box;
  }
  /* width:100% stretches text inputs / selects / textareas to fill
     the field column on phones — except checkbox and radio, which
     would otherwise eat the whole row and push their sibling label
     off to the right edge. :not([type=…]) keeps the floor without
     touching the boolean controls. */
  #view-root .field input:not([type="checkbox"]):not([type="radio"]),
  #view-root .field textarea,
  #view-root .field select {
    width: 100%;
    box-sizing: border-box;
  }

  /* Tables / list-blocks scroll horizontally inside their container
     instead of forcing the page wide. Wraps any direct table descendant
     of #view-root in an implicit overflow context via its parent. */
  #view-root table { display: block; max-width: 100%; overflow-x: auto; }

  /* Breadcrumbs wrap to multiple lines on a phone — long view-path
     chains (Library › Authors › newAuthor › …) would otherwise push
     horizontal scroll on the whole page. */
  #breadcrumbs { flex-wrap: wrap; }

  /* Public-app / share surfaces already hide the debug rail via
     [data-role]; nothing extra needed for them. */

  /* Toasts: full-width across the bottom on a phone instead of
     stacked in the corner. 14px font is easier to read at arm's
     length than the desktop 13px. */
  #toast-host {
    left: 8px;
    right: 8px;
    bottom: 8px;
    max-width: none;
  }
  .toast { font-size: 14px; }
}

/* card-list block — one field-mapped card layout for any entity. A leading
   checkbox appears when the block is configured with a `toggle` field. */
.card-list { display: flex; flex-direction: column; gap: 8px; }
.card-list-card {
  display: flex; align-items: flex-start; gap: 10px;
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: 6px; background: var(--panel); padding: 11px 13px;
}
.card-list-card .card-check { margin-top: 3px; flex: 0 0 auto; }
/* Template-mode cards stack their body + behavior rows (toggles, etc.)
   instead of flexing them as siblings — same visual rhythm as the slot
   path's bodyCol-wrapped layout but the body is now author-authored. */
.card-list-card.templated { flex-direction: column; }
.card-list-card .card-body { flex: 1 1 auto; min-width: 0; }
.card-eyebrow { font-size: 12px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; }
/* Authors sometimes bind card-eyebrow to a value that may be empty
   (a user with no groups, a row with no status). Hide the chip
   entirely rather than leaving an empty pill at the top of the card. */
.card-eyebrow:empty { display: none; }
.card-title { font-size: 15px; font-weight: 600; color: var(--fg); margin: 2px 0 4px; }
.card-desc { font-size: 14px; color: var(--fg); line-height: 1.5; }
.card-footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; align-items: center; }
.card-chip {
  font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
}
/* Footer pill: flush-left next to siblings. (Previously had
   margin-left: auto, which pushed the badge to fill remaining row
   space — fine on a card with one badge, but it produced ragged
   alignment when the badge sat next to another sibling like
   .card-progress-chip: short content left more slack, so the badge
   floated right; long content packed it left. Authors who want the
   badge right-aligned can wrap it or set justify-content on the
   parent .card-footer.) */
.card-badge {
  font-size: 11px; padding: 2px 8px; border-radius: 4px;
  background: color-mix(in srgb, var(--hl) 18%, transparent);
  color: var(--hl); font-weight: 600;
}
/* Hide empty badges so optional fields (e.g. card-time bound to a
   null etaTime) don't leave a stray dot of background in the footer.
   :empty + selector matches only when the element has no children
   AND no text — exactly the "bind resolved to null/empty" case. */
.card-badge:empty { display: none; }
/* Disabled-account badge: red pill so it reads as a status warning,
   not a positive label. The whole card also fades via opacity at the
   row level so a quick glance separates active members from frozen
   ones without zooming in on the badge. */
.member-disabled-badge {
  background: color-mix(in srgb, var(--warn) 22%, transparent);
  color: var(--warn);
  align-self: flex-start;
}
.card:has(.member-disabled-badge) { opacity: 0.6; }
.card-group-title {
  margin: 14px 0 4px; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--accent);
}
/* Checked (toggle) cards dim + strike, so a checklist reads as "what's left". */
.card-list-card.checked { opacity: 0.5; }
.card-list-card.checked .card-title { text-decoration: line-through; }

/* hill-chart-block — Shape Up-style sine-curve chart. Each row is a
   draggable dot riding the curve; the field's value snaps to an x-percent
   from the author's `positions` map. The SVG scales to container width
   via preserveAspectRatio + the viewBox; dot sizes / fonts therefore
   read consistent across phone / desktop without per-breakpoint tuning. */
.hill-chart {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  margin: 8px 0;
  /* SVG sets its own aspect ratio via viewBox; cap height so a wide
     desktop screen doesn't blow the chart up to "feature" size when
     it's meant to be a single-screen overview. */
  max-width: 720px;
}
.hill-chart-svg {
  display: block; width: 100%; height: auto;
  touch-action: none;  /* SVG owns horizontal drag; bypass scroll */
  user-select: none;
}
.hill-chart-title { margin-bottom: 6px; font-size: 12px; }

.hill-baseline {
  stroke: var(--border); stroke-width: 1;
}
.hill-mid {
  stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 3;
  opacity: 0.6;
}
.hill-curve {
  fill: none;
  stroke: color-mix(in srgb, var(--accent) 60%, transparent);
  stroke-width: 1.5;
}
.hill-axis-label {
  font-size: 11px;
  fill: var(--dim);
  font-family: inherit;
}

.hill-dot { cursor: pointer; }
.hill-dot.editable { cursor: grab; }
.hill-dot.editable.dragging { cursor: grabbing; }
.hill-dot-circle {
  fill: var(--accent);
  stroke: var(--panel);
  stroke-width: 2;
  transition: r 80ms ease;
}
.hill-dot:hover .hill-dot-circle {
  fill: color-mix(in srgb, var(--accent) 80%, white 20%);
}
.hill-dot.dragging .hill-dot-circle {
  fill: color-mix(in srgb, var(--accent) 75%, white 25%);
  filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
.hill-dot-label {
  font-size: 11px;
  fill: var(--fg);
  font-family: inherit;
  pointer-events: none;   /* let pointer events fall through to the dot */
}

/* Status coloring — same data-value slug convention as card-list's
   eyebrow / badge so a row reads consistently across blocks. The
   renderer copies the colorBy field's slug onto the circle's
   data-value attribute. Off-list values inherit the default .hill-dot
   accent fill. */
.hill-dot-circle[data-value="planned"]     { fill: var(--dim); }
.hill-dot-circle[data-value="ordered"]     { fill: var(--accent); }
.hill-dot-circle[data-value="in-transit"]  { fill: var(--accent); }
.hill-dot-circle[data-value="scheduled"]   { fill: var(--hl, var(--accent)); }
.hill-dot-circle[data-value="in-progress"] { fill: #f59e0b; }
.hill-dot-circle[data-value="done"]        { fill: #10b981; }
.hill-dot-circle[data-value="blocked"]     { fill: #dc2626; }

/* progress-bar-block — horizontal "X of Y done" with an optional urgent
   banner above. Both come from one client-side computation so the parent
   header reads instantly once the entity list lands. Status colors via
   data-state on the fill (empty / partial / done). The highlight banner
   pulls the danger color treatment so 🚨 reads at a glance. */
.progress-bar-block { display: flex; flex-direction: column; gap: 6px; }
.progress-bar-highlight {
  font-size: 13px; font-weight: 500;
  padding: 6px 10px; border-radius: 6px;
  background: color-mix(in srgb, #dc2626 14%, transparent);
  color: #fca5a5;
  border: 1px solid color-mix(in srgb, #dc2626 35%, transparent);
}
.progress-bar-row {
  display: flex; align-items: center; gap: 10px;
}
.progress-bar-track {
  flex: 1 1 auto;
  height: 10px;
  background: color-mix(in srgb, var(--border) 60%, transparent);
  border-radius: 999px;
  overflow: hidden;
  position: relative;
}
.progress-bar-fill {
  height: 100%;
  background: var(--accent);
  transition: width 200ms ease;
  border-radius: 999px;
}
.progress-bar-fill[data-state="empty"]   { background: var(--dim); }
.progress-bar-fill[data-state="partial"] { background: var(--accent); }
.progress-bar-fill[data-state="done"]    { background: #10b981; }
.progress-bar-label {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;   /* keeps "2 of 6" steady on update */
}
.progress-bar-skeleton {
  opacity: 0.5;
  animation: progress-bar-pulse 1200ms ease-in-out infinite alternate;
}
@keyframes progress-bar-pulse {
  from { opacity: 0.3; }
  to   { opacity: 0.6; }
}

/* record-header — the at-a-glance card for any detail screen. Title +
   status chip + meta lines + an optional embedded progress-bar-block.
   `record-header-block` builds this DOM, but any app can also write
   <box class="record-header"> directly and compose by hand. Layout
   mirrors a mobile compact-card so the header takes ~30% of viewport
   vertical space and the tab content below stays the focus. */
.record-header {
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  padding: 14px 16px;
}
.record-header-title { align-items: center; flex-wrap: wrap; }
.record-header-title h3 { margin: 0; }
.record-header-meta { font-size: 14px; }
.record-header-meta .dim { font-size: 13px; }

/* status-chip — colored pill rendered next to a heading. Reads its
   value via `bind`, then CSS [data-value=…] paints by status. We
   capture the bound value into data-value via a small JS hook on
   text nodes (already in place: bind sets dataset.value if class
   includes "status-chip"). Falls back to a neutral accent if the
   value isn't in the known list. */
.status-chip {
  font-size: 11px; font-weight: 600;
  padding: 3px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  text-transform: capitalize;
  align-self: center;
}
.status-chip[data-value="planning"]    { color: #f59e0b;
  background: color-mix(in srgb, #f59e0b 14%, transparent);
  border-color: color-mix(in srgb, #f59e0b 30%, transparent); }
.status-chip[data-value="in-progress"] { color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
.status-chip[data-value="complete"]    { color: #10b981;
  background: color-mix(in srgb, #10b981 14%, transparent);
  border-color: color-mix(in srgb, #10b981 35%, transparent); }

/* Cycle pill — when card-list has `cycle: { field, values, ... }` and the
   slot rendering that field becomes tap-to-advance. The element gets the
   .card-cycle class layered onto whatever base class it already had
   (.card-eyebrow / .card-badge / .card-title / .card-desc), so the pill
   styling rides on top of the slot's existing typography. The "▸" hint
   tells the user this isn't decorative — pill responds to tap. data-value
   is preserved so status-color CSS rules still paint the pill the right
   color (e.g. .card-eyebrow[data-value="done"] green). */
.card-cycle {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  cursor: pointer; user-select: none;
  transition: background 80ms ease;
}
.card-cycle::after {
  content: '\25b8';   /* ▸ — black right-pointing small triangle */
  font-size: 0.85em; opacity: 0.6;
}
.card-cycle:hover {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}
.card-cycle:active {
  transform: translateY(1px);   /* tiny push feedback on tap */
}
/* When a card-cycle lands on the .card-eyebrow slot, the eyebrow's
   own uppercase / dim styling looks odd inside an accented pill —
   normalize: pop to normal case, full-contrast text. */
.card-eyebrow.card-cycle {
  text-transform: none; letter-spacing: 0;
  color: var(--accent); font-weight: 500;
  font-size: 12px; align-self: flex-start;
}

/* Multi-channel toggle pills — when card-list is configured with the
   `toggles` array prop (e.g. SMS + Email opt-in on a notification
   contact). Sits as a row below the card body / footer; each pill is
   a checkbox + label that PATCHes its own field independently. The
   `.on` class is added when the box is checked so the pill paints
   accent-colored, mirroring the .chip style. Touch-friendly:
   32px min-height, 8px label gap so the native checkbox doesn't
   crowd the text. */
.card-toggles {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 8px;
}
.card-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 13px; padding: 5px 12px; border-radius: 999px;
  background: var(--panel-soft, color-mix(in srgb, var(--border) 35%, transparent));
  border: 1px solid var(--border);
  color: var(--dim);
  cursor: pointer; user-select: none;
  min-height: 32px; line-height: 1;
}
/* Native checkbox: lock to a small fixed size, zero its native
   margin, and route the checked state through accent-color so the
   tick paints in the theme accent instead of platform blue. */
.card-toggle input[type="checkbox"] {
  width: 14px; height: 14px; margin: 0; flex: 0 0 14px;
  accent-color: var(--accent);
  cursor: pointer;
}
.card-toggle.on {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  font-weight: 500;
}

/* tab-bar block — declarative tab strip with an active highlight. */
.tab-bar { display: flex; flex-wrap: wrap; gap: 6px; }
.tab-btn {
  font-size: 13px; padding: 5px 12px; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--border); background: var(--panel); color: var(--fg);
}
.tab-btn:hover { border-color: var(--accent); }
.tab-btn.active {
  background: var(--accent); border-color: var(--accent);
  color: var(--bg); font-weight: 600;
}

/* Smoke-check panel in the apps editor. Findings from the server's
   screen-graph dry-walk, returned inline on SAVE. Left border colour
   encodes severity; rows stay compact so a handful of notes don't push
   the tree/preview below the fold. */
.lints-panel {
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--panel);
  padding: 8px 10px;
}
.lints-panel .lints-head { align-items: center; justify-content: space-between; }
.lints-panel .lints-head h4 { margin: 0; }
/* Clean result: a single green line so an explicit Check confirms it ran. */
.lints-panel.lints-clean { align-items: center; justify-content: space-between; border-left: 3px solid var(--ok); }
.lints-panel.lints-clean h4 { margin: 0; color: var(--ok); }
.lints-panel .lint {
  margin: 0;
  padding: 2px 8px;
  border-left: 3px solid var(--dim);
  font-size: 13px;
  line-height: 1.4;
}
.lints-panel .lint-error   { border-left-color: var(--warn); }
.lints-panel .lint-warning { border-left-color: var(--hl); }
.lints-panel .lint-info    { border-left-color: var(--accent); }
.lints-panel .lint-icon  { font-weight: 600; }
.lints-panel .lint-error   .lint-icon { color: var(--warn); }
.lints-panel .lint-warning .lint-icon { color: var(--hl); }
.lints-panel .lint-info    .lint-icon { color: var(--accent); }
.lints-panel .lint-path { color: var(--dim); font-family: ui-monospace, monospace; font-size: 12px; }

/* ============================================================
   Modern UI refresh (2026-06).

   The legacy styles above are intentionally developer-tool-aesthetic:
   monospace 13px body, tight padding, 2-4px radii, no shadows — the
   look of a code editor's command palette. This trailing block layers
   a more SaaS-app-ready treatment for substrate-rendered apps:

     - Sans-serif body (system stack); monospace reserved for code,
       JSON, and tabular data-list cells where it actually helps
     - Slightly larger base font (14px) with proper line-height
     - Modern radii (6 / 8 / 12px) and subtle elevation shadows on
       card surfaces (card-list, data-list, kanban, form-section)
     - Visible focus rings on inputs / buttons / textareas / selects
     - App-content treatment for h3 / h4 (the legacy h1 / h2 uppercase
       chrome is kept — those are the frame-header / nav rails)
     - An opt-in `.primary` / `.btn-primary` class for the dominant
       CTA on a screen (filled accent background instead of bordered)

   Pure visual polish: no padding, no class renames, no runtime
   changes. The substrate's developer-tool sections still read as
   themselves (frame-header, push-input, debug pane). The change is
   carried by typography + radii + shadows + focus, all of which
   flow through the existing CSS-variable / theme system — dark,
   light, and warm themes all benefit automatically.
   ============================================================ */

:root {
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", system-ui, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --radius-sm: 4px;
  --radius:    6px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
  --shadow-lg: 0 10px 28px rgba(0, 0, 0, 0.10);
  --focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}

/* Body: sans-serif, 14px, comfortable line-height. The body rule wins
   over the earlier monospace declaration because it sits later in the
   file at equal specificity. */
body {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Monospace stays where it earns its keep: inline code, code blocks,
   JSON syntax highlights, and tabular data-list cells. */
code, pre, kbd, samp,
.data-list-cell,
.json-string, .json-number, .json-boolean, .json-null, .json-file,
.action-debug-row code,
.lints-panel .lint-path {
  font-family: var(--font-mono);
}

/* App-content heading treatment for h3 / h4. (h1 / h2 stay as the
   frame-header uppercase chrome they already are.) */
h3 {
  font-size: 18px; font-weight: 600; letter-spacing: -0.005em;
  color: var(--fg); margin: 0 0 8px;
  text-transform: none;
}
h4 {
  font-size: 15px; font-weight: 600; letter-spacing: 0;
  color: var(--fg); margin: 0 0 6px;
  text-transform: none;
}
#view-root h3, #view-root h4 {
  letter-spacing: 0;
  text-transform: none;
  color: var(--fg);
}

/* Inputs + buttons + selects + textareas: modern radius, visible focus
   ring, smooth transitions. Padding unchanged so admin-chrome rows
   (push input, frame-back, sequence editor) keep their compact layout. */
input, button, textarea, select {
  border-radius: var(--radius);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
button:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Opt-in primary button: filled accent, used for the dominant CTA on
   a screen. e.g. <button label="Save" class="primary"> in screen tree. */
button.primary, button.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
  font-weight: 600;
}
button.primary:hover, button.btn-primary:hover {
  background: color-mix(in srgb, var(--accent) 88%, var(--fg));
  border-color: color-mix(in srgb, var(--accent) 88%, var(--fg));
  color: var(--bg);
}
button.primary:active, button.btn-primary:active {
  background: color-mix(in srgb, var(--accent) 75%, var(--fg));
}

/* Card surfaces (card-list cards, data-list rows): subtle elevation,
   slightly larger radius. Hover state lifts elevation on clickables. */
.card-list-card, .data-list-row {
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.card-list-card:hover { box-shadow: var(--shadow-md); }
.data-list-row.clickable:hover { box-shadow: var(--shadow-md); }

/* Tab bar pills: smooth transitions; active tab gets a faint elevation. */
.tab-btn { transition: all 0.15s ease; }
.tab-btn.active { box-shadow: var(--shadow-sm); }

/* Kanban + form-section + calendar: rounder corners + subtle elevation. */
.kanban-card     { border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.kanban-column   { border-radius: var(--radius-md); }
.fs-fields-editor, .fs-options-editor, .fs-object-editor {
  border-radius: var(--radius-md);
}

/* Paragraph + form-section labels: comfortable reading rhythm. */
#view-root p { line-height: 1.6; }

/* Inbox-block + revisions tables: subtle hover so the row the user is
   pointing at reads as "this one." */
.inbox-row { transition: background 0.15s ease; }
.inbox-row:hover { background: color-mix(in srgb, var(--panel) 60%, var(--bg)); }

/* card-list `onCardClick` cards — clickable affordance. */
.card-list-card.clickable { cursor: pointer; }

/* card-list progress chip — "X of Y done" rollup. Color-coded by
   completion percentage so a glance reads "nothing started" /
   "in progress" / "all done" without parsing the number. */
.card-progress-chip {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  background: color-mix(in srgb, currentColor 12%, transparent);
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
}
.card-progress-chip[data-progress="empty"]   { color: var(--dim); }
.card-progress-chip[data-progress="partial"] { color: var(--hl);  }
.card-progress-chip[data-progress="done"]    { color: var(--ok);  }
.card-progress-chip[data-progress="count"]   { color: var(--dim); }

/* card-list `highlightField` — red left-border + soft warning wash to flag
   rows that need attention (urgent items, blocked tasks, overdue, etc.). */
.card-list-card.card-list-card-highlight {
  border-left: 4px solid var(--warn);
  background: color-mix(in srgb, var(--warn) 8%, var(--panel));
}
.card-list-card.card-list-card-highlight .card-title { color: var(--warn); }
.card-list-card.card-list-card-highlight::after {
  content: 'URGENT';
  position: absolute; top: 8px; right: 12px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.06em;
  color: var(--warn);
  padding: 2px 6px;
  border: 1px solid var(--warn);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
}
.card-list-card { position: relative; }

/* ============================================================
   End-user app polish (2026-06).

   Layered on top of the legacy + earlier modern refresh. Tightly
   focused on the patio-tracker / construction app experience: bigger
   touch targets, status-color encoding so a non-savvy user sees
   what's going on at a glance, calendar mobile fixes, pleasant
   tab + card sizing. Reuses the existing CSS-variable system so all
   3 themes (dark / light / warm) get the polish.

   Design north star: a foreman pulling up the app one-handed on
   a phone should see "what's the status, what's urgent, what's
   next" in one glance — no toolbar dive, no font squint.
   ============================================================ */

/* ---- card-list polish: bigger title, more breathing room ---- */
.card-list-card {
  padding: 16px 18px;
  border-left-width: 4px;
  gap: 12px;
}
.card-list-card .card-title { font-size: 17px; font-weight: 600; line-height: 1.3; margin: 4px 0 6px; }
.card-list-card .card-desc  { font-size: 14px; line-height: 1.55; }
.card-list-card .card-footer { gap: 10px; margin-top: 12px; align-items: center; }

/* ---- pill-shaped eyebrows + status color encoding ----
   The card-list block emits data-value=<slug> on every eyebrow / badge.
   These rules color-code by the slug — so any field bound to eyebrow
   that holds a known status renders as a colored pill, no per-app CSS
   needed. Add more rules for new status values as you create them.
*/
.card-eyebrow {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: color-mix(in srgb, currentColor 12%, transparent);
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
}
/* Generic statuses — applied to .card-eyebrow AND .card-badge so either
   slot can hold the status field. */
.card-eyebrow[data-value="planned"],
.card-badge[data-value="planned"]      { color: var(--dim); }
.card-eyebrow[data-value="ordered"],
.card-badge[data-value="ordered"]      { color: var(--accent); }
.card-eyebrow[data-value="in-transit"],
.card-badge[data-value="in-transit"]   { color: var(--hl); }
.card-eyebrow[data-value="scheduled"],
.card-badge[data-value="scheduled"]    { color: var(--accent); }
.card-eyebrow[data-value="in-progress"],
.card-badge[data-value="in-progress"]  { color: var(--hl); }
.card-eyebrow[data-value="done"],
.card-badge[data-value="done"]         { color: var(--ok); }
.card-eyebrow[data-value="blocked"],
.card-badge[data-value="blocked"]      { color: var(--warn); }
/* Project-level statuses */
.card-eyebrow[data-value="planning"],
.card-badge[data-value="planning"]     { color: var(--dim); }
.card-eyebrow[data-value="complete"],
.card-badge[data-value="complete"]     { color: var(--ok); }

/* The card's left-border accent stays generic accent by default, but
   override per status when the eyebrow carries one (gives the WHOLE
   card a status tint, not just the chip). */
.card-list-card:has(.card-eyebrow[data-value="done"])       { border-left-color: var(--ok); }
.card-list-card:has(.card-eyebrow[data-value="blocked"])    { border-left-color: var(--warn); }
.card-list-card:has(.card-eyebrow[data-value="in-progress"]),
.card-list-card:has(.card-eyebrow[data-value="in-transit"]) { border-left-color: var(--hl); }

/* ---- card-group titles (group headings inside card-list) — softer ---- */
.card-list h4.card-group-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 22px 0 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.card-list h4.card-group-title:first-child { margin-top: 4px; }

/* ---- card-list entrance animation — subtle fade on first render ---- */
@keyframes card-fade-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.card-list-card { animation: card-fade-in 0.25s ease-out both; }
@media (prefers-reduced-motion: reduce) {
  .card-list-card { animation: none; }
}

/* ---- tab-bar polish: bigger pills, smoother transitions ---- */
#view-root .tab-bar { gap: 8px; margin: 4px 0 8px; }
#view-root .tab-btn {
  font-size: 14px; font-weight: 500;
  padding: 9px 18px;
  border-radius: var(--radius-md);
  border-width: 1px;
}
#view-root .tab-btn.active { font-weight: 600; }
#view-root .tab-btn:focus-visible { box-shadow: var(--focus-ring); }

/* ---- frame-header: friendlier breadcrumb for end users ---- */
.frame-header {
  font-size: 12px;
  letter-spacing: 0.04em;
  margin-bottom: 18px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.frame-header code { font-size: 12px; }

/* ---- calendar mobile + status colors for events ---- */
.calendar-host .fc { font-size: 13px; }
.calendar-host .fc-toolbar { gap: 8px; }
.calendar-host .fc-toolbar-title { font-weight: 600; }
.calendar-host .fc-button { border-radius: var(--radius); font-weight: 500; }
.calendar-host .fc-button-primary { background: var(--panel); color: var(--fg); border-color: var(--border); }
.calendar-host .fc-button-primary:not(:disabled):hover { background: var(--bg); border-color: var(--accent); color: var(--accent); }
.calendar-host .fc-button-primary:not(:disabled).fc-button-active {
  background: var(--accent); border-color: var(--accent); color: var(--bg);
}
.calendar-host .fc-event { cursor: pointer; border-radius: var(--radius-sm); padding: 2px 4px; font-weight: 500; }

/* Deliverable-status palette on calendar events (kind='colorBy: status'). */
.fc-event.fc-color-planned     { background: var(--dim); border-color: var(--dim); }
.fc-event.fc-color-ordered     { background: var(--accent); border-color: var(--accent); }
.fc-event.fc-color-in-transit  { background: var(--hl); border-color: var(--hl); color: #222; }
.fc-event.fc-color-scheduled   { background: var(--accent); border-color: var(--accent); }
.fc-event.fc-color-in-progress { background: var(--hl); border-color: var(--hl); color: #222; }
.fc-event.fc-color-done        { background: var(--ok); border-color: var(--ok); color: #222; }
.fc-event.fc-color-blocked     { background: var(--warn); border-color: var(--warn); }

/* Calendar mobile: stack the toolbar, tighter cells, generous touch. */
@media (max-width: 760px) {
  .calendar-host .fc-toolbar {
    flex-direction: column !important;
    align-items: stretch !important;
    gap: 10px;
  }
  .calendar-host .fc-toolbar > .fc-toolbar-chunk {
    display: flex;
    justify-content: center;
    width: 100%;
  }
  .calendar-host .fc-toolbar-title { text-align: center; font-size: 1.05em; }
  .calendar-host .fc-button { min-height: 38px; padding: 0 12px; }
  .calendar-host .fc-event { min-height: 22px; padding: 4px 6px; }
  /* FullCalendar's .fc-daygrid-body <table> collapses to intrinsic
     width on narrow viewports, leaving a 0-height grid. Force it to
     fill its container so the days render. */
  .calendar-host .fc-daygrid-body,
  .calendar-host .fc-daygrid-body table { width: 100% !important; }
}

/* ---- mobile polish: bigger fonts, friendlier tap targets ---- */
@media (max-width: 760px) {
  body { font-size: 16px; }                   /* iOS-zoom-prevent + readable */
  #view-root { padding: 4px 0; }
  #view-root h3 { font-size: 22px; }
  #view-root h4 { font-size: 17px; }
  #view-root p { font-size: 15px; line-height: 1.6; }

  /* Tap targets — 44px is the iOS HIG minimum. */
  #view-root button:not(table button),
  #view-root .tab-btn {
    min-height: 44px;
    font-size: 15px;
    padding: 10px 16px;
  }

  /* Card list — even more breathing room on a phone. */
  .card-list-card { padding: 18px; }
  .card-list-card .card-title { font-size: 18px; }
  .card-list-card .card-desc { font-size: 15px; }

  /* Inputs land at 16px so iOS Safari doesn't auto-zoom on focus. */
  #view-root input, #view-root textarea, #view-root select { font-size: 16px; }

  /* Frame header — quieter on a phone (less prominent breadcrumb). */
  .frame-header { font-size: 11px; margin-bottom: 12px; padding-bottom: 8px; }
}

/* ---- empty-state polish: friendlier copy when a list is blank ---- */
#view-root p.empty {
  text-align: center;
  padding: 32px 16px;
  color: var(--dim);
  font-size: 14px;
  background: color-mix(in srgb, var(--panel) 50%, var(--bg));
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
}

/* ---- primary button: stronger presence so a CTA reads as one ---- */
button.primary, button.btn-primary {
  padding: 9px 18px;
  font-size: 14px;
  box-shadow: var(--shadow-sm);
}
button.primary:hover, button.btn-primary:hover { box-shadow: var(--shadow-md); }

/* ============================================================
   Kanban mobile polish (2026-06-04).

   The base kanban (1670-1709) is desktop-tuned: 240px columns, tight
   8px padding, browser-default horizontal-scroll. On a phone that's
   cramped — columns are narrow, headers scroll out of view, and the
   thumb-reach for drag-and-drop is awkward.

   This block adds desktop polish (slightly bigger cards, modern
   shadow, sticky column headers always) and a dedicated mobile pass
   (≤ 760px: wider columns + CSS scroll-snap so swiping lands on a
   column edge, generous card padding for thumb tap targets, sticky
   column heads survive vertical card scrolling inside a column).

   SortableJS long-press (kanban-block.js, delay: 250 on touch only)
   complements this: short tap on a card opens editDeliverable; long-
   press grabs the card to drag. Touch + scroll feel right.
   ============================================================ */

/* Desktop refinements applied everywhere. */
.kanban-card {
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .kanban-card.clickable:hover { box-shadow: var(--shadow-md); }
}
.kanban-column {
  border-radius: var(--radius-md);
  background: var(--panel);
}
/* Sticky column header — visible even when a column has many cards
   and the user is scrolling them vertically. */
.kanban-column-head {
  position: sticky;
  top: 0;
  background: var(--panel);
  z-index: 1;
  border-top-left-radius: var(--radius-md);
  border-top-right-radius: var(--radius-md);
  padding: 10px 12px;
}
/* Column title is the loudest element in the head — first thing the
   eye hits in the Z-pattern scan down the board. The owner label
   below it is a quiet sub-tag, not a competing heading. */
.kanban-column-head { flex-direction: column; align-items: flex-start; gap: 0; }
.kanban-column-title-row {
  display: flex; align-items: baseline; gap: 6px; width: 100%;
  justify-content: space-between;
}
.kanban-column-name {
  font-size: 17px; font-weight: 700; letter-spacing: 0; line-height: 1.2;
}
.kanban-column-count { font-size: 12px; font-weight: 500; }
.kanban-column-owner {
  font-size: 11px; font-weight: 400; letter-spacing: 0.02em;
  text-transform: none; color: var(--dim); margin-top: 2px;
}
/* "+ N more in archive →" link on a capped column. Borderless,
   accent-colored, full-width inside the column so it reads as
   the natural next step after the last visible card rather than
   another card itself. Hidden when the column has nothing to
   reveal — the JS only appends it past the cap. */
.kanban-column-overflow {
  background: transparent;
  color: var(--accent);
  border: 1px dashed color-mix(in srgb, var(--accent) 40%, var(--border));
  border-radius: 4px;
  padding: 6px 8px;
  font-size: 12px;
  font-weight: 500;
  text-align: center;
  cursor: pointer;
  margin-top: 4px;
}
.kanban-column-overflow:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.kanban-column-overflow:disabled {
  opacity: 0.5; cursor: default;
}
/* Templated cards (cardTemplate prop) hand the layout to the author,
   so reset our default padding and let the inner tree own spacing. */
.kanban-card.templated { padding: 0; background: transparent; border: 1px solid var(--border); border-radius: 6px; }
.kanban-card.templated > * { padding: 8px 10px; }
/* Patio-tracker dashboard card: customer + address + phase date.
   .card-phase-date is the computed currentPhaseDateLine — either
   "At YYYY-MM-DD" / "By YYYY-MM-DD" or "⚠️ no date set". Color
   the warning state via the emoji prefix CSS attr-substring match. */
.kanban-project-card .card-title { font-size: 14px; }
/* Address line — shrunk to 11px so the pill (12px bold) reads as the
 * primary identifier and the address is a quieter sub-label. Visual
 * size at 12px non-bold ended up reading larger than the pill due to
 * unweighted character widths; 11px brings the perceived size back
 * down. */
.kanban-project-card .card-desc  { font-size: 11px; }
.kanban-project-card .card-phase-date { font-size: 11px; color: var(--dim); }
/* Promoted project pill at the top of each kanban card. align-self
   pins it to the left so the pill width hugs its text — without the
   override it stretches across the flex column. */
.kanban-project-card .card-title-pill { align-self: flex-start; font-size: 12px; }

/* Hybrid datetime control: side-by-side date + time-select. The select
   carries only step-aligned options so the team can never accidentally
   set a 7:23 appointment via the popup picker. Inputs sized to fit
   their content rather than stretching the full form width — a date
   picker only needs ~160px to show 'MM/DD/YYYY' and a time select
   only needs ~110px for '10:00 AM'. */
.dt15-group { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
/* min-width override: the global #view-root input rule pins a
   280px floor for keyboard hit-area on long form fields, but date /
   datetime / time pickers don't need that much room and look silly
   stretched out. Override here. */
#view-root .dt15-group .dt15-date { flex: 0 0 160px; width: 160px; min-width: 0; }
#view-root .dt15-group .dt15-time { flex: 0 0 130px; width: 130px; min-width: 0; }
/* Plain date inputs on auto-CRUD forms (Design/Order by, Delivery by)
   also get capped — same reasoning. */
/* Plain date inputs match the date side of the dt15-group's pair
   (160px) so a Schedule fieldset with date-only rows next to
   date+time rows reads as one consistent column instead of "the
   single-date inputs look 20px wider for no reason." */
#view-root .field input[type="date"] { width: 160px; min-width: 0; }
/* Compact numeric / currency inputs — short integer / decimal values
   look silly stretched the whole form width. user-entities.js stamps
   compact:true on integer / decimal / currency schemas so this rule
   catches all three without each having to opt in by hand. */
#view-root .field.compact input[type="number"],
#view-root .field.compact input[type="text"] {
  width: 160px;
  min-width: 0;
}
/* Narrow phones (iPhone SE class): 160+6+130 = 296px overflows
   the ~280-290px usable column width once the form gutters are
   deducted. flex-wrap on the group lets the time-select drop to
   a second row on those widths; both inputs go full-column-width
   so the user gets thumb-sized tap targets instead of a partially
   clipped select. */
@media (max-width: 380px) {
  #view-root .dt15-group .dt15-date,
  #view-root .dt15-group .dt15-time {
    flex: 1 1 100%;
    width: 100%;
  }
}
/* Notes hint — bare emoji in the top-right corner of the card, no
   background or padding. Position absolute so the rest of the card
   layout ignores it. :empty hides it when the project has no notes. */
.kanban-project-card { position: relative; }
.kanban-project-card .card-notes-hint {
  position: absolute; top: 4px; right: 6px;
  font-size: 14px; line-height: 1; opacity: 0.7;
}
.kanban-project-card .card-notes-hint:empty { display: none; }
/* When the phase-date line begins with ⚠️, recolor to the warn token
   so missing-date jobs pop without an extra binding. */
.kanban-project-card .card-phase-date:has-text("⚠"),
.kanban-project-card .card-phase-date { /* fallback; :has-text is non-standard */ }

/* Customer-promise line — small accent text beneath the phase date.
 * Empty when promisedInstallBy is unset so the row collapses and the
 * card stays tight. Mirrors .card-notes-hint's :empty pattern. */
.kanban-project-card .card-promised {
  font-size: 10px;
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.03em;
}
.kanban-project-card .card-promised:empty { display: none; }

/* Phase Age line — small left-aligned text beneath the phase date.
 * e.g. "Phase 3d" or "Phase today". Staleness drives the colour
 * (ok=dim, warn=amber, late=red) so a slow-moving card still reads
 * at a glance. Empty content (Complete-phase cards) collapses the
 * row via :empty. */
.kanban-project-card .card-phase-age {
  font-size: 11px;
  color: var(--dim);
  font-weight: 600;
}
.kanban-project-card .card-phase-age:empty { display: none; }
.kanban-project-card .card-phase-age[data-value="ok"]   { color: var(--dim); }
.kanban-project-card .card-phase-age[data-value="warn"] { color: #d49a3a; }
.kanban-project-card .card-phase-age[data-value="late"] { color: #e36161; }

/* Owner status admin screen — one card per phase owner. The chip on
 * the right shares the kanban time-in-phase vocabulary so the same
 * green/amber/red painting carries between dashboard and admin
 * surfaces. Metrics row is two short text fragments under the
 * phase label, dim so the chip wins visual weight. */
.owner-status-card .owner-status-headline {
  align-items: center;
  justify-content: space-between;
}
.owner-status-card .owner-status-name { margin: 0; }
.owner-status-card .owner-status-chip {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  color: var(--dim);
  background: rgba(255, 255, 255, 0.04);
  white-space: nowrap;
}
.owner-status-card .owner-status-chip[data-value="warn"] {
  color: #d49a3a;
  border-color: #d49a3a;
  background: rgba(212, 154, 58, 0.10);
}
.owner-status-card .owner-status-chip[data-value="late"] {
  color: #e36161;
  border-color: #e36161;
  background: rgba(227, 97, 97, 0.14);
}
.owner-status-card .owner-status-metrics { font-size: 12px; }

/* Overdue admin screen — same chip vocabulary as the kanban + owner
 * status surfaces so the colour language stays consistent. Headline
 * row: project name on the left, chip pinned right. Phase line below
 * carries the "Xd in Phase — Owner" string the server pre-built. */
.overdue-card .overdue-headline {
  align-items: center;
  justify-content: space-between;
}
.overdue-card .overdue-name { margin: 0; }
.overdue-card .overdue-chip {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  color: var(--dim);
  background: rgba(255, 255, 255, 0.04);
  white-space: nowrap;
}
.overdue-card .overdue-chip[data-value="warn"] {
  color: #d49a3a;
  border-color: #d49a3a;
  background: rgba(212, 154, 58, 0.10);
}
.overdue-card .overdue-chip[data-value="late"] {
  color: #e36161;
  border-color: #e36161;
  background: rgba(227, 97, 97, 0.14);
}
.overdue-card .overdue-phase-line { font-size: 12px; color: var(--dim); }

/* SQL query admin view — large textarea, results table, history
 * dropdown. Read-only at the server (Mode=ReadOnly), but the surface
 * is otherwise unstyled because it's an admin-only tool that should
 * read like a console, not a polished form. */
#view-root .sql-input {
  width: 100%;
  min-height: 120px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.45;
  padding: 10px 12px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg);
  resize: vertical;
  margin: 8px 0;
  box-sizing: border-box;
}
#view-root .sql-toolbar {
  margin-top: 8px;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
#view-root .sql-history {
  max-width: 100%;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
}
#view-root .sql-error {
  margin-top: 12px;
  padding: 10px 14px;
  background: rgba(227, 97, 97, 0.12);
  border: 1px solid #e36161;
  border-radius: var(--radius);
  color: #e36161;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  white-space: pre-wrap;
}
#view-root .sql-results {
  margin-top: 12px;
  max-height: 60vh;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
#view-root .sql-results table {
  width: 100%;
  border-collapse: collapse;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
}
#view-root .sql-results thead th {
  position: sticky;
  top: 0;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  padding: 6px 10px;
  text-align: left;
  font-weight: 600;
  color: var(--fg);
}
#view-root .sql-results tbody td {
  padding: 4px 10px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
  max-width: 480px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Mobile: the whole #view-root is user-select:none for scroll
     hygiene, but query results are the one place where copy-paste
     is the entire point. Opt back in. */
  -webkit-user-select: text;
  user-select: text;
}
#view-root .sql-results .sql-null { color: var(--dim); font-style: italic; }
#view-root .sql-footer { margin-top: 6px; font-size: 11px; }

/* Mine / All segmented control. Two buttons sit flush together as
 * a single pill group; the active one fills with the accent so the
 * live state is unambiguous at a glance. Inactive button stays
 * outlined and dim so the toggle reads as a single object rather
 * than two competing CTAs. */
.seg-toggle {
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
  background: var(--panel);
  padding: 2px;
  align-items: stretch;
}
.seg-toggle .seg-btn {
  border: 0;
  background: transparent;
  color: var(--dim);
  padding: 4px 12px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
}
.seg-toggle .seg-btn:hover { color: var(--fg); }
.seg-toggle .seg-btn.seg-active {
  background: var(--accent);
  color: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}
.seg-toggle .seg-btn.seg-active:hover { color: #fff; }
/* Shared seg-toggle sizing — applies on both Projects and Calendar
 * titlebars so the Mine/All pill reads identical across pages.
 * #view-root prefix gives it specificity 1,2,0 so it beats the
 * global mobile tap-target rule (#view-root button:not(table button),
 * specificity 1,0,1) — otherwise the pill ballooned to 44px on
 * mobile. min-height:0 explicitly defeats the same 44px floor. */
#view-root .seg-toggle .seg-btn {
  padding: 3px 10px;
  font-size: 12px;
  min-height: 0;
  line-height: 1.3;
}
@media (max-width: 760px) {
  #view-root .seg-toggle .seg-btn {
    padding: 3px 9px;
    font-size: 11px;
    min-height: 0;
    line-height: 1.3;
  }
  /* On mobile, push the Mine/All toggle to the right edge of the
   * titlebar row. Saves the thumb a trip across the screen and
   * lets the heading own the left edge. Projects' Calendar /
   * Archive / +New cluster keeps its own margin-left:auto, so it
   * either pairs with the toggle on the same row or wraps to the
   * next — flex-wrap on the titlebar handles both naturally. */
  #view-root .dashboard-titlebar .seg-toggle,
  #view-root .calendar-titlebar  .seg-toggle {
    margin-left: auto;
  }
}
/* Project-detail schedule list: phase label on the left, date + time
   on the right. Date/time text inherit from .card-badge for color, no
   background here. */
.project-schedule .schedule-row { align-items: baseline; }
.project-schedule .phase-label { font-weight: 600; min-width: 160px; }
.project-schedule .phase-date  { color: var(--accent); }
.project-schedule .phase-time  { color: var(--dim); font-size: 12px; }

/* Color the column header by status (works for any kanban whose
   groupBy values match these names — patio-tracker deliverable
   statuses, but also any future workflow with the same vocabulary). */
.kanban-column:has(.kanban-column-cards[data-status="not started"]) .kanban-column-name { color: var(--dim); }
.kanban-column:has(.kanban-column-cards[data-status="ordered"])     .kanban-column-name { color: var(--accent); }
.kanban-column:has(.kanban-column-cards[data-status="in-transit"])  .kanban-column-name { color: var(--hl); }
.kanban-column:has(.kanban-column-cards[data-status="scheduled"])   .kanban-column-name { color: var(--accent); }
.kanban-column:has(.kanban-column-cards[data-status="in-progress"]) .kanban-column-name { color: var(--hl); }
.kanban-column:has(.kanban-column-cards[data-status="done"])        .kanban-column-name { color: var(--ok); }
.kanban-column:has(.kanban-column-cards[data-status="blocked"])     .kanban-column-name { color: var(--warn); }

/* ---- mobile pass: scroll-snap columns + bigger touch targets ---- */
@media (max-width: 760px) {
  .kanban-board {
    /* CSS scroll-snap: swipes land on a column edge. scroll-padding
       leaves a thin gutter so the next column peeks in from the right,
       hinting at horizontal-scroll-ability without an arrow icon. */
    scroll-snap-type: x mandatory;
    scroll-padding-left: 8px;
    scroll-padding-right: 24px;
    gap: 14px;
    padding: 4px 4px 12px;
    /* Web-platform hint: tell the browser this is a horizontally-
       scrolling area so it doesn't fight touch gestures. */
    overscroll-behavior-x: contain;
  }
  .kanban-column {
    flex: 0 0 280px;
    min-width: 280px;
    max-width: 280px;
    scroll-snap-align: start;
  }
  .kanban-column-head { padding: 12px 14px; font-size: 14px; }
  .kanban-column-name { font-size: 14px; }
  .kanban-column-cards {
    padding: 10px;
    gap: 8px;
    /* Allow card lists to scroll vertically inside a tall column on
       a phone (e.g. 'planned' bucket with 20+ entries) without the
       whole page scrolling away from the kanban. */
    max-height: 70vh;
    overflow-y: auto;
  }
  .kanban-card {
    min-height: 44px;      /* iOS HIG tap target */
    padding: 12px 14px;
    font-size: 15px;
    line-height: 1.35;
    /* Don't let the flex column shrink cards when many of them
       overflow the 70vh max-height — the inner span tree doesn't
       shrink with the box and the phase-age line bleeds out the
       bottom border. flex-shrink:0 keeps each card at its natural
       height; the column's `overflow-y: auto` handles the excess
       via internal scroll. */
    flex-shrink: 0;
  }
}

/* Landscape on a phone (short viewport, wider than tall): drop the
   70vh column-height cap and the inner overflow. In portrait the
   cap stops a 'planned' bucket with 20 cards from pushing the
   rest of the page out of reach; in landscape that same cap leaves
   only ~220px of card area, and SortableJS's drag clone + the
   re-flowing siblings end up visually overlapping each other as
   the user drags. Letting the column grow naturally and using
   page-level scroll keeps the drag UX clean. */
@media (max-width: 900px) and (orientation: landscape) {
  .kanban-column-cards {
    max-height: none;
    overflow-y: visible;
  }
}

