: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);
}

/* 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); }

/* Warm, mobile-first "cream" theme (kanban redesign handoff). A soft
   parchment canvas with white cards, a friendly coral accent, and the
   rounded Nunito face. Tokens map the handoff palette onto the app's
   theme variables; the kanban + card-detail visuals layer further
   warm-specific rules deeper in this file (scoped to this theme so the
   other presets are untouched). Pairs with Shoelace's LIGHT theme —
   kept OUT of the sl-theme-dark sets in index.html / settings.js. */
[data-theme="cream"] {
  --bg: #F6F1E9;            /* warm cream — page background */
  --panel: #FBF7F0;         /* column / section-card background */
  --border: #EAE0D2;        /* card / input / panel borders */
  --fg: #3B332B;            /* ink — titles, input text */
  --dim: #6B6157;           /* ink-2 — dates, body text */
  --accent: #E08763;        /* coral — primary buttons, FAB, drag highlight */
  --hl: #B5852A;            /* amber — "needs a date", code/headings */
  --warn: #C98A86;          /* soft terracotta — danger */
  --ok: #7FA88B;            /* sage green — success / ready */
  --json-string: #7FA88B;
  --json-number: #C7836A;
  --json-boolean: #8268A8;
  --json-null: #A89C8C;
  --json-file: #8FB0D9;
  --fault-bg: #FBE7E9;      /* pale rose wash */
  --overlay: rgba(60, 40, 20, 0.42);
  --shadow: 0 34px 70px -30px rgba(60, 40, 20, 0.42);
}
/* Nunito is the core of the soft feel. Scoped to the cream theme so we
   don't ship a web-font swap to every other preset (they stay on the
   system monospace stack). Falls back to a rounded humanist sans, then
   the system sans, if the Google Fonts link is blocked. */
[data-theme="cream"] body {
  font-family: 'Nunito', ui-rounded, 'Segoe UI', system-ui, sans-serif;
}

* { box-sizing: border-box; }

/* 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;
  height: 100vh;
}

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; }

/* build stamp — login screen + Settings. Quiet, monospaced, unobtrusive:
   it's a deploy-confirmation marker, not chrome to read every visit. */
.build-version {
  color: var(--dim);
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  opacity: 0.7;
  margin-top: 4px;
}

/* Subtle text-link on the login screen that opens the admin-password
   reset help popup. Styled as a quiet link, not a chunky button, so it
   sits below the primary SIGN IN / REGISTER actions unobtrusively. */
.admin-help-link {
  align-self: flex-start;
  background: none;
  border: none;
  padding: 0;
  margin-top: 8px;
  color: var(--dim);
  font-size: 12px;
  text-decoration: underline;
  cursor: pointer;
}
.admin-help-link:hover { color: var(--fg); }

/* Admin-password help popup (login screen). Fixed full-screen backdrop
   with a centered card; sits above app chrome but below the command
   palette (z 110) so nothing traps focus over it. */
.admin-help-overlay {
  position: fixed;
  inset: 0;
  z-index: 105;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 0, 0, 0.5);
}
.admin-help-card {
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 4px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35);
  width: min(560px, 100%);
  max-height: 85vh;
  overflow: auto;
}
.admin-help-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  background: var(--bg);
}
.admin-help-head button {
  background: none;
  border: none;
  color: var(--dim);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}
.admin-help-head button:hover { color: var(--fg); }
.admin-help-body { padding: 12px 14px 16px; }
.admin-help-body p { margin: 0 0 10px; }
/* Restore numbered markers — a global `ol { list-style: none }` reset
   strips them app-wide, but these are genuine ordered steps. */
.admin-help-body ol { margin: 0 0 10px; padding-left: 22px; list-style: decimal; }
.admin-help-body li { margin-bottom: 4px; }
.admin-help-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  background: var(--code-bg, rgba(127, 127, 127, 0.15));
  padding: 1px 4px;
  border-radius: 2px;
}
.admin-help-body pre {
  background: var(--code-bg, rgba(127, 127, 127, 0.15));
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 10px;
  overflow-x: auto;
  margin: 0 0 10px;
}
.admin-help-body pre code { background: none; padding: 0; white-space: pre; }
.admin-help-tip { color: var(--dim); font-size: 12px; }

.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;
  /* Wider gap BETWEEN fields than the label→input gap below, so each label
     clearly belongs to the field under it (was 10px, nearly equal to the
     label's 7px float and read as belonging to the field above). */
  gap: 16px;
  max-width: 520px;
}
#view-root .field { display: flex; flex-direction: column; gap: 4px; }
/* margin-bottom:0 — the flex gap alone spaces the label from its input; the
   generic `#view-root label { margin-bottom:4px }` otherwise pushed the label
   up into no-man's-land between fields. */
#view-root .field label { color: var(--dim); font-size: 12.5px; margin-bottom: 0; text-transform: uppercase; letter-spacing: 0.06em; }
/* Phones: bump the (uppercase, easily-squinted) field labels a touch more. */
@media (max-width: 760px) {
  #view-root .field label { font-size: 13.5px; }
}
#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; }
/* Password fields: inline show/hide eye toggle (renderField wraps the
   input in .pw-wrap). The button overlays the right edge of the input;
   extra right padding keeps typed text clear of it. */
#view-root .field .pw-wrap { position: relative; display: block; }
#view-root .field .pw-wrap input { padding-right: 34px; }
#view-root .field .pw-toggle {
  position: absolute;
  top: 1px; bottom: 1px; right: 1px;
  width: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 2px;
  background: transparent;
  color: var(--dim);
  cursor: pointer;
}
#view-root .field .pw-toggle:hover { color: var(--fg); }
#view-root .field .pw-toggle .pw-ico { display: block; }
#view-root .field .pw-toggle .pw-ico--hide,
#view-root .field .pw-toggle.is-on .pw-ico--show { display: none; }
#view-root .field .pw-toggle.is-on .pw-ico--hide { display: block; }
#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;
}
/* Phase panel field rows. Spacing between the (date / assignee / status)
   fields lives on the wrapper. */
#view-root .phase-note-body .phase-field { margin-bottom: 10px; }
#view-root .phase-note-body .phase-field:last-child { margin-bottom: 0; }
/* Date + Assignee: label on the SAME line as the control (saves vertical
   space). min-width aligns the two controls; margin-bottom:0 since the row
   is horizontal now. */
#view-root .phase-field-date .field,
#view-root .phase-field-assignee .field {
  flex-direction: row;
  align-items: center;
  gap: 10px;
}
#view-root .phase-field-date .field > label,
#view-root .phase-field-assignee .field > label {
  flex: 0 0 auto;
  min-width: 72px;
  margin-bottom: 0;
}
/* Job Checklist only (customChecklist sections, tagged data-checklist-style
   on the row — see phase-notes-widget.js): match the built-in date fields'
   label to the custom checklist items' plain-text label style instead of
   the default #view-root .field label small-caps treatment (uppercase,
   letter-spaced, dim). Scoped to data-checklist-style rows so Patio Cover
   Tracker's own date fields (finalMeasureAt, designOrderDate, …), which
   never set customChecklist, keep their existing look untouched. */
#view-root .phase-note-row[data-checklist-style="1"] .phase-field-date .field > label {
  text-transform: none;
  letter-spacing: 0;
  font-size: 13px;
  color: var(--fg);
}
/* Same leading calendar glyph the custom date items get (see
   .checklist-item-date .checklist-item-text::before) — a checkbox row's
   own glyph gives it a "control, then label" shape a plain date label
   didn't have, so the two row types read as misaligned side by side. */
#view-root .phase-note-row[data-checklist-style="1"] .phase-field-date .field > label::before {
  content: "📅 ";
}
#view-root .phase-note-row[data-checklist-style="1"] .phase-field-date .field input[type="date"] {
  width: 160px;
}
#view-root .phase-field-date .field > :not(label),
#view-root .phase-field-assignee .field > :not(label) {
  flex: 1 1 auto;
  min-width: 0;
  /* A plain date/datetime-local input has no natural cap, so it stretched
     to fill the entire remaining row width. 220px comfortably fits a
     datetime-local (date + time) without looking absurd for a plain date. */
  max-width: 220px;
}
/* Status (notes textarea) stays stacked, but pull the label down tight to
   its field so it reads as the field's label, not a floating heading. */
#view-root .phase-field-status .field { gap: 1px; }
#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;
}

/* Custom checklist items (phase-notes-widget.js, CustomChecklist rider) —
   an editable list of ad-hoc items nested inside a phase panel, below its
   fixed fields. No divider from the fixed fields above and no
   strikethrough on done — reads as just more items in the same
   checklist, not a visually distinct "extra" section. */
#view-root .phase-checklist {
  margin-top: 10px;
}
#view-root .checklist-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
}
#view-root .checklist-item-label {
  display: flex;
  align-items: center;
  gap: 8px;
  /* NOT flex:1 — that stretched the checkbox+label to fill the row,
     shoving the × delete button to the far right edge instead of right
     next to the label (compare the date row: its label/input don't
     stretch either, so its × already sits snug beside the input). */
  flex: 0 1 auto;
  min-width: 0;
  cursor: pointer;
  /* Undo the generic #view-root label { color: var(--dim) } default —
     this wraps a checkbox item's own text, not a field's caption, so it
     should read exactly like the built-in checkbox fields' label
     (#view-root .field-checkbox label) rather than a dimmed form label. */
  color: var(--fg);
  font-size: 13px;
  margin-bottom: 0;
}
#view-root .checklist-item-text {
  overflow-wrap: anywhere;
}
#view-root .checklist-item-del {
  flex: 0 0 auto;
  /* Pushes the button to the row's right edge unconditionally, rather
     than relying on some *other* sibling growing to consume the slack.
     A checkbox item's flush-right × comes from .checklist-item-label's
     own flex:1 (it's the only child of the outer row); a date item has
     no such grower — .checklist-item-text and .checklist-item-value are
     both flex:0 0 auto — so its × sat wherever the 160px-wide date input
     happened to end, short of the row's actual right edge and out of
     column with a checkbox item's × directly above/below it. */
  margin-left: auto;
  background: none;
  border: none;
  color: var(--text-dim, var(--text));
  opacity: 0.5;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}
#view-root .checklist-item-del:hover { opacity: 1; }
/* Date items: static label, then the item's own date input — no
   checkbox, "done" (see isItemDone in phase-notes-widget.js) is just
   "has a value", same visual treatment (no strikethrough, see above).
   Deliberately NO custom box chrome (background/border/padding) here —
   #view-root .field input[type="date"] (the built-in date fields' own
   rule) only sets width/min-width and leaves everything else at the
   browser's native date-picker look, so this input needs to match that
   exactly: same 160px width, same hands-off everything else. Custom
   styling here would read as visually different from the built-in
   fields right next to it in the same panel. */
#view-root .checklist-item-text {
  flex: 0 0 auto;
}
/* A checkbox row's own glyph gives it a small leading "control" before the
   text; a date row had nothing there, so its label sat flush against the
   panel's left edge while checkbox labels sat indented past their
   checkbox — the two row types read as misaligned against each other.
   A leading calendar glyph gives the date row the same "control, then
   label" shape (and doubles as the calendar-icon association asked for
   when date items were designed) without hardcoding a checkbox's exact
   native pixel width, which varies by browser/OS anyway. */
#view-root .checklist-item-date .checklist-item-text::before {
  content: "📅 ";
}
#view-root .checklist-item-value {
  flex: 0 0 auto;
  width: 160px;
  min-width: 0;
  font: inherit;
}
/* "+ Add item" toggle — compact, so a panel with nothing to add right
   now doesn't show the full kind/input/button row by default. */
#view-root .phase-checklist-add-toggle {
  margin-top: 6px;
  background: none;
  border: 1px dashed var(--border);
  border-radius: 4px;
  color: var(--dim);
  font: inherit;
  font-size: 12px;
  padding: 4px 8px;
}
#view-root .phase-checklist-add-toggle:hover {
  color: var(--text);
  border-color: var(--text-dim, var(--text));
}
#view-root .phase-checklist-add {
  display: flex;
  gap: 6px;
  margin-top: 6px;
}
/* #view-root .phase-checklist-add already sets display:flex, which
   outranks the UA [hidden] default on specificity — same override this
   codebase already uses for .card-prefs-panel[hidden]. */
#view-root .phase-checklist-add[hidden] {
  display: none;
}
#view-root .phase-checklist-add-cancel {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: var(--text-dim, var(--text));
  opacity: 0.6;
  font-size: 16px;
  line-height: 1;
  padding: 2px 6px;
}
#view-root .phase-checklist-add-cancel:hover { opacity: 1; }
#view-root .phase-checklist-kind {
  flex: 0 0 auto;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 5px 6px;
  font: inherit;
}
#view-root .phase-checklist-input {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 5px 8px;
  font: inherit;
}
#view-root .phase-checklist-add-btn {
  flex: 0 0 auto;
}

/* ── Client message thread (messageThread screen) ───────────────────
   A card-list of message rows rendered as chat bubbles: outbound hugs
   the right with an accent wash, inbound hugs the left in a neutral
   panel. data-value (=$row.direction) drives the side + colour. */
#view-root .message-thread { max-width: 640px; padding: 4px 14px 8px; }
/* Breathing room around the bubble stack so messages don't run to the edge. */
#view-root .message-thread .card-list { padding: 4px 2px; }
/* Flatten the card-list's own card chrome inside the thread so each
   message reads as a free-floating chat bubble, not a row in a list.
   Applies to the standalone messageThread screen AND the two-pane
   inbox's reading pane (same thread card-list, different ancestor). */
#view-root .message-thread .card-list-card,
#view-root .msg-split-detail .card-list-card {
  border: none;
  background: transparent;
  padding: 0;
  box-shadow: none;
}
#view-root .message-thread .card-list-card:hover,
#view-root .msg-split-detail .card-list-card:hover { box-shadow: none; }
#view-root .message-card {
  max-width: 78%;
  padding: 11px 15px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--panel);
}
#view-root .message-card.message-out {
  margin-left: auto;            /* push the bubble to the right */
  background: color-mix(in srgb, var(--accent) 16%, var(--panel));
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}
#view-root .message-card.message-in {
  margin-right: auto;           /* keep inbound on the left */
}
#view-root .message-meta {
  font-size: 11px;
  color: var(--dim);
  margin-bottom: 2px;
}
#view-root .message-text {
  white-space: pre-wrap;
  line-height: 1.45;
  /* A pasted URL or other long unbroken token must not blow out the
     bubble width. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* Long emails (3-page quotes, threaded replies) would otherwise make one
   bubble run the length of the thread. Clamp tall ones and let a tap open
   them. .is-long is added by the runtime only when the body overflows the
   clamp, so short messages keep no affordance. */
#view-root .message-text.is-long {
  cursor: pointer;
}
#view-root .message-text.is-long:not(.expanded) {
  max-height: 220px;
  overflow: hidden;
  position: relative;
}
#view-root .message-text.is-long:not(.expanded)::after {
  content: "Show more ⌄";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 18px 0 2px;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  text-align: center;
  background: linear-gradient(to bottom,
    transparent,
    color-mix(in srgb, var(--panel) 88%, transparent) 55%,
    var(--panel) 100%);
}
/* Match the fade to the outbound bubble's tinted background. */
#view-root .message-card.message-out .message-text.is-long:not(.expanded)::after {
  background: linear-gradient(to bottom,
    transparent,
    color-mix(in srgb, var(--accent) 16%, var(--panel)) 70%);
}
/* ============================================================
   Two-pane (Outlook-style) inbox. The #messageInbox screen renders a
   conversation list (.msg-split-list) and a reading pane
   (.msg-split-detail) inside one .msg-split container. Whether they sit
   side by side or stack to one-at-a-time is purely responsive — the
   seed JSON renders both panes and toggles a .has-selection class; CSS
   decides the layout, so the same screen serves phone and desktop.
   ============================================================ */
/* Desktop / wide: side by side, both always visible. The list is a fixed
   sidebar; the reading pane fills the rest. (#view-root .msg-split beats
   the .box-col flex-direction set by the box renderer.) */
@media (min-width: 761px) {
  #view-root .msg-split {
    flex-direction: row;
    align-items: stretch;
    gap: 18px;
  }
  #view-root .msg-split-list {
    flex: 0 0 320px;
    max-width: 320px;
    min-width: 0;
    border-right: 1px solid var(--border);
    padding-right: 14px;
    /* x:hidden so a long preview clips with an ellipsis instead of
       widening the row (overflow-y:auto alone promotes overflow-x to
       auto, which let the card scroll past the pane). y:auto keeps the
       list independently scrollable. */
    overflow: hidden auto;
    /* Independent scroll for the list so a long thread doesn't push it
       off-screen. Header chrome is ~140px. */
    max-height: calc(100vh - 150px);
  }
  #view-root .msg-split-detail {
    flex: 1 1 auto;
    min-width: 0;
  }
  /* The list is always present on wide screens, so the in-pane "back to
     conversations" affordance is redundant. */
  #view-root .msg-detail-back { display: none; }
  /* The reading pane's bubbles already cap their own width; let the
     thread use the wider pane. */
  #view-root .msg-split-detail .message-thread,
  #view-root .msg-split-detail .card-list { max-width: none; }
}
/* Mobile / narrow: one pane at a time, like Outlook on a phone. Show the
   list by default; once a conversation is open (.has-selection), the
   reading pane takes over and the list hides. The empty placeholder is
   irrelevant here because the detail pane is hidden until a tap. */
@media (max-width: 760px) {
  #view-root .msg-split:not(.has-selection) .msg-split-detail { display: none; }
  #view-root .msg-split.has-selection  .msg-split-list   { display: none; }
}
/* The mobile "← Conversations" button: a low-key pill, left-aligned. */
#view-root .msg-detail-back {
  align-self: flex-start;
  font-size: 13px;
  padding: 4px 12px;
  border-radius: 999px;
}
#view-root .msg-detail-empty {
  padding: 32px 8px;
  text-align: center;
}

/* App-editor Custom CSS textarea — monospace + preserve whitespace so it
   reads like a code editor. */
#view-root .css-panel .css-editor {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  white-space: pre;
  tab-size: 2;
}

#view-root .message-compose {
  border-top: 1px solid var(--border);
  padding-top: 10px;
  margin-top: 4px;
}
#view-root .message-compose textarea {
  width: 100%;
  resize: vertical;
  min-height: 64px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font: inherit;
}
#view-root .message-compose-row { align-items: center; }
#view-root .message-compose-hint { font-size: 12px; }
/* Forward chip on an inbound bubble — small, low-key. */
#view-root .msg-fwd-chip {
  align-self: flex-start;
  margin-top: 4px;
  font-size: 11px;
  padding: 1px 7px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--dim);
  cursor: pointer;
}
#view-root .msg-fwd-chip:hover { color: var(--text); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
/* Small messaging affordance on the kanban card. */
#view-root .card-msg-btn {
  align-self: flex-start;
  margin-top: 4px;
  font-size: 11px;
  padding: 2px 8px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--dim);
  cursor: pointer;
  /* Keep the label on one line so a count like "📷 Photos (3)" grows the
     pill WIDER, not taller. On a narrow mobile card the default would wrap
     the "(3)" to a second line and balloon the pill's height. */
  white-space: nowrap;
}
#view-root .card-msg-btn:hover {
  color: var(--text);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
@media (max-width: 760px) {
  /* The global mobile tap-target rule (#view-root button:not(table button))
     pins every button to min-height:44px, which leaves the small card pills
     (💬 Messages / 📷 Photos) looking far too tall for their 11px label. Drop
     them ~25% to a tighter 33px. #view-root .card-msg-btn (specificity 1,1,0)
     beats that 44px rule, and min-height:0 isn't needed since we set an
     explicit height. Still a comfortable thumb target. */
  #view-root .card-msg-btn {
    min-height: 33px;
    padding: 4px 10px;
    line-height: 1.2;
  }
}

/* Email-signature HTML preview — frame it like a little email card so the
   rendered source reads as "what the client sees". */
#view-root .sig-preview-wrap { max-width: 640px; }
#view-root .sig-preview {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: #ffffff;
  color: #111827;
  padding: 12px 14px;
}
#view-root .sig-preview a { color: #2563eb; }

/* Job-photo gallery — responsive grid of thumbnails. */
#view-root .photo-grid .card-list-card { border: none; background: transparent; padding: 0; }
#view-root .photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
  max-width: 760px;
}
#view-root .photo-card { gap: 4px; }
#view-root .photo-img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--panel);
  display: block;
}
#view-root .photo-caption { font-size: 12px; }
#view-root .photo-by { font-size: 11px; }
/* Project name on the cross-project gallery cards. */
#view-root .photo-project { font-size: 12px; font-weight: 600; color: var(--accent); }

/* Full-size photo view (photoView screen). */
#view-root .photo-view { max-width: 640px; }
#view-root .photo-view-img {
  width: 100%;
  max-height: 70vh;
  object-fit: contain;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: #000;
  display: block;
}
#view-root .photo-view-caption { font-size: 15px; }
#view-root .photo-delete {
  align-self: flex-start;
  color: #dc2626;
  border: 1px solid color-mix(in srgb, #dc2626 45%, var(--border));
  background: transparent;
}
#view-root .photo-delete:hover { background: color-mix(in srgb, #dc2626 12%, transparent); }

/* 📷 photo count on the kanban card — collapses when empty. */
#view-root .card-photo-count { font-size: 11px; color: var(--dim); }
#view-root .card-photo-count:empty { display: none; }

/* Unread-mail badge on the board's 📬 Messages button. Overlaps the
   button's top-right corner like a notification dot. */
#view-root .msg-nav { position: relative; }
#view-root .msg-nav-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  background: #dc2626;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  min-width: 16px;
  text-align: center;
  padding: 3px 5px;
  border-radius: 999px;
  pointer-events: none;
}

/* Unsorted bar at the top of the inbox — amber so it reads as "needs
   attention" without being an error. */
#view-root .inbox-unsorted-bar {
  align-items: center;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, #f59e0b 55%, var(--border));
  background: color-mix(in srgb, #f59e0b 12%, var(--panel));
  border-radius: 8px;
}
#view-root .inbox-unsorted-badge {
  background: #f59e0b;
  color: #1a1a1a;
  font-size: 11px;
  font-weight: 700;
  min-width: 18px;
  text-align: center;
  padding: 1px 7px;
  border-radius: 999px;
}
#view-root .unsorted-row { padding: 2px 0; }

/* ── Combined inbox (messageInbox screen) ───────────────────────────
   One row per conversation. Unread rows get an accent left-rail + a
   count badge so replies that need attention stand out at a glance. */
#view-root .message-inbox { max-width: 680px; }
/* Each conversation is one card-list-card (padded, with the unread rail);
   the inner .inbox-row is just a content wrapper. It shares the class name
   with the inbound-webhook queue, so neutralize that border/bg/padding
   here and let it flex-fill the card with min-width:0 so the preview can
   ellipsize instead of widening the row past the pane (which overflowed
   the two-pane sidebar). Scoped to .message-inbox so the queue is safe. */
#view-root .message-inbox .inbox-row {
  border: none;
  background: transparent;
  padding: 0;
  flex: 1 1 auto;
  min-width: 0;
}
#view-root .message-inbox .card-list,
#view-root .message-inbox .card-list-card {
  min-width: 0;
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}
/* The card-list-card is normally a flex row (icon + body). A conversation
   has a single body child, and that flex layout let the inner row keep its
   nowrap min-content width (so the preview never ellipsized). Block layout
   makes the body a normal full-width child, so the preview clips with an
   ellipsis at the card width. */
#view-root .message-inbox .card-list-card { display: block; }
#view-root .inbox-row-top { align-items: baseline; gap: 8px; }
#view-root .inbox-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#view-root .inbox-when { flex: 0 0 auto; font-size: 12px; }
#view-root .inbox-preview {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
#view-root .inbox-badge {
  flex: 0 0 auto;
  background: var(--accent);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  min-width: 18px;
  text-align: center;
  padding: 1px 6px;
  border-radius: 999px;
}
/* Unread conversation: accent rail + stronger name. card-list wraps each
   row in .card-list-card, so paint the rail there via :has(). */
#view-root .message-inbox .card-list-card:has(.inbox-unread) {
  border-left: 3px solid var(--accent);
}
#view-root .inbox-unread .inbox-name { font-weight: 700; }
#view-root .inbox-unread .inbox-preview { color: var(--text); }

/* 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. */
/* Bounded panel treatment — shared by both screens (same background/
 * border/radius) so the toolbar reads as one persistent, static piece
 * of chrome regardless of which one you're on; only the board/calendar
 * content beneath it visibly swaps when you switch. */
#view-root .dashboard-titlebar,
#view-root .calendar-titlebar {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 16px;
}
#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: 10vh;
  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: 70vh;
  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);   /* 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: 88vh;
  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; }

/* myTasks view — cross-board "assigned to me" list, one row per card
 * across every workflow-enabled entity. Mirrors .inbox-row's card
 * treatment; justify-content spreads title/meta left vs date right. */
.my-tasks-row {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 8px 10px;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
}
.my-tasks-row:hover { border-color: var(--accent); }
.my-tasks-row-meta { align-items: flex-end; text-align: right; }
.my-tasks-section-label {
  font-size: 12px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--dim);
}

/* attachments view — one row per file attached to a row, any entity. */
.attachment-row {
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 8px 10px;
  align-items: center;
}
.attachment-row-info { flex: 1; min-width: 0; }
.attachment-thumb-link { cursor: pointer; flex-shrink: 0; }
.attachment-thumb {
  width: 44px;
  height: 44px;
  border-radius: 4px;
  object-fit: cover;
  border: 1px solid var(--border);
  flex-shrink: 0;
}
.attachment-thumb-generic {
  align-items: center;
  justify-content: center;
  font-size: 20px;
  background: var(--bg);
}

/* <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; }
/* Card-prefs toolbar — the "⚙ Card" show/hide-fields control, drawn above
   a board that declares hideableFields. Right-aligned so it sits out of the
   way above the columns. */
.kanban-toolbar { display: flex; justify-content: flex-end; margin-bottom: 6px; }
.card-prefs-menu { position: relative; }
.card-prefs-toggle {
  font-size: 12px; padding: 3px 10px; border-radius: 999px;
  background: transparent; border: 1px solid var(--border); color: var(--dim);
  cursor: pointer; white-space: nowrap;
}
.card-prefs-toggle:hover {
  color: var(--text);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.card-prefs-panel {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 20;
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); padding: 8px; min-width: 190px;
  display: flex; flex-direction: column; gap: 4px;
}
/* The panel toggles open/closed via its `hidden` attribute. Without this the
   class's `display: flex` (equal specificity to UA `[hidden]`, but later) wins
   and the popover stays open permanently. */
.card-prefs-panel[hidden] { display: none; }
.card-prefs-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; cursor: pointer; padding: 2px 4px;
}
.card-prefs-row input { margin: 0; flex: 0 0 auto; }
.kanban-board {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  padding-bottom: 8px;
  /* stretch (not flex-start): every swimlane grows to the tallest column's
     height, so a sparse lane is still a full-height drop target — dragging a
     card into a near-empty lane no longer means aiming at a tiny box. The
     collapsed-lane CSS already assumes this. */
  align-items: stretch;
  /* Columns cap at max-width: 360px, so a narrowed board (e.g. the "Mine"
     scope leaving 1–3 phases) doesn't fill the viewport width — center the
     remaining columns instead of letting them pack to the left. `safe`
     degrades to start-alignment once columns overflow, so the first column
     stays reachable by horizontal scroll rather than getting clipped. */
  justify-content: safe center;
}
/* 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. */
@media (min-width: 900px) {
  body[data-role="user"]   .kanban-board,
  body[data-role="public"] .kanban-board,
  body.debug-collapsed     .kanban-board {
    width: calc(100vw - 40px);
    max-width: none;
    margin-left:  calc(50% - 50vw + 20px);
    margin-right: calc(50% - 50vw + 20px);
  }
}
.kanban-column {
  /* Grow to share the board width (so a wide screen fills instead of
     leaving dead space), but never below a readable 240px — past that the
     board scrolls. Cap growth at 360px: with few columns (e.g. the "Mine"
     scope's 1–3) they'd otherwise stretch to 600–1200px each, making cards
     — and their drag image — absurdly wide. */
  flex: 1 1 240px;
  min-width: 240px;
  max-width: 360px;
  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 */
  flex: 1;              /* fill the (now stretched) column so the whole lane
                           below the last card is a drop target */
}
.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; }
/* "You just moved this" highlight. Set by the kanban-block's drag-drop
   onEnd handler on the card that landed in a new column, so it's easy to
   spot which card you just moved on a busy board. Starts as a filled
   accent wash + ring and fades back to the card's normal look over ~2s.
   fill-mode forwards leaves it at the transparent end state, so even if
   no further render fires the card settles cleanly. */
@keyframes kanban-just-moved-fade {
  0%   { background: color-mix(in srgb, var(--accent) 22%, transparent);
         border-color: var(--accent);
         box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent); }
  100% { background: transparent;
         border-color: var(--border);
         box-shadow: 0 0 0 0 transparent; }
}
.kanban-card.kanban-card-just-moved {
  animation: kanban-just-moved-fade 2s ease-out forwards;
}
/* Collapsible terminal column (e.g. Complete), Fizzy-style. Driven by the
   kanban-block's `collapsibleColumns` prop. Collapsed = a narrow vertical
   bar showing just the name + count (no cards); click the bar to expand the
   column inline. Expanded state is a plain class toggle on the column. */
.kanban-column-collapsible .kanban-column-head { cursor: pointer; user-select: none; }
.kanban-column-collapsed {
  /* Override the normal 240px column sizing — shrink to a thin bar that
     still spans the board height so it's a comfortable drop target. */
  flex: 0 0 auto;
  min-width: 0;
  width: 40px;
}
.kanban-column-collapsed .kanban-column-owner { display: none; }
/* Stand the header text up so the bar reads top-to-bottom. Content height
   (not 100%) so the cards container below it stays a usable drop zone. */
.kanban-column-collapsed .kanban-column-head {
  writing-mode: vertical-rl;
  border-bottom: none;
  justify-content: flex-start;
  gap: 8px;
  white-space: nowrap;
  flex: 0 0 auto;
}
.kanban-column-collapsed .kanban-column-title-row {
  display: flex;
  flex-direction: row;   /* vertical writing-mode turns this into a column visually */
  align-items: center;
  gap: 8px;
}
/* Keep the cards container as a live drop target (hide the CARDS, not the
   zone) so a job can be dragged straight onto the collapsed bar to complete
   it — Sortable reads the lane from this element's data-status. A dashed
   tint marks it as droppable. */
.kanban-column-collapsed .kanban-column-cards {
  flex: 1 1 auto;
  min-height: 60px;
  padding: 4px;
  border: 1px dashed transparent;
}
.kanban-column-collapsed .kanban-column-cards > .kanban-card { display: none; }
/* Hide the "+ N more in archive" overflow link while the lane is collapsed
   (it wrapped awkwardly in the narrow bar); it reappears when expanded —
   the expand toggle just removes .kanban-column-collapsed. */
.kanban-column-collapsed .kanban-column-cards > .kanban-column-overflow { display: none; }
@media (hover: hover) and (pointer: fine) {
  .kanban-column-collapsed:hover .kanban-column-cards {
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
    border-radius: 6px;
  }
}
/* 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); }

/* ============================================================
   Kanban redesign — STRUCTURAL / FUNCTIONAL layer (theme-agnostic).
   These rules style behaviors wired into the board + form on EVERY
   theme — the mobile phase-pills/FAB (mobilePills), the per-column
   accent dot (columnAccents), and the reusable contact / compactRow
   section widgets + the accordion's assignee badge. They use theme
   TOKENS for color so each theme renders them coherently
   in its own palette. The warm-specific visuals (white cards, Nunito,
   curated chips) layer on top, scoped to [data-theme="cream"], below.
   Plain (low-specificity) selectors so the cream overrides win there.
   ============================================================ */
/* Per-phase accent dot — inline-block so it sits before the column
   name without needing the header to be flex. Color comes inline. */
.kanban-column-dot {
  display: inline-block; width: 10px; height: 10px; border-radius: 50%;
  margin-right: 3px; vertical-align: middle; flex: none;
}
/* ---- Mobile phase pills + FAB (mobilePills mode) ---- */
.kanban-pills {
  display: flex; gap: 8px; overflow-x: auto;
  padding: 2px 2px 10px; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.kanban-pills::-webkit-scrollbar { display: none; }
.kanban-pill {
  flex: none; display: flex; align-items: center; gap: 7px;
  padding: 8px 13px; border-radius: 999px;
  font-family: inherit; font-weight: 800; font-size: 13px;
  white-space: nowrap; cursor: pointer;
  border: 1.5px solid var(--border); background: var(--panel); color: var(--dim);
}
.kanban-pill.active {
  border-color: var(--col-accent, var(--accent));
  background: color-mix(in srgb, var(--col-accent, var(--accent)) 18%, var(--panel));
  color: var(--fg);
}
.kanban-pill-count {
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  font-size: 10.5px; font-weight: 800;
  background: color-mix(in srgb, var(--fg) 12%, transparent); color: var(--dim);
}
.kanban-pill.active .kanban-pill-count {
  background: var(--col-accent, var(--accent)); color: var(--bg);
}
/* Single-lane board in pills mode: one full-width column, no scroll strip. */
.kanban-board-pills { display: block; overflow: visible; padding: 0; }
.kanban-board-pills .kanban-column {
  width: auto; min-width: 0; max-width: none; flex: none;
}
.kanban-fab {
  position: fixed; right: 20px; bottom: 22px;
  width: 56px; height: 56px; border-radius: 50%; border: none;
  background: var(--accent); color: var(--bg); font-size: 30px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 12px 24px -6px rgba(0, 0, 0, 0.4);
  cursor: pointer; z-index: 50;
}
/* ---- Form: name+Details, contact panel, compactRow, assignee badge ---- */
.name-input-row { display: flex; gap: 8px; align-items: stretch; }
.name-input-row > input { flex: 1; min-width: 0; }
.details-toggle {
  flex: none; display: inline-flex; align-items: center; gap: 5px;
  padding: 0 12px; border-radius: 11px;
  font-family: inherit; font-size: 12.5px; font-weight: 800; cursor: pointer;
  border: 1.5px solid var(--border); background: var(--panel); color: var(--dim);
}
.details-toggle.active {
  border-color: var(--accent); color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--panel));
}
.details-toggle-icon {
  width: 14px; height: 14px; background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 19v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1'/%3E%3Ccircle cx='9.5' cy='8' r='3.2'/%3E%3Cpath d='M18 8.5h4M20 6.5v4'/%3E%3C/svg%3E") center/contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 19v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1'/%3E%3Ccircle cx='9.5' cy='8' r='3.2'/%3E%3Cpath d='M18 8.5h4M20 6.5v4'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* Collapsible contact panel — the collapse MUST be theme-agnostic, or
   the Details toggle would be a no-op on non-cream themes. */
.contact-section.collapsed .contact-panel { display: none; }
.contact-panel {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 14px; padding: 15px; margin: 8px 0 4px;
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
}
.contact-panel > .field:first-child { grid-column: 1 / -1; }
/* Folded-in Details fields (width/projection/price/dates) fill their grid cell
   rather than the fixed ~160px date/number inputs use elsewhere, so they line
   up edge-to-edge with the contact text inputs. */
#view-root .contact-panel .field input { width: 100%; }
/* compactRow — Dimensions & price (3 cells) / Schedule (2 cells). Arranges
   the fields side-by-side; each keeps its normal label on top (more
   mobile-friendly + screen-reader correct). Theme-agnostic. min-width:0 lets
   the grid cells shrink so a compact input doesn't force overflow. */
.compact-row {
  display: grid; gap: 12px; margin: 0;
  /* Auto-wrap: e.g. 5 fields lay out 3 + 2 on a desktop form, fewer per row on
     a phone — instead of cramming into one N-column strip. */
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.compact-cell { min-width: 0; }
/* Compact numeric / date inputs fill their grid cell rather than the fixed
   160px they use elsewhere, so they line up edge-to-edge in the grid. */
#view-root .compact-cell .field.compact input,
#view-root .compact-cell .field input[type="date"] { width: 100%; }
/* Accordion collapsed-row assignee badge (rendered on every theme). */
.phase-note-assignee {
  margin-left: auto; flex: none; display: inline-flex; align-items: center;
}
.phase-assignee-avatar {
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--accent); color: var(--bg);
  font-size: 10.5px; font-weight: 800;
  display: inline-flex; align-items: center; justify-content: center;
}
.phase-assignee-empty {
  font-size: 10.5px; font-weight: 800; color: var(--dim);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  border-radius: 999px; padding: 3px 9px;
}
/* Per-person avatar tint (theme-agnostic — these warm pastels read fine
   on dark too). data-value is the assignee name slugged + lowercased by
   the widget's slugValue() / runtime dataValueBind, so the keys are
   lowercase. Unknown names fall through to the var(--accent) default
   above. Same palette is applied to the board card avatar below. */
.phase-assignee-avatar[data-value="kevin"] { background: #E7B65C; }
.phase-assignee-avatar[data-value="mark"]  { background: #8FBE9C; }
.phase-assignee-avatar[data-value="dana"]  { background: #E08C95; }
.phase-assignee-avatar[data-value="tonya"] { background: #8FB0D9; }

/* ============================================================
   Warm "cream" kanban redesign — DECORATIVE layer. Scoped to
   [data-theme="cream"] so the other themes keep their compact
   default board + cards. Recreates the JobCard / board handoff:
   white rounded cards on soft-cream columns, curated warm chips,
   and the warm overrides for the pills/accordion the structural
   layer above renders with theme tokens.
   ============================================================ */
[data-theme="cream"] .kanban-board { gap: 14px; padding-bottom: 6px; }
[data-theme="cream"] .kanban-column {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 13px 11px;
}
[data-theme="cream"] .kanban-column-head {
  border-bottom: none;
  padding: 2px 4px 11px;
  text-transform: none;
  align-items: center;
}
[data-theme="cream"] .kanban-column-title-row {
  display: flex; align-items: center; gap: 7px;
}
[data-theme="cream"] .kanban-column-name {
  font-weight: 800; font-size: 14px; color: var(--fg); letter-spacing: -0.01em;
}
[data-theme="cream"] .kanban-column-count {
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: #EFE7D9; color: #8A8073; font-size: 10.5px; font-weight: 800;
}
[data-theme="cream"] .kanban-column-cards { padding: 0; gap: 11px; }
[data-theme="cream"] .kanban-column-owner {
  font-size: 11px; font-weight: 800; letter-spacing: 0.06em;
  text-transform: uppercase; color: #A89C8C; padding: 0 4px;
}
/* Warm overrides for the pills + Details toggle (the structural layer
   above already shapes them; these just apply the curated handoff
   colors on cream). */
[data-theme="cream"] .kanban-pill { background: #fff; color: #7A7064; }
[data-theme="cream"] .kanban-pill.active {
  background: color-mix(in srgb, var(--col-accent, var(--accent)) 18%, #fff);
  color: var(--fg);
}
[data-theme="cream"] .kanban-pill-count { background: #EFE7D9; color: #8A8073; }
[data-theme="cream"] .kanban-pill.active .kanban-pill-count {
  background: var(--col-accent, var(--accent)); color: #fff;
}
[data-theme="cream"] .kanban-fab {
  box-shadow: 0 12px 24px -6px rgba(180, 90, 50, 0.55);
}
[data-theme="cream"] .details-toggle.active {
  border-color: #E8C9A0; background: #FBF1E4; color: #B5852A;
}

/* ---- Card (JobCard) ---- */
[data-theme="cream"] .kanban-card {
  background: #fff;
  border: 1px solid #EFE5D7;
  border-radius: 16px;
  padding: 13px 14px;
  box-shadow: 0 2px 6px rgba(80, 55, 25, 0.05);
  font-size: 12.5px;
}
/* Whole-card staleness tint (.kanban-card[data-value]) ties the rule above on
   specificity (both 0,2,0) and loses on source order, so cream cards stayed
   #fff. Re-assert the tint scoped to cream — mixed over #fff (the card base)
   so cards stay opaque — at a higher specificity so it wins. */
[data-theme="cream"] .kanban-card[data-value="warn"] {
  background: color-mix(in srgb, #d49a3a 9%, #fff);
  border-color: color-mix(in srgb, #d49a3a 45%, #EFE5D7);
}
[data-theme="cream"] .kanban-card[data-value="late"] {
  background: color-mix(in srgb, #e36161 11%, #fff);
  border-color: color-mix(in srgb, #e36161 50%, #EFE5D7);
}
[data-theme="cream"] .kanban-card.clickable { cursor: grab; }
@media (hover: hover) and (pointer: fine) {
  [data-theme="cream"] .kanban-card.clickable:hover {
    border-color: #E3D6C2;
    box-shadow: 0 4px 12px rgba(80, 55, 25, 0.09);
  }
}
[data-theme="cream"] .kanban-project-card { gap: 7px; }
/* Name — the primary identifier, promoted to the top of the card. */
[data-theme="cream"] .card-title-pill {
  font-weight: 700; font-size: 14.5px; color: var(--fg);
  line-height: 1.25; letter-spacing: -0.01em;
  background: none; padding: 0; border: none;        /* shed any .card-badge chrome */
}
/* Address — muted, with a pin glyph rendered via an SVG mask so it
   tints with the text color. */
[data-theme="cream"] .card-desc {
  display: flex; align-items: center; gap: 6px;
  color: #8A8073; font-size: 12.5px; font-weight: 600; line-height: 1.2;
}
[data-theme="cream"] .card-desc::before {
  content: ""; flex: none; width: 13px; height: 13px; background: #B4A99A;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21s6.5-5.5 6.5-10.5A6.5 6.5 0 0 0 5.5 10.5C5.5 15.5 12 21 12 21Z'/%3E%3Ccircle cx='12' cy='10.2' r='2.2'/%3E%3C/svg%3E") center/contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21s6.5-5.5 6.5-10.5A6.5 6.5 0 0 0 5.5 10.5C5.5 15.5 12 21 12 21Z'/%3E%3Ccircle cx='12' cy='10.2' r='2.2'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* Phase date line — calendar glyph + muted date text. */
[data-theme="cream"] .card-phase-date {
  display: flex; align-items: center; gap: 6px;
  color: var(--dim); font-size: 12.5px; font-weight: 700;
}
[data-theme="cream"] .card-phase-date::before {
  content: ""; flex: none; width: 13px; height: 13px; background: #A89C8C;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='5' width='17' height='15.5' rx='3.5'/%3E%3Cpath d='M3.5 9.5h17M8 3v3.5M16 3v3.5'/%3E%3C/svg%3E") center/contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='5' width='17' height='15.5' rx='3.5'/%3E%3Cpath d='M3.5 9.5h17M8 3v3.5M16 3v3.5'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* Promised pill — purple, self-collapsing when empty. */
[data-theme="cream"] .card-promised:not(:empty) {
  align-self: flex-start;
  background: #EFE9F5; color: #8268A8;
  font-size: 11.5px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
}
/* Footer: messages / photos / assignee stacked in the left rail so every
   leading icon (pin, calendar, 💬, 📷) lines up in one vertical column.
   The chips are de-chromed into plain icon+text rows to match the address
   and date lines above them. */
[data-theme="cream"] .kanban-project-card .card-action-row {
  flex-direction: column; align-items: flex-start; gap: 4px; margin-top: 3px;
}
[data-theme="cream"] .card-msg-btn {
  display: inline-flex; align-items: center; gap: 6px;
  background: transparent; border: none; padding: 0; margin: 0;
  color: var(--dim); font-size: 12.5px; font-weight: 700; cursor: pointer;
}
[data-theme="cream"] .card-msg-btn:hover { color: var(--text); }
/* Assignee: name only, left-aligned in the rail. The colored per-person
   dot was removed at the user's request. */
[data-theme="cream"] .card-assignee:not(:empty) {
  margin-left: 0;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 700; color: #6B6157;
}
/* Needs-a-date pill — exact handoff colors. The .kanban-project-card
   prefix keeps this above the theme-agnostic base (5269 block) on the
   specificity tie. */
[data-theme="cream"] .kanban-project-card .card-needs-date:not(:empty) {
  background: #FBF1DC; color: #B5852A;
}
[data-theme="cream"] .kanban-project-card .card-needs-date:not(:empty)::before {
  background: #E0A93D;
}

/* ---- Phase-notes accordion (warm decoration only; structure +
   assignee badge are in the theme-agnostic layer above) ---- */
[data-theme="cream"] .phase-note-row {
  border: 1px solid var(--border); border-radius: 14px;
  background: #fff;
}
[data-theme="cream"] .phase-note-row[open] {
  border-color: #E8C9A0;
  box-shadow: 0 2px 10px rgba(80, 55, 25, 0.07);
}
[data-theme="cream"] .phase-note-summary { align-items: center; padding: 11px 13px; }
[data-theme="cream"] .phase-note-label { font-weight: 800; font-size: 13px; }

/* ---- 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. */
/* Back/nav strip. Reserves a consistent height so the title row sits at the
   same y on entry and inner screens (no jump switching Cards ↔ Calendar). */
#view-root .screen-chrome {
  min-height: 44px;
  display: flex;
  align-items: center;
  margin-bottom: 2px;
}
/* Normal-size navigation button (NOT a tiny pill) — a proper tap target on
   every theme + mobile. Pills stay for on-card chips; navigation reads as a
   real button. Inherits the global 44px tap-target min-height. */
#view-root .screen-back-chip {
  font-size: 13px;
  font-weight: 600;
  padding: 8px 16px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--fg);
  cursor: pointer;
}
#view-root .screen-back-chip:hover {
  color: var(--accent);
  border-color: var(--accent);
}
/* 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); }
/* FullCalendar can set an explicit pixel width on the day-grid <table>
   that runs wider than its host (esp. after a resize), so the last
   weekday columns (Fri/Sat) overflow and clip. 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;
  /* The element keeps the .header-icon class (for sizing), which carries
     the armillary spin animation — cancel it here so a custom logo is
     still by default. Opt back in via .header-icon-spin (Branding toggle). */
  animation: none;
}
.header-icon-custom.header-icon-spin {
  animation: armillary-rotate 15s linear infinite;
}
.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's own live-search box (node.searchable) — sits directly above
   the card column, not wrapped in a .field/.form-fields container, so it
   needs its own base styling matching those (same tokens, no label row). */
.card-list-search {
  font: inherit; background: var(--bg); color: var(--fg);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 7px 10px; margin-bottom: 8px; width: 100%; min-width: 0;
}

/* card-list's Prev/Next pager (node.paginate) — sits below the cards. */
.card-list-pagination {
  display: flex; flex-direction: row; align-items: center;
  justify-content: center; gap: 12px; margin-top: 10px;
}
.card-list-pagination-summary { color: var(--dim); font-size: 0.9em; }
.card-list-pagination button:disabled { opacity: 0.4; cursor: default; }

/* CRM customerDetail screen (Data/CrmSeed.cs). .box-row already handles
   the button-width fix (buttons default to full-width stretch inside a
   plain .box-col — no align-items override there — so every button on
   this screen is wrapped in a row instead); these two are just the
   section chrome: a heading + its action button sharing one line, and a
   divider between the Quotes and Jobs sections so they read as distinct
   groups rather than one long flat list. */
.crm-section-header {
  display: flex; flex-direction: row; flex-wrap: wrap;
  align-items: center; justify-content: space-between; gap: 8px;
}
.crm-section-header h4 { margin: 0; }
.crm-section + .crm-section {
  border-top: 1px solid var(--border);
  padding-top: 18px;
}

/* 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;
}
/* Drag-and-drop (card-list drag:true). Grab affordance + SortableJS state
   classes; the just-moved tint fades a card dropped into another list so
   the user can see where it landed. */
.card-list.draggable .card-list-card { cursor: grab; }
.card-list.draggable .card-list-card:active { cursor: grabbing; }
.card-list-ghost { opacity: 0.4; }
.card-list-chosen { box-shadow: 0 2px 10px rgba(0,0,0,0.14); }
.card-list-drag { opacity: 0.9; }
.card-list-card-just-moved { animation: card-list-just-moved 2.2s ease-out; }
@keyframes card-list-just-moved {
  from { background: color-mix(in srgb, var(--accent) 22%, var(--panel)); }
  to   { background: var(--panel); }
}
@media (prefers-reduced-motion: reduce) {
  .card-list-card-just-moved { animation: none; }
}
.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;
}
/* Gold "credit" chip (goldChips prop) — same pill shape as .card-chip,
   recolored with --hl (the theme's amber/highlight token, already used
   for "needs attention" accents elsewhere) so a finished person/section
   reads as earned rather than just another neutral tag. */
.card-chip-gold {
  background: color-mix(in srgb, var(--hl) 22%, transparent);
  color: var(--hl); border-color: color-mix(in srgb, var(--hl) 45%, transparent);
  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 every
   theme (cream / light / dark / outdoor) gets 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); }
/* Quote Sign statuses (QuoteStatus enum) — CRM's customerDetail quotes
   card-list badge. */
.card-eyebrow[data-value="draft"],
.card-badge[data-value="draft"]        { color: var(--dim); }
.card-eyebrow[data-value="sent"],
.card-badge[data-value="sent"]         { color: var(--accent); }
.card-eyebrow[data-value="viewed"],
.card-badge[data-value="viewed"]       { color: var(--hl); }
.card-eyebrow[data-value="approved"],
.card-badge[data-value="approved"]     { color: var(--ok); }
.card-eyebrow[data-value="rejected"],
.card-badge[data-value="rejected"]     { color: var(--warn); }

/* 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); }
/* The default card text reads a touch small (both on phones and on
   desktop). Bump it on every width. The
   #view-root id prefix gives these (1,2,0) specificity so they win over both
   the structural and cream card rules (each 0,2,0–0,3,0) regardless of file
   order. One size set serves every theme and breakpoint. */
#view-root .kanban-project-card .card-title-pill { font-size: 16px; }
#view-root .kanban-project-card .card-desc       { font-size: 13.5px; }
#view-root .kanban-project-card .card-phase-date { font-size: 13.5px; }
#view-root .kanban-project-card .card-needs-date:not(:empty) { font-size: 12.5px; }
#view-root .kanban-project-card .card-promised   { font-size: 12.5px; }
#view-root .kanban-project-card .card-assignee   { font-size: 13px; }
#view-root .kanban-project-card .card-msg-btn    { font-size: 12.5px; }
/* The date line is now date-only (the "needs a date" warning moved to
   its own pill below). Collapse the row — and, on cream, its calendar
   ::before icon — when the current phase has no date. */
.kanban-project-card .card-phase-date:empty { display: none; }
/* Needs-a-date pill — theme-agnostic base built from var(--hl). Empty
   (date present, or a no-date-bearing / Complete phase) collapses it so
   only one of the date line / this pill ever shows. Cream overrides the
   exact colors above. */
.kanban-project-card .card-needs-date:empty { display: none; }
.kanban-project-card .card-needs-date:not(:empty) {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 700;
  padding: 3px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--hl) 18%, var(--panel));
  color: var(--hl);
}
.kanban-project-card .card-needs-date:not(:empty)::before {
  content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%;
  background: var(--hl);
}
/* 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. */
/* margin-right reserves room for the absolute top-right corner cluster
   (phase-age chip + notes hint) so a long name wraps clear of it instead
   of running underneath. */
.kanban-project-card .card-title-pill { align-self: flex-start; font-size: 12px; margin-right: 44px; }

/* 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; }
/* Top-right corner cluster: the notes hint + the compact phase-age chip.
   Absolute so the title pill / address / date below lay out as if it
   weren't there (the title pill reserves room via margin-right). */
.kanban-project-card .card-corner {
  position: absolute; top: 5px; right: 7px;
  display: flex; flex-direction: row; align-items: center; gap: 5px;
  z-index: 1;
}
.kanban-project-card .card-notes-hint {
  font-size: 14px; line-height: 1; opacity: 0.7;
}
.kanban-project-card .card-notes-hint:empty { display: none; }
/* Current-phase Status icon (📄) — greyed out by default (no status
   written for the stage the card is in); full color when the row
   reports a non-whitespace status (data-value="true"), so it reads as
   "there's text here" at a glance. Bigger than the old dot (16px vs
   11px) for an easier tap target. Desktop shows the status text on
   hover via the title attribute (titleBind). Emoji ignore the `color`
   property, so grey-vs-color uses filter/opacity instead. */
.kanban-project-card .card-status-dot {
  font-size: 16px; line-height: 1; cursor: default;
  filter: grayscale(1); opacity: 0.5;
}
.kanban-project-card .card-status-dot[data-value="true"] {
  filter: none; opacity: 1;   /* full color = status present */
}
/* Current-phase assignee line — small "person on this card now" label.
   Collapses when unassigned so the card stays tight. */
.kanban-project-card .card-assignee {
  font-size: 12px; color: var(--accent); opacity: 0.9; margin-top: 2px;
}
.kanban-project-card .card-assignee:empty { display: none; }
.kanban-project-card .card-assignee::before { content: "👤 "; opacity: 0.7; }
/* In the action row, sit at the bottom-right of the card (opposite the
   Photos chip): margin-left:auto pushes it to the far edge; no top margin
   since the row already spaces itself. */
.kanban-project-card .card-action-row .card-assignee {
  margin-left: auto; margin-top: 0; align-self: center; white-space: nowrap;
}
/* 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 chip — compact pill in the card's top-right corner ("today"
 * / "5d"). 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 it via :empty. */
.kanban-project-card .card-phase-age {
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  padding: 3px 7px;
  border-radius: 999px;
  color: var(--dim);
  background: color-mix(in srgb, var(--dim) 14%, transparent);
}
.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;
  background: color-mix(in srgb, #d49a3a 16%, transparent);
}
.kanban-project-card .card-phase-age[data-value="late"] {
  color: #e36161;
  background: color-mix(in srgb, #e36161 18%, transparent);
}
/* Messages + Photos side by side. Override the standalone chip's
   margin-top (the row's own spacing handles it) and allow wrap on a
   very narrow column. */
.kanban-project-card .card-action-row { margin-top: 2px; flex-wrap: wrap; align-items: center; }
.kanban-project-card .card-action-row .card-msg-btn { margin-top: 0; }

/* 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) {
  /* Larger, thumb-friendly Mine/All toggle on phones (was a cramped 11px
     pill). Applies on both the Projects and Calendar titlebars. */
  #view-root .seg-toggle .seg-btn {
    padding: 9px 18px;
    font-size: 14px;
    min-height: 40px;
    line-height: 1.2;
  }
  /* 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;
  }
  /* The mobile 280px column sizing above also matches collapsed terminal
     columns (equal specificity, later in the file) and would balloon the
     Complete bar to full width on a phone. Keep it the same thin 40px bar
     it is on desktop. */
  .kanban-column-collapsed {
    flex: 0 0 auto;
    min-width: 0;
    max-width: none;
    width: 40px;
  }
  .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;
  }
}

/* ============================================================
   Quote builder. Baseline styling for the quotes list + the quote
   document (.quote-doc). Deliberately plain — a business brands the
   printed/shared output via the app's Custom CSS (targeting .quote-doc,
   .quote-line, .quote-total-grand, etc.). These rules just make it
   readable before any branding is applied.
   ============================================================ */
#view-root .quote-row { padding: 2px 0; }
#view-root .quote-row-top { align-items: baseline; }
#view-root .quote-cust-name { flex: 1 1 auto; min-width: 0; }
#view-root .quote-summary { font-size: 13px; }
#view-root .quote-date { font-size: 12px; }
/* Status chip — colour-coded by data-value (draft/sent/accepted). */
#view-root .quote-status-chip {
  flex: 0 0 auto;
  font-size: 11px; font-weight: 700;
  padding: 2px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--dim) 16%, transparent);
  color: var(--dim);
}
#view-root .quote-status-chip[data-value="sent"] {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
#view-root .quote-status-chip[data-value="accepted"] {
  color: var(--ok);
  background: color-mix(in srgb, var(--ok) 18%, transparent);
}

#view-root .quote-doc { max-width: 720px; }
#view-root .quote-head { border-bottom: 2px solid var(--border); padding-bottom: 10px; }
#view-root .quote-doc-title {
  font-size: 13px; font-weight: 700; letter-spacing: 0.18em; color: var(--dim);
}
#view-root .quote-head .quote-cust-name { font-size: 20px; margin: 2px 0; }
/* Line items: description grows, the three figures align right + fixed-ish. */
/* display:block so each line's grid fills the full card width — otherwise
   the card's flex layout sizes the grid to its content and the columns
   drift row to row. */
#view-root .quote-lines .card-list-card { display: block; border: none; background: transparent; padding: 6px 0; box-shadow: none; }
/* Grid (not flex) so every row shares the same column tracks and the
   figures line up vertically down the page regardless of how long or
   multi-line a description is. align-items:start keeps qty/price/total
   level with the first line of a wrapped description. */
#view-root .quote-line {
  display: grid;
  grid-template-columns: 1fr 54px 104px 112px;
  gap: 10px;
  align-items: start;
}
#view-root .quote-line-desc  { min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
#view-root .quote-line-qty   { text-align: right; color: var(--dim); }
#view-root .quote-line-price { text-align: right; color: var(--dim); }
#view-root .quote-line-total { text-align: right; font-weight: 600; }
/* Row wrapper holds the column grid + a screen-only delete chip. The grid
   flexes to fill, so columns still line up; the chip sits to its right. */
#view-root .quote-line-row { align-items: start; gap: 8px; flex-wrap: nowrap; }
#view-root .quote-line-row .quote-line { flex: 1 1 auto; min-width: 0; }
/* Description-only line: drop the columns and let the text use full width. */
#view-root .quote-line-row--desc .quote-line { display: block; }
#view-root .quote-del-chip {
  flex: 0 0 auto; align-self: start; margin-top: 2px;
  border: none; background: transparent; color: var(--dim);
  font-size: 14px; padding: 2px 4px; line-height: 1; cursor: pointer;
}
#view-root .quote-del-chip:hover { color: var(--warn); }
/* Linked-artifacts panel on the quote (e.g. the job it spawned). */
#view-root .quote-links .card-list-card { display: block; border: none; background: transparent; padding: 2px 0; box-shadow: none; }
#view-root .quote-link-row { align-items: baseline; gap: 6px; }
#view-root .quote-link-rel { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
#view-root .quote-link-label { font-weight: 600; }
#view-root .quote-sharebox { margin-top: 4px; }
#view-root .quote-shareurl {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
  word-break: break-all; user-select: all; color: var(--accent);
}
/* Totals block — right-aligned ledger. */
#view-root .quote-totals-list .card-list-card { border: none; background: transparent; box-shadow: none; }
#view-root .quote-totals { max-width: 320px; margin-left: auto; }
#view-root .quote-total-row { justify-content: space-between; }
#view-root .quote-total-label { color: var(--dim); }
#view-root .quote-total-grand {
  border-top: 2px solid var(--border); padding-top: 6px; margin-top: 2px;
  font-size: 18px; font-weight: 700;
}
#view-root .quote-total-grand .quote-total-label { color: var(--fg); }
#view-root .quote-legal h3 { margin-bottom: 6px; }
#view-root .quote-legal p { font-size: 13px; line-height: 1.5; }

/* Full-contract layout: letterhead, buyer/application grid, payment
   schedule, signature lines. Structure only — the app's brand colours +
   fonts ride on the app's Custom CSS. */
#view-root .quote-letterhead {
  align-items: center; gap: 16px; flex-wrap: nowrap;
  border-bottom: 2px solid var(--border); padding-bottom: 10px;
}
#view-root .quote-logo { height: 64px; width: auto; }
#view-root .quote-letterhead-info { font-size: 13px; line-height: 1.4; }
#view-root .quote-meta { margin-left: auto; text-align: right; }
#view-root .quote-meta-num { font-weight: 700; font-size: 15px; }
#view-root .quote-section-h {
  margin: 6px 0 0; padding-bottom: 2px;
  border-bottom: 1px solid var(--border);
  text-transform: uppercase; letter-spacing: 0.05em; font-size: 13px;
}
#view-root .quote-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 6px 28px;
}
#view-root .quote-field { gap: 0; }
#view-root .quote-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim);
}
#view-root .quote-value { font-size: 14px; }
#view-root .quote-fineprint { font-size: 11px; font-style: italic; }
#view-root .quote-pay { max-width: 420px; margin-left: auto; }
#view-root .quote-pay-row { align-items: baseline; flex-wrap: nowrap; }
#view-root .quote-pay-label { flex: 1 1 auto; }
#view-root .quote-pay-val { flex: 0 0 auto; font-weight: 600; }
#view-root .quote-pay-via { flex: 0 0 auto; font-size: 12px; }
#view-root .quote-instr .quote-label { margin-bottom: 2px; }
#view-root .quote-sign { margin-top: 8px; }
#view-root .quote-rep { font-weight: 600; }
#view-root .quote-sign-row { gap: 20px; flex-wrap: nowrap; }
#view-root .quote-sign-cell { flex: 1 1 0; min-width: 0; }
#view-root .quote-sign-line { border-bottom: 1px solid var(--fg); height: 30px; }
#view-root .quote-sign-label { font-size: 11px; margin-top: 2px; }
#view-root .quote-accept-text { font-size: 12px; line-height: 1.45; }
#view-root .quote-cancel-note { font-size: 11px; font-weight: 700; line-height: 1.4; }

/* ============================================================
   Print primitive. A substrate capability: any screen can be printed
   (or saved to PDF) as a clean document. The op:'print' action fires
   window.print(); these rules strip the app chrome so only the rendered
   screen (#view-root) ends up on paper. Authors control the document with
   three helper classes, usable via a node's @class:
     .no-print     — hide on paper (controls, the Print button itself)
     .print-only   — hidden on screen, shown on paper (e.g. a footer)
     .break-before / .break-after — force a page break (the legalese pages
       of a multi-page quote); .break-avoid keeps a block from splitting.
   ============================================================ */
/* Screen-side defaults so the helper classes exist (no-op visually until
   print, except print-only which must hide on screen). */
.print-only { display: none; }
.break-before { break-before: page; page-break-before: always; }
.break-after  { break-after: page;  page-break-after: always; }
.break-avoid  { break-inside: avoid; page-break-inside: avoid; }

@media print {
  body { background: #fff !important; }
  /* Strip every chrome surface — print only the screen content. */
  header,
  .frame-header,
  #fault,
  #debug-pane,
  #toast-host,
  #palette-overlay,
  #help-overlay,
  .session-bar,        /* admin app-info bar inside the screen */
  .screen-chrome,      /* workforce back-pill inside the screen */
  .no-print { display: none !important; }
  /* main is a flex row (view-pane + debug-pane); collapse to the content
     column at full page width now that the debug pane is gone. */
  main { display: block !important; }
  #view-pane, #view-root { max-width: none !important; width: 100% !important; }
  .print-only { display: revert !important; }
}


/* Global artifact-link viewer (built-in `artifactLinks` view). */
#view-root .link-list { max-width: 920px; }
#view-root .link-row {
  align-items: baseline; gap: 8px; padding: 5px 0;
  border-bottom: 1px solid var(--border); flex-wrap: nowrap;
}
#view-root .link-from, #view-root .link-to { flex: 1 1 40%; min-width: 0; overflow-wrap: anywhere; }
#view-root .link-rel {
  flex: 0 0 auto; font-size: 11px; padding: 1px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent);
}
#view-root .link-meta { flex: 0 0 auto; font-size: 11px; }

/* Loading spinner — shown in place of the dev-y "… pending" request state
   and the transient empty-call-stack message during navigation. */
#view-root .fpwg-loading,
.fpwg-loading {
  display: flex; align-items: center; justify-content: center;
  padding: 48px 0; min-height: 80px;
}
.fpwg-spinner {
  width: 28px; height: 28px; border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--dim) 30%, transparent);
  border-top-color: var(--accent);
  animation: fpwg-spin 0.7s linear infinite;
}
@keyframes fpwg-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .fpwg-spinner { animation-duration: 1.6s; } }

/* ---- Clickable artifact-link ends -------------------------------------- */
/* The from/to endpoints are buttons now; strip the chrome so they read as
   inline links rather than form controls, but keep them obviously tappable. */
#view-root .link-end {
  flex: 1 1 40%; min-width: 0; overflow-wrap: anywhere; text-align: left;
  background: none; border: none; padding: 2px 0; color: var(--accent);
  cursor: pointer; font: inherit;
}
#view-root .link-end:hover { text-decoration: underline; }

/* ---- Triage feature flag: header FOCUS button ----
   Button hidden in all cases per product decision (the triage screen +
   backend remain intact and reachable; only the header entry point is
   gone). To restore the button, re-enable the data-feat-triage rule below. */
#triage-btn { display: none; }
/* body[data-feat-triage="1"] #triage-btn { display: inline-block; }  (disabled) */
/* Don't leak the FOCUS button onto public reader / share surfaces. */
body[data-role="public"] #triage-btn { display: none; }

/* ---- Google sign-in button -------------------------------------------- */
#view-root button.google-signin {
  align-self: flex-start; display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 16px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--panel); color: var(--fg); cursor: pointer; font-weight: 500;
}
#view-root button.google-signin::before {
  content: 'G'; font-weight: 700; color: var(--accent);
}
#view-root button.google-signin:hover { border-color: var(--accent); }

/* ---- Record detail (generic read-only row viewer) --------------------- */
#view-root .record-fields { max-width: 720px; }
#view-root .record-field {
  align-items: baseline; gap: 12px; padding: 5px 0;
  border-bottom: 1px solid var(--border);
}
#view-root .record-field-label { flex: 0 0 200px; font-size: 13px; }
#view-root .record-field-value { flex: 1 1 auto; overflow-wrap: anywhere; }

/* ---- Triage / focus view ---------------------------------------------- */
/* One item at a time, centred, with everything else stripped away — the
   point of the surface is to remove distraction, so the card carries the
   whole screen. */
#view-root .triage-focus {
  align-items: center; max-width: 640px; margin: 24px auto 0;
}
#view-root .triage-nav { align-items: center; }
#view-root .triage-counter { font-size: 13px; color: var(--dim); min-width: 64px; text-align: center; }
#view-root .triage-nav button {
  background: none; border: 1px solid var(--border); border-radius: 999px;
  width: 32px; height: 32px; cursor: pointer; color: var(--fg); font-size: 16px;
}
#view-root .triage-card {
  width: 100%; background: var(--panel); border: 1px solid var(--border);
  border-radius: 14px; padding: 28px 26px;
  border-left: 5px solid var(--accent);
}
#view-root .triage-card h2 { margin: 0; font-size: 24px; line-height: 1.25; }
#view-root .triage-kicker {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--dim);
}
#view-root .triage-body { color: var(--fg); white-space: pre-wrap; }
#view-root .triage-flag.sev-high { border-left-color: var(--warn); }
#view-root .triage-flag.sev-med  { border-left-color: var(--hl); }
#view-root .triage-announcement  { border-left-color: var(--accent); }
#view-root .triage-actions { flex-wrap: wrap; margin-top: 8px; }
#view-root .triage-actions button.primary {
  background: var(--accent); color: var(--bg); border-color: var(--accent); font-weight: 600;
}
#view-root .triage-actions button.danger,
#view-root .session-bar button.danger {
  border-color: color-mix(in srgb, var(--warn) 60%, transparent); color: var(--warn);
}
#view-root .triage-empty {
  align-items: center; text-align: center; margin: 48px auto 0; max-width: 420px;
}
#view-root .triage-empty-icon { font-size: 48px; }
@media (max-width: 640px) {
  #view-root .triage-focus { margin-top: 12px; }
  #view-root .triage-card { padding: 20px 18px; }
  #view-root .triage-card h2 { font-size: 20px; }
  #view-root .record-field-label { flex-basis: 120px; }
}

/* Manage Entities — per-app group headers. */
#view-root .entity-group-head {
  margin: 6px 0 2px; padding-bottom: 3px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}
#view-root .entity-group { margin-bottom: 6px; }
