:root {
  --bg: #1b1f24;
  --panel: rgba(28, 32, 38, 0.82);
  --panel-solid: #21262d;
  --border: rgba(255, 255, 255, 0.08);
  --text: #e8e6df;
  --muted: #9aa1a9;
  --sage: #8ba888;
  --wood: #c8a06a;
  --terra: #c0714f;

  /* Agent status: one hue per family, shades within it. Mirrors STATUS_GROUPS
     in src/config.js — the swatches themselves are set from there, so these are
     for the handful of places that name a status in CSS. */
  --status-idle: #9aa1a9;
  --status-resting: #78848f;
  --status-drinking: #c2c9cf;
  --status-working: #59b36f;
  --status-researching: #3f9f8e;
  --status-waiting: #9fbf5e;
  --status-delivering: #b489e0;
  --status-error: #e06767;
  --status-walking: #6fb1e0;

  /* Task outcomes: warm, because a task is paper and an agent is a person.
     Mirrors TASK_COLORS in src/config.js. */
  --task-active: #e8ab52;
  --task-done: #ab7f57;
  --task-error: #a85a33;

  /* Feed connection: a third vocabulary again, and the smallest. Kept as its own
     set so retuning the agent palette can never quietly restate what "waiting to
     connect" looks like. */
  --conn-live: #7bc47f;
  --conn-waiting: #e0b24f;
  --conn-connecting: #6fb1e0;
  --conn-offline: #e06767;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

#app { position: relative; width: 100vw; height: 100vh; }

#scene { display: block; width: 100%; height: 100%; }

/* ---- UI overlay ---- */
#ui { position: absolute; inset: 0; pointer-events: none; }
#ui > * { pointer-events: auto; }

#title-bar {
  position: absolute;
  top: 16px; left: 16px; right: 16px;
  display: flex; align-items: center; justify-content: space-between;
  pointer-events: none;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand .dot {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--sage);
  box-shadow: 0 0 12px var(--sage);
}
.brand h1 {
  margin: 0; font-size: 18px; font-weight: 650; letter-spacing: 0.3px;
  text-shadow: 0 1px 6px rgba(0,0,0,0.5);
}
/* A way out to reception, styled as the title it already was: an underline here
   would make the office's own name look like a footnote. */
.brand h1 a { color: inherit; text-decoration: none; pointer-events: auto; }
.brand h1 a:hover { color: var(--sage); }
/* The wall-mounted LED panel above the coffee machine is the clock now. */

/* ---- Keycard chip ----
   The office's address, in the title bar. Monospace and tracked for the same
   reason the reception field is: this is a code read one character at a time. */
.keycard-chip { position: relative; display: flex; align-items: center; pointer-events: auto; }

.kc-button {
  display: flex; align-items: center; gap: 7px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px; font-weight: 600; letter-spacing: 1.2px;
  color: var(--muted);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 999px; padding: 5px 11px;
  cursor: pointer; backdrop-filter: blur(10px);
  transition: border-color 0.15s, color 0.15s;
}
.kc-button:hover { border-color: rgba(255, 255, 255, 0.28); color: var(--text); }
.kc-button:focus-visible { outline: 2px solid var(--sage); outline-offset: 2px; }

/* Lit when this office holds the machine's adapter feed. There is one endpoint per
   machine, so this is exclusive — and worth showing, because the alternative is
   wondering which room your agents walked into. */
.kc-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--conn-live); box-shadow: 0 0 6px var(--conn-live);
}
/* "Link copied" belongs under the key, in the same lozenge as the key itself:
   as bare text in the title bar it sat on whatever the scene happened to be —
   a pale wall, a bright window — and was unreadable about half the time. */
.kc-note {
  position: absolute; top: calc(100% + 6px); left: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.6px;
  color: var(--sage); white-space: nowrap;
  background: var(--panel-solid);
  border: 1px solid color-mix(in srgb, var(--sage) 40%, var(--border));
  border-radius: 999px; padding: 4px 10px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
}
.kc-note:empty { display: none; }

/* ---- Scene switcher ---- */
/* #title-bar disables pointer events so drags reach the canvas; the switcher
   opts back in. */
.switcher { position: relative; pointer-events: auto; margin-left: 4px; }

.switcher-trigger {
  display: flex; align-items: center; gap: 8px;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--text);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 999px; padding: 6px 12px;
  cursor: pointer; backdrop-filter: blur(10px);
  transition: border-color 0.15s, background 0.15s;
}
.switcher-trigger:hover { border-color: rgba(255, 255, 255, 0.28); }
/* Caret is an inline SVG that rotates to indicate open state. Uses currentColor
   to inherit the muted palette colour, and flex-shrink: 0 to prevent squishing. */
.switcher-caret { flex-shrink: 0; color: var(--muted); transition: transform 0.18s; }
.switcher-caret svg { display: block; }
.switcher.open .switcher-caret { transform: rotate(180deg); }

/* `display: flex` below outranks the user-agent's `[hidden] { display: none }`,
   so the hidden attribute alone cannot hide this menu. This rule restores it:
   class+attribute beats the bare class, keeping `hidden` the single source of
   truth for open state (which also keeps it out of the accessibility tree). */
.switcher-menu[hidden] { display: none; }

.switcher-menu {
  position: absolute; top: calc(100% + 8px); left: 0;
  /* Wide enough that a two-source scene name and the trash can can share a row
     without the name wrapping to three lines. */
  min-width: 276px;
  display: flex; flex-direction: column; gap: 2px;
  padding: 6px;
  background: var(--panel-solid, rgba(24, 27, 32, 0.94));
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(14px);
  z-index: 20;
}
/* A row is the source's mark beside a two-line label, so which offices are live
   harness feeds and which are simulated is visible without opening anything. */
.switcher-item {
  position: relative;
  display: flex; align-items: center; text-align: left;
  border-radius: 8px;
  transition: background 0.12s;
}
.switcher-item:hover { background: rgba(255, 255, 255, 0.07); }
.switcher-item.active { background: rgba(255, 255, 255, 0.11); }
/* The row is a container now: choosing the scene and deleting it are two separate
   controls, so the whole row can no longer be one button. `.switcher-choose` takes
   all the width the trash can leaves, which is what keeps a click anywhere along
   the name doing the obvious thing. */
.switcher-choose {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 10px; text-align: left;
  font: inherit; color: var(--text);
  background: transparent; border: 0; border-radius: 8px;
  padding: 8px 10px; cursor: pointer;
}

/* Hidden until the row is hovered or the button has focus: a menu of scenes should
   read as a list of rooms, not a list of things to delete. Focus-visible matters as
   much as hover here — a keyboard user cannot hover, and an invisible control they
   can still tab to is worse than no control. */
.switcher-trash {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; margin-right: 6px;
  color: var(--muted); background: transparent;
  border: 1px solid transparent; border-radius: 7px;
  cursor: pointer; opacity: 0;
  transition: opacity 0.12s, color 0.12s, border-color 0.12s, background 0.12s;
}
.switcher-item:hover .switcher-trash { opacity: 1; }
.switcher-trash:focus-visible { opacity: 1; outline: none; border-color: var(--muted); }
.switcher-trash:hover { color: var(--status-error); background: rgba(224, 103, 103, 0.12); }
/* Armed: the first click asked, and this is the row saying so while it waits for a
   second. Loud on purpose — the next click is the destructive one. */
.switcher-item.arming { background: rgba(224, 103, 103, 0.14); }
.switcher-item.arming .switcher-trash {
  opacity: 1; color: var(--status-error);
  border-color: color-mix(in srgb, var(--status-error) 55%, transparent);
  background: rgba(224, 103, 103, 0.18);
}
.switcher-item.arming .switcher-item-name::after { content: ' · delete?'; color: var(--status-error); }
.switcher-item-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.switcher-item-mark {
  flex: 0 0 18px; display: flex; color: var(--accent, var(--muted));
}
.switcher-item-mark svg { width: 18px; height: 18px; display: block; }
.switcher-item-glyph { display: flex; color: var(--accent, var(--muted)); }
/* Several sources overlap into the width of one, so a mixed office does not push
   every name in the menu across. The negative margin is undone on the first child
   so the stack still starts on the same left edge as a single mark. */
.switcher-item-mark.stacked { flex-basis: 26px; }
.switcher-item-mark.stacked svg { width: 14px; height: 14px; }
.switcher-item-mark.stacked .switcher-item-glyph { margin-left: -5px; }
.switcher-item-mark.stacked .switcher-item-glyph:first-child { margin-left: 0; }
.switcher-item-name { font-size: 13px; font-weight: 600; }
.switcher-item-where { font-size: 11px; color: var(--muted); }
.switcher-item.active .switcher-item-name::after {
  content: ' ●'; color: var(--sage); font-size: 9px; vertical-align: middle;
}
/* An armed row says "delete?" instead of showing the active dot: two markers on one
   name is a sentence nobody can read. */
.switcher-item.active.arming .switcher-item-name::after { content: ' · delete?'; }

/* The last row sits below a divider so it reads as an action, not a scene — whether
   it adds one ("New scene") or leaves for an office of your own. */
.switcher-add {
  display: flex; align-items: center; gap: 10px; text-align: left;
  font: inherit; color: var(--text); cursor: pointer;
  background: transparent; border: 0;
  padding: 8px 10px;
  margin-top: 4px; padding-top: 10px;
  border-top: 1px solid var(--border); border-radius: 0 0 8px 8px;
}
.switcher-add .switcher-item-name { color: var(--sage); }
.switcher-add .switcher-item-mark { color: var(--sage); }

/* ---- Source badge ---- */
.source-badge {
  display: flex; align-items: center; gap: 7px;
  pointer-events: auto; margin-left: 2px;
  font-size: 11.5px; color: var(--muted);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 999px; padding: 5px 11px 5px 9px;
  backdrop-filter: blur(10px);
  transition: border-color 0.15s;
}
.source-badge:empty { display: none; }
.source-badge.clickable { cursor: pointer; }
.source-badge.clickable:hover { border-color: rgba(255, 255, 255, 0.28); }
.sb-marks { display: flex; align-items: center; }
.sb-mark { display: flex; color: var(--accent, var(--muted)); }
.sb-mark svg { width: 15px; height: 15px; display: block; }
/* Same overlap as the switcher rows: the pill sits in the title bar beside the
   office name and cannot grow a mark's width per source it listens to. */
.sb-marks.stacked .sb-mark { margin-left: -4px; }
.sb-marks.stacked .sb-mark:first-child { margin-left: 0; }
.sb-marks.stacked svg { width: 13px; height: 13px; }
.sb-label { font-weight: 600; color: var(--text); }
.sb-state { font-variant-numeric: tabular-nums; }
/* The dot is the honest bit: a live feed can show an empty room for minutes, so
   "listening" and "no adapter" must not look the same. */
.sb-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--muted); box-shadow: 0 0 6px currentColor;
}
.sb-dot[data-state='live'] { background: var(--conn-live); }
.sb-dot[data-state='waiting'] { background: var(--conn-waiting); }
.sb-dot[data-state='connecting'] { background: var(--conn-connecting); }
.sb-dot[data-state='offline'] { background: var(--conn-offline); }
.sb-dot[data-state='idle'] { background: var(--muted); }

/* Anchored bottom-left. Deliberately no overflow on the panel itself: the task
   flag pokes out past its right edge, and any overflow value other than visible
   would clip it. The roster scrolls internally instead. */
#agent-panel {
  position: absolute;
  bottom: 16px; left: 16px;
  width: 232px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 14px 14px 10px;
  backdrop-filter: blur(10px);
  box-shadow: 0 8px 30px rgba(0,0,0,0.35);
}
/* Hidden with A, for a clear look at the room — the roster covers the nearest corner
   of it, which is the corner you are usually working on. */
#agent-panel.hidden { display: none; }
#agent-panel h2 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
#agent-list {
  list-style: none; margin: 0 0 12px; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
  max-height: 40vh; overflow-y: auto;
}
/* Two lines, on a grid rather than nested flexes: the glyphs occupy column one of
   the first row, and the status line is pinned to column two of the second, which
   is what left-aligns it with the name instead of with the source mark. The glyph
   cell simply shrinks when an office has no source, so nothing needs an override
   for that case — a hardcoded indent would have. */
.agent-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  column-gap: 9px; row-gap: 1px; align-items: center;
  padding: 7px 9px; border-radius: 9px; cursor: pointer;
  background: rgba(255,255,255,0.03);
  border: 1px solid transparent;
  transition: background 0.15s, border-color 0.15s;
}
.agent-row:hover { background: rgba(255,255,255,0.07); }
.agent-row.selected { border-color: var(--sage); background: rgba(139,168,136,0.14); }
.agent-row .a-marks { grid-column: 1; grid-row: 1; display: flex; align-items: center; gap: 7px; }
.agent-row .a-src { display: flex; flex: 0 0 auto; }
.agent-row .a-src svg { width: 12px; height: 12px; display: block; }
.agent-row .swatch { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.agent-row .name { grid-column: 2; grid-row: 1; font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-row .status { grid-column: 2; grid-row: 2; font-size: 11px; color: var(--muted); }

.legend { border-top: 1px solid var(--border); padding-top: 10px; }
.legend h3 { margin: 0 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
/* Two columns of families, each a heading and its members. The count sits hard
   against the right of its own row, so the numbers line up as a census. */
#legend-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 9px 14px; }
#legend-list .legend-group { display: flex; flex-direction: column; gap: 3px; }
#legend-list .legend-head {
  display: flex; align-items: center; gap: 7px;
  font-size: 11px; color: var(--text); font-weight: 600;
}
#legend-list .legend-title { text-transform: capitalize; }
#legend-list .legend-members { list-style: none; margin: 0; padding: 0 0 0 7px; display: flex; flex-direction: column; gap: 3px; }
#legend-list .legend-members li { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--muted); }
#legend-list .legend-label { text-transform: capitalize; }
#legend-list .swatch { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
#legend-list .legend-count { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--muted); font-weight: 500; }
/* Nobody in this state right now: still listed, because the legend is also the
   key to the rings on the floor — just stepped back out of the way. */
#legend-list .empty { opacity: 0.42; }

/* ---- Task Delivery / Task Removal ---- */
.delivery {
  position: relative;
  margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--border);
}
.delivery h3 {
  margin: 0 0 8px; font-size: 11px; text-transform: uppercase;
  letter-spacing: 1px; color: var(--muted);
  transition: color 0.2s;
}
.delivery.removal h3 { color: var(--task-error); }

#delivery-body { font-size: 11.5px; color: var(--muted); line-height: 1.45; }
.mail-item { display: flex; flex-direction: column; gap: 2px; }
.mail-item .when {
  display: flex; align-items: center; gap: 6px;
  font-variant-numeric: tabular-nums; font-size: 10.5px;
  letter-spacing: 0.4px; color: var(--muted);
}
/* Who an addressed envelope is for, pushed to the far end of the time line so the
   clock and the name each keep their own edge. */
.mail-item .to {
  display: flex; align-items: center; gap: 4px;
  margin-left: auto;
  letter-spacing: 0.2px; color: var(--text);
}
/* A folded dart, matching the planes: nose to the right, tail notched. Painted in
   the recipient's own colour, set inline, so it echoes the tint of the flight. */
.mail-item .to .dart {
  width: 9px; height: 8px; flex: 0 0 9px;
  background: var(--muted);
  clip-path: polygon(0 0, 100% 50%, 0 100%, 24% 50%);
}
.mail-item .what { color: var(--text); font-weight: 600; font-size: 12px; }
.delivery.removal .mail-item .what { color: var(--task-error); }
.mail-item .extra { font-size: 10.5px; color: var(--wood); }
.mail-empty { color: var(--muted); font-style: italic; }

/* Clipping slot sits flush with the panel's outer edge (panel padding 14px +
   1px border), so a flag translated fully left is hidden behind the panel. */
.flag-slot {
  position: absolute;
  left: calc(100% + 15px);
  top: 8px;
  width: 52px; height: 24px;
  overflow: hidden;
  pointer-events: none;
}
.mail-flag {
  display: flex; align-items: stretch; height: 100%;
  transform: translateX(-100%);
  transition: transform 0.42s cubic-bezier(0.34, 1.5, 0.64, 1);
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
}
.mail-flag.up { transform: translateX(0); }
.mail-flag .pole {
  width: 4px; flex: 0 0 4px;
  background: linear-gradient(#d8dbe0, #9aa1a9);
  border-radius: 0 2px 2px 0;
}
.mail-flag .pennant {
  flex: 1 1 auto;
  background: linear-gradient(#e2493f, #c62c26);
  clip-path: polygon(0 0, 100% 0, 78% 50%, 100% 100%, 0 100%);
}

#inspector {
  position: absolute;
  bottom: 16px; right: 16px;
  width: 300px; max-height: calc(100vh - 96px);
  overflow: auto;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 16px;
  backdrop-filter: blur(10px);
  box-shadow: 0 8px 30px rgba(0,0,0,0.35);
}
#inspector.hidden { display: none; }
/* Just the agent's name: status and source are stated in full in the rows below,
   each next to its own glyph, so the heading carries no lozenge or mark. */
#inspector h2 {
  margin: 0 0 12px; font-size: 16px; color: var(--text);
  display: flex; align-items: center; gap: 9px;
  padding-right: 24px;
}
#inspector h2 .insp-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#inspector-close {
  position: absolute; top: 10px; right: 12px;
  background: none; border: none; color: var(--muted);
  font-size: 22px; line-height: 1; cursor: pointer;
}
#inspector-close:hover { color: var(--text); }
#inspector-body { font-size: 13px; color: var(--muted); line-height: 1.55; }
#inspector-body .kv { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; border-bottom: 1px solid var(--border); }
#inspector-body .kv .k { color: var(--muted); }
#inspector-body .kv .v { color: var(--text); font-weight: 600; text-align: right; }
/* Rows whose value is a glyph plus a word. Right-aligned like every other value,
   so the glyph travels with the text rather than sitting in a column of its own. */
#inspector-body .kv .v-status,
#inspector-body .kv .v-source { display: flex; align-items: center; justify-content: flex-end; gap: 7px; }

#inspector-body .kv .v-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.35);
}
#inspector-body .kv .v-src { display: flex; flex: 0 0 auto; }
#inspector-body .kv .v-src svg { width: 13px; height: 13px; display: block; }
#inspector-body .task { margin-top: 12px; padding: 10px; border-radius: 9px; background: rgba(255,255,255,0.04); color: var(--text); }

/* ---- Work log ---- */
#inspector-body .log-head {
  margin: 16px 0 8px; font-size: 11px; text-transform: uppercase;
  letter-spacing: 1px; color: var(--muted);
  display: flex; justify-content: space-between; align-items: baseline;
}
#inspector-body .log-head .count { text-transform: none; letter-spacing: 0; }
#inspector-body .log { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
#inspector-body .log li {
  display: grid; grid-template-columns: 10px 1fr; gap: 8px;
  padding: 6px 8px; border-radius: 8px;
  background: rgba(255, 255, 255, 0.035);
}
#inspector-body .log .dot {
  width: 8px; height: 8px; border-radius: 50%; margin-top: 5px;
}
#inspector-body .log .label {
  color: var(--text); font-size: 12px; font-weight: 600; line-height: 1.35;
}
#inspector-body .log .times {
  font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums;
  margin-top: 2px;
}
#inspector-body .log .outcome { text-transform: capitalize; }
#inspector-body .log li.active .label { color: var(--task-active); }
#inspector-body .log li.error .label { color: var(--task-error); }
#inspector-body .log-empty { font-size: 12px; font-style: italic; }

/* ---- The bottom strips (press D, press E) ---- */
/* One column holding every panel that lives along the bottom, inset clear of the roster
   on the left so the two never overlap (16 + 232 + 16 = 264).

   The stacking is the column's job rather than each panel's. Both strips wrap to a
   second or third row depending on how much room the inspector has left them, so any
   fixed offset between them is a guess that is wrong as soon as one of them grows —
   which is precisely how they used to end up on top of one another. Laid out bottom-up
   so the lowest panel stays pinned to the bottom edge and the others rise off it.

   The inspector shares the bottom-right, but it is usually closed, and reserving its
   332px permanently made the strips narrow enough to scroll. Since the inspector
   precedes this container in the document, its open state can be read directly: give way
   only while it is actually on screen.

   The roster on the left is read the same way, and for the same reason: hiding it with A
   is asking for a clear look at the room, and a strip that went on holding a 232px gutter
   for a panel that is not there would keep its controls crowded to pay for empty screen.
   Both neighbours are earlier siblings, so each gives way exactly while it is open. */
#bottom-stack {
  position: absolute;
  bottom: 16px; left: 264px; right: 16px;
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: 12px;
  /* The gap between two panels is still the room, so clicks fall through it. Each panel
     takes the pointer back for itself. */
  pointer-events: none;
}
#inspector:not(.hidden) ~ #bottom-stack { right: 332px; }
#agent-panel.hidden ~ #bottom-stack { left: 16px; }

#dev-panel, #editor-panel {
  pointer-events: auto;
  display: flex;
  column-gap: 18px; row-gap: 10px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 10px 14px;
  backdrop-filter: blur(10px);
  box-shadow: 0 8px 30px rgba(0,0,0,0.35);
}
#dev-panel.hidden, #editor-panel.hidden { display: none; }

/* Both strips are a column: what the panel is, on its own line, then its sections in a
   row underneath. The title shape is the roster's, so all three permanent panels
   introduce themselves the same way instead of the editor wearing its name as a pill
   tucked in beside the first control. */
#dev-panel, #editor-panel { flex-direction: column; row-gap: 8px; }
#dev-panel h2, #editor-panel h2 {
  margin: 0; font-size: 13px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 1px; color: var(--muted);
}
/* The one thing the editor's title does differently, and it is not really format: the
   colour it always had as a badge. Edit mode changes what a click does, so the panel's
   name is also the warning, and terracotta is how the rest of the room says "mode"
   rather than "setting". */
#editor-panel h2 { color: var(--terra); }

/* The sections themselves, in a row that holds its order at any width.
   `nowrap` is the whole point: see devpanel.js for why the row must not reflow. When
   even the shrunken sections will not fit — a very narrow window with the inspector
   open — the row scrolls rather than dropping anything off the end. */
.dev-panel-body {
  display: flex; flex-wrap: nowrap; align-items: flex-start;
  column-gap: 18px;
  overflow-x: auto;
}

/* A group takes the width it wants and gives it up under pressure, down to its widest
   single control. That is what turns a squeeze into wrapped lozenges — a group that
   refused to shrink would push the row wider instead and the pills would never wrap.
   `min-width: 0` because a flex item will not shrink past its content otherwise. */
.dev-group { display: flex; flex-direction: column; gap: 7px; flex: 0 1 auto; min-width: 0; }
.dev-group + .dev-group { border-left: 1px solid var(--border); padding-left: 18px; }
.dev-group h4 {
  margin: 0; font-size: 10px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 1px; color: var(--muted);
}
.dev-group-body { display: flex; flex-direction: column; gap: 4px; }

/* Readouts are polled 8×/second, so they are monospaced and tabular: variable
   digit widths would make the numbers jitter as they change.

   The row wraps so that under pressure the value drops below its label instead of being
   cut off at the panel's edge. Half a coordinate is worse than no coordinate — it reads
   as a number rather than as a number with its end missing — and the value itself must
   not wrap internally, hence `nowrap` on it below. Camera is the last section, so it is
   the first to feel a squeeze; being the only one nobody clicks, that is the right way
   round. */
.dev-kv { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 8px; font-size: 11px; }
.dev-k { color: var(--muted); min-width: 38px; }
.dev-v, .dev-time {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
/* A readout may wrap at the spaces it already contains, so a narrow panel folds
   `az 41deg . tilt 42deg . d 32` onto two lines rather than running off the edge. Safe
   because every value is formatted to a fixed number of decimals and the digits are
   tabular: the line is always the same width, so the wrap lands in the same place every
   time instead of hopping about as the camera moves. Never inside a number, though. */
.dev-v { overflow-wrap: normal; word-break: keep-all; }
.dev-time { white-space: nowrap; }

/* The clock, its slider and the Live button. Wraps, so Live drops onto a second line
   rather than the slider being squeezed to a stub — the slider is the control that needs
   its length to be usable, and a button is perfectly happy one line down. */
.dev-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 10px; }
.dev-time { font-size: 15px; font-weight: 600; min-width: 54px; flex: 0 0 auto; }
.dev-slider { flex: 0 1 auto; width: 148px; min-width: 96px; accent-color: var(--wood); cursor: pointer; }

.dev-seg { display: flex; flex-wrap: wrap; gap: 6px; }
.dev-btn, .dev-seg button {
  font: inherit; font-size: 11px; color: var(--muted);
  background: rgba(255,255,255,0.05);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 4px 10px; cursor: pointer; white-space: nowrap; flex: 0 0 auto;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.dev-btn:hover, .dev-seg button:hover { color: var(--text); background: rgba(255,255,255,0.10); }
.dev-btn.active, .dev-seg button.active {
  color: var(--bg); background: var(--wood); border-color: var(--wood); font-weight: 600;
}
/* A disabled pill still has to be readable, because the reason it is disabled is in its
   tooltip and an invisible control is not one anybody hovers. Dimmed and un-hoverable
   rather than hidden: the furniture picker's Delete says "no" for a reason worth reading. */
.dev-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.dev-btn:disabled:hover { color: var(--muted); background: rgba(255,255,255,0.05); }
/* The one pill that is a link rather than a button (the debug log), which needs the
   two things an anchor brings and a button does not. */
.dev-link { text-decoration: none; display: inline-block; }

/* A dropdown in the same clothes as the pills, for a list too long to be a row of them.
   `field-sizing` keeps it as wide as its longest entry where it is supported and is
   harmlessly ignored where it is not; the explicit max stops "Coffee machine — one only"
   from setting the width of the whole strip. */
.dev-select {
  font: inherit; font-size: 11px; color: var(--text);
  background: rgba(255,255,255,0.05);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 4px 10px; cursor: pointer; flex: 0 1 auto;
  max-width: 168px; field-sizing: content;
}
.dev-select:hover { background: rgba(255,255,255,0.10); }
.dev-select option { color: var(--text); background: var(--panel, #1c1a18); }

/* ---- Furniture editor panel (press E) ---- */
/* Wears the developer panel's clothes — same box, groups, readouts and pill buttons, all
   shared above — and is placed by #bottom-stack rather than by itself. */

/* The layout blob, in and out. Monospaced and fixed-height: it holds JSON, and a box
   that grew with its content would push the strip up the screen on every export. */
.editor-blob {
  width: 260px; height: 52px; resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10px; line-height: 1.4;
  color: var(--text); background: rgba(0,0,0,0.28);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 8px;
}
.editor-blob:focus { outline: 1px solid var(--wood); }

/* Says what just happened, including why a drop was refused — the one place the editor
   can explain itself in words rather than in colour. */
.editor-status { font-size: 10px; color: var(--muted); max-width: 260px; }
.editor-status.bad { color: var(--status-error); }

/* ---- Shortcuts modal (press ?) ---- */
/* The host is the backdrop: shortcuts.js dismisses on clicks that land on it
   directly, so the card must be its only child. */
#shortcuts-panel {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: rgba(12, 14, 17, 0.55);
  backdrop-filter: blur(3px);
}
#shortcuts-panel.hidden { display: none; }

.sc-card {
  width: min(560px, 100%); max-height: min(70vh, 640px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--panel-solid);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 24px 70px rgba(0,0,0,0.55);
}
.sc-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 18px 12px; border-bottom: 1px solid var(--border);
}
.sc-head h2 { margin: 0; font-size: 15px; letter-spacing: 0.3px; }
.sc-close {
  background: none; border: 0; color: var(--muted);
  font-size: 22px; line-height: 1; cursor: pointer; padding: 0 2px;
}
.sc-close:hover { color: var(--text); }

.sc-body { padding: 4px 18px 14px; overflow-y: auto; }
.sc-group { margin-top: 14px; }
.sc-group h3 {
  margin: 0 0 6px; font-size: 10px;
  text-transform: uppercase; letter-spacing: 1px; color: var(--muted);
}
.sc-row { display: flex; align-items: baseline; gap: 12px; padding: 5px 0; }
.sc-keys { flex: 0 0 96px; display: flex; align-items: center; justify-content: flex-end; gap: 3px; }
.sc-row kbd {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;
  color: var(--text); background: rgba(255,255,255,0.07);
  border: 1px solid var(--border); border-bottom-width: 2px;
  border-radius: 5px; padding: 2px 6px; min-width: 20px; text-align: center;
}
.sc-plus { color: var(--muted); font-size: 10px; }
.sc-label { font-size: 12.5px; color: var(--text); }
.sc-foot {
  padding: 10px 18px 13px; border-top: 1px solid var(--border);
  font-size: 11px; color: var(--muted); font-style: italic;
}

/* ---- Source picker ----
   Same arrangement as the shortcuts modal: the host is the dismissing backdrop
   and the card is its only child. */
#source-picker {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: rgba(12, 14, 17, 0.6);
  backdrop-filter: blur(3px);
  z-index: 30;
}
#source-picker.hidden { display: none; }

.sp-card {
  width: min(860px, 100%);
  background: var(--panel-solid);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}
.sp-head { padding: 20px 22px 4px; }
.sp-head h2 { margin: 0; font-size: 17px; letter-spacing: 0.3px; }
.sp-sub { margin: 6px 0 0; font-size: 12.5px; color: var(--muted); }

/* Five equal columns: this is a one-time choice for a new office, so the options
   are large and side by side rather than a list to be scanned. Wraps to two rows
   on a narrow window instead of shrinking the marks into illegibility. */
.sp-grid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px;
  padding: 18px 22px 6px;
}
@media (max-width: 780px) {
  .sp-grid { grid-template-columns: repeat(2, 1fr); }
}

.sp-option {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 20px 12px 14px;
  font: inherit; color: var(--text); text-align: center;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border); border-radius: 14px;
  cursor: pointer;
  transition: background 0.14s, border-color 0.14s, transform 0.14s;
}
.sp-option:hover, .sp-option:focus-visible {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  transform: translateY(-2px);
  outline: none;
}
.sp-option.selected {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* Several sources can be on at once, so "chosen" has to be unmistakable and not
   merely "the one with a ring". The tick only exists when selected — an empty
   checkbox on all five would read as a form to be filled in. */
.sp-check {
  position: absolute; top: 9px; right: 9px;
  display: none; align-items: center; justify-content: center;
  width: 17px; height: 17px; border-radius: 50%;
  color: var(--bg); background: var(--accent);
}
.sp-option.selected .sp-check { display: flex; }
.sp-logo { color: var(--accent); }
.sp-logo svg { width: 44px; height: 44px; display: block; }
.sp-label { font-size: 14px; font-weight: 650; }
.sp-blurb { font-size: 11.5px; color: var(--muted); line-height: 1.35; }
.sp-tag {
  margin-top: 2px; padding: 2px 8px;
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px;
  color: var(--muted);
  border: 1px solid var(--border); border-radius: 999px;
}
.sp-tag.ready { color: var(--sage); border-color: color-mix(in srgb, var(--sage) 45%, transparent); }

/* A source the demo office cannot be pointed at. Dimmed and inert, but still legible
   and still wearing its own mark: the card's job here is to show what the room can do
   elsewhere, so an option scrubbed of its colour would be advertising nothing. The
   lift and wash on hover go, because both are promises of a click that will not come —
   `not-allowed` says the same thing with the cursor instead. */
.sp-option.locked {
  opacity: 0.42;
  cursor: not-allowed;
  filter: saturate(0.6);
}
.sp-option.locked:hover {
  background: rgba(255, 255, 255, 0.03);
  border-color: var(--border);
  transform: none;
}

/* The note explains, the button commits. Side by side so the action sits at the
   card's bottom-right where a modal's confirm belongs, with the explanation given
   the remaining width rather than a column of its own. */
.sp-foot {
  display: flex; align-items: flex-end; gap: 18px;
  padding: 12px 22px 18px;
  font-size: 11.5px; color: var(--muted); line-height: 1.45;
}
.sp-note { margin: 0; flex: 1; min-width: 0; }

/* The way out of a locked office, sitting inside the sentence that explains why one
   is needed. A button by element and a link by every other measure — inline, in the
   accent, underlined — because it reads as part of the prose rather than as a second
   action competing with the confirm button beside it. */
.sp-link {
  display: inline; padding: 0;
  font: inherit; color: var(--sage);
  background: none; border: none;
  text-decoration: underline; text-underline-offset: 2px;
  cursor: pointer;
}
.sp-link:hover { filter: brightness(1.15); }
.sp-link:focus-visible { outline: 2px solid var(--sage); outline-offset: 2px; border-radius: 3px; }

.sp-confirm {
  flex: 0 0 auto;
  padding: 9px 16px;
  font: inherit; font-size: 12.5px; font-weight: 650;
  color: var(--bg); background: var(--sage);
  border: 1px solid transparent; border-radius: 9px;
  cursor: pointer;
  transition: filter 0.14s, opacity 0.14s;
}
.sp-confirm:hover:not(:disabled) { filter: brightness(1.08); }
.sp-confirm:focus-visible { outline: 2px solid var(--sage); outline-offset: 2px; }
/* Disabled rather than hidden: the button is where the eye goes for "now what",
   and its label is what says a choice is still needed. */
.sp-confirm:disabled {
  color: var(--muted); background: transparent;
  border-color: var(--border); cursor: default;
}

/* ---- First Person panel (press F with an agent selected) ----
   Across the top middle, in the slot the title bar has just vacated — see the
   `.riding` rule below. Centred rather than inset left-and-right because it is a
   caption for the view behind it, and the view's subject is in the middle.

   Pointer events stay off throughout: it is something to read, not to operate, and
   the canvas underneath is the whole point. */
#fpv-hud {
  position: absolute;
  top: 16px; left: 50%; transform: translateX(-50%);
  width: min(680px, calc(100vw - 560px));
  display: flex; flex-direction: column; gap: 9px;
  pointer-events: none;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 11px 16px 13px;
  backdrop-filter: blur(10px);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
}
#fpv-hud.hidden { display: none; }

/* While riding, the office's own furniture goes: the brand, the office switcher and
   the source badge all answer "which office is this", which is not the question you
   are asking from inside somebody's head. Faded rather than cut, to match the
   vignette, and made unclickable on the way out — #title-bar is absolutely
   positioned, so none of this moves anything else. */
#ui.riding #title-bar {
  opacity: 0; visibility: hidden;
  transition: opacity 0.25s ease, visibility 0.25s ease;
}
#title-bar { transition: opacity 0.25s ease; }

.fpv-head { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.fpv-eye { color: var(--sage); display: flex; }
.fpv-eye svg { width: 15px; height: 15px; display: block; }
/* Source glyph and status dot, sized as in the roster row so the same agent reads
   the same in both. */
.fpv-marks { display: flex; align-items: center; flex: 0 0 auto; }
.fpv-marks svg, .fpv-src svg { width: 13px; height: 13px; display: block; }
.fpv-src { display: flex; flex: 0 0 auto; }
.fpv-swatch { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
/* The agent's name is the title of the panel, so it takes the task's size. */
.fpv-name { font-weight: 600; font-size: 16px; line-height: 1.2; }
/* Pushed to the far edge so the name reads as a title and the key as an aside. */
.fpv-exit { margin-left: auto; color: var(--muted); }
.fpv-exit kbd {
  font: inherit; font-size: 10.5px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--border); border-radius: 4px;
  padding: 1px 5px; margin-right: 4px;
}

/* Two groups in the dev panel's language, since they are the same kind of readout.
   "Doing" gets a fixed 30% rather than being sized by its text: it rewrites itself
   every few seconds as an agent sits down, stands up and picks things up, and a
   column that resized with it would drag the divider — and the task beside it —
   back and forth the whole time. The task takes whatever is left. */
.fpv-body { display: flex; align-items: stretch; gap: 16px; min-width: 0; }
.fpv-group { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.fpv-group:first-child { flex: 1 1 auto; }
.fpv-group + .fpv-group {
  flex: 0 0 30%; border-left: 1px solid var(--border); padding-left: 16px;
}
.fpv-group h4 {
  margin: 0; font-size: 9.5px; font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
}
.fpv-group-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

/* The one thing the panel exists to say, so the one thing set at reading size. */
.fpv-task-label {
  font-size: 16px; font-weight: 600; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fpv-task-label.idle { color: var(--muted); font-weight: 500; font-style: italic; }
.fpv-task-times { font-size: 11px; color: var(--muted); }
.fpv-task-times strong {
  color: var(--text); font-weight: 600;
  /* Tabular figures: the seconds tick every second, and proportional digits make
     the whole line jitter as they do. */
  font-variant-numeric: tabular-nums;
}
/* Wraps inside its fixed column, and reserves both lines up front: the longest
   thing it says ("on their feet, carrying a drink") needs two, and letting the panel
   grow and shrink a line at a time would just trade the sideways jump for a
   vertical one. Two lines still fit inside the task's height, so this costs nothing. */
.fpv-doing { font-size: 12px; padding-top: 2px; line-height: 1.35; min-height: 2.7em; }


#loading {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; color: var(--muted);
  background: var(--bg);
  transition: opacity 0.6s ease;
}
#loading.done { opacity: 0; pointer-events: none; }

/* How the work arrived: a flat envelope for a letter, a cube for a package. Sat on
   the task line in the delivery feed, where the shape says it faster than a word
   would. */
.mail-item .parcel-mark {
  display: inline-block;
  vertical-align: middle;
  margin-right: 6px;
  border-radius: 1px;
}
.mail-item .parcel-mark.letter {
  width: 13px;
  height: 9px;
  background: #f4f1e8;
  /* The red airmail stripe the envelopes in the mailbox carry. */
  border-top: 2px solid #d23b3b;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.mail-item .parcel-mark.package {
  width: 11px;
  height: 11px;
  background: #bf9b6a;
  /* The tape down the middle, as on the boxes beside the mailbox. */
  border-left: 3px solid #d9c39a;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}

/* ---- Reception (home.html) ----
   The front door, and the only page that is not the office. It shares the
   palette and nothing else: no canvas, no overlay, no panels — so `overflow:
   hidden` on the body has to be undone, because this one is a page you can
   scroll rather than a room you look into. */
body.reception { overflow: auto; }

/* Split down the middle: what this is on the left, the way in on the right. Two
   equal columns rather than a wide-and-narrow pair, because neither side is the
   sidebar — the scene is the pitch and the keycard is the point.

   Centred in the viewport, both ways. Reception holds one screen's worth and is a
   room you stand in rather than a document you read down, so it is laid out to sit
   in the middle of the glass; `min-height` rather than `height` because a short
   window (or a long list of remembered offices) must still be able to scroll. */
#wizard {
  max-width: 1260px;
  min-height: 100vh;
  margin: 0 auto;
  padding: 40px 28px;
  display: grid; grid-template-columns: 1fr 1fr; gap: 48px;
  align-items: center; align-content: center;
}

.wz-tell, .wz-door { display: flex; flex-direction: column; gap: 20px; }

/* One column when there is no middle to split: the pitch, then the door. */
@media (max-width: 880px) {
  #wizard {
    max-width: 620px; grid-template-columns: 1fr; gap: 24px;
    min-height: 0; padding: 6vh 28px 48px;
  }
}

.wz-brand { margin: 0; }
/* No lamp beside it any more: with one line of pitch under it, the title is the only
   thing at the top of the page, so it is sized to be that rather than to sit level
   with a dot. Fluid, so it stays the same gesture on a narrow window. */
.wz-brand h1 {
  margin: 0; font-weight: 650; letter-spacing: -0.4px; line-height: 1.1;
  font-size: clamp(30px, 3.4vw, 44px);
}

.wz-lede { margin: 0; font-size: 15.5px; line-height: 1.6; color: var(--muted); }

/* What a keycard is, at the top of the card that asks for one. */
.wz-card-lede {
  margin: 0 0 18px; font-size: 13px; line-height: 1.6; color: var(--muted);
}

/* The stage the vignette plays on. It owns the size — the canvas and the drawing
   both fill it — so the swap from one to the other cannot move the page. The mask
   fades the far end of the floor into the background instead of ending it at a hard
   edge, which is what lets the scene be rendered without a room around it. */
.wz-stage {
  position: relative;
  width: 100%; aspect-ratio: 8 / 5;
  /* Tuned by measuring rather than by eye. A radius here is a fraction of the box's
     own width or height (not half of it), so a radius has to stay short of the
     distance from the centre to the nearest edge on each axis or the fade never
     finishes: the box crops it first, and what should be a curve reads as a flat cut
     running along that side. 54% both ways did exactly that — on the vertical axis
     it overshot the 50%-of-height edge, so the top and bottom of the spotlight were
     never more than a fifth faded, flattened rather than closed off. These radii sit
     inside both edges with room to spare, so the ellipse closes to fully transparent
     on every side before the box crops it — an oval throughout, not a flattened
     circle — while the first stop is raised to keep the desk itself just as fully
     painted as before. */
  -webkit-mask-image: radial-gradient(44% 46% at 52% 50%, #000 66%, transparent 100%);
  mask-image: radial-gradient(44% 46% at 52% 50%, #000 66%, transparent 100%);
}

/* Rendered by src/scene/vignette.js. Sized by CSS and told its pixels by the
   renderer, so devicePixelRatio never changes the layout. */
.wz-scene {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0; transition: opacity 0.5s ease;
}
.wz-stage.live .wz-scene { opacity: 1; }

/* Holds the spot in the same box while the scene loads. Small and centred rather
   than a placeholder image, so it never claims to be a preview of what's coming —
   just that something is. Fades out as the real scene fades in, and keeps spinning
   for good if three.js never arrives. */
.wz-spinner {
  position: absolute; top: 50%; left: 50%;
  width: 28px; height: 28px; margin: -14px 0 0 -14px;
  border: 3px solid var(--border);
  border-top-color: var(--sage);
  border-radius: 50%;
  animation: wz-spin 0.8s linear infinite;
  transition: opacity 0.5s ease;
}
.wz-stage.live .wz-spinner { opacity: 0; }

@keyframes wz-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .wz-spinner { animation: none; }
}

.wz-card {
  padding: 22px;
  background: var(--panel-solid);
  border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
}

.wz-ways {
  display: grid; grid-template-columns: 1fr auto 1fr; gap: 20px;
  align-items: stretch;
}

.wz-label { display: block; font-size: 13px; font-weight: 650; margin: 0 0 9px; }
/* Stacked, now that each way in has a column of its own rather than the width of
   the whole card: the field, then the button that acts on it. */
.wz-row { display: flex; flex-direction: column; gap: 10px; align-items: stretch; }

/* Monospace and widely tracked: a keycard is read one character at a time, out
   loud or off a screenshot, and proportional glyphs at this length are how a Q
   becomes an O — which is exactly the mistake the alphabet was chosen to kill. */
.wz-input {
  flex: 0 0 auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 20px; font-weight: 600; letter-spacing: 3px;
  color: var(--text); background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border); border-radius: 10px;
  padding: 10px 16px;
  /* Fills its column, but never gets narrower than the thing it holds: nine
     characters plus eight gaps of letter-spacing, and the tracking is applied after
     the last character too — so `ch` units alone come up short and clip the final X.
     `calc` states that arithmetic instead of guessing at a round number. */
  width: 100%;
  min-width: calc(9ch + 9 * 3px + 32px);
  text-transform: uppercase;
}
.wz-input::placeholder { color: rgba(154, 161, 169, 0.45); letter-spacing: 3px; }
.wz-input:focus { outline: none; border-color: var(--sage); background: rgba(255, 255, 255, 0.06); }

.wz-go, .wz-new {
  font: inherit; font-size: 13px; font-weight: 650;
  color: var(--bg); background: var(--sage);
  border: 1px solid transparent; border-radius: 10px;
  padding: 10px 18px; cursor: pointer;
  width: 100%;
  transition: filter 0.14s;
}
.wz-go:hover:not(:disabled), .wz-new:hover:not(:disabled) { filter: brightness(1.08); }
.wz-go:focus-visible, .wz-new:focus-visible { outline: 2px solid var(--sage); outline-offset: 2px; }
/* Disabled rather than hidden, same reasoning as the source picker's confirm: the
   button is where the eye goes for "now what", and the hint beneath says why not. */
.wz-go:disabled {
  color: var(--muted); background: transparent;
  border-color: var(--border); cursor: default;
}

.wz-hint { margin: 9px 0 0; font-size: 11.5px; color: var(--muted); line-height: 1.5; min-height: 1.3em; }
.wz-hint.bad { color: var(--conn-offline); }

/* A rule with a word in it, because "or" is doing real work here: these are two
   ways in, not a form and its footnote. Stood on end between the two columns, where
   it separates them instead of following one of them. */
.wz-or {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  margin: 0; color: var(--muted); font-size: 11px;
  text-transform: uppercase; letter-spacing: 1.2px;
}
.wz-or::before, .wz-or::after {
  content: ''; flex: 1; width: 1px; min-height: 12px; background: var(--border);
}

/* No room for two columns: back to a horizontal rule between two stacked ways in,
   which is the same statement in the only shape that fits. */
@media (max-width: 560px) {
  .wz-ways { grid-template-columns: 1fr; gap: 18px; }
  .wz-or { flex-direction: row; }
  .wz-or::before, .wz-or::after { width: auto; height: 1px; min-height: 0; }
}

.wz-recent h2 {
  margin: 0 0 10px; font-size: 12px; font-weight: 650;
  text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted);
}
.wz-recent.hidden { display: none; }
.wz-recent ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.wz-recent li { display: flex; align-items: baseline; gap: 10px; }

.wz-recent-card {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px; font-weight: 600; letter-spacing: 1.5px;
  color: var(--text); text-decoration: none;
  padding: 5px 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border); border-radius: 8px;
  transition: border-color 0.14s, background 0.14s;
}
.wz-recent-card:hover { border-color: var(--sage); background: rgba(255, 255, 255, 0.07); }
.wz-recent-when { font-size: 11.5px; color: var(--muted); }
/* A reaped office is struck through rather than removed: it was yours a moment
   ago, and silently dropping the row would read as us having lost it. */
.wz-recent li.gone .wz-recent-card {
  color: var(--muted); text-decoration: line-through; border-style: dashed;
}

.wz-foot { font-size: 11.5px; color: var(--muted); line-height: 1.6; }
.wz-foot p { margin: 0 0 8px; }
.wz-foot a { color: var(--sage); }

/* The loading overlay doubles as the place a lost office is reported, so it needs
   one button — see officeLost() in src/main.js. */
#loading { flex-direction: column; gap: 14px; text-align: center; padding: 24px; }
.loading-action {
  font: inherit; font-size: 12.5px; font-weight: 650;
  color: var(--bg); background: var(--sage);
  border: 1px solid transparent; border-radius: 9px;
  padding: 8px 16px; cursor: pointer;
}
.loading-action:hover { filter: brightness(1.08); }

/* ---- Debug log (debuglog.html) ----

   A document rather than an overlay, so the page scrolls and the header rides along
   at the top of it. `body` is the scrolling box because `html` is pinned to the
   viewport for the sake of the room — the same arrangement reception uses.

   The look is deliberately the office's own: same tokens, same header, same pills.
   A debug view that looked like a debug view would be one more thing to learn, and
   this one already assumes you are having a bad day. */
body.debuglog { overflow: auto; }

body.debuglog #title-bar {
  position: sticky;
  top: 0; left: 0; right: 0;
  z-index: 5;
  padding: 12px 16px;
  /* Rows scroll under it, so it has to be opaque enough to read over — a blur alone
     leaves monospace text legible-ish, which is not the same as legible. */
  background: linear-gradient(to bottom, var(--bg) 65%, rgba(27, 31, 36, 0.92));
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(6px);
}
/* The overlay's title bar ignores the pointer and lets its children take it back;
   here the bar is a real strip in the document and nothing sits behind it. */
body.debuglog #title-bar { pointer-events: auto; }
body.debuglog .brand { flex-wrap: wrap; row-gap: 8px; }

.linkout {
  font-size: 11.5px; font-weight: 600; color: var(--muted);
  text-decoration: none; padding: 4px 10px;
  border: 1px solid var(--border); border-radius: 999px;
  white-space: nowrap;
}
.linkout:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.18); }

#debuglog { padding: 14px 16px 40px; }

/* ---- the controls strip ---- */

.log-controls {
  display: flex; align-items: center; gap: 8px;
  padding: 0 0 10px;
  font-size: 11.5px;
}
.lc-status {
  display: inline-flex; align-items: center; gap: 6px;
  font-weight: 650; letter-spacing: 0.02em;
  color: var(--conn-connecting);
}
.lc-status::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; box-shadow: 0 0 8px currentColor;
}
.lc-status[data-state="live"] { color: var(--conn-live); }
.lc-status[data-state="connecting"] { color: var(--conn-connecting); }
.lc-status[data-state="offline"] { color: var(--conn-offline); }

.lc-count {
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
}
/* How many tabs are on this office — the running total the join and leave rows are
   the history of, and the only place in the app that shows it. */
.lc-watchers { color: var(--muted); opacity: 0.8; }
.lc-watchers::before { content: '·'; margin-right: 8px; opacity: 0.5; }

/* Readouts left, buttons right — hung off the first button rather than the last
   readout, because the watcher count is absent until the first heartbeat answers and
   an alignment that depends on it would lurch when it arrives. */
.lc-button:first-of-type { margin-left: auto; }

.lc-button {
  font: inherit; font-size: 11.5px; font-weight: 600;
  color: var(--muted); background: transparent;
  border: 1px solid var(--border); border-radius: 999px;
  padding: 4px 12px; cursor: pointer;
}
.lc-button:hover:not(:disabled) { color: var(--text); border-color: rgba(255, 255, 255, 0.18); }
.lc-button.on { color: var(--bg); background: var(--sage); border-color: transparent; }
.lc-button:disabled { opacity: 0.4; cursor: default; }

/* ---- the rows ---- */

.log-rows { list-style: none; margin: 0; padding: 0; }

/* One grid per row rather than one for the list, because a row can expand and its
   JSON must be free to be as wide as it likes without dragging the columns with it.
   The columns are fixed so the eye can run down `type` without re-finding it on
   every line — the single most useful property of a log laid out in text. */
.log-row {
  border-bottom: 1px solid rgba(255, 255, 255, 0.045);
  font-size: 12px;
}
.log-row:hover { background: rgba(255, 255, 255, 0.028); }

.lr-line {
  display: grid;
  grid-template-columns: auto 16px 128px 136px 78px minmax(0, 1fr);
  align-items: baseline;
  gap: 10px;
  padding: 5px 8px;
  cursor: pointer;
  /* The family's colour arrives as a stripe down the left edge: the type column
     already says which family it is, and this makes it findable without reading. */
  border-left: 2px solid var(--family, transparent);
}
.lr-line:focus-visible { outline: 1px solid var(--sage); outline-offset: -1px; }

.lr-time {
  display: flex; align-items: baseline; gap: 8px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.lr-abs { color: var(--muted); font-size: 11px; }
/* The relative time is the one that answers "is this still happening?", so it is
   the brighter of the two, and right-aligned in a fixed box so a hundred rows of
   it do not shuffle sideways every second as `9s` becomes `10s`. */
.lr-ago {
  color: var(--text); opacity: 0.75; font-size: 11px;
  min-width: 58px; text-align: right;
}

.lr-mark { display: flex; align-items: center; color: var(--accent, var(--muted)); }
.lr-mark svg { width: 13px; height: 13px; display: block; }
.lr-mark.unknown { color: var(--muted); font-size: 14px; line-height: 1; }

.lr-type {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px; font-weight: 600;
  color: var(--family, var(--text));
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lr-who, .lr-session {
  color: var(--muted); font-size: 11px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lr-session {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  opacity: 0.75;
}
/* The summary is the sentence, so it gets the width and the ink — but only one line
   of it: a wrapped row breaks the columns of every row below it. */
.lr-summary {
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* One hue per family (docs/aop-spec.md §4), borrowed from the vocabularies the room
   already uses so that `error` is the same red in both places. */
.log-row[data-family="session"] { --family: var(--status-walking); }
.log-row[data-family="turn"] { --family: var(--status-working); }
.log-row[data-family="tool"] { --family: var(--status-researching); }
.log-row[data-family="attention"] { --family: var(--status-error); }
.log-row[data-family="task"] { --family: var(--task-active); }
.log-row[data-family="other"] { --family: var(--muted); }
/* Somebody opening or closing a tab on this office. Deliberately the quietest hue in
   the list: it is context for the events around it rather than an event itself. */
.log-row[data-family="presence"] { --family: var(--sage); }
.log-row[data-kind="viewer"] .lr-who,
.log-row[data-kind="viewer"] .lr-summary { font-style: italic; }
.log-row[data-kind="viewer"] .lr-summary { color: var(--muted); }
.log-row[data-kind="viewer"] .lr-mark { color: var(--sage); font-size: 11px; }

/* An event from a harness this scene does not listen to. Kept rather than filtered,
   because it is the answer to "why is my agent not in the room?" — but dimmed, so
   it never reads as something the office is showing. */
.log-row.unwatched { opacity: 0.5; }
.log-row.unwatched:hover { opacity: 0.85; }

/* Simulated stage directions are not protocol events, and must not be mistaken for
   them: a dashed left edge says "this one never crossed the network". */
.log-row[data-kind="office"] .lr-line { border-left-style: dashed; }

.lr-json {
  margin: 0; padding: 10px 14px 14px 40px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px; line-height: 1.55;
  color: var(--muted);
  white-space: pre-wrap; word-break: break-word;
  background: rgba(0, 0, 0, 0.22);
}
.log-row:not(.open) .lr-json { display: none; }

/* A thing that happened to the office rather than in it — the receiver restarting,
   the office being deleted — set in the stream where it happened. */
.log-note {
  padding: 8px 10px; margin: 4px 0;
  font-size: 11.5px; color: var(--task-active);
  border-left: 2px solid var(--task-active);
  background: rgba(232, 171, 82, 0.07);
}

.log-empty {
  max-width: 62ch; padding: 28px 10px;
  font-size: 12.5px; line-height: 1.7; color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  body.debuglog #title-bar { backdrop-filter: none; }
}
