/* ============================================================================
   TAKEOFF WALKTHROUGH — cyanotype developing into CAD.
   Chrome is a dark precision instrument; the DRAWING is the thing that
   transforms: Prussian-blue blueprint -> warm CAD paper as steps confirm.
   ========================================================================== */

:root {
  /* instrument chrome */
  --ink-0: #0a0f15;
  --ink-1: #0f151d;
  --ink-2: #151d27;
  --hair: rgba(151, 183, 211, 0.13);
  --hair-strong: rgba(151, 183, 211, 0.28);
  --txt-0: #e9f1f8;
  --txt-1: #9fb4c6;
  --txt-2: #5d7389;

  /* the two worlds of the canvas */
  --blueprint: #0e2740;
  --blueprint-deep: #091d31;
  --paper: #f5f2ea;

  /* semantic layer colours */
  --electric: #2f80ff;   /* BRIGHT electric blue — every AI-detected element on the drawing */
  --det: #2f80ff;        /* chrome accent follows the electric blue */
  --vlm: #ff5e3a;        /* VLM / AI passes in flight (red boxes in the architecture) */
  --keep: #3fdf9f;       /* confirmed — panel stats only */
  --flag: #ffb020;       /* drafting amber — flags, pending review */
  --dim: #8fa7bd;        /* dimension annotations */

  --font-d: "Archivo", system-ui, sans-serif;
  --font-m: "IBM Plex Mono", ui-monospace, monospace;

  --rail-w: 216px;
  --panel-w: 344px;
  --top-h: 52px;
  /* How much room the ready band is taking under the topbar. Zero unless it has rows — it sets this
     itself (js/readybanner.js), so nothing else has to know whether it is there. */
  --banner-h: 0px;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background:
    radial-gradient(1200px 800px at 70% -10%, #12202e 0%, transparent 55%),
    var(--ink-0);
  color: var(--txt-0);
  font-family: var(--font-d);
  font-size: 14px;
  overflow: hidden;
}
.hidden { display: none !important; }
button { font-family: inherit; cursor: pointer; }

/* ---------------------------------------------------------------- topbar */
#topbar {
  position: fixed; inset: 0 0 auto 0; height: var(--top-h); z-index: 50;
  display: flex; align-items: center; gap: 20px; padding: 0 20px;
  background: color-mix(in oklab, var(--ink-0) 82%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hair);
}
.brand { display: flex; align-items: center; gap: 9px; }
.brand-mark {
  /* brand-mark.png — the plan, SQUARE and full-bleed, rendered from brand-plan.svg. It used to be
     cropped to a circle, which cost it twice: the canvas was padded out so the sheet's corners
     could survive the crop, and the crop then threw those corners away, leaving the drawing in a
     little over half the box. Square, the ground reaches all four edges and the sheet spans 91% of
     the width, so at 20px there are enough pixels for the plan to read at all. The url is relative
     to this stylesheet, which sits beside the file, so it resolves under both the Worker (page at
     /) and a local dev server. */
  width: 20px; height: 20px;
  /* A FLOOR, not a size: the mark never renders under 30px, whatever the box around it says. A
     drawing needs a minimum number of pixels before any of it reads, and below this the rooms and
     the stair collapse into texture. It wins over the 20px above — that stays as the base the
     minimum is measured against, so raising or dropping the floor is one number. 30 clears the
     52px topbar with room either side. */
  min-width: 30px; min-height: 30px;
  background: url("brand-mark.png") center / contain no-repeat;
}
.brand-name {
  font-weight: 800; letter-spacing: 0.22em; font-size: 14px;
  font-stretch: 118%;
}
/* the [+] and the switch read as ONE control, so they are one flex item with their own gap — loose
   in the topbar's 20px row they drift apart and the [+] reads as belonging to the brand */
.doc-tools { display: flex; align-items: center; gap: 6px; }
.doc-switch { position: relative; display: inline-flex; align-items: center; }
.doc-chip {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-m); font-size: 11.5px; color: var(--txt-1);
  border: 1px solid var(--hair); border-radius: 6px; padding: 4px 10px;
  background: var(--ink-1); cursor: pointer; transition: border-color 0.2s, color 0.2s;
  text-transform: uppercase; /* address/key shown uppercase (CAD-plate convention) */
}
/* ONE HOVER FOR ALL THREE CHROME PICKERS — add a plan, switch plan, switch dwelling. They sit in a
   row asking the same shape of question, so they answer the pointer the same way: the blue outline,
   and the label brought up to full strength. An OPEN chip holds that outline while its menu is
   down, so it is obvious which of the two menus is the one showing. */
.doc-chip:hover, .doc-chip.open, .doc-add:hover {
  border-color: var(--det); color: var(--txt-0);
}
.doc-chip:focus-visible, .doc-add:focus-visible {
  outline: 1px solid var(--det); outline-offset: 1px;
}
.doc-caret { color: var(--txt-2); font-size: 9px; }
.doc-chip:hover .doc-caret, .doc-chip.open .doc-caret { color: var(--det); }
/* ADD A PLAN — square, the chip's own border and background, so it belongs to the chip rather
   than floating beside it. The accent on hover is the only thing that says it does something new. */
.doc-add {
  display: inline-grid; place-items: center; width: 25px; height: 25px;
  font-family: var(--font-d); font-size: 16px; line-height: 1; color: var(--txt-1);
  border: 1px solid var(--hair); border-radius: 6px; background: var(--ink-1); cursor: pointer;
  transition: border-color 0.2s, color 0.2s;
}

.doc-menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 60; min-width: 340px;
  background: var(--ink-1); border: 1px solid var(--hair-strong); border-radius: 9px;
  padding: 5px; box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.85);
}
.doc-item {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-m); font-size: 12px; color: var(--txt-1);
  text-decoration: none; border-radius: 6px; padding: 9px 11px;
}
.doc-item::before { content: "📁"; font-size: 12px; }
.doc-item:hover { background: rgba(255, 255, 255, 0.04); color: var(--txt-0); }
.doc-item.current { color: var(--electric); }
.doc-item.current::after { content: "current"; margin-left: auto; font-size: 9.5px; color: var(--txt-2); letter-spacing: 0.1em; text-transform: uppercase; }
.doc-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-transform: uppercase; }

/* ------------------------------------------------------- progress tracker */
/* One step per stage across the topbar. The circle carries the state and the name carries the
   meaning, so the two are never separated: a row of unlabelled dots is a puzzle, not a tracker. */
.track-slot { flex: 1; min-width: 0; display: flex; justify-content: center; }
.track { display: flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; }
.track-step {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  padding: 5px 9px; border: 0; background: transparent; border-radius: 999px;
  font-family: var(--font-d); font-size: 11px; letter-spacing: 0.02em; color: var(--txt-2);
  white-space: nowrap;
}
/* the separator between steps — a hairline, not a chevron: this is a sequence, not a wizard */
.track-step + .track-step::before {
  content: ""; width: 10px; height: 1px; background: var(--hair-strong); margin-right: 2px;
}
.track-dot {
  width: 12px; height: 12px; flex: none; border-radius: 50%; box-sizing: border-box;
  display: inline-grid; place-items: center;
}
.track-dot svg { width: 9px; height: 9px; }

/* NOT READY — the pipeline has not produced it. Dashed says "nothing here yet", not "forbidden". */
.track-step.waiting { color: var(--txt-2); opacity: 0.55; }
.track-step.waiting .track-dot { border: 1px dashed var(--hair-strong); }
/* READY, unsigned */
.track-step.ready .track-dot { border: 1px solid var(--txt-2); }
.track-step.ready { color: var(--txt-1); }
/* HERE */
.track-step.current { color: var(--txt-0); }
.track-step.current .track-dot {
  border: 2px solid var(--electric);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--electric) 18%, transparent);
}
/* CONFIRMED */
.track-step.done { color: var(--keep); }
.track-step.done .track-dot { border: 1px solid var(--keep); background: var(--keep); color: var(--ink-0); }

/* only a step you may go back to is a button, and only that one reacts to the pointer */
button.track-step { cursor: pointer; }
button.track-step:hover { background: var(--ink-2); color: var(--txt-0); }
button.track-step:focus-visible { outline: 1px solid var(--electric); outline-offset: 1px; }

/* Narrow: the names go before the tracker does. The circles alone still carry the sequence, and the
   title attribute still names each one. Below that it leaves entirely rather than crushing the
   doc chip, which is the control a reviewer actually needs on a small screen. */
@media (max-width: 1180px) { .track-name { display: none; } .track-step { padding: 5px 6px; } }
@media (max-width: 760px) { .track-slot { display: none; } }

/* ---------------------------------------------------------------- scenes */
.scene { position: fixed; inset: calc(var(--top-h) + var(--banner-h)) 0 0 0; }

/* ------------------------------------------------ the ready band */
/* One row per dwelling with something to review, directly under the topbar. The whole row is the
   button: it is a single offer — open this dwelling — and a target the width of the window is
   easier to hit than a word at the end of a sentence. */
.ready-banner {
  position: fixed; inset: var(--top-h) 0 auto 0; z-index: 45;
  display: flex; flex-direction: column;
  border-bottom: 1px solid var(--hair);
  background: color-mix(in oklab, var(--keep) 12%, var(--ink-0));
  backdrop-filter: blur(10px);
}
.rb-row {
  display: flex; align-items: center; gap: 10px;
  height: 38px; padding: 0 20px;
  background: none; border: 0; border-top: 1px solid color-mix(in oklab, var(--hair) 60%, transparent);
  color: var(--txt-0); font-family: var(--font-d); font-size: 13px; text-align: left;
  transition: background 0.15s ease;
}
.rb-row:first-child { border-top: 0; }
.rb-row:hover { background: color-mix(in oklab, var(--keep) 14%, transparent); }
/* The pulse is the only moving thing: it says this arrived rather than having always been here. */
.rb-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--keep); box-shadow: 0 0 0 0 color-mix(in oklab, var(--keep) 70%, transparent);
  animation: rb-pulse 2.4s ease-out infinite;
}
@keyframes rb-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--keep) 70%, transparent); }
  70%, 100% { box-shadow: 0 0 0 7px transparent; }
}
.rb-text { flex: 1; }
.rb-go { color: var(--keep); font-family: var(--font-m); }
@media (prefers-reduced-motion: reduce) { .rb-dot { animation: none; } }

/* ------------------------------------------------- the workbench skeleton */
/* The wait before a run bundle lands, drawn as the pane it is ABOUT TO BE: the same 30/70 split,
   the same banded side pane, the same thumbnail grid and rows. Filling in beats rearranging — a
   skeleton that is not the real shape moves everything the moment the data arrives.
   Deliberately dim and textless: it must never be mistaken for content. */

/* Both classes are set by boot.js from <head>, before anything paints. `route-workbench` is
   one-way — the classification scene, which is the document at rest, must never flash on a
   workbench URL — and `wb-loading` is what app.js drops when the surface has drawn. */
html.route-workbench #scene-opener { display: none; }
.wb-skeleton { display: none; }
html.wb-loading .wb-skeleton {
  position: fixed; inset: var(--top-h) 0 0 0; z-index: 5;
  display: flex; align-items: stretch; background: var(--ink-0);
}
.wbs-side {
  flex: 0 0 30%; min-width: 0; display: flex; flex-direction: column; gap: 10px;
  padding: 12px; overflow: hidden;
  background: color-mix(in oklab, var(--ink-1) 60%, transparent);
}
.wbs-main {
  flex: 1 1 70%; min-width: 0; display: flex; flex-direction: column; gap: 14px; padding: 14px 18px;
  border-left: 1px solid var(--hair-strong);
}
/* a side-pane band, matching .rev-band's frame exactly */
.wbs-band {
  flex: none; display: flex; flex-direction: column; gap: 9px;
  border: 1px solid var(--hair); border-radius: 10px; background: var(--ink-1);
  padding: 11px 12px 12px;
}
.wbs-locked { opacity: 0.45; } /* the totals band, locked until the work above it is confirmed */

/* Every grey block, sweeping for as long as the bundle takes.
 *
 *  What makes a sweep feel fast is not the duration, it is how long the BRIGHT BAND takes to cross
 *  one point — travel distance over band width, not the clock. So the geometry is set first and the
 *  duration follows it: a band 20% of a 300%-wide gradient (six tenths of the element) travelling
 *  1.6 element-widths over 2.9s, which is a crossing of about a second.
 *
 *  LINEAR, and starting where the band already touches the edge. An ease-in spent its first tenth
 *  barely moving, and the keyframes used to begin with the band a full element-width off the block,
 *  so between them roughly half a second passed before anything visibly happened.
 *
 *  It LOOPS, with a rest. 4.0s of cycle: the 2.9s pass, then 1.1s of flat grey before the next one.
 *  The rest is held in the keyframes rather than taken out of the duration, so lengthening or
 *  shortening it leaves the sweep itself travelling at exactly the same speed. */
.wbs-bar, .wbs-thumb, .wbs-sheet {
  border-radius: 6px;
  background: linear-gradient(100deg,
    color-mix(in oklab, var(--ink-2) 80%, transparent) 40%,
    color-mix(in oklab, var(--hair-strong) 30%, transparent) 50%,
    color-mix(in oklab, var(--ink-2) 80%, transparent) 60%);
  background-size: 300% 100%;
  animation: wbs-sweep 4s linear infinite;
}
.wbs-band-head { height: 13px; width: 52%; }
.wbs-note { height: 9px; width: 100%; opacity: 0.55; }
.wbs-note.short { width: 72%; opacity: 0.55; }
.wbs-subline { height: 10px; width: 38%; margin-top: 2px; }
/* the page thumbnails, at the plan aspect and the same track size .rev-thumbs uses */
.wbs-thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(105px, 1fr)); gap: 8px; }
.wbs-thumb { aspect-ratio: 1191/842; border-radius: 7px; }
.wbs-row { height: 22px; border-radius: 6px; }
.wbs-confirm { height: 28px; width: 96px; border-radius: 8px; align-self: flex-end; }
.wbs-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.wbs-stat { height: 48px; border-radius: 8px; }
.wbs-toolbar { height: 13px; width: 26%; flex: none; }
.wbs-sheet { flex: 1; min-height: 0; border: 1px solid var(--hair); }
/* 90% -> 10%, not 100% -> 0%. Those last tenths at each end are the band travelling across ground
   that is off the block: at this size the ramp only enters the element's window below 90% and has
   left it by 10%, so a pass over the full range spent its first tenth — half a second, at the old
   duration — visibly doing nothing. Both ends still sit clear of the ramp, which is why the rest
   reads as a rest and the restart does not jump: the block is flat grey at each end of the pass.

   72.5% is where the sweep finishes — 2.9s of the 4s cycle — and the block then holds flat for the
   remaining 1.1s. Keeping the rest here rather than in the duration means the pass always travels
   90%->10% in 2.9s however long the rest is set to. */
@keyframes wbs-sweep {
  0% { background-position: 90% 0; }
  72.5%, 100% { background-position: 10% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .wbs-bar, .wbs-thumb, .wbs-sheet { animation: none; }
}

/* ============================================================ OPENER ==== */
/* two phases: the intro (full-width land/classify/flip, title shown) settles into the working
   layout (review 40% + view 60%, always open) with the title gone. */
#scene-opener { display: flex; flex-direction: column; overflow: hidden; }
#scene-opener[data-phase="intro"] .workbench { display: none; }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* ========================================================= WORKBENCH ==== */
/* The REUSABLE two-pane working layout: a work/review side panel (30%) and a main viewer (70%).
   The classification page is one instance; other surfaces reuse the same skeleton. Markup:

     <div class="workbench">
       <aside class="wb-side">
         <div class="wb-tabs"></div>      optional — a tab bar across the top (e.g. per lot)
         <div class="wb-head">            optional — title + a right-aligned meta/progress slot
           <span class="wb-title">…</span><span class="wb-meta">…</span>
         </div>
         <div class="wb-body">…</div>     the ONLY scrolling region
         <div class="wb-foot">…</div>     optional — a pinned action bar (notice + primary button)
       </aside>
       <section class="wb-main">…</section>   fills; its own children manage overflow
     </div>

   Every element keeps a stable id for JS; these classes carry the layout only. */
.workbench { flex: 1; display: flex; min-height: 0; align-items: stretch; }
.wb-side {
  flex: 0 0 30%; min-width: 0; display: flex; flex-direction: column;
  background: color-mix(in oklab, var(--ink-1) 60%, transparent);
}
.wb-main {
  flex: 1 1 70%; min-width: 0; overflow: hidden; display: flex; flex-direction: column;
  border-left: 1px solid var(--hair-strong); background: var(--ink-0);
}
.wb-head {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 14px 16px 10px; border-bottom: 1px solid var(--hair);
}
.wb-title { font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--txt-1); }
.wb-meta { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
.wb-meta .done { color: var(--keep); }
.wb-body { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
.wb-foot { flex: none; padding: 14px 16px; border-top: 1px solid var(--hair); display: flex;
  flex-direction: column; align-items: flex-end; }
/* a tab bar across the top of the side pane */
.wb-tabs { display: flex; gap: 4px; flex-wrap: wrap; padding: 12px 16px 0; }
.wb-tabs.hidden { display: none; }
.wb-tab {
  font-size: 12px; font-weight: 600; letter-spacing: 0.02em; color: var(--txt-2);
  background: var(--ink-2); border: 1px solid var(--hair); border-bottom: none;
  border-radius: 7px 7px 0 0; padding: 7px 14px; cursor: pointer; transition: color 0.15s, background 0.15s;
}
.wb-tab:hover { color: var(--txt-0); }
.wb-tab.active { color: var(--txt-0); background: color-mix(in oklab, var(--ink-1) 70%, transparent); }
.wb-tab.done::after { content: " ✓"; color: var(--keep); }
/* a clone of an intro sheet flying into its review-pane thumbnail slot as the layout resolves */
.fly-clone { border: 1px solid var(--hair-strong); border-radius: 7px; overflow: hidden; pointer-events: none; }
.fly-clone img { display: block; width: 100%; height: 100%; object-fit: cover;
  filter: invert(0.92) sepia(0.4) saturate(2.2) hue-rotate(170deg) brightness(0.95); }
.rev-thumb.flying { opacity: 0; }

.thumb-group { margin-bottom: 26px; }
.thumb-group-title {
  display: flex; align-items: center; gap: 10px;
  font-size: 12px; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--txt-1); margin: 0 0 12px;
  opacity: 0; transition: opacity 0.5s;
}
.thumb-group-title.shown { opacity: 1; }
.thumb-group-title::after { content: ""; flex: 1; height: 1px; background: var(--hair); }
.thumb-group-title .g-count { color: var(--electric); font-family: var(--font-m); }
.thumb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(146px, 1fr)); gap: 14px; }

.sheet-card {
  position: relative; border: 1px solid var(--hair); border-radius: 8px;
  background: var(--ink-1); overflow: hidden;
  opacity: 0; transform: translateY(16px) scale(0.96);
  transition: opacity 1s cubic-bezier(0.2, 0.7, 0.2, 1),
              transform 1s cubic-bezier(0.2, 0.7, 0.2, 1),
              border-color 0.4s, box-shadow 0.4s;
}
.sheet-card.landed { opacity: 1; transform: none; }
.sheet-card.classified { border-color: var(--hair-strong); }
.sheet-card.is-target { border-color: color-mix(in oklab, var(--det) 55%, transparent); box-shadow: 0 0 0 1px color-mix(in oklab, var(--det) 35%, transparent), 0 6px 24px -12px var(--det); }
.sheet-card img {
  display: block; width: 100%; aspect-ratio: 1191/842; object-fit: cover;
  filter: invert(0.92) sepia(0.4) saturate(2.6) hue-rotate(170deg) brightness(0.75);
  transition: filter 0.6s;
}
.sheet-card.classified img { filter: invert(0.92) sepia(0.4) saturate(2.2) hue-rotate(170deg) brightness(0.95); }
/* the intro card carries the drawing and nothing else — the page number, the title and the type
   chips are gone with the markup that held them (opener.introCard) */

.btn-primary {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  font-size: 14px; font-weight: 700; letter-spacing: 0.04em;
  color: var(--ink-0); text-decoration: none; cursor: pointer;
  background: linear-gradient(100deg, var(--det), #7fd4ff);
  border: 0; border-radius: 10px; padding: 11px 20px;
  box-shadow: 0 8px 30px -8px color-mix(in oklab, var(--det) 70%, transparent);
  transition: transform 0.2s, box-shadow 0.2s, filter 0.2s;
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 14px 36px -8px color-mix(in oklab, var(--det) 80%, transparent); }
.btn-primary[aria-disabled="true"] { filter: grayscale(0.85) brightness(0.6); box-shadow: none; pointer-events: none; cursor: default; }
.btn-arrow { font-size: 18px; }

/* the DWELLING picker, beside the doc chip — workbench routes on a development only.
   It IS the doc chip (.doc-chip .doc-menu .doc-item), with only the things that differ set here:
   a narrower panel, because a lot name is a word and an address is a line. */
.lot-slot { display: flex; align-items: center; }
.lot-slot.hidden { display: none; }
.lot-switch { position: relative; display: inline-flex; align-items: center; }
.lot-chip-label { letter-spacing: 0.04em; }
.lot-menu { min-width: 180px; }
.lot-item::before { content: "⌂"; }   /* a dwelling, where the plan menu shows a folder */

/* ---------------------------- classification review (a .workbench instance) -------- */

.rev-band {
  border: 1px solid var(--hair); border-radius: 10px; background: var(--ink-1);
  padding: 11px 12px 12px; transition: border-color 0.25s, box-shadow 0.25s, opacity 0.25s;
}
.rev-band[data-focus] { border-color: color-mix(in oklab, var(--electric) 55%, transparent);
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--electric) 30%, transparent), 0 8px 22px -14px var(--electric); }
.rev-band-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 9px; }
.rev-band-name { font-size: 13px; font-weight: 700; color: var(--txt-0); }
.rev-band-count { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
/* Unclassified is a passive catch-all: no confirm, no flags, no add, no remove */
.rev-band[data-bucket] { background: transparent; border-style: dashed; opacity: 0.9; }

/* the project address: the candidates discovery read, one selectable row each, plus free text */
.rev-addrs { display: flex; flex-direction: column; gap: 6px; margin-bottom: 11px; }
.rev-addr {
  border: 1px solid var(--hair); border-radius: 8px; background: var(--ink-2); cursor: pointer;
  padding: 8px 10px; transition: border-color 0.15s, background 0.15s;
}
.rev-addr:hover { border-color: color-mix(in oklab, var(--electric) 40%, var(--hair)); }
.rev-addr:focus-visible { outline: 1px solid var(--electric); outline-offset: 1px; }
.rev-addr.on { border-color: color-mix(in oklab, var(--electric) 60%, transparent);
  background: color-mix(in oklab, var(--electric) 8%, var(--ink-2)); }
.rev-addr-line { display: flex; align-items: center; gap: 9px; }
.rev-addr-dot {
  flex: none; width: 12px; height: 12px; border-radius: 50%;
  border: 1px solid color-mix(in oklab, var(--txt-2) 70%, transparent); background: transparent;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.rev-addr.on .rev-addr-dot { border-color: var(--electric);
  box-shadow: inset 0 0 0 2.5px color-mix(in oklab, var(--electric) 85%, transparent); }
.rev-addr-text { font-size: 12.5px; color: var(--txt-1); line-height: 1.35; }
.rev-addr.on .rev-addr-text { color: var(--txt-0); font-weight: 600; }
.rev-addr.other .rev-addr-text { color: var(--txt-2); font-style: italic; }
.rev-addr-input {
  width: 100%; margin: 8px 0 1px; font-size: 12.5px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--electric); border-radius: 6px; padding: 5px 8px;
}
.rev-addr-input::placeholder { color: var(--txt-2); font-style: italic; }

/* page thumbnails inside a band (they replaced the text chips) */
.rev-thumbs {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(105px, 1fr));
  gap: 8px; margin-bottom: 9px;
}
/* Floor Plans sub-divides by storey on a multi-storey plan: a ruled line labelled per storey */
.rev-sub + .rev-sub { margin-top: 10px; }
.rev-sub-line {
  display: flex; align-items: center; gap: 10px; margin: 2px 0 8px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--txt-1);
}
.rev-sub-line::after { content: ""; flex: 1; height: 1px; background: var(--hair); }
.rev-thumb {
  position: relative; cursor: pointer; border: 1px solid var(--hair); border-radius: 7px;
  overflow: hidden; background: var(--ink-2); transition: border-color 0.15s, box-shadow 0.15s;
}
.rev-thumb:hover { border-color: var(--hair-strong); }
.rev-thumb.selected { border-color: var(--electric); box-shadow: 0 0 0 1px var(--electric); }
/* WHICH DRAWING THIS IS. Two thumbnails of one storey are two drawings of the same rooms — the
   Floor Plan it is presented on and the set-out it was measured from — and the page number in the
   opposite corner does not say which. Bottom-left, clear of `.rt-num`. */
.rt-sheet {
  position: absolute; left: 4px; bottom: 4px; z-index: 2;
  font-family: var(--font-d); font-size: 8.5px; letter-spacing: 0.06em;
  color: var(--txt-1); background: color-mix(in oklab, var(--ink-0) 78%, transparent);
  border-radius: 4px; padding: 1px 4px; pointer-events: none;
}
.rev-thumb.selected .rt-sheet { color: var(--txt-0); }
.rev-thumb img {
  display: block; width: 100%; aspect-ratio: 1191/842; object-fit: cover;
  filter: invert(0.92) sepia(0.4) saturate(2.2) hue-rotate(170deg) brightness(0.95);
}
.rev-thumb .rt-num {
  position: absolute; top: 3px; left: 4px; font-family: var(--font-m); font-size: 9px;
  color: #fff; background: var(--ink-0);
  padding: 0 3px; border-radius: 3px;
}
.rev-thumb.flagged .rt-flag {
  position: absolute; top: 3px; right: 4px; color: var(--flag); font-size: 10px;
  text-shadow: 0 0 3px var(--ink-0);
}
.rev-thumb .rt-x {
  position: absolute; bottom: 3px; right: 3px; display: grid; place-items: center;
  width: 16px; height: 16px; border-radius: 4px; border: 0; cursor: pointer;
  color: #fff; font-size: 11px; line-height: 1;
  background: color-mix(in oklab, var(--ink-0) 55%, transparent); opacity: 0; transition: opacity 0.15s;
}
.rev-thumb:hover .rt-x { opacity: 1; }
.rev-thumb .rt-x:hover { background: var(--vlm); }
/* the add-page cell is the same size/shape as a page thumbnail, sitting in the grid */
.rev-add {
  width: 100%; aspect-ratio: 1191/842; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px;
  font-family: var(--font-m); font-size: 9px; letter-spacing: 0.04em; color: var(--electric);
  background: transparent; border: 1px dashed color-mix(in oklab, var(--electric) 45%, transparent);
  border-radius: 7px; cursor: pointer;
}
.rev-add .plus { font-size: 18px; line-height: 1; font-weight: 400; }
.rev-add:hover { background: color-mix(in oklab, var(--electric) 12%, transparent); }
.rev-note { font-size: 10.5px; color: color-mix(in oklab, var(--flag) 85%, var(--txt-1)); line-height: 1.4; margin-bottom: 8px; }
.rev-note .rev-flag.resolved { color: var(--txt-2); text-decoration: line-through; } /* a split page's flag is now resolved */
/* blue INFORMATIVE flags (ⓘ) — context, not a warning */
.rev-info { font-size: 10.5px; line-height: 1.5; margin: 0 0 8px; color: var(--electric); }
.rev-info .ri-ic { vertical-align: -2px; margin-right: 4px; }
/* a required category with no pages blocks processing — orange, left-aligned, at the card foot */
.rev-warn { font-size: 13.5px; font-weight: 600; text-align: left; color: var(--flag); line-height: 1.4; margin-top: 10px; }
/* confirm sizes to its label and sits at the right of the band (button is inline-block by default,
   so it needs display:block for margin-left:auto to right-align it) */
.rev-confirm {
  display: block; width: fit-content; margin-left: auto;
  font-size: 12px; font-weight: 700; letter-spacing: 0.03em; cursor: pointer;
  color: var(--keep); background: color-mix(in oklab, var(--keep) 12%, transparent);
  border: 1px solid color-mix(in oklab, var(--keep) 45%, transparent); border-radius: 8px; padding: 7px 16px;
  transition: background 0.15s;
}
.rev-confirm:hover { background: color-mix(in oklab, var(--keep) 20%, transparent); }
/* once confirmed it becomes a plain "Confirmed" marker — no outline, no fill, not clickable */
.rev-confirm.confirmed {
  background: transparent; border-color: transparent; padding-right: 0; cursor: default;
  color: color-mix(in oklab, var(--keep) 85%, var(--txt-1));
}
/* disabled because the required category is empty — plain grey, not the green actionable state */
.rev-confirm:disabled:not(.confirmed) {
  color: var(--txt-2); background: var(--ink-2); border-color: var(--hair); cursor: not-allowed;
}
/* THE OUTLINE ACTION, at the foot of a storey's own thumbnails row — right-aligned, under the
   sheets it is drawn on. No rule above it and no heading of its own: the storey is already named
   at the top of the row, and a second divider would read as a second storey. */
.rev-perim { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  justify-content: flex-end; margin: -3px 0 9px; }
.rp-actions { display: inline-flex; gap: 6px; margin-left: auto; }
/* Small ACTIONS, not page tiles: the add-page button next door is authored at a sheet's aspect
   ratio, which is right for a thing that stands in for a drawing and absurd for a verb. */
.rp-draw, .rp-clear {
  font-family: var(--font-m); font-size: 9.5px; letter-spacing: 0.04em; color: var(--electric);
  background: transparent; border: 1px solid color-mix(in oklab, var(--electric) 45%, transparent);
  border-radius: 6px; padding: 5px 9px; cursor: pointer; white-space: nowrap;
}
.rp-draw:hover, .rp-clear:hover { background: color-mix(in oklab, var(--electric) 12%, transparent); }
.rp-draw:disabled { color: var(--txt-2); border-color: var(--hair); cursor: not-allowed;
  background: transparent; }
.rp-draw.on { color: var(--det); border-color: var(--det); }
.rp-clear { color: var(--txt-2); border-color: var(--hair); padding-inline: 8px; }
.rp-clear:hover { color: var(--vlm); border-color: color-mix(in oklab, var(--vlm) 55%, transparent);
  background: color-mix(in oklab, var(--vlm) 12%, transparent); }
.rp-why { flex-basis: 100%; text-align: right; font-size: 10.5px; color: var(--txt-2); }

/* The outline over the sheet, and the crosshair while one is being traced. The layer rides inside
   the zoom container with the drawing, and is never hit-tested — the clicks are taken on the frame
   (see mountPerimeterLayer). */
.view-perim { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.vp-shape polygon { fill: color-mix(in oklab, var(--keep) 12%, transparent);
  stroke: var(--keep); stroke-width: 2; vector-effect: non-scaling-stroke; }
/* one discovery found and nobody has redrawn — a reading, in the colour every other pipeline
   reading wears on this app */
.vp-discovery polygon { fill: color-mix(in oklab, var(--electric) 10%, transparent);
  stroke: var(--electric); }
.vp-shape.tracing polygon { display: none; } /* redrawing: the old outline gets out of the way */
/* THE CUT, while a half of a split sheet is being traced. The tool is clamped to one side of it,
   so the line is not decoration: it is the edge the outline cannot cross, and the reviewer needs to
   see which half they are measuring before they start clicking. */
.vp-cut { stroke: var(--det); stroke-width: 1.2; stroke-dasharray: 4 4; opacity: 0.75;
  vector-effect: non-scaling-stroke; }
/* the enclosed area, at the middle of its own outline — haloed so it reads over a dense drawing */
.vp-area { fill: var(--keep); font-family: var(--font-m); font-weight: 500; paint-order: stroke;
  stroke: rgba(9, 20, 33, 0.85); stroke-width: 3.5; }
.vp-discovery .vp-area { fill: var(--electric); }
.vp-shape.tracing .vp-area { display: none; }
.view-frame.tracing { cursor: crosshair; }

/* why a Confirm is dead, next to the button it blocks */
.rev-gate { font-size: 10.5px; color: var(--flag); text-align: right; margin: 0 0 6px; }
/* The missing-garage-door question, at the foot of the lowest storey: the reader's words and the way
   out, ringed in the flag colour so it reads as the thing holding the Confirm shut. The ring goes
   once it is answered; the note stays, struck through, so the reviewer can see what they decided. */
.op-garage-gate, .op-frame-gate, .rev-missing-gate { display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
  line-height: 1.4; border: 1px solid var(--flag); border-radius: 6px; padding: 7px 9px; }
.op-garage-gate .og-note, .op-frame-gate .og-note, .rev-missing-gate .og-note { text-align: right; }
.og-override { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-family: var(--font-m); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; }
/* A plain box that fills in, in whatever colour the gate is currently wearing — the platform
   checkbox renders as a green ticked pill, which reads as an emoji dropped into a CAD panel.
   `color: inherit` is load-bearing: a form control does not inherit colour, the UA sheet gives it
   black, and `currentColor` then paints a black border on a near-black panel — invisible. */
.og-override input {
  appearance: none; -webkit-appearance: none; margin: 0; cursor: pointer;
  color: inherit;
  width: 11px; height: 11px; border-radius: 2px; background: transparent;
  border: 1px solid currentColor;
}
.og-override input:checked { background: currentColor; }
.op-garage-gate.resolved, .op-frame-gate.resolved, .rev-missing-gate.resolved { color: var(--txt-2); border-color: transparent; }
.op-garage-gate.resolved .og-note, .op-frame-gate.resolved .og-note,
.rev-missing-gate.resolved .og-note { text-decoration: line-through; }

/* add-page picker popover */
.rev-picker { position: relative; }
.rev-pop {
  position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; min-width: 220px; max-height: 240px; overflow-y: auto;
  background: var(--ink-2); border: 1px solid var(--hair-strong); border-radius: 8px; padding: 5px;
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.85); display: flex; flex-direction: column; gap: 2px;
}
.rev-pop-item {
  display: flex; align-items: center; gap: 7px; text-align: left; cursor: pointer;
  font-family: var(--font-m); font-size: 10.5px; color: var(--txt-1);
  background: transparent; border: 0; border-radius: 5px; padding: 5px 7px;
}
.rev-pop-item:hover { background: rgba(255, 255, 255, 0.05); color: var(--txt-0); }
.rev-pop-empty { font-size: 10.5px; color: var(--txt-2); padding: 6px 7px; }

/* -------------------------------------------------- view pane ---------- */
/* head: page number · title · category (the category is the reclassify dropdown) — no footer */
.view-head {
  flex: none; display: flex; align-items: center; gap: 12px; padding: 12px 16px;
  border-bottom: 1px solid var(--hair); white-space: nowrap;
}
.view-head .vp-num { flex: none; font-family: var(--font-m); font-size: 12px; color: var(--txt-2); }
.view-title { flex: 0 1 auto; min-width: 0; font-size: 13px; font-weight: 700; color: var(--txt-0);
  overflow: hidden; text-overflow: ellipsis; } /* shrinks/truncates first so the classification stays put */
/* Lot / Floor / Classification label+dropdowns sit left-aligned immediately after the title. All
   three share ONE dropdown style (the view-pane header style). */
.view-cat-wrap, .view-floor-wrap, .view-lot-wrap { flex: none; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.view-cat-label { font-size: 13px; font-weight: 600; color: var(--txt-1); }
.view-head .view-cat, .view-head .view-floor, .view-head .view-lot {
  min-width: 118px; background: var(--ink-1); color: var(--txt-0);
  border: 1px solid var(--hair); border-radius: 6px; padding: 6px 10px;
  font-size: 13px; font-weight: 600; cursor: pointer; /* matched to the title so it reads as important */
}
.view-head .view-cat { min-width: 132px; } /* Classification carries the longest label */
.view-head .view-cat:hover, .view-head .view-floor:hover, .view-head .view-lot:hover { border-color: var(--hair-strong); }
.view-head select:disabled { color: var(--txt-1); cursor: default; opacity: 0.8; } /* the locked "Split" state */

/* the Unassigned bucket reads as a to-do (like a warning), unlike the neutral Unclassified */
.rev-band[data-band="unassigned"] .rev-band-name { color: var(--flag); }

/* process notice — shown when Process is clicked while blocked (unassigned pages / unconfirmed) */
.process-notice { margin: 0 0 10px; font-size: 12.5px; font-weight: 600; line-height: 1.4; color: var(--flag); }
.process-notice.ok { color: var(--keep); } /* the save-succeeded confirmation reads green, not a warning */
.process-notice.hidden { display: none; }
/* muted (not-ready) Process button: greyed but STILL clickable, so a click can surface the notice */
.btn-primary.muted { filter: grayscale(0.85) brightness(0.62); box-shadow: none; }
.btn-primary.muted:hover { transform: none; }

/* -------- floor-plan split flow -------- */
.rev-sub-line.needs-split { color: var(--flag); }                                   /* the flag group heading */
.rev-thumb.needs-split { border-color: color-mix(in oklab, var(--flag) 55%, var(--hair)); }
.rt-darken { position: absolute; top: 0; bottom: 0; background: rgba(3, 6, 10, 0.66); pointer-events: none; } /* the OTHER half */

/* view-pane split UI: a draggable vertical line over the drawing + a floor dropdown per side */
.view-body.split-mode { position: relative; }
/* The layer fills the frame — which IS the drawing's rect — and sits OUTSIDE the camera transform,
   so the line stays glued to the page (paint() re-places it from the camera) while the handle and
   the storey dropdowns keep their screen size at any zoom. */
.split-layer { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.split-line { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--electric);
  pointer-events: auto; cursor: ew-resize; z-index: 2; }
.split-handle { position: absolute; top: 50%; left: 50%; width: 16px; height: 42px; transform: translate(-50%, -50%);
  border-radius: 4px; background: var(--electric); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55); }
/* a side's floor picker flanks the drag handle — a tight [dropdown | handle | dropdown] cluster the
   eye reads as one task. JS anchors both AT the line (left:x%); the transforms sit them either side. */
.split-pick { position: absolute; top: 50%; pointer-events: auto; z-index: 3; }
.split-pick.left { transform: translate(calc(-100% - 14px), -50%); }  /* right edge just left of the line */
.split-pick.right { transform: translate(14px, -50%); }               /* left edge just right of the line */
/* header-dropdown style + a lift over the drawing. Orange outline while flagged (no storey picked),
   blue once a storey is chosen. */
.split-floor { background: var(--ink-1); color: var(--txt-0); border: 1.5px solid var(--flag);
  border-radius: 6px; padding: 6px 10px; font-size: 13px; font-weight: 600; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5); }
.split-floor.chosen { border-color: var(--electric); }
.split-floor:hover { filter: brightness(1.06); }
/* the page always fits fully inside the pane and never scrolls or drifts. The element box is
   pinned to the pane (width/height:100%) so a <canvas> — whose width:auto would otherwise resolve
   to its huge backing width and drive the height past the pane bottom as the window widens — can
   never overflow; object-fit:contain then scales the drawing to full-height-contain, and equally
   scales the small thumb placeholder up to fill rather than sitting tiny. */
.view-body { flex: 1; min-height: 0; padding: 0; overflow: hidden; }
/* the thumb placeholder and the 300-DPI canvas share the same fit + blueprint tint */
.view-body img, .view-body canvas {
  width: 100%; height: 100%; object-fit: contain; object-position: center;
  filter: invert(0.92) sepia(0.4) saturate(2.2) hue-rotate(170deg) brightness(0.95);
}
.view-body .view-ph { opacity: 0.55; } /* the low-res placeholder reads as "loading" until the render lands */
/* the classification view's camera: the page moves inside the body, which stays where it is.
   Scoped to .view-zoomable so the split-mode body, whose own layer is positioned against it, is
   left exactly as it was. */
.view-zoomable { display: grid; place-items: center; overscroll-behavior: contain; }
/* the frame is sized in JS to the drawing's own letterboxed rect, so the page fills it exactly and
   leaves no band for the camera to be thrown off by. aspect-ratio cannot do this on its own: with
   max-height also capping it, the height is clamped while the width stays put and the ratio breaks. */
.view-frame { position: relative; overflow: hidden; }
.view-zoom { position: absolute; inset: 0; transform-origin: 0 0; }

/* -------- review modal: context shown once the review pane has resolved -------- */
.review-modal {
  position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
  background: rgba(3, 6, 10, 0.62); backdrop-filter: blur(2px);
  animation: modal-fade 1.2s ease both; /* dims the scene over 1.2s while the fly-in plays behind it */
}
.review-modal.hidden { display: none; }
@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
.review-modal-card {
  position: relative; width: min(460px, calc(100vw - 48px));
  background: var(--ink-1); border: 1px solid var(--hair-strong); border-radius: 12px;
  padding: 18px 26px 24px; box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
  /* stays hidden until .card-in — starts fading in ON TOP at 0.8s (0.4s before the overlay finishes) */
  opacity: 0; transform: translateY(10px) scale(0.98);
  transition: opacity 0.8s cubic-bezier(0.2, 0.7, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.review-modal.card-in .review-modal-card { opacity: 1; transform: none; }
/* header row: the info glyph (left) balances the ✕ (right) */
.review-modal-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.review-modal-info { display: inline-flex; color: var(--det); } /* the info icon, in the accent blue */
.review-modal-info.ok { color: var(--keep); } /* the same mark, in the confirmed green */
.review-modal-x {
  width: 28px; height: 28px; margin: -2px -6px 0 0;
  display: grid; place-items: center; border-radius: 6px;
  background: transparent; border: none; color: var(--txt-2); font-size: 13px; cursor: pointer;
}
.review-modal-x:hover { background: var(--ink-2); color: var(--txt-0); }
.review-modal-text { margin: 0 0 24px; font-size: 14.5px; line-height: 1.55; color: var(--txt-0); }
/* right-aligned, sized to its content (not full width) — matches the review-pane Confirm */
.review-modal-ok { display: flex; width: fit-content; margin-left: auto; }

/* -------- uploaded, not yet read: the page a reviewer lands on after adding a plan -------- */
/* Below the topbar rather than over it, because the doc switch and the [+] stay usable: somebody
   waiting on one plan can open another, or add the next one. */
/* `.plan-pending`, NOT `.pending`. A bare state word is not a class name on this app: the ceilings
   stage writes a gable's own state into its class list (`cl-g-row pending`), so a rule named for
   the state alone dressed every unresolved gable as a full-screen fixed overlay — opaque, over the
   whole scene, painting the page out. A block class has to name the BLOCK. */
.plan-pending {
  position: fixed; inset: var(--top-h) 0 0 0; z-index: 40;
  display: grid; place-items: center; padding: 24px;
  background: var(--ink-0); overflow: auto;
}
.plan-pending.hidden { display: none; }
.pending-card {
  width: min(560px, 100%); text-align: center;
  animation: pending-in 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes pending-in {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}
.pending-mark { display: inline-flex; color: var(--keep); margin-bottom: 14px; }
.pending-title {
  margin: 0 0 6px; font-family: var(--font-d); font-size: 22px; font-weight: 800;
  letter-spacing: -0.01em; color: var(--txt-0);
}
/* the job, in the CAD-plate convention the doc chip uses — so the plan being talked about is the
   one named in the chrome above it */
.pending-name {
  margin: 0 0 20px; font-family: var(--font-m); font-size: 11.5px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--det);
}
.pending-name:empty { display: none; }
.pending-text {
  margin: 0 auto 14px; max-width: 72ch; font-size: 14px; line-height: 1.6; color: var(--txt-1);
}
.pending-text strong { color: var(--txt-0); font-weight: 600; }
/* the way onward, where there is one — centred under the copy rather than right-aligned like the
   modal's OK, because nothing here is a dialog being dismissed */
.pending-go { display: inline-flex; margin-top: 8px; text-decoration: none; }
.pending-go.hidden { display: none; }
.pending-foot { margin: 22px 0 0; font-size: 12.5px; color: var(--txt-2); }

/* THE SHEETS BEING READ — the opener's grid and the opener's cards, so the wait and Reading the
   drawing set are plainly the same page at two moments: as many across as fit, same size, same
   blueprint. All this adds is the BREATH — a card not yet rendered pulses, and stops when it is
   `classified`, which is the same class that surface sets when a sheet has been looked at.
   The RING and the CHIN are the tracing surface's own (.op-tracing-ring / -label / -say), reused
   rather than restated: "something is happening to your drawing" should look the same wherever it
   happens, and one sweep cannot drift from another if there is only one. */
.pending-work {
  position: relative; margin: 32px 0 18px; padding: 28px 14px 14px; border-radius: 10px;
}
.pending-work.hidden { display: none; }
.pending-work .thumb-grid { text-align: left; }
.pending-work .sheet-card:not(.classified) { animation: sheet-breathe 2.4s ease-in-out infinite; }
@keyframes sheet-breathe {
  0%, 100% { opacity: 0.34; }
  50% { opacity: 0.8; }
}
/* the wait is a grid of sheets, so it takes the page's width — the prose keeps its own measure */
.plan-pending .pending-card:has(.pending-work:not(.hidden)) { width: min(1400px, 100%); }

/* -------- upload a plan: the [+] beside the doc switch -------- */
/* wider than the notice modals: a drop target that reads as a place to put something needs room */
.up-card { width: min(520px, calc(100vw - 48px)); }
.up-title {
  margin: 0; font-family: var(--font-d); font-size: 15px; font-weight: 700;
  letter-spacing: 0.02em; color: var(--txt-0);
}
/* THE DROP TARGET. A label wrapping a hidden input, so a click anywhere on it opens the picker. */
.up-drop {
  display: grid; justify-items: center; gap: 5px; padding: 26px 18px 24px;
  border: 1px dashed var(--hair-strong); border-radius: 10px; background: var(--ink-0);
  color: var(--txt-1); cursor: pointer; text-align: center;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.up-drop:hover { border-color: var(--det); color: var(--txt-0); }
/* while a file is over it — the same accent as hover, held, so the drag has somewhere to land */
.up-drop.over {
  border-color: var(--det); color: var(--txt-0);
  background: color-mix(in oklab, var(--det) 9%, var(--ink-0));
}
.up-drop-icon { color: var(--txt-2); margin-bottom: 2px; }
.up-drop:hover .up-drop-icon, .up-drop.over .up-drop-icon { color: var(--det); }
.up-drop-lead { font-family: var(--font-d); font-size: 14px; color: inherit; }
.up-drop-sub { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
.up-drop-link { color: var(--det); }
.up-file { display: none; }
/* THE SET — it REPLACES the drop target rather than sitting under it, so the panel is never
   inviting a file underneath the files it already holds. */
.up-picked {
  display: grid; gap: 6px; margin: 0; padding: 0; list-style: none;
  border-radius: 8px; transition: box-shadow 0.15s;
}
.up-picked.hidden { display: none; }
/* a file dragged over the list, once the drop panel has gone — there must still be a target */
.up-picked.over { box-shadow: 0 0 0 1px var(--det), 0 0 0 5px color-mix(in oklab, var(--det) 14%, transparent); }
.up-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px 9px 12px;
  border: 1px solid var(--hair); border-radius: 8px; background: var(--ink-2);
}
.up-item-mark {
  flex: none; min-width: 22px; text-align: center;
  font-family: var(--font-m); font-size: 9.5px; letter-spacing: 0.08em; color: var(--det);
  border: 1px solid color-mix(in oklab, var(--det) 45%, transparent);
  border-radius: 4px; padding: 2px 5px;
}
.up-item-name {
  flex: 1; min-width: 0; font-size: 12.5px; color: var(--txt-0);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.up-item-size { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
.up-item-x {
  width: 22px; height: 22px; flex: none; display: grid; place-items: center; border-radius: 5px;
  background: transparent; border: none; color: var(--txt-2); font-size: 11px; cursor: pointer;
}
.up-item-x:hover { background: var(--ink-1); color: var(--txt-0); }
/* "Add another PDF" — the same picker as the panel, worded for a set that has already begun */
.up-more {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin: 8px 0 0; font-family: var(--font-m); font-size: 11px;
}
.up-more.hidden { display: none; }
.up-more-link { color: var(--det); cursor: pointer; }
.up-more-link:hover { text-decoration: underline; }
.up-count { color: var(--txt-2); }
/* why a file was refused — in the flag amber, where the file would have been listed */
.up-error { margin: 10px 0 0; font-size: 12px; line-height: 1.45; color: var(--flag); }
.up-error.hidden { display: none; }
.up-field { display: grid; gap: 6px; margin: 18px 0 22px; }
.up-label {
  font-family: var(--font-m); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--txt-2);
}
.up-input {
  font-family: var(--font-d); font-size: 13.5px; color: var(--txt-0);
  border: 1px solid var(--hair); border-radius: 8px; padding: 10px 12px;
  background: var(--ink-0); outline: none; transition: border-color 0.15s;
}
.up-input::placeholder { color: var(--txt-2); }
.up-input:focus { border-color: var(--det); }

/* ======================================================= PERIMETERS ==== */
/* a .workbench instance: one Perimeter & Area panel per floor + the traced sheet in the main pane */
#scene-perimeters { display: flex; flex-direction: column; overflow: hidden; }
.rev-sub-line.done { color: var(--keep); }
.rev-sub-line .rs-tick { color: var(--keep); margin-left: 6px; }
/* the storey's key facts: full width, under its thumbnail and above the Confirm */
.perim-stats { grid-template-columns: 1fr 1fr; gap: 8px; margin: 0 0 10px; }
.perim-stats .stat { padding: 8px 10px; }
.perim-stats .s-value { font-size: 15px; }
.perim-empty { padding: 18px; font-size: 12.5px; line-height: 1.5; color: var(--flag); }
/* the sheet + overlay stack: sized to the page aspect so the point-space overlay lands exactly.
   The viewport inside carries the camera transform (framed on the plan envelope), so the raster
   and the overlay scale together; the stack clips whatever falls outside. */
.perim-body { display: flex; align-items: center; justify-content: center; padding: 18px; }
.perim-stack { position: relative; max-width: 100%; max-height: 100%; height: 100%; overflow: hidden; }
.perim-viewport { position: absolute; inset: 0; transform-origin: 0 0; }
.perim-raster {
  display: block; width: 100%; height: 100%; object-fit: contain;
  filter: invert(0.92) sepia(0.4) saturate(2.2) hue-rotate(170deg) brightness(0.95);
}
.perim-viewport .overlay { position: absolute; inset: 0; width: 100%; height: 100%; }

/* ============================================================ WALLS ===== */
/* a .workbench instance: Add/Remove then Extend, each driving which wall controls the view reveals */
#scene-walls { display: flex; flex-direction: column; overflow: hidden; }
.wall-panel-note { margin: 0 0 10px; font-size: 11.5px; line-height: 1.5; color: var(--txt-2); }
.rev-band[data-locked] { opacity: 0.45; pointer-events: none; } /* Extend, until Add/Remove is confirmed */
.rev-band[data-locked] .rev-band-count { color: var(--txt-2); }
/* the view toggles sit CENTRED in the header, independent of the title/chip either side */
.wall-head { position: relative; }
.wall-toggles { position: absolute; left: 50%; transform: translateX(-50%); display: flex; gap: 6px; }
.wall-toggle {
  font-family: var(--font-m); font-size: 11px; letter-spacing: 0.04em; color: var(--txt-2);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 7px;
  padding: 5px 11px; cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.wall-toggle:hover { color: var(--txt-0); border-color: var(--hair-strong); }
.wall-toggle.on {
  color: var(--electric); border-color: color-mix(in oklab, var(--electric) 55%, transparent);
  background: color-mix(in oklab, var(--electric) 12%, var(--ink-1));
}
/* the mm annotation lives inside its own wall's group and shows only while that wall is hovered —
   every label at once was unreadable clutter */
/* WALL DIMENSIONS — shown for every wall while the toggle is on, not one at a time on hover.
   `--wd` is the label's height set by drawDims: 80 mm at the floor's plan scale, floored at 8pt.
   The compound selector is deliberate — .ov-dim-label sets the chrome text colour further down
   this file and would otherwise win the fill on equal specificity. */
.wall-dim { paint-order: stroke; pointer-events: none; font-size: var(--wd, 8px); }
.ov-dim-label.wall-dim { fill: var(--keep); }
/* …and the one under the pointer at twice the height, so reading a particular wall does not mean
   zooming the whole drawing in to it */
.ov-wallcand .wall-dim { transition: font-size 0.12s; }
.ov-wallcand:hover .wall-dim,
.ov-wallcand.dragging .wall-dim { font-size: calc(var(--wd, 8px) * 2); }

/* DOOR WIDTHS — the same annotation on the doors stage: every disc labelled while the toggle is on,
   the one under the pointer at twice the height. Same green, same rule, same custom property. */
.door-dim { paint-order: stroke; pointer-events: none; font-size: var(--wd, 8px); }
.ov-dim-label.door-dim { fill: var(--keep); }
.door-mark .door-dim { transition: font-size 0.12s; }
.door-mark:hover .door-dim,
.door-mark.selected .door-dim { font-size: calc(var(--wd, 8px) * 2); }

.wall-mode-chip {
  margin-left: auto; flex: none; font-family: var(--font-m); font-size: 10.5px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--det); border: 1px solid color-mix(in oklab, var(--det) 45%, transparent);
  border-radius: 999px; padding: 3px 10px;
}
/* ADD/REMOVE: the crosshair for drawing, plus — on hover — the ✕ and the perpendicular move arrows.
   (The legacy add-wall tool hid every control; here each control stops the event so they coexist.)
   The END handles stay hidden: changing a wall's LENGTH belongs to the Extend panel. */
.wallstage-addremove { cursor: crosshair; }
/* THE HANDLES AND THE ARROWS ARE FETCHED FROM THE MIDDLE OF THE WALL (see WallsView.armHandles).
   A control sitting at a wall's end is exactly where the click that starts a new wall from that end
   has to land, so neither is there until the pointer has come along the wall to get it — and both go
   as soon as the wall is left, which puts the pointer back in the state where a click draws. Once up
   they stay for the whole length, so an end can be reached and taken hold of. A drag keeps them
   whatever the pointer is over.

   THE ✕ IS THE EXCEPTION and is on wherever the wall is hovered: it is the one control that does not
   have to be aimed at a particular part of the wall, and removing a wall is the thing a reviewer
   reaches for without first deciding where. */
.ov-wallcand .wall-handle,
.ov-wallcand .wall-move { opacity: 0; pointer-events: none; transition: opacity 0.12s; }
.ov-wallcand.ends .wall-handle, .ov-wallcand.dragging .wall-handle,
.ov-wallcand.ends .wall-move, .ov-wallcand.dragging .wall-move { opacity: 1; pointer-events: auto; }
/* A confirmed storey is a record, not a work surface: the walls still draw, every control goes.
   display:none rather than a pointer-events rule, so nothing can be hovered into view either. */
.wallstage-locked .disc-actions,
.wallstage-locked .wall-handles,
.wallstage-locked .wall-move { display: none; }
.wallstage-locked { cursor: default; }

/* Mid-draw: from the click that starts a wall until the one that closes it, no other wall offers a
   control. Drawing across a plan means crossing walls, and a ✕ lighting up under the pointer is one
   mis-click away from deleting the wall you were measuring against. */
.wallstage-addremove.drawing .disc-actions,
.wallstage-addremove.drawing .wall-handles { display: none; }
/* EXTEND: drag handles on hover (the base .ov-wallcand:hover rule), with no verdict controls */
.wallstage-extend .disc-actions { display: none; }

/* ============================================================ DOORS ===== */
/* a .workbench instance: per-floor DOOR LIST (rows, not thumbnails — a door is a numeric record) */
#scene-doors { display: flex; flex-direction: column; overflow: hidden; }
.door-rows { display: flex; flex-direction: column; gap: 2px; margin: 8px 0; }
.door-empty { font-size: 11px; color: var(--txt-2); padding: 4px 2px; }
.door-row {
  display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 6px;
  border: 1px solid transparent; cursor: pointer; font-size: 11.5px; flex-wrap: wrap;
}
.door-row:hover { background: var(--ink-2); }
.door-row.selected { border-color: var(--electric); background: color-mix(in oklab, var(--electric) 12%, transparent); }
.dr-ref { font-family: var(--font-m); color: var(--txt-2); min-width: 26px; }
/* the door's TYPE, and the control that corrects it — the openings row's pill, in the doors list */
.dr-kind { font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 6px;
  border-radius: 999px; border: 1px solid var(--hair); color: var(--txt-1); }
.dr-kind.editable { cursor: pointer; }
.dr-kind.editable::after { content: "▾"; margin-left: 4px; color: var(--txt-2); }
.dr-kind.editable:hover { border-color: var(--electric); color: var(--txt-0); }
/* the reviewer's own answer, not the pipeline's reading */
.dr-kind.edited { border-color: color-mix(in oklab, var(--electric) 55%, transparent); color: var(--txt-0); }
.dr-kind-pick {
  font-family: var(--font-d); font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 999px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--electric);
}
.dr-kind-pick:focus { outline: none; }
.dr-width { font-family: var(--font-m); color: var(--txt-0); margin-left: auto; }
.dr-width-input {
  margin-left: auto; width: 62px; text-align: right; font-family: var(--font-m); font-size: 11.5px;
  color: var(--txt-0); background: var(--ink-1); border: 1px solid var(--hair); border-radius: 5px; padding: 3px 5px;
}
.dr-width-input:focus { outline: none; border-color: var(--electric); }
/* an OUTSTANDING flagged door: the width wears the flag colour until it is edited or its floor confirmed */
.dr-width-input.flagged { border-color: var(--flag); }
.dr-unit { font-family: var(--font-m); font-size: 10px; color: var(--txt-2); }
.dr-flag { flex-basis: 100%; font-size: 10.5px; line-height: 1.4; color: var(--flag); }
.dr-flag.resolved { color: var(--txt-2); text-decoration: line-through; } /* dealt with */
/* A slider nothing has ruled on. It sits under the floor's doors wearing the flag colour, because
   it is the same "someone has to look at this" state — and it leaves the list the moment it is
   kept, since it is then an ordinary added door. */
.door-row.pending-slider {
  border-color: color-mix(in oklab, var(--flag) 35%, transparent);
  background: color-mix(in oklab, var(--flag) 7%, transparent);
  cursor: pointer;
}
.door-row.pending-slider .dr-ref { color: var(--flag); }
.dr-acts { display: inline-flex; gap: 4px; flex-basis: 100%; justify-content: flex-end; }
/* ?raw=1 — the pipeline's own figures. Loud on purpose: these omit the reviewer's corrections, so
   the sheet must never be mistaken for the take-off someone orders board against. */
.tk-raw {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  margin: 0 0 10px; padding: 7px 12px; border-radius: 8px; font-size: 11.5px; color: var(--flag);
  background: color-mix(in oklab, var(--flag) 10%, transparent);
  border: 1px solid color-mix(in oklab, var(--flag) 40%, transparent);
}
.tk-raw-tag {
  font-family: var(--font-m); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em;
  color: var(--ink-0); background: var(--flag); border-radius: 4px; padding: 2px 6px;
}
.tk-raw-out { margin-left: auto; color: var(--electric); text-decoration: none; }
.tk-raw-out:hover { text-decoration: underline; }

/* A write that did not land, said out loud in the pane header. It replaces the confirmed-count
   until dismissed, because a failed edit matters more than the tally it interrupts. */
.doors-failed { display: inline-flex; align-items: center; gap: 8px; color: var(--vlm); font-size: 11px; }
.doors-failed-x {
  font-family: var(--font-m); font-size: 10px; line-height: 1; cursor: pointer; color: var(--txt-2);
  background: transparent; border: 1px solid var(--hair); border-radius: 5px; padding: 2px 5px;
}
.door-actions { display: flex; align-items: center; gap: 6px; margin-bottom: 9px; }
.door-add {
  font-size: 11px; color: var(--det); background: var(--ink-1); cursor: pointer;
  border: 1px dashed color-mix(in oklab, var(--det) 50%, transparent); border-radius: 7px; padding: 5px 10px;
}
.door-add.on { border-style: solid; background: color-mix(in oklab, var(--det) 15%, var(--ink-1)); }
.door-add-kind { font-family: var(--font-m); font-size: 11px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 6px; padding: 4px 6px; }
/* the door marks on the plan — the other half of the two-way highlight */
.doors-placing { cursor: crosshair; }
/* the disc under the pointer while placing: the door that is about to exist, at its own size on
   this sheet — the doors answer to the walls tool's add-square */
.door-cursor { pointer-events: none; }
.door-cursor-dot { fill: color-mix(in oklab, var(--det) 22%, transparent); stroke: var(--det);
  stroke-width: 1.6; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }
.door-mark { cursor: pointer; }
.door-dot { fill: color-mix(in oklab, var(--det) 70%, transparent); stroke: #fff; stroke-width: 1.6;
  vector-effect: non-scaling-stroke; }
/* an unresolved flagged door reads amber on the plan, the same signal its row carries */
.door-mark.outstanding .door-dot { fill: color-mix(in oklab, var(--flag) 55%, transparent); }
/* A slider awaiting a verdict. Amber like a flagged door, but DASHED and hollower, so it never
   reads as one of the counted doors while it is still a question — it is where a door WOULD go. */
.door-mark.pending-slider .door-dot {
  fill: color-mix(in oklab, var(--flag) 18%, transparent);
  stroke: var(--flag); stroke-dasharray: 3.5 2.5;
}
.door-mark.pending-slider .door-ref { fill: var(--flag); }
/* Hovering OPENS the disc: it grows by half again, the fill fades away, the ring thins and the
   label goes with it — everything that was covering the doorway gets out of the way, leaving a
   light ring saying where to look. The grow is on an inner group whose origin is the disc centre,
   so the outer placement transform (sheet position + sheet scale) is untouched. */
.door-body { transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1); }
.door-mark:hover .door-body { transform: scale(1.5); }
.door-dot { transition: fill 0.18s ease-out, stroke-width 0.18s ease-out; }
.door-ref { transition: opacity 0.18s ease-out; }
/* highlighting swaps the disc's OUTER RING from white to blue — the fill keeps its own meaning.
   A door still carrying an unresolved flag rings AMBER instead, matching its fill; once resolved it
   returns to the blue treatment with every other door. */
.door-mark.selected .door-dot { stroke: var(--electric); stroke-width: 2.6;
  filter: drop-shadow(0 0 7px var(--electric)); }
.door-mark.outstanding.selected .door-dot { stroke: var(--flag);
  filter: drop-shadow(0 0 7px var(--flag)); }
/* the hover state comes AFTER the selected one so it thins a selected disc's ring too — same
   specificity, so order is what decides it */
.door-mark:hover .door-dot, .door-mark.outstanding:hover .door-dot,
.door-mark.selected:hover .door-dot { fill: transparent; stroke-width: 0.9; }
.door-mark:hover .door-ref { opacity: 0; }
.door-ref { fill: #fff; font-family: var(--font-m); font-weight: 700; pointer-events: none; }
/* remove-on-hover, on the drawing where the door can actually be seen */
.door-x { opacity: 0; pointer-events: none; cursor: pointer; transition: opacity 0.12s; }
.door-mark:hover .door-x { opacity: 1; pointer-events: auto; }
.door-x circle { fill: var(--vlm); stroke: #fff; stroke-width: 1.2; vector-effect: non-scaling-stroke; }
.door-x text { fill: #fff; font-weight: 700; pointer-events: none; }

/* ============================================================= STAGE ==== */
/* the single row is clamped to the scene height so tall content (the trades browser)
   scrolls INSIDE its column instead of stretching the whole stage */
#scene-stage { display: grid; grid-template-columns: var(--rail-w) 1fr var(--panel-w); grid-template-rows: minmax(0, 1fr); }

/* ---- timeline rail ---- */
.timeline {
  border-right: 1px solid var(--hair); background: var(--ink-1);
  padding: 18px 0; overflow-y: auto;
}
.tl-step {
  position: relative; display: flex; gap: 12px; align-items: flex-start;
  width: 100%; text-align: left; background: none; border: 0; color: var(--txt-2);
  padding: 10px 14px 10px 20px; transition: color 0.25s, background 0.25s;
}
.tl-step:hover { color: var(--txt-1); background: rgba(255,255,255,0.02); }
.tl-step .tl-node {
  flex: 0 0 auto; width: 11px; height: 11px; margin-top: 3px;
  border: 1.5px solid var(--txt-2); background: var(--ink-1);
  transition: all 0.3s;
}
.tl-step.k-det .tl-node { border-radius: 3px; }
.tl-step.k-vlm .tl-node { transform: rotate(45deg); border-radius: 2px; }
.tl-step .tl-label { font-size: 12.5px; font-weight: 600; letter-spacing: 0.02em; line-height: 1.25; }
.tl-step .tl-sub { display: block; font-family: var(--font-m); font-size: 9.5px; color: var(--txt-2); margin-top: 2px; letter-spacing: 0.04em; }
.tl-step::before {
  content: ""; position: absolute; left: 25px; top: 26px; bottom: -12px; width: 1.5px;
  background: var(--hair);
}
.tl-step:last-child::before { display: none; }
.tl-step.done { color: var(--txt-1); }
.tl-step.done .tl-node { background: var(--det); border-color: var(--det); box-shadow: 0 0 8px -2px var(--det); }
.tl-step.done.k-vlm .tl-node { background: var(--vlm); border-color: var(--vlm); box-shadow: 0 0 8px -2px var(--vlm); }
.tl-step.done::before { background: linear-gradient(var(--det), var(--hair)); }
.tl-step.active { color: var(--txt-0); background: rgba(255,255,255,0.035); }
.tl-step.active .tl-node { transform: scale(1.35); }
.tl-step.active.k-vlm .tl-node { transform: rotate(45deg) scale(1.35); }

/* ---- canvas ---- */
.stage { position: relative; display: flex; flex-direction: column; min-width: 0; }
.floor-tabs {
  position: absolute; top: 14px; left: 18px; z-index: 20; display: flex; gap: 6px;
}
.floor-tab {
  font-family: var(--font-m); font-size: 11px; letter-spacing: 0.08em;
  color: var(--txt-1); background: color-mix(in oklab, var(--ink-1) 88%, transparent);
  border: 1px solid var(--hair-strong); border-radius: 99px; padding: 5px 14px;
  backdrop-filter: blur(6px); transition: all 0.25s;
}
.floor-tab.active { color: var(--ink-0); background: var(--txt-0); border-color: var(--txt-0); font-weight: 600; }
.floor-tab:disabled { opacity: 0.35; cursor: default; }

/* ---- CAD tools (dims toggle · measure · draw-to-zoom · fit) ---- */
.cad-tools {
  position: absolute; top: 14px; right: 18px; z-index: 30;
  display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end;
  max-width: calc(100% - 260px); /* never slides under the floor tabs; wraps instead */
}
.cad-btn {
  font-family: var(--font-m); font-size: 11px; letter-spacing: 0.06em;
  color: var(--txt-1); background: color-mix(in oklab, var(--ink-1) 88%, transparent);
  border: 1px solid var(--hair-strong); border-radius: 99px; padding: 5px 12px;
  backdrop-filter: blur(6px); transition: all 0.25s; cursor: pointer;
}
.cad-btn:hover { color: var(--txt-0); border-color: var(--electric); }
.cad-btn.active { color: var(--ink-0); background: var(--electric); border-color: var(--electric); font-weight: 600; }
.cad-btn:disabled { opacity: 0.35; cursor: default; }
.canvas.mode-measure, .canvas.mode-zoom { cursor: crosshair; }
.zoom-marquee {
  position: fixed; z-index: 60; pointer-events: none;
  border: 1.5px dashed var(--electric);
  background: color-mix(in oklab, var(--electric) 10%, transparent);
}
/* dim annotations (mm integers, CAD dim-text style) */
.ov-dim-label {
  fill: var(--txt-0); font-family: var(--font-m); font-weight: 500;
  paint-order: stroke; stroke: rgba(9, 20, 33, 0.8); stroke-width: 2.6;
  text-anchor: middle;
}
.ov-dim-area { fill: var(--electric); font-weight: 600; }
/* pinned measurements */
.ov-measure-line { stroke: var(--flag); stroke-width: 2; fill: none; stroke-dasharray: 7 4; stroke-linejoin: round; }
.ov-measure-fill { fill: color-mix(in oklab, var(--flag) 12%, transparent); stroke: none; }
.ov-measure-dot { fill: var(--flag); stroke: #fff; stroke-width: 1; }
.ov-measure-label {
  fill: var(--flag); font-family: var(--font-m); font-weight: 600;
  paint-order: stroke; stroke: rgba(9, 20, 33, 0.85); stroke-width: 3;
}

.canvas-wrap { flex: 1; display: grid; place-items: center; padding: 26px 30px 74px; min-height: 0; }
.canvas {
  position: relative; max-width: 100%; max-height: 100%;
  /* the wheel zooms the camera here, so no travel may leak out to the page or to the browser's own
     back-swipe once the camera is against a stop */
  overscroll-behavior: contain;
  aspect-ratio: 1191 / 842;
  width: min(100%, calc((100vh - var(--top-h) - 120px) * 1191 / 842));
  border-radius: 6px; overflow: hidden;
  background: var(--blueprint);
  box-shadow:
    0 0 0 1px var(--hair-strong),
    0 30px 80px -30px rgba(0, 0, 0, 0.8),
    0 0 120px -40px color-mix(in oklab, var(--det) 30%, transparent);
  transition: background-color 2.2s ease;
}
/* the develop trick: same raster twice — CAD paper always solid below, the
   blueprint-inverted copy above fades away as the pipeline confirms the drawing. */
.raster {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  transition: opacity 2.2s ease, filter 2.2s ease;
}
.raster-blue {
  /* quadratic fade: the blueprint clears quickly once the pipeline starts confirming,
     avoiding the muddy 50/50 crossfade band */
  opacity: calc((1 - var(--dev, 0)) * (1 - var(--dev, 0)));
  filter: invert(0.93) sepia(0.45) saturate(2.8) hue-rotate(172deg) brightness(0.82) contrast(0.95);
}
.canvas.developed { background: var(--paper); }
/* the camera: zoom/pan by transforming the whole raster+overlay stack */
.viewport {
  position: absolute; inset: 0; transform-origin: 0 0;
  transition: transform 2.9s cubic-bezier(0.55, 0, 0.15, 1);
}
.overlay { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 10; }

/* ---- overlay svg vocabulary (classes used by js/steps.js) ---- */
.ov-dimline { stroke: var(--dim); stroke-width: 1.2; opacity: 0.85; }
.ov-token { fill: var(--dim); font-family: var(--font-m); }
.ov-halo { fill: none; stroke: var(--electric); stroke-width: 2; opacity: 0; filter: drop-shadow(0 0 6px var(--electric)); }
.ov-envelope { stroke: var(--electric); fill: none; stroke-width: 2.4; filter: drop-shadow(0 0 6px color-mix(in oklab, var(--electric) 60%, transparent)); }
.ov-gridline { stroke: var(--electric); stroke-width: 0.8; opacity: 0.4; stroke-dasharray: 6 5; }
.ov-perim { stroke: var(--electric); fill: color-mix(in oklab, var(--electric) 8%, transparent); stroke-width: 4; stroke-linejoin: round; filter: drop-shadow(0 0 8px color-mix(in oklab, var(--electric) 55%, transparent)); }
.ov-perim.final { stroke: var(--keep); fill: color-mix(in oklab, var(--keep) 7%, transparent); filter: drop-shadow(0 0 8px color-mix(in oklab, var(--keep) 55%, transparent)); }
/* 80%: a wall is evidence drawn OVER the plan, so the faces and poché it was matched from have to
   stay readable underneath it. Owned here, not by the reveal animation — see WallsView.fadeIn. */
.ov-wall { stroke: var(--electric); stroke-width: var(--wall-pt); stroke-linecap: butt; opacity: 0.8; }
.ov-wall.vlm { stroke: var(--vlm); }
.ov-wall.kept { stroke: var(--electric); filter: drop-shadow(0 0 5px color-mix(in oklab, var(--electric) 60%, transparent)); }
.ov-wall.rejected { stroke: var(--txt-2); }
.ov-wall.review { stroke: var(--flag); }
/* hover keep/reject controls (Internal walls review) — mirror the doors discs */
.ov-wall-hit { stroke: transparent; stroke-width: 16; stroke-linecap: round; pointer-events: stroke; }
.ov-wallcand.rejected .ov-wall { opacity: 0.2; } /* fade the wall, not its hover controls */
.ov-wallcand .disc-actions { opacity: 0; pointer-events: none; transition: opacity 0.14s; }
.ov-wallcand:hover .disc-actions { opacity: 1; pointer-events: auto; }
/* drag handles at each wall end (hover to reveal; drag to resize, orientation kept) */
.ov-wallcand .wall-handles { opacity: 0; pointer-events: none; transition: opacity 0.14s; }
.ov-wallcand:hover .wall-handles, .ov-wallcand.dragging .wall-handles { opacity: 1; pointer-events: auto; }
.ov-wallcand:not(.kept) .wall-handles { display: none; } /* endpoints only on kept walls */
/* while the add-wall tool is active, no hover controls (handles or keep/reject) */
.canvas.mode-wall .wall-handles, .canvas.mode-wall .disc-actions { display: none; }
.wall-handle { fill: var(--electric); stroke: #fff; stroke-width: 1.4; vector-effect: non-scaling-stroke; filter: drop-shadow(0 0 4px color-mix(in oklab, var(--electric) 70%, transparent)); }
.wall-handle.v { cursor: ns-resize; }
.wall-handle.h { cursor: ew-resize; }
/* perpendicular move arrows at the wall centre: click nudges 10 mm, drag moves the whole wall sideways */
.wall-move-hit { fill: transparent; }
.wall-move-tri { fill: var(--electric); stroke: #fff; stroke-width: 1.1; vector-effect: non-scaling-stroke; pointer-events: none; filter: drop-shadow(0 0 4px color-mix(in oklab, var(--electric) 70%, transparent)); }
.wall-move.h { cursor: ew-resize; } /* V wall moves horizontally */
.wall-move.v { cursor: ns-resize; } /* H wall moves vertically */
/* add-wall tool: blue square under the pointer + the horizontal/vertical preview */
.wall-add { pointer-events: none; }
.wall-add-cursor { fill: color-mix(in oklab, var(--electric) 35%, transparent); stroke: var(--electric); stroke-width: 1.6; vector-effect: non-scaling-stroke; }
.wall-preview { stroke: var(--electric); stroke-width: 5; stroke-linecap: round; opacity: 0.85; stroke-dasharray: 9 6; }
.wall-preview-label { fill: var(--electric); font-family: var(--font-m); font-weight: 700; paint-order: stroke; stroke: rgba(9, 20, 33, 0.85); stroke-width: 3.2; }
.ov-door-dot { fill: var(--electric); stroke: #fff; stroke-width: 1.2; opacity: 0.8; }
.ov-disc circle { fill: var(--vlm); stroke: #fff; stroke-width: 1.4; }
.ov-disc text { fill: #fff; font-family: var(--font-m); font-weight: 600; text-anchor: middle; }
/* 80%, same reason as the walls: the swing arc / cavity gap the disc sits on is the evidence for
   it. On .pop-target so the hover keep/reject controls stay fully opaque (as .rejected does). */
.ov-disc.kept .pop-target { opacity: 0.8; }
.ov-disc.kept circle { fill: var(--electric); }
.ov-disc.pending circle { fill: var(--flag); }
.ov-disc.rejected .pop-target { opacity: 0.25; } /* fade the disc body, not its hover actions */
.ov-disc.rejected circle { fill: var(--txt-2); }
/* hover keep/reject controls (Internal doors review) */
.disc-hit { fill: transparent; pointer-events: all; }
.ov-disc .disc-actions { opacity: 0; pointer-events: none; transition: opacity 0.14s; }
.ov-disc:hover .disc-actions { opacity: 1; pointer-events: auto; }
.disc-act { cursor: pointer; }
.disc-act circle { stroke: #fff; stroke-width: 1.3; }
.disc-act.act-keep circle { fill: var(--keep); }
.disc-act.act-reject circle { fill: var(--vlm); }
.disc-act text { fill: #fff; font-weight: 700; text-anchor: middle; pointer-events: none; }
.disc-act:hover circle { filter: drop-shadow(0 0 5px currentColor); }
.ov-corner { fill: var(--electric); stroke: #fff; stroke-width: 1.5; filter: drop-shadow(0 0 8px var(--electric)); }
/* the aspect label sits INSIDE the diamond — white on the electric fill, no halo needed */
.ov-corner-label { fill: #fff; font-family: var(--font-m); font-weight: 700; letter-spacing: -0.02em; }
.ov-ladder circle { fill: none; stroke: var(--vlm); stroke-width: 1.5; opacity: 0.8; transition: stroke 0.6s, opacity 0.6s; }
.ov-ladder circle.rejected { stroke: var(--txt-2); opacity: 0.3; }
.ov-callout { fill: var(--txt-0); font-family: var(--font-m); font-weight: 500; paint-order: stroke; stroke: rgba(9, 20, 33, 0.85); stroke-width: 3.5; }
.ov-band { fill: color-mix(in oklab, var(--flag) 16%, transparent); stroke: var(--flag); stroke-width: 1; stroke-dasharray: 5 4; }
@keyframes halo-pulse { 0%, 100% { opacity: 0; } 45% { opacity: 0.9; } }

@keyframes disc-pop { from { transform: scale(0); } 60% { transform: scale(1.25); } to { transform: scale(1); } }
@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes fade-out { to { opacity: 0.18; } }
@keyframes glow-once { 0% { filter: none; } 40% { filter: drop-shadow(0 0 10px currentColor); } 100% { filter: none; } }

/* perimeter walk: per-side half-traces grow from the cornerstones and meet mid-edge;
   the fill arrives once the ring is closed */
.ov-perim-trace {
  stroke: var(--electric); fill: none; stroke-width: 4;
  stroke-linejoin: round; stroke-linecap: round;
  filter: drop-shadow(0 0 8px color-mix(in oklab, var(--electric) 55%, transparent));
}
.ov-perim-fill { fill: color-mix(in oklab, var(--electric) 8%, transparent); stroke: none; }

/* ---- trades browser (post-takeoff: trade packs, searchable) ---- */
.trades-browser { flex: 1; min-height: 0; overflow-y: auto; padding: 22px 34px 90px; }
.tb-inner { max-width: 900px; margin: 0 auto; }
.tb-head { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; }
.tb-search {
  flex: 1; font-family: var(--font-m); font-size: 13px; color: var(--txt-0);
  background: var(--ink-2); border: 1px solid var(--hair-strong); border-radius: 9px;
  padding: 10px 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s;
}
.tb-search::placeholder { color: var(--txt-2); }
.tb-search:focus { border-color: var(--electric); box-shadow: 0 0 0 3px color-mix(in oklab, var(--electric) 22%, transparent); }
.tb-count { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); white-space: nowrap; }
.tb-list { display: flex; flex-direction: column; gap: 10px; }
.tb-trade {
  border: 1px solid var(--hair); border-radius: 10px; background: var(--ink-1);
  overflow: hidden; transition: border-color 0.25s;
}
.tb-trade[open] { border-color: var(--hair-strong); }
.tb-sum {
  display: flex; align-items: center; gap: 12px; padding: 12px 16px;
  cursor: pointer; list-style: none; user-select: none;
}
.tb-sum::-webkit-details-marker { display: none; }
.tb-sum::after {
  content: "›"; margin-left: auto; color: var(--txt-2); font-size: 17px;
  transition: transform 0.25s; transform: rotate(0deg);
}
.tb-trade[open] .tb-sum::after { transform: rotate(90deg); color: var(--electric); }
.tb-sum:hover { background: rgba(255, 255, 255, 0.025); }
.tb-name { font-weight: 700; font-size: 14px; letter-spacing: 0.01em; }
.tb-chips { display: flex; gap: 6px; }
.tb-chip {
  font-family: var(--font-m); font-size: 10px; letter-spacing: 0.05em;
  color: var(--txt-1); border: 1px solid var(--hair-strong); border-radius: 99px;
  padding: 2px 9px; white-space: nowrap;
}
.tb-chip.primary { color: var(--electric); border-color: color-mix(in oklab, var(--electric) 45%, transparent); }
.tb-chip.match { color: var(--flag); border-color: color-mix(in oklab, var(--flag) 45%, transparent); }
.tb-md { border-top: 1px solid var(--hair); padding: 6px 18px 18px; }
.tb-actions { display: flex; gap: 8px; justify-content: flex-end; padding: 12px 0 2px; }
.tb-btn {
  font-family: var(--font-m); font-size: 11px; letter-spacing: 0.05em;
  color: var(--txt-1); background: var(--ink-2);
  border: 1px solid var(--hair-strong); border-radius: 7px; padding: 6px 14px;
  transition: color 0.2s, border-color 0.2s;
}
.tb-btn:hover { color: var(--txt-0); border-color: var(--electric); }
.tb-md .md-h1 { display: none; } /* the accordion header already names the trade */
.tb-md .md-h2 {
  font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--txt-2); margin: 18px 0 8px; font-weight: 700;
}
.tb-md .md-note { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); margin: 10px 0 4px; }
.tb-md .md-p { font-size: 12.5px; color: var(--txt-1); line-height: 1.55; }
.tb-md .md-ul { margin: 6px 0; padding-left: 18px; }
.tb-md .md-ul li { font-size: 12.5px; color: var(--txt-1); line-height: 1.55; margin-bottom: 6px; }
.md-table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 6px 0 4px; }
.md-table th {
  text-align: left; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--txt-2); font-weight: 600; padding: 6px 8px; border-bottom: 1px solid var(--hair-strong);
}
.md-table td {
  padding: 7px 8px; border-bottom: 1px solid var(--hair); color: var(--txt-1);
  vertical-align: top; line-height: 1.5;
}
.md-table td:first-child { color: var(--txt-0); font-weight: 600; }
.md-table .md-detail { width: 48%; min-width: 240px; }
.md-table code { font-family: var(--font-m); font-size: 11px; color: var(--det); }
mark { background: color-mix(in oklab, var(--electric) 35%, transparent); color: var(--txt-0); border-radius: 2px; padding: 0 1px; }

/* ---- transport ---- */
.transport {
  position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 20;
  display: flex; gap: 8px; align-items: center;
  background: color-mix(in oklab, var(--ink-1) 90%, transparent);
  border: 1px solid var(--hair-strong); border-radius: 99px; padding: 6px 10px;
  backdrop-filter: blur(8px);
}
.t-btn {
  width: 34px; height: 34px; border-radius: 50%; border: 0;
  background: transparent; color: var(--txt-1); font-size: 17px;
  display: grid; place-items: center; transition: all 0.2s;
}
.t-btn:hover { background: rgba(255,255,255,0.07); color: var(--txt-0); }
.t-play { background: var(--txt-0); color: var(--ink-0); font-size: 13px; }
.t-play:hover { background: #fff; color: var(--ink-0); }
.t-play.playing { background: var(--vlm); color: #fff; }

/* ---- right panel ---- */
.panel {
  border-left: 1px solid var(--hair); background: var(--ink-1);
  overflow-y: auto; padding: 22px 22px 40px;
}
.p-kicker {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-m); font-size: 10px; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--txt-2); margin-bottom: 6px;
}
.p-kicker .k-badge {
  padding: 2px 8px; border-radius: 99px; letter-spacing: 0.1em; font-weight: 600;
}
.p-kicker .k-badge.det { color: var(--det); border: 1px solid color-mix(in oklab, var(--det) 45%, transparent); }
.p-kicker .k-badge.vlm { color: var(--vlm); border: 1px solid color-mix(in oklab, var(--vlm) 45%, transparent); }
.p-title { margin: 0 0 10px; font-size: 21px; font-weight: 800; font-stretch: 112%; letter-spacing: -0.01em; }
.p-narrative { color: var(--txt-1); font-size: 13px; line-height: 1.55; margin: 0 0 18px; }
.p-narrative strong { color: var(--txt-0); }

.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 18px; }
.stat {
  border: 1px solid var(--hair); border-radius: 8px; padding: 10px 12px;
  background: var(--ink-2);
}
.stat.wide { grid-column: 1 / -1; }
.stat .s-label { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--txt-2); margin-bottom: 4px; }
.stat .s-value { font-family: var(--font-m); font-size: 17px; font-weight: 600; color: var(--txt-0); }
.stat .s-value .u { font-size: 11px; color: var(--txt-2); margin-left: 3px; }
.stat .s-value.accent { color: var(--det); }
.stat .s-value.good { color: var(--keep); }
.stat .s-value.warn { color: var(--flag); }

.p-flags { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.flag-item {
  display: flex; gap: 9px; align-items: flex-start;
  border: 1px solid color-mix(in oklab, var(--flag) 30%, transparent);
  background: color-mix(in oklab, var(--flag) 7%, transparent);
  border-radius: 8px; padding: 9px 11px;
  font-size: 12px; line-height: 1.45; color: color-mix(in oklab, var(--flag) 75%, var(--txt-0));
}
.flag-item::before { content: "⚑"; flex: 0 0 auto; font-size: 12px; }

.p-method {
  border-top: 1px solid var(--hair); padding-top: 12px;
  font-family: var(--font-m); font-size: 11px; line-height: 1.6; color: var(--txt-2);
}
.p-method .m-label { letter-spacing: 0.16em; text-transform: uppercase; font-size: 9.5px; margin-bottom: 5px; color: var(--txt-2); }

.p-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 16px; }
.p-table th { text-align: left; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--txt-2); font-weight: 600; padding: 5px 6px; border-bottom: 1px solid var(--hair-strong); }
.p-table td { padding: 5px 6px; border-bottom: 1px solid var(--hair); color: var(--txt-1); font-family: var(--font-m); font-size: 11.5px; }
.p-table td:last-child { text-align: right; color: var(--txt-0); }
.p-table tr.total td { color: var(--keep); font-weight: 600; border-top: 2px solid var(--hair-strong); }

/* ---- takeoff: raw <-> reviewed. The panel shows the REVIEWED totals by default: that is the
   figure someone orders board against, so it must be the headline, not a footnote. ---- */
.tk-toggle { display: flex; gap: 0; margin: 0 0 14px; border: 1px solid var(--hair-strong);
  border-radius: 3px; overflow: hidden; width: fit-content; }
.tk-mode { appearance: none; border: 0; background: transparent; cursor: pointer;
  font-family: var(--font-m); font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--txt-2); padding: 6px 12px; transition: background 120ms, color 120ms; }
.tk-mode + .tk-mode { border-left: 1px solid var(--hair-strong); }
.tk-mode:hover { color: var(--txt-0); }
.tk-mode.active { background: color-mix(in oklab, var(--keep) 18%, transparent); color: var(--keep);
  font-weight: 600; }
.tk-mode-note { font-size: 10.5px; color: var(--txt-2); margin: -8px 0 16px; }
.tk-mode-note.raw { color: var(--flag); }
.tk-review-flag { font-size: 11px; color: var(--flag); margin: 0 0 8px; }
/* The reviewer has edits that could NOT be folded in. Loud on purpose: a silent fallback to raw
   figures is how someone orders board against the wrong number. */
.tk-review-error { margin: 0 0 14px; padding: 10px 12px; border: 1px solid var(--flag);
  border-left: 3px solid var(--flag); border-radius: 3px;
  background: color-mix(in oklab, var(--flag) 10%, transparent); }
.tk-review-error-head { font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--flag); font-weight: 700; margin-bottom: 5px; }
.tk-review-error-body { font-size: 11.5px; color: var(--txt-1); line-height: 1.45; }

/* ---- external openings: traced frames + focus dimensions (overlay) ---- */
.ov-oband, .ov-opening-frame, .ov-focus { pointer-events: none; }
.ov-oband rect { fill: none; stroke: var(--hair-strong); stroke-width: 0.8; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; }
.ov-oband-label { fill: var(--txt-2); font-family: var(--font-m); letter-spacing: 0.04em; }
.ov-opening-frame polygon {
  fill: color-mix(in oklab, var(--keep) 9%, transparent);
  stroke: var(--keep); stroke-width: 1.6; stroke-linejoin: round;
}
.ov-opening-frame.review polygon { fill: color-mix(in oklab, var(--flag) 12%, transparent); stroke: var(--flag); }
.ov-opening-frame.dim { opacity: 0.25; transition: opacity 0.4s; }
.ov-opening-frame.focused polygon {
  fill: color-mix(in oklab, var(--electric) 14%, transparent);
  stroke: var(--electric); stroke-width: 2.4;
  filter: drop-shadow(0 0 5px color-mix(in oklab, var(--electric) 65%, transparent));
}
/* the ref, in the frame's own colour, set outside the frame's top-left corner — up and to the left,
   so it clears both the frame and the dimension chain that runs above every elevation. The tie
   between a drawn outline and its row when a schedule holds twenty near-identical elevations. */
.ov-open-ref { fill: var(--keep); font-family: var(--font-m); font-weight: 600; letter-spacing: 0.04em;
  text-anchor: end;
  paint-order: stroke; stroke: var(--ink-0); stroke-width: 0.3em; stroke-linejoin: round;
  stroke-opacity: 0.85;
  pointer-events: none; }
.ov-opening-frame.review .ov-open-ref { fill: var(--flag); }
.ov-opening-frame.focused .ov-open-ref { fill: var(--electric); }
.ov-dim-line { stroke: var(--electric); stroke-width: 1.2; vector-effect: non-scaling-stroke; opacity: 0.85; }
/* focused opening: drag its traced frame to the true shape. A rectangle gets corner + side squares
   (the wall step's idiom); any other traced shape gets a circle per vertex (the gable step's). The
   frame layers are pointer-events:none, so the handles have to opt back in. */
.ov-open-handle {
  fill: var(--electric); stroke: #fff; stroke-width: 1.4; pointer-events: all;
  vector-effect: non-scaling-stroke;
  filter: drop-shadow(0 0 4px color-mix(in oklab, var(--electric) 70%, transparent));
}
.ov-open-handle.nw, .ov-open-handle.se { cursor: nwse-resize; }
.ov-open-handle.ne, .ov-open-handle.sw { cursor: nesw-resize; }
.ov-open-handle.n, .ov-open-handle.s { cursor: ns-resize; }
.ov-open-handle.e, .ov-open-handle.w { cursor: ew-resize; }
.ov-open-handle.pt { cursor: grab; }
.ov-open-handle.pt.dragging { cursor: grabbing; }
/* while a canvas tool owns the pointer, the handles get out of its way (as the wall ones do) */
.canvas.mode-measure .ov-open-handles, .canvas.mode-zoom .ov-open-handles { display: none; }

/* ---- openings review panel (classify · adjust · confirm) ---- */
.op-review { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.op-review:empty { display: none; }
.op-review-head {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-m); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--flag); font-weight: 600;
}
.op-review-icon { font-size: 13px; }
.op-review-item {
  display: flex; align-items: center; gap: 10px;
  border: 1px solid color-mix(in oklab, var(--flag) 34%, transparent);
  background: color-mix(in oklab, var(--flag) 8%, transparent);
  border-radius: 8px; padding: 9px 11px;
}
.op-review-txt { font-size: 12px; line-height: 1.45; color: color-mix(in oklab, var(--flag) 78%, var(--txt-0)); }
.op-review-txt strong { color: var(--txt-0); font-family: var(--font-m); }
.op-review-go {
  flex: 0 0 auto; margin-left: auto; white-space: nowrap;
  font-family: var(--font-m); font-size: 11px; color: var(--flag);
  background: transparent; border: 1px solid color-mix(in oklab, var(--flag) 45%, transparent);
  border-radius: 99px; padding: 4px 11px; transition: all 0.2s;
}
.op-review-go:hover { color: var(--ink-0); background: var(--flag); border-color: var(--flag); }

.op-list { margin-bottom: 16px; }
.op-group { margin-bottom: 12px; }
.op-group-name.current { color: var(--electric); } /* the sheet currently shown on the canvas */
.op-group-head {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 4px 2px 6px; border-bottom: 1px solid var(--hair-strong); margin-bottom: 4px;
}
.op-group-name { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--txt-2); font-weight: 600; }
.op-group-sub { font-family: var(--font-m); font-size: 11px; color: var(--txt-1); }

.op-row { border-bottom: 1px solid var(--hair); }
.op-row.focused { background: color-mix(in oklab, var(--electric) 8%, transparent); border-radius: 6px; }
.op-row-main {
  display: flex; align-items: center; gap: 9px; width: 100%;
  background: transparent; border: 0; padding: 7px 6px; text-align: left; color: var(--txt-1);
}
.op-row-main:hover { color: var(--txt-0); }
.op-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--hair-strong); flex: 0 0 auto; }
.op-dot.mod { background: var(--electric); box-shadow: 0 0 6px var(--electric); }
.op-dot.resolved { background: var(--keep); box-shadow: 0 0 6px var(--keep); }
.op-ref { font-family: var(--font-m); font-size: 12px; color: var(--txt-0); min-width: 30px; }
.op-kind {
  font-family: var(--font-m); font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 2px 7px; border-radius: 99px; border: 1px solid var(--hair-strong); color: var(--txt-2);
}
/* UNTYPED, and saying so quietly: an opening nobody has classified is not a warning, it is a
   question for the reviewer — so it wears the muted ink rather than the flag colour. */
.op-kind.k-opening, .orw-kind.k-opening { color: var(--dim); border-color: color-mix(in oklab, var(--dim) 45%, transparent); }
.op-kind.k-window { color: var(--electric); border-color: color-mix(in oklab, var(--electric) 45%, transparent); }
.op-kind.k-door { color: var(--keep); border-color: color-mix(in oklab, var(--keep) 45%, transparent); }
.op-kind.k-garage_door { color: var(--flag); border-color: color-mix(in oklab, var(--flag) 45%, transparent); }
.op-dim { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
.op-m2 { margin-left: auto; font-family: var(--font-m); font-size: 12px; color: var(--txt-0); }
.op-badge {
  font-family: var(--font-m); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--flag); border: 1px solid color-mix(in oklab, var(--flag) 45%, transparent);
  border-radius: 99px; padding: 1px 6px;
}
.op-row.is-review .op-ref { color: var(--flag); }

.op-editor { display: flex; flex-direction: column; gap: 10px; padding: 4px 6px 12px; }
.op-seg, .op-dims-edit, .op-actions { display: flex; align-items: center; gap: 8px; }
.op-seg-label { font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--txt-2); min-width: 34px; }
.op-seg-btn {
  font-family: var(--font-m); font-size: 11px; color: var(--txt-1);
  background: var(--ink-2); border: 1px solid var(--hair-strong); border-radius: 6px; padding: 4px 10px;
  transition: all 0.2s;
}
.op-seg-btn:hover { color: var(--txt-0); border-color: var(--electric); }
.op-seg-btn.active { color: var(--ink-0); background: var(--electric); border-color: var(--electric); font-weight: 600; }
.op-num {
  width: 62px; font-family: var(--font-m); font-size: 12px; color: var(--txt-0);
  background: var(--ink-2); border: 1px solid var(--hair-strong); border-radius: 6px; padding: 4px 7px;
}
.op-num:focus { outline: none; border-color: var(--electric); box-shadow: 0 0 0 2px color-mix(in oklab, var(--electric) 22%, transparent); }
.op-num:disabled { opacity: 0.5; }
.op-x, .op-unit { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
.op-area-out { margin-left: auto; font-family: var(--font-m); font-size: 12px; color: var(--keep); }
.op-confirm, .op-reset {
  font-family: var(--font-m); font-size: 11px; border-radius: 99px; padding: 5px 13px; transition: all 0.2s;
}
.op-confirm { color: var(--keep); background: transparent; border: 1px solid color-mix(in oklab, var(--keep) 50%, transparent); }
.op-confirm:hover { color: var(--ink-0); background: var(--keep); border-color: var(--keep); }
.op-reset { color: var(--txt-1); background: transparent; border: 1px solid var(--hair-strong); }
.op-reset:hover { color: var(--txt-0); border-color: var(--electric); }
.op-hint { font-size: 11px; color: var(--txt-2); font-style: italic; }

.op-note {
  border-top: 1px solid var(--hair); padding-top: 12px; margin-bottom: 16px;
  font-size: 11.5px; line-height: 1.5; color: var(--txt-2);
}

/* ---- ceiling: gable-end triangles (overlay) ---- */
.ov-gable, .ov-gable * { pointer-events: none; }
.ov-gable polygon {
  fill: color-mix(in oklab, var(--flag) 12%, transparent);
  stroke: var(--flag); stroke-width: 1.8; stroke-linejoin: round;
}
.ov-gable.dim { opacity: 0.25; transition: opacity 0.4s; }
.ov-gable.kept polygon { fill: color-mix(in oklab, var(--keep) 12%, transparent); stroke: var(--keep); }
.ov-gable.removed { opacity: 0.2; }
.ov-gable.removed polygon { fill: none; stroke: var(--txt-2); stroke-dasharray: 5 4; }
/* a flat-ceiling (attic) gable: a clear grey dashed outline + no ceiling-angle label — UNTIL it is
   kept, at which point .kept repaints it solid (setState flips the class off .flat-attic:not(.kept)). */
.ov-gable.flat-attic:not(.kept) { opacity: 0.9; }
.ov-gable.flat-attic:not(.kept) .ov-gable-poly { fill: none; stroke: var(--txt-2); stroke-width: 2; stroke-dasharray: 6 4; }
.ov-gable.flat-attic:not(.kept) .ov-gable-lbl { display: none; }
.ov-gable.focused polygon {
  fill: color-mix(in oklab, var(--electric) 14%, transparent);
  stroke: var(--electric); stroke-width: 2.4;
  filter: drop-shadow(0 0 5px color-mix(in oklab, var(--electric) 65%, transparent));
}
/* the storey height it READ, ringed in green; the two sloped ceiling lines dimensioned in blue */
.ov-height-read circle { fill: none; stroke: var(--keep); stroke-width: 2.5; filter: drop-shadow(0 0 5px color-mix(in oklab, var(--keep) 55%, transparent)); }
.ov-dim-slope { fill: var(--electric); font-family: var(--font-m); font-weight: 700; paint-order: stroke; stroke: rgba(9, 20, 33, 0.85); stroke-width: 3.2; }
/* manually-drawn ceiling areas (the "Add ceiling area" tool) */
.ov-added, .ov-added * { pointer-events: none; }
.ov-added polygon { fill: color-mix(in oklab, var(--electric) 13%, transparent); stroke: var(--electric); stroke-width: 1.8; stroke-linejoin: round; }
.ov-added.removed { opacity: 0.25; }
.ov-added.removed polygon { fill: none; stroke: var(--txt-2); stroke-dasharray: 5 4; }

/* ---- ceiling review panel ---- */
.cl-source { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
.cl-src-label { font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--txt-2); }
.cl-src-val {
  font-family: var(--font-m); font-size: 11px; letter-spacing: 0.06em;
  padding: 2px 9px; border-radius: 99px; border: 1px solid var(--hair-strong); color: var(--txt-1);
}
.cl-src-val.vec { color: var(--keep); border-color: color-mix(in oklab, var(--keep) 45%, transparent); }
.cl-src-val.ocr { color: var(--electric); border-color: color-mix(in oklab, var(--electric) 45%, transparent); }

.cl-storey { border: 1px solid var(--hair); border-radius: 8px; background: var(--ink-2); padding: 10px 12px; margin-bottom: 10px; }
.cl-storey-head { display: flex; align-items: baseline; gap: 8px; }
.cl-storey-name { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--txt-2); font-weight: 600; }
.cl-storey-h { margin-left: auto; font-family: var(--font-m); font-size: 18px; font-weight: 600; color: var(--txt-0); }
.cl-storey-h.good { color: var(--keep); }
.cl-storey-h.warn { color: var(--flag); }
.cl-storey-h .u { font-size: 11px; color: var(--txt-2); margin-left: 3px; }
.cl-tag { font-family: var(--font-m); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; padding: 1px 6px; border-radius: 99px; }
.cl-tag.warn { color: var(--flag); border: 1px solid color-mix(in oklab, var(--flag) 45%, transparent); }
.cl-reads { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.cl-read { font-family: var(--font-m); font-size: 10.5px; color: var(--txt-1); background: color-mix(in oklab, var(--keep) 9%, transparent); border-radius: 4px; padding: 2px 7px; }

.cl-gables { margin: 4px 0 16px; }
.cl-gables-head { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--flag); font-weight: 600; margin: 8px 0 6px; }
.cl-group-head { display: flex; align-items: baseline; justify-content: space-between; padding: 4px 2px 5px; border-bottom: 1px solid var(--hair-strong); margin: 8px 0 4px; }
.cl-group-name { font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--txt-2); font-weight: 600; }
.cl-group-name.current { color: var(--electric); }
.cl-group-sub { font-family: var(--font-m); font-size: 11px; color: var(--txt-1); }
.cl-gable { border-bottom: 1px solid var(--hair); }
.cl-gable.focused { background: color-mix(in oklab, var(--electric) 8%, transparent); border-radius: 6px; }
.cl-gable-main { display: flex; align-items: center; gap: 9px; width: 100%; background: transparent; border: 0; padding: 7px 6px; text-align: left; color: var(--txt-1); }
.cl-gable-main:hover { color: var(--txt-0); }
.cl-gable-sec { font-family: var(--font-m); font-size: 12px; color: var(--txt-0); min-width: 34px; }
.cl-gable.is-review .cl-gable-sec { color: var(--flag); }
.cl-gable-dim { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
.cl-gable-area { margin-left: auto; font-family: var(--font-m); font-size: 12px; color: var(--txt-0); }
.cl-gable-card { display: flex; flex-direction: column; gap: 8px; padding: 4px 8px 12px; }
.cl-q { font-size: 12px; line-height: 1.5; color: var(--txt-1); }
.cl-q-label { font-family: var(--font-m); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--txt-2); margin-right: 8px; }

/* the three key figures at the top of the ceiling panel */
.cl-key { border: 1px solid var(--hair); border-radius: 8px; background: var(--ink-2); padding: 11px 13px; margin-bottom: 10px; }
.cl-key-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.cl-key-label { font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--txt-2); font-weight: 600; }
.cl-key > .cl-key-label { display: block; margin-bottom: 6px; }
.cl-key-val { font-family: var(--font-m); font-size: 20px; font-weight: 600; color: var(--txt-0); }
.cl-key-val.good { color: var(--keep); }
.cl-key-val.warn { color: var(--flag); }
.cl-key-val .u { font-size: 11px; color: var(--txt-2); margin-left: 3px; }
.cl-review {
  margin-top: 8px; padding: 7px 10px; border-radius: 6px; font-size: 11px; line-height: 1.4;
  color: color-mix(in oklab, var(--flag) 80%, var(--txt-0));
  background: color-mix(in oklab, var(--flag) 9%, transparent);
  border: 1px solid color-mix(in oklab, var(--flag) 32%, transparent);
}
.cl-key-note { font-size: 10.5px; color: var(--txt-2); margin-top: 5px; line-height: 1.4; }
.cl-key-row { display: flex; align-items: center; gap: 7px; }
.cl-pct { width: 54px; font-family: var(--font-m); font-size: 15px; font-weight: 600; color: var(--txt-0); background: var(--ink-1); border: 1px solid var(--hair-strong); border-radius: 6px; padding: 4px 8px; text-align: right; }
.cl-pct:focus { outline: none; border-color: var(--electric); box-shadow: 0 0 0 2px color-mix(in oklab, var(--electric) 22%, transparent); }
.cl-pct-sign { font-family: var(--font-m); font-size: 14px; color: var(--txt-2); margin-right: 4px; }
/* ceiling-lining thickness split (13 mm / 10 mm) */
.cl-lining-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
.cl-lining-th { font-family: var(--font-m); font-size: 12px; color: var(--txt-1); min-width: 46px; }
.cl-lining-in { width: 100px; font-family: var(--font-m); font-size: 15px; font-weight: 600; color: var(--txt-0); background: var(--ink-1); border: 1px solid var(--hair-strong); border-radius: 6px; padding: 4px 8px; text-align: right; }
.cl-lining-in:focus { outline: none; border-color: var(--electric); box-shadow: 0 0 0 2px color-mix(in oklab, var(--electric) 22%, transparent); }
.cl-lining-in.stale { border-color: var(--flag); color: var(--flag); background: color-mix(in oklab, var(--flag) 8%, var(--ink-1)); }
.cl-lining-unit { font-family: var(--font-m); font-size: 11px; color: var(--txt-2); }
.cl-key-note.warn { color: var(--flag); }

/* gable Keep / Remove + area override */
.cl-gable-edit { display: flex; align-items: center; gap: 8px; }
.cl-gable.is-removed .cl-gable-sec { color: var(--txt-2); text-decoration: line-through; }
.cl-gable-area.removed { color: var(--txt-2); text-decoration: line-through; }
.op-dot.removed { background: var(--txt-2); box-shadow: none; }
.cl-keep, .cl-remove { font-family: var(--font-m); font-size: 11px; border-radius: 99px; padding: 5px 13px; background: transparent; transition: all 0.2s; }
.cl-keep { color: var(--keep); border: 1px solid color-mix(in oklab, var(--keep) 50%, transparent); }
.cl-keep:hover, .cl-keep.active { color: var(--ink-0); background: var(--keep); border-color: var(--keep); font-weight: 600; }
.cl-remove { color: var(--vlm); border: 1px solid color-mix(in oklab, var(--vlm) 50%, transparent); }
.cl-remove:hover, .cl-remove.active { color: #fff; background: var(--vlm); border-color: var(--vlm); font-weight: 600; }
.cl-keep, .cl-remove { padding: 4px 10px; }
/* added-area rows: inline sqm edit + keep/remove, no expand */
.cl-added-row .cl-gable-main { cursor: default; flex-wrap: wrap; }
.cl-added-row .cl-gable-edit { margin-left: auto; }
.cl-added-hint { font-size: 11px; color: var(--txt-2); line-height: 1.45; margin: 8px 2px 0; }
/* rename an added area (pencil ↔ text input) */
.cl-added-name { display: inline-flex; align-items: center; gap: 4px; }
.cl-rename { background: transparent; border: 0; color: var(--txt-2); font-size: 12px; line-height: 1; padding: 0 2px; cursor: pointer; }
.cl-rename:hover { color: var(--electric); }
.cl-name-input { width: 96px; font-family: var(--font-m); font-size: 12px; color: var(--txt-0); background: var(--ink-1); border: 1px solid var(--electric); border-radius: 5px; padding: 2px 6px; }
.cl-name-input:focus { outline: none; box-shadow: 0 0 0 2px color-mix(in oklab, var(--electric) 22%, transparent); }

.count-up { font-variant-numeric: tabular-nums; }

/* scrollbars in-theme */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--ink-2); border-radius: 6px; border: 2px solid var(--ink-1); }
::-webkit-scrollbar-track { background: transparent; }

/* ========================================================= OPENINGS ===== */
/* a .workbench instance: the schedule SHEETS under "All Floors", then the openings split by floor */
#scene-openings { display: flex; flex-direction: column; overflow: hidden; }
.op-rows { display: flex; flex-direction: column; gap: 2px; margin: 8px 0; }
.op-row2 {
  display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 6px;
  border: 1px solid transparent; cursor: pointer; font-size: 11.5px; flex-wrap: wrap;
}
.op-row2:hover { background: var(--ink-2); }
.op-row2.selected { border-color: var(--electric); background: color-mix(in oklab, var(--electric) 12%, transparent); }
.orw-ref { font-family: var(--font-m); color: var(--txt-2); min-width: 30px; }
.orw-kind { font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 6px;
  border-radius: 999px; border: 1px solid var(--hair); color: var(--txt-1); }
/* the type is EDITABLE — a caret says the pill is a control, and it shows only where it is one */
.orw-kind.editable { cursor: pointer; }
.orw-kind.editable::after { content: "▾"; margin-left: 4px; color: var(--txt-2); }
.orw-kind.editable:hover { border-color: var(--electric); color: var(--txt-0); }
/* the pill, opened: the native picker, sized and coloured to sit exactly where the pill was */
.orw-kind-pick {
  font-family: var(--font-d); font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 999px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--electric);
}
.orw-kind-pick:focus { outline: none; }
/* DRAW FRAME: the action that ANSWERS "no frame", in the flag colour of the label it replaces. It
   sits where that label sat — immediately left of the measurements it is about to make checkable. */
.orw-drawframe {
  margin-left: auto; font-family: var(--font-d); font-size: 9.5px; letter-spacing: 0.06em;
  text-transform: uppercase; cursor: pointer; padding: 3px 8px; border-radius: 999px;
  color: var(--flag); background: transparent;
  border: 1px dashed color-mix(in oklab, var(--flag) 55%, transparent);
}
.orw-drawframe:hover { background: color-mix(in oklab, var(--flag) 14%, transparent); }
.orw-drawframe.on { border-style: solid; background: color-mix(in oklab, var(--flag) 20%, transparent); }
.orw-dims { margin-left: auto; display: inline-flex; align-items: center; gap: 3px; }
.orw-num {
  width: 54px; text-align: right; font-family: var(--font-m); font-size: 11.5px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 5px; padding: 3px 5px;
}
.orw-num:focus { outline: none; border-color: var(--electric); }
.orw-num:disabled { color: var(--txt-2); cursor: not-allowed; }
/* an OUTSTANDING flagged reading: the size wears the flag colour until it is edited or confirmed */
.orw-num.flagged { border-color: var(--flag); }
.orw-x, .orw-unit { font-family: var(--font-m); font-size: 10px; color: var(--txt-2); }
.orw-m2 { font-family: var(--font-m); font-size: 11px; color: var(--keep); min-width: 52px; text-align: right; }
/* the area + its ✕ and ⧉, as one group that cannot break — and the ref gives up width first, so a
   long one ("garage door") is elided rather than pushing the controls onto a line of their own */
.orw-tail { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; white-space: nowrap; }
.orw-ref { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* NO FRAME: the size was read from somewhere other than a frame on this sheet, so there is nothing
   on the drawing to check it against. It sits immediately left of the measurements it qualifies,
   and takes the area with it into the flag colour — an unverifiable number should not read green. */
.orw-noframe {
  margin-left: auto; font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--flag);
}
.orw-noframe + .orw-dims, .orw-drawframe + .orw-dims { margin-left: 8px; }
.orw-m2.noframe { color: var(--flag); }
/* the pipeline's set-level review notes, on the panel rather than on any one row */
.op-review-flag { font-size: 10.5px; line-height: 1.45; color: var(--flag); margin: 0 0 8px; }
.orw-x-btn { font-size: 11px; line-height: 1; color: var(--vlm); background: none; border: none;
  cursor: pointer; padding: 2px 4px; }
.orw-x-btn:hover { color: #fff; }
/* ⧉ another opening built to this frame. The schedule draws one elevation per FRAME and a house can
   be fitted with the same window twice, so the count is the reviewer's to give — each press lays
   another of this opening's area on its own line beneath, still inside the one row. */
.orw-dup-btn { font-size: 12px; line-height: 1; color: var(--txt-2); background: none; border: none;
  cursor: pointer; padding: 2px 4px; }
.orw-dup-btn:hover { color: var(--electric); }
.orw-dup {
  flex: 0 0 100%; display: flex; align-items: center; gap: 8px;
  padding: 3px 0 0 38px; /* clear of the ref, so the areas stack under the first one */
}
.orw-dup-n { font-family: var(--font-m); font-size: 10px; color: var(--txt-2); }
.orw-dup-dims { margin-left: auto; font-family: var(--font-m); font-size: 11px; color: var(--txt-1); }

/* Perimeter editing: a square on every vertex, and a grab at every segment's midpoint that moves
   that segment perpendicular to itself. Before the first drag the editor's outline sits exactly on
   the animated trace and reads as the same line; from the first drag the trace is removed and this
   one carries it, in the reviewer's own colour so a corrected outline is never mistaken for the
   pipeline's. */
.pe-layer { pointer-events: none; }
.pe-shape { fill: none; stroke: var(--electric); stroke-width: 2; stroke-linejoin: round;
  vector-effect: non-scaling-stroke; }
.pe-layer.edited .pe-shape { stroke: var(--keep);
  fill: color-mix(in oklab, var(--keep) 10%, transparent); }
.pe-layer.edited .pe-arms .wall-move-tri { fill: var(--keep); }
/* the arrows are the ONLY way to MOVE a segment; the line itself takes a pointer for the BREAK tool,
   which cuts rather than shifts, so a click on the outline can never move it.
   ONE SEGMENT AT A TIME, and only once the pointer has been along the middle of it (see
   PerimeterEditor.armSeg) — an arrow parked on a corner is in the way of the next side. */
.pe-arms .wall-move { opacity: 0; pointer-events: none; transition: opacity 0.12s; }
.pe-arms .wall-move.on { opacity: 1; pointer-events: all; }
.pe-hit { stroke: transparent; pointer-events: stroke; cursor: cell; }
.pe-break { pointer-events: none; }
/* The break mark under the pointer — the ─/ /─ a builder draws where a line is cut. In the CUTTING
   colour, not the outline's: drawn in --det it was an electric-blue mark on an electric-blue line and
   could not be seen at all. Each stroke is laid twice, a dark backing then the mark, so it reads on
   top of the line it is cutting. */
.pe-mark path { fill: none; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.pe-mark-back { stroke: var(--ink-0); stroke-width: 7; stroke-opacity: 0.9; }
.pe-mark-ink { stroke: var(--vlm); stroke-width: 3; }
/* the span between the first click and the pointer, in the same cutting colour and heavy enough to
   read over the solid line beneath it, with the length it has reached so far */
.pe-span-back { stroke: var(--ink-0); stroke-width: 8; stroke-opacity: 0.75; stroke-linecap: round;
  vector-effect: non-scaling-stroke; }
.pe-span { stroke: var(--vlm); stroke-width: 4.5; stroke-dasharray: 9 6; stroke-linecap: butt;
  vector-effect: non-scaling-stroke; }
.pe-span-len { fill: var(--vlm); font-family: var(--font-m); font-weight: 600; paint-order: stroke;
  stroke: var(--ink-0); stroke-width: 4; stroke-linejoin: round; }
/* the broken line's own arrows are hidden outright (see redraw); the rest stay as they are */
.pe-layer.dragging .pe-shape { stroke-width: 2.6; }
/* the live area on the drawing, in the reviewer's colour like the outline it belongs to */
.pe-callout { fill: var(--keep); font-family: var(--font-m); font-weight: 500; paint-order: stroke;
  stroke: rgba(9, 20, 33, 0.85); stroke-width: 3.5; }
.perim-edited { color: var(--keep); }
/* the animated trace and the cornerstones are a picture, not a control surface: they sit ABOVE the
   editor's outline (a diamond marks a corner and belongs on top of it) and must not eat its clicks */
.ov-perim-trace, .ov-perim-fill, .ov-perim, .ov-corner, .ov-corner-label, .ov-callout {
  pointer-events: none;
}
/* HIDDEN ON THE PERIMETERS SURFACE — the cornerstone diamonds, the pipeline's flags on each storey,
   and the "outline moved by hand" note. Deliberately stylesheet rules and nothing else:
   perimeters.js still draws all three, the run bundle still carries the flags, and deleting these
   two rules brings everything back exactly as it was. Scoped to the scene rather than hiding the
   classes outright, so none of it is taken away from the other surfaces that use them.

   .rev-note is BOTH notes here — the flags and the edited marker — so one selector covers them. */
#scene-perimeters .ov-corner,
#scene-perimeters .ov-corner-label { display: none; }
#scene-perimeters .rev-note { display: none; }

/* REDRAW ARMED. The outline being replaced gets out of the way — a reviewer redrawing a perimeter
   is tracing the DRAWING, and the wrong line laid over it is the one thing in the way. Its arrows
   and break marks go with it so a stray click cannot drag the shape that is being discarded. */
.perim-stack.redrawing { cursor: crosshair; }
/* PRESS AND HOLD TO PAN, on every zoomable surface (see overlay.pressPan). `touch-action` has to be
   declared BEFORE the gesture: the browser reads it when the gesture STARTS, so setting it from the
   pointerdown handler is already too late and the surface pans the page instead. `user-select` and
   the callout stop a hold raising the text-selection UI over the drawing on the way. */
.pan-surface, .view-frame {
  touch-action: none; user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* THE FOUR-WAY ARROW while panning — the pan cursor, which says the drawing moves in any direction
   under the pointer. Not the closed fist, and not the arrow it was a moment ago.
   The first selector is not redundant: the read-only lock a tracing run puts on the Openings sheet
   (`.autotracing .op-stack`) is written LATER in this file at the same weight, so it took the
   cursor back. Panning changes nothing on the drawing and stays available while a fleet writes to
   it, so the pan cursor has to outrank the lock rather than merely precede it. */
.autotracing .op-stack.panning,
.perim-stack.panning, .view-frame.panning { cursor: move; }
/* the drawing follows the pointer, so nothing under it should be reacting to being crossed */
.perim-stack.panning .overlay, .view-frame.panning .view-perim { pointer-events: none; }
.perim-stack.redrawing .pe-layer,
.perim-stack.redrawing .pe-break { display: none; }

/* the schedule sheet: same camera as the other surfaces, with a transition so focus GLIDES in */
.op-viewport { transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1); }
.op-stack { cursor: default; }
/* Remove ✕ on a frame — revealed on hover, as the door and wall ones are.
   The frame LAYER is pointer-events:none (the legacy step hit-tests clicks itself, see
   .ov-opening-frame above), so on this surface the frames have to opt back in or nothing about them
   is hoverable and the ✕ can never be reached. Scoped to .op-stack so the legacy step keeps its own
   arrangement. The ✕ takes the pointer only once it is visible — an invisible control must not
   swallow a click meant for the trace tool — and it gets out of the way entirely while tracing. */
.op-stack .ov-opening-frame { pointer-events: auto; }
.op-stack .ov-opening-frame polygon { pointer-events: all; }
.op-rm { opacity: 0; pointer-events: none; cursor: pointer; transition: opacity 0.15s; }
.op-stack .ov-opening-frame:hover .op-rm { opacity: 1; pointer-events: all; }
.op-stack.tracing .ov-opening-frame { pointer-events: none; }
.op-rm circle { fill: var(--vlm); stroke: #fff; stroke-width: 1.2; vector-effect: non-scaling-stroke; }
.op-rm text { fill: #fff; font-family: var(--font-m); pointer-events: none; }
/* the trace tool: a crosshair over the sheet, and the outline being drawn */
.op-stack.tracing { cursor: crosshair; }
.op-trace { pointer-events: none; }
.op-trace-line { fill: none; stroke: var(--det); stroke-width: 1.6; stroke-dasharray: 5 3;
  vector-effect: non-scaling-stroke; }
.op-trace-dot { fill: var(--det); stroke: #fff; stroke-width: 1; vector-effect: non-scaling-stroke; }
/* the first vertex, ringed once the outline can close on it */
.op-trace-close { fill: none; stroke: var(--keep); stroke-width: 2.4; vector-effect: non-scaling-stroke; }

/* A SPLIT sheet: the other storey's half of the drawing, dimmed, with the cut ruled between them.
   Sits under this storey's own evidence and takes no clicks — the tool is held to the half by the
   clamp, and the dimming is what says why. Matches the pre-flight thumbnail's .rt-darken. */
.half-mask rect { fill: rgba(3, 6, 10, 0.66); pointer-events: none; }
.half-cut { stroke: var(--electric); stroke-width: 1.2; stroke-dasharray: 6 4; opacity: 0.7;
  vector-effect: non-scaling-stroke; pointer-events: none; }

/* ========================================================= CEILINGS ===== */
/* a .workbench instance: section sheets under "All Floors", then heights and the adjustments */
#scene-ceilings { display: flex; flex-direction: column; overflow: hidden; }
/* one storey's height: name, raked tag, source, the editable reading, then what corroborated it */
.cl-row { padding: 7px 2px 9px; border-bottom: 1px solid var(--hair); }
.cl-row:last-of-type { border-bottom: none; }
.cl-row-head { display: flex; align-items: center; gap: 8px; }
.cl-row-name { font-size: 12px; color: var(--txt-0); font-weight: 600; }
.cl-h-box { margin-left: auto; display: inline-flex; align-items: center; gap: 4px; }
.cl-h-input {
  width: 68px; text-align: right; font-family: var(--font-m); font-size: 11.5px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 5px; padding: 3px 5px;
}
.cl-h-input:focus { outline: none; border-color: var(--electric); }
.cl-h-input.edited { border-color: var(--electric); color: var(--electric); }
.cl-h-read { font-family: var(--font-m); font-size: 11.5px; color: var(--txt-0); }
/* the stud picker: the same chrome as the number field it replaced, so the row does not move */
/* the door WIDTH picker on each row: the standard sizes, then Other… reveals the typed field */
.dr-width-select {
  font-family: var(--font-m); font-size: 11px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 5px; padding: 2px 4px;
}
.dr-width-select:focus { outline: none; border-color: var(--electric); }
.dr-width-select.flagged { border-color: var(--flag); color: var(--flag); }
.dr-width-input.hidden { display: none; }
/* what the drawing gave, when the picker has snapped it onto a standard */

/* the door-leaf picker, in each floor's band on the Doors stage */
.door-height { display: flex; align-items: center; gap: 8px; margin: 10px 0 2px; }
.dh-label {
  font-family: var(--font-d); font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--txt-2);
}
.dh-read { font-family: var(--font-m); font-size: 11.5px; color: var(--txt-0); }
.dh-select {
  margin-left: auto; font-family: var(--font-m); font-size: 11.5px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 5px; padding: 3px 5px;
}
.dh-select:focus { outline: none; border-color: var(--electric); }
.dh-select.edited { border-color: var(--electric); color: var(--electric); }

.cl-h-select {
  font-family: var(--font-m); font-size: 11.5px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 5px; padding: 3px 5px;
}
.cl-h-select:focus { outline: none; border-color: var(--electric); }
.cl-h-select.edited { border-color: var(--electric); color: var(--electric); }
/* `Other…` reveals the typed field beside the picker, so both are readable at once */
.cl-h-input.hidden, .cl-h-unit.hidden { display: none; }
/* the heights are a PROPOSAL until the panel is confirmed — said once, above the button */
.cl-pending-note {
  font-family: var(--font-m); font-size: 10.5px; line-height: 1.45; color: var(--flag);
  margin: 10px 0 0;
}
.cl-commit-error { font-size: 12px; font-weight: 600; color: var(--flag); margin: 8px 0 0; }
.cl-h-unit { font-family: var(--font-m); font-size: 10px; color: var(--txt-2); }

/* a gable end, or a ceiling area drawn on the section */
.cl-g-row {
  display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 6px;
  border: 1px solid transparent; cursor: pointer; font-size: 11.5px; flex-wrap: wrap;
}
.cl-g-row:hover { background: var(--ink-2); }
.cl-g-row.selected { border-color: var(--electric); background: color-mix(in oklab, var(--electric) 12%, transparent); }
.cl-g-row.removed { opacity: 0.45; }
.cl-g-row.added { cursor: default; }
.cl-g-sec { font-family: var(--font-m); color: var(--txt-2); min-width: 34px; }
.cl-g-dim { font-family: var(--font-m); font-size: 10.5px; color: var(--txt-1); }
.cl-g-area { margin-left: auto; display: inline-flex; align-items: center; gap: 3px; }
/* the review badge sits immediately LEFT of the area it is about, so the pair travels together to
   the right of the row — the badge takes the auto margin the area otherwise would */
.cl-g-row .op-badge { margin-left: auto; }
.cl-g-row .op-badge + .cl-g-area { margin-left: 8px; }
/* a read-only area still to be ruled on, in the flag colour the editable one wears via .orw-num */
.cl-g-area.flagged { color: var(--flag); }
.cl-g-acts { display: inline-flex; gap: 4px; }
.clg-verdict {
  font-family: var(--font-m); font-size: 10px; padding: 3px 8px; border-radius: 6px; cursor: pointer;
  background: var(--ink-1); border: 1px solid var(--hair); color: var(--txt-2); transition: all 0.15s;
}
.clg-verdict:hover:not(:disabled) { color: var(--txt-0); border-color: var(--hair-strong); }
/* the verdict IN FORCE reads as settled, not as an alarm: greyed and unclickable, with the row's
   own dimming carrying "removed". Its opposite stays live so one click reverses the decision. */
.clg-verdict:disabled { color: var(--txt-2); background: var(--ink-2); border-color: var(--hair);
  cursor: default; opacity: 0.75; }
/* the CHOSEN verdict is tinted, not filled — the row's own dimming already says "removed", and a
   solid red block on every unkept gable shouts louder than the decision deserves */
/* a stats row holding ONE figure sits in the RIGHT-HAND column — the same tile at the same width,
   just on the other side; stranded on the left it read as a missing second column. */
.perim-stats .stat:only-child { grid-column: 2; }

/* the uplift control + the accounting note under it */
.cl-pct-row { display: flex; align-items: center; gap: 6px; margin: 6px 0 2px; }
.cl-pct-label { font-size: 11px; color: var(--txt-2); }
.cl-note { font-size: 10.5px; line-height: 1.45; color: var(--txt-2); margin: 4px 0 8px; }
.cl-group-head { display: flex; align-items: baseline; gap: 8px; margin: 8px 0 2px; }
.cl-group-name { font-family: var(--font-m); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--txt-2); }
.cl-group-name.current { color: var(--electric); }
.cl-group-sub { font-family: var(--font-m); font-size: 10px; color: var(--txt-2); margin-left: auto; }

/* the view-header page control: the page number with prev/next either side (see svg.js pageNav).
   An arrow is absent, not disabled, when there is no page that way. */
.vp-nav {
  flex: none; display: inline-flex; align-items: stretch; overflow: hidden;
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 7px;
}
.vp-step {
  font-family: var(--font-m); font-size: 10.5px; letter-spacing: 0.04em; color: var(--txt-1);
  background: none; border: none; padding: 4px 9px; cursor: pointer;
}
.vp-step:hover { color: var(--txt-0); background: var(--ink-2); }
.view-head .vp-nav .vp-num { display: flex; align-items: center; padding: 4px 10px; }
/* a divider only where two segments actually meet, so a first/last page reads clean */
.vp-nav .vp-step + .vp-num, .vp-nav .vp-num + .vp-step { border-left: 1px solid var(--hair); }

/* ========================================================== TAKEOFF ===== */
/* the last surface: the calculation as a spreadsheet, full width — no side pane to repeat it */
#scene-takeoff { display: flex; flex-direction: column; overflow: hidden; }
.tk-body { padding: 14px 16px; overflow: auto; }
.tk-sheet { width: 60%; min-width: 640px; margin: 0 auto; border-collapse: collapse;
  font-size: 12px; }
.tk-sheet th {
  font-family: var(--font-m); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--txt-2); font-weight: 600; text-align: right; padding: 8px 12px;
  border-bottom: 1px solid var(--hair-strong);
}
.tk-sheet th.tk-q { text-align: left; }
.tk-sheet td { padding: 5px 12px; text-align: right; border-bottom: 1px solid var(--hair); }
.tk-sheet td.tk-q { text-align: left; color: var(--txt-1); }
.tk-label { color: var(--txt-0); }
.tk-unit { font-family: var(--font-m); font-size: 10px; color: var(--txt-2); margin-left: 6px; }
.tk-val { font-family: var(--font-m); color: var(--txt-0); }
/* a section rule down the sheet */
.tk-group td {
  font-family: var(--font-m); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--txt-2); padding: 14px 12px 4px; text-align: left; border-bottom: 1px solid var(--hair-strong);
}
.tk-sheet tr:first-child .tk-group td { padding-top: 4px; }
/* the Set column is the answer column */
.tk-sheet .tk-set { background: color-mix(in oklab, var(--electric) 6%, transparent); }
.tk-sheet .tk-set .tk-val { color: var(--det); font-weight: 600; }
.tk-emph td { border-top: 1px solid var(--hair-strong); }
.tk-emph .tk-label { font-weight: 700; }
/* the sheet's answer rows, at the weight the flow gives the same figures: the Set column's ground
   deepened from 6% to the headline's 13%, and the figure at 19px. Electric blue throughout — the
   green said "confirmed", which is not what a total is. */
.tk-sheet .tk-emph .tk-set { background: color-mix(in oklab, var(--electric) 13%, transparent); }
.tk-emph .tk-set .tk-val { font-size: 19px; }
/* a sub-section inside a section: the same rule, set in from the heading it sits under */
.tk-group-sub td { padding-top: 9px; padding-left: 24px; border-bottom-color: var(--hair); }

/* a typed cell. Pinned means the reviewer replaced what the drawings measured — it says so. */
.tk-cell { display: inline-flex; align-items: center; gap: 5px; }
/* the unit sits in a FIXED slot beside its figure, so "mm" and "m²" cannot shift the column */
.tk-u { display: inline-block; width: 20px; margin-left: 7px; text-align: left;
  font-family: var(--font-m); font-size: 10px; color: var(--txt-2); }
.tk-input {
  width: 84px; text-align: right; font-family: var(--font-m); font-size: 12px; color: var(--txt-0);
  background: var(--ink-1); border: 1px solid var(--hair); border-radius: 5px; padding: 3px 6px;
}
.tk-input:hover { border-color: var(--hair-strong); }
.tk-input:focus { outline: none; border-color: var(--electric);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--electric) 20%, transparent); }
.tk-cell.pinned .tk-input { border-color: var(--flag); color: var(--flag); }
/* the revert control leads the cell and is ALWAYS in the layout — hidden until the value is
   pinned — so every field lines up down the column whether or not a row has been typed over */
.tk-unpin { visibility: hidden; font-size: 12px; line-height: 1; width: 15px; color: var(--flag);
  background: none; border: none; cursor: pointer; padding: 0; }
.tk-cell.pinned .tk-unpin { visibility: visible; }
.tk-unpin:hover { color: var(--txt-0); }
.tk-pins { margin-left: auto; font-family: var(--font-m); font-size: 10.5px; color: var(--flag); }
.tk-state { font-family: var(--font-m); font-size: 10.5px; color: var(--txt-2); margin-left: 10px; }
/* the copy control sits with its figure at all times — copying a set figure is what this column
   is for, so hiding it until hover would bury the affordance */
.tk-set .tkf-copy { margin-left: 8px; vertical-align: middle; }

/* Takeoff Results: every set total, each copyable */
.tk-flag { font-size: 10.5px; line-height: 1.45; color: var(--flag); padding: 4px 0; }

/* ---- takeoff: the CALCULATION-FLOW style (?view=flow), an A/B against the sheet ---- */
.tk-styles { display: inline-flex; gap: 6px; margin-left: 14px; }
/* SEGMENTED CONTROL — two views of one thing, exactly one live. One track holds both segments, so
   the border reads as a single control rather than two buttons that happen to sit together, and the
   filled segment is unmistakably the one you are looking at. */
.seg {
  gap: 0; padding: 2px; border: 1px solid var(--hair); border-radius: 8px; background: var(--ink-1);
}
.seg-item {
  font-family: var(--font-m); font-size: 11px; letter-spacing: 0.04em; color: var(--txt-2);
  background: transparent; border: 0; border-radius: 6px; padding: 4px 13px; cursor: pointer;
  transition: color 0.15s, background 0.15s;
}
.seg-item:hover:not(.on) { color: var(--txt-0); }
.seg-item.on {
  color: var(--electric); cursor: default;
  background: color-mix(in oklab, var(--electric) 16%, var(--ink-2));
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--electric) 45%, transparent);
}
.seg-item:focus-visible { outline: 1px solid var(--electric); outline-offset: -1px; }
.tkf-body { padding: 18px 20px; overflow: auto; }
.tkf-canvas { position: relative; display: flex; gap: 64px; align-items: flex-start; min-width: max-content; }
/* every column the same width, so no box reads as more important than another */
.tkf-col { flex: none; width: 390px; display: flex; flex-direction: column; gap: 22px; }
/* the connectors sit behind the boxes and never take the pointer */
.tkf-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.tkf-line { fill: none; stroke: color-mix(in oklab, var(--electric) 55%, transparent); stroke-width: 1.6; }
.tkf-dot { fill: var(--electric); }

.tkf-box { position: relative; background: var(--ink-1); border: 1px solid var(--hair); border-radius: 10px;
  padding: 12px 14px 0; }
.tkf-box-head { font-size: 12px; font-weight: 700; color: var(--txt-0); margin-bottom: 6px; }
.tkf-rows { display: flex; flex-direction: column; }
/* one grid for the header and every row, so the columns cannot drift apart */
.tkf-colhead, .tkf-row, .tkf-sub { display: flex; align-items: center; gap: 8px; }
.tkf-colhead { padding-bottom: 4px; border-bottom: 1px solid var(--hair);
  font-family: var(--font-m); font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--txt-2); }
.tkf-row { padding: 3px 0; }
/* the operator leads the row it operates on, in a fixed margin band */
.tkf-op { flex: none; width: 13px; font-family: var(--font-m); font-size: 13px; font-weight: 700;
  color: var(--electric); }
.tkf-rl { font-size: 11.5px; color: var(--txt-1); flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tkf-rv { flex: none; width: 112px; display: inline-flex; align-items: center; justify-content: flex-end; }
.tkf-colhead .tkf-rv { justify-content: flex-end; }
/* what this term puts into the box — the column that adds up to the total below */
.tkf-contrib { flex: none; width: 80px; text-align: right; font-family: var(--font-m);
  font-size: 11px; color: var(--txt-2); }
/* a box whose measure IS an area holds its field in the Area column instead */
.tkf-contrib.tkf-field { width: 112px; display: inline-flex; justify-content: flex-end; }
/* A HEADING OVER A RUN OF TERMS — a floor and which of its two orders they belong to. It names
   the rows under it and adds nothing, so it takes no column: no operator, no figure. */
.tkf-grp { padding: 7px 0 2px; font-family: var(--font-m); font-size: 9.5px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--txt-2); }
.tkf-grp:first-child { padding-top: 3px; }
.tkf-sub { padding: 5px 0; margin-top: 3px; border-top: 1px solid var(--hair); }
.tkf-sub .tkf-contrib { color: var(--txt-1); }
/* a reported figure carries its own unit and can carry a percentage with it, so it takes the width
   it needs and the label beside it gives way */
.tkf-contrib.wide { width: auto; white-space: nowrap; }

/* NOT THE BOARD TYPE COLOURS. On Areas and on Boards those colours are how a wall is told from the
   wall beside it; here there is no drawing, only two figures being read against each other, and
   five colours in the middle of a calculation read as five different kinds of number. */

/* the figure a box produces: the end of a line, and the thing worth copying */
.tkf-total { display: flex; align-items: center; gap: 8px; margin: 8px -14px 0; padding: 9px 14px;
  border-top: 1px solid var(--hair-strong);
  background: color-mix(in oklab, var(--electric) 7%, transparent);
  border-radius: 0 0 10px 10px; }
/* every box's bottom bar is the end of a line — its title reads in the same blue as its figure */
.tkf-tl { font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--det); white-space: nowrap; }
.tkf-tv { margin-left: auto; font-family: var(--font-m); font-size: 15px; font-weight: 600; color: var(--det); }
.tkf-tv .u { font-size: 10px; color: var(--txt-2); margin-left: 3px; }
/* the set total is the end of every line — blue throughout, title included */
.tkf-total.headline { background: color-mix(in oklab, var(--electric) 13%, transparent); }
.tkf-total.headline .tkf-tl { font-weight: 700; }
.tkf-total.headline .tkf-tv { font-size: 19px; color: var(--det); }
/* the Total box carries the figures everything else feeds — all of them read as the answer, and
   the board bought to cover that total is read the same way */
.tkf-box[data-node="total"] .tkf-contrib,
.tkf-box[data-node="boards"] .tkf-row .tkf-contrib { color: var(--det); font-weight: 700; }
.tkf-copy { display: inline-flex; color: #fff; background: none; border: none; cursor: pointer;
  padding: 3px; border-radius: 5px; }
.tkf-copy:hover { color: var(--electric); background: var(--ink-2); }
.tkf-copy.copied { color: var(--keep); }
/* the workbook download, centred in the takeoff header — the stage's CTA, sized for a header */
/* z-index belongs HERE, not on the note it contains: the translateX makes this a stacking context
   of its own, so a child's z-index can only order it against its siblings inside. The flow canvas
   is positioned and comes later in the DOM, so without this the note paints underneath it. */
.tk-download { position: absolute; left: 50%; transform: translateX(-50%); z-index: 6; }
.tk-dl { font-size: 12px; padding: 7px 16px; }
.tk-dl.is-busy { opacity: 0.6; cursor: progress; }
/* Where the quote had to depart from the take-off, or why it could not be built. Under the button
   that raised it, and wrapped to the header's width rather than pushing the layout wider. */
.tk-note {
  position: absolute; left: 50%; transform: translateX(-50%); top: calc(100% + 6px);
  width: max-content; max-width: 34rem; text-align: center;
  /* the header sets nowrap for its controls, so max-content never wraps and max-width only clips */
  white-space: normal; text-wrap: balance;
  /* and it hangs BELOW the header, over the flow, so it needs a surface and a layer of its own --
     without them the second line reads through the cards it lands on */
  padding: 5px 12px; border-radius: 8px;
  background: var(--ink-1); border: 1px solid var(--ink-2); box-shadow: 0 6px 18px rgba(0,0,0,0.45);
  font-size: 11.5px; line-height: 1.45; color: var(--muted);
}
.tk-note.bad { color: var(--flag); font-weight: 600; }

/* The take-off stage with its numbers withheld: corrections exist but could not be folded in.
   Deliberately carries NO figures — anything numeric here would be read as the answer. */
.tk-blocked {
  max-width: 46rem; margin: 3rem auto; padding: 1.5rem 1.75rem;
  border: 1px solid var(--flag); border-radius: 10px; background: rgba(255, 176, 32, 0.06);
}
.tk-blocked h3 { margin: 0 0 .6rem; color: var(--flag); font-size: 1.05rem; }
.tk-blocked p { margin: 0 0 .8rem; color: var(--txt-1); line-height: 1.5; }
.tk-blocked .tk-blocked-why {
  font-family: var(--mono); font-size: .82rem; color: var(--txt-2);
  padding: .55rem .7rem; border-radius: 6px; background: rgba(0, 0, 0, .25);
}

/* A thumbnail that has not been rasterised yet. The glyph sits back so a pending sheet reads as
   pending rather than as a drawing that came out faint, and the real page fades in over it. */
img.thumb-pending { opacity: .45; }
.sheet-card img, .rev-thumb img, img.view-ph { transition: opacity .25s ease; }

/* The live marker on the openings step — another session's work arriving while you read.
   Deliberately the dim ink and the small size: it explains a list that moved, and is meant to be
   ignorable by someone measuring. */
.op-live {
  margin-left: .75rem;
  font-size: .78rem;
  color: var(--dim);
  white-space: nowrap;
}

/* The pen wears the TRACE TOOL's own classes (.op-trace-line / -dot / -close), so an arriving frame
   is drawn in the same blue dashes, with the same dot at the pen and the same green close marker, as
   one a reviewer draws by hand. Nothing to style here: giving it its own rules is how the two looks
   drift apart. The group exists only so the whole pen can be removed in one call. */

/* "Auto-tracing in progress…" — the fleet at work, drawn AROUND the page render.
   A chip in a corner was easy to miss while watching the drawing. The ring surrounds the sheet and
   sweeps with the same band the loading skeleton uses (@keyframes wbs-sweep), so the surface that is
   busy is the surface that looks busy. Never hit-testable: it lies over where a reviewer traces. */
.op-stack { position: relative; }
.op-tracing { position: absolute; inset: 0; z-index: 6; pointer-events: none;
              transition: opacity .6s ease; }

/* The border itself. The gradient fills the whole box and a MASK cuts the middle out, leaving only
   the 2px edge — which is what lets a border carry a moving gradient at all. Same stops, same
   300% background-size and same 4s wbs-sweep as .wbs-bar, so the two read as one system: one
   sweep for "loading", the same sweep for "still working". */
.op-tracing-ring {
  position: absolute; inset: 0;
  border-radius: 10px;
  padding: 2px;
  background: linear-gradient(100deg,
    color-mix(in oklab, var(--det) 22%, transparent) 40%,
    var(--det) 50%,
    color-mix(in oklab, var(--det) 22%, transparent) 60%);
  background-size: 300% 100%;
  animation: wbs-sweep 4s linear infinite;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  transition: filter .5s ease;
}

/* The words, on the top edge of the ring like a fieldset legend. A SIBLING of the ring, never a
   child: the mask that cuts the ring's middle out would cut the label out with it. */
.op-tracing-label {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  display: flex; gap: .5rem; align-items: baseline;
  padding: .35rem .9rem;
  border: 1px solid color-mix(in oklab, var(--det) 45%, transparent);
  border-top: 0;
  border-radius: 0 0 8px 8px;
  background: color-mix(in oklab, var(--bg) 92%, transparent);
  backdrop-filter: blur(6px);
  font-size: .8rem;
  text-align: left;
}
.op-tracing-fixed { color: var(--txt-2); letter-spacing: .02em; white-space: nowrap; }
/* the changing half: WIDE ENOUGH FOR THE LONGEST PHRASE ("checking doors and windows") so the label
   holds one width whatever it currently says, rather than twitching every 12s at the edge of vision */
.op-tracing-say {
  color: var(--det);
  font-family: var(--font-m);
  min-width: 27ch;
  white-space: nowrap;
  animation: op-say-in .5s ease;
}
@keyframes op-say-in { from { opacity: 0; } to { opacity: 1; } }

/* A delta landed: the ring brightens once. The WORDS keep time; the RING keeps score. */
.op-tracing.pulse .op-tracing-ring { animation: wbs-sweep 4s linear infinite, op-pulse .7s ease; }
@keyframes op-pulse {
  0%   { filter: none; }
  35%  { filter: brightness(1.9) drop-shadow(0 0 6px color-mix(in oklab, var(--det) 60%, transparent)); }
  100% { filter: none; }
}
.op-tracing.done { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .op-tracing-ring { animation: none; } }

/* AUTO-TRACING: the surface is read-only while a fleet is writing to it.
   Not a safety measure — a reviewer's edit would apply fine. It is that an opening they drag is one
   an agent may reframe a second later (and the merge holds back whatever is under the cursor, so the
   two would take turns overwriting each other), and a ✕ struck through a frame while its neighbours
   are still arriving is a decision made against half a drawing. Watching is the only thing to do, so
   nothing else is offered. */
.autotracing .door-add,
.autotracing .orw-drawframe,
.autotracing .orw-x-btn,
.autotracing .orw-dup-btn,
.autotracing .rev-confirm {
  pointer-events: none;
  opacity: 0.35;
  filter: grayscale(1);
}
/* The controls that live ON the drawing go entirely, rather than dimming: a ghost ✕ hovering over a
   frame that is being redrawn reads as something you can still press. */
.autotracing .op-rm,
.autotracing .ov-open-handles { display: none !important; }
/* …and the frames stop taking the pointer at all, so a click cannot focus-and-drag one mid-arrival. */
.autotracing .op-stack .ov-opening-frame,
.autotracing .op-stack .ov-opening-frame polygon { pointer-events: none; }
.autotracing .op-stack { cursor: default; }


/* -------- openings: which storey each fitting is on -------- */
/* The cell reads like the type pill beside it — same height, same click-to-change — because they
   are the same kind of question about the same row. */
.orw-floor {
  font-family: var(--font-m); font-size: 10.5px; letter-spacing: 0.04em;
  color: var(--txt-1); border: 1px solid var(--hair); border-radius: 5px; padding: 2px 7px;
  white-space: nowrap;
}
.orw-floor.editable { cursor: pointer; }
.orw-floor.editable:hover { border-color: var(--det); color: var(--txt-0); }
/* NOT YET ANSWERED — the flag amber, because an opening with no floor is a deduction nobody has
   taken off a wall, and the confirm is blocked until the band is empty. */
.orw-floor.unset {
  color: var(--flag); border-color: color-mix(in oklab, var(--flag) 45%, transparent);
  border-style: dashed;
}
/* SUGGESTED, not settled: read off the ref series or the garage rule. Legible, and visibly not the
   same thing as an answer a person gave. */
.orw-floor.suggested { color: var(--det); border-style: dashed; }
.orw-dup .orw-floor { margin-left: 6px; }

/* the one press that empties All Floors, under the rows it acts on */
.op-allocate {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  margin: 10px 0 2px; padding: 9px 11px;
  border: 1px dashed color-mix(in oklab, var(--flag) 40%, transparent); border-radius: 8px;
  background: color-mix(in oklab, var(--flag) 5%, transparent);
}
.op-allocate-say { font-size: 12px; line-height: 1.45; color: var(--txt-1); }
.op-allocate-acts { display: flex; gap: 6px; }
.op-allocate-btn {
  font-family: var(--font-m); font-size: 11px; color: var(--txt-1);
  border: 1px solid var(--hair-strong); border-radius: 6px; padding: 5px 9px;
  background: var(--ink-1); cursor: pointer; white-space: nowrap;
  transition: border-color 0.15s, color 0.15s;
}
.op-allocate-btn:hover { border-color: var(--det); color: var(--txt-0); }

/* A STOREY WITH NO OPENINGS — the block, explained, at the top of the band it blocks. */
.op-empty-storey {
  display: flex; gap: 10px; margin: 12px 0 2px; padding: 10px 12px;
  border: 1px solid color-mix(in oklab, var(--flag) 45%, transparent); border-radius: 8px;
  background: color-mix(in oklab, var(--flag) 7%, transparent);
}
.oes-mark { color: var(--flag); font-size: 13px; line-height: 1.4; }
.oes-body { display: grid; gap: 4px; }
.oes-head { font-size: 12.5px; font-weight: 600; color: var(--flag); }
.oes-say { font-size: 12px; line-height: 1.5; color: var(--txt-1); }

/* ---------------------------------------------------------------- Areas (?stage=areas)
   The wet/dry surface. One row per derived area in the side pane, and the areas themselves drawn
   over the floor plan — the spike's top view, in the overlay's own page-point frame.

   WET IS ITS OWN COLOUR and not one already spoken for. Amber is "pending review" everywhere in
   this app and red is a VLM pass in flight, so a wet area wearing either would be read as a state
   the reviewer has to clear rather than an answer they gave. Teal is unused and reads as water. */
:root {
  /* A WALL FACE'S TYPE IS A PRODUCT, and each one gets its own colour. None of these is a colour
     already spoken for on a drawing: --electric is detected geometry, --flag is pending review and
     --vlm is a pass in flight, so a face wearing one of those would be read as a state rather than
     as an answer somebody gave. */
  /* TWO YELLOWS, AND THEY ARE NOT INTERCHANGEABLE. `--dry` is the board type wherever it is READ —
     a figure in the panel, an area outline, a face label — and it has to hold its own against a
     dark panel and against printed line-work. `--dry-solid` is that same board as a SURFACE in the
     isometric view — wall or ceiling — where it is filled area in a drawing and the paler tone
     would glare.
     GREY WAS NOT WORKING FOR THE PEOPLE USING IT. Standard board is the one every plan is mostly
     made of, and a neutral read as an absence of an answer rather than as the commonest answer
     there is. Yellow says the same thing the trade's own drawings do, and it is further from wet's
     teal and from brace's blue than a grey ever was: on the panel ground it reads at 13.1 against
     the grey's 10.1, and against wet at 1.93 where the grey managed 1.50. */
  --dry: #f4d26c;     /* Standard  — yellow, as read */
  --dry-solid: #f1c232; /*          — and deeper as a surface in the Boards view */
  --wet: #18b059;     /* Aqualine  — teal */
  /* BRACELINE IS BLUE, which is how a bracing element is shown on a drawing — and the drawing's
     blue is a dark one. As a filled wall it is right; as ten-pixel text it scores 1.9 against this
     panel, where 4.5 is the floor for reading. So the read colour is that indigo lifted.
     LIFTED, NOT WASHED OUT. The first attempt kept the saturation down to stay clear of
     `--electric` and landed on a pastel that sat 69 from the dry grey — a reviewer could not tell
     a braced wall from a dry one, which is the only comparison that matters here. Held at 85%
     saturation it sits 122 from grey and still clears the threshold at 5.25. `--electric` is no
     longer any board's colour, so being blue near it costs nothing. */
  --brace: #5f83f2;   /* Braceline — blue, as read */
  --brace-solid: #253f8e; /*        — and as a surface in the Boards view */
  --fire: #ef4b3d;    /* Fyreline  — red */
  /* NOISELINE, and the same split again: the swatch purple scores 2.0 on this panel, so it draws
     the surface and a lift of it is what a figure is read in — held at saturation for the same
     reason brace is. A lilac sat 51 from the dry grey, which is nearer than the pastel blue that
     had to be replaced. */
  --it: #b85ff2;      /* Noiseline — purple, as read */
  --it-solid: #662a8d; /*          — and as a surface in the Boards view */
  --extwall: #662a8d; /* the EXTERNAL perimeter wall on the Walls surface — see .ov-extwall-run */

  /* A CUT, WHICH IS NOT A BOARD TYPE. The Areas split tool drew its dashed line in `--brace`
     because that token used to hold a red; brace is blue now, and a blue split line would both
     lose the meaning and collide with `--electric`. So the tool has its own colour, holding the
     red it has always drawn in, and it is unaffected by anything the board types do. */
  --cut: #ff3b52;
  /* HOW WIDE A WALL IS DRAWN, in page points. A control holds to this as its floor — no arrow or
     handle is drawn narrower than the wall it belongs to, and none closes in nearer than that. One
     definition: the walls surface reads it off a real wall, the perimeters surface off this token. */
  --wall-pt: 6;
}
/* one place the five types are turned into a colour; every control below inherits from it */
/* ONE PLACE the five types become a colour — read by the drawing and by the panel's figures alike,
   so a number beside a floor and a wall on the plan are recognisably the same thing. */
/* A BOARD TYPE'S COLOUR, wherever the type is named. Scoped to two surfaces once and then wanted
   by a third — the class says what the thing IS, so the mapping belongs to the class and not to
   the place it happens to be used. Nothing reads `--t` unless a rule asks for it. */
/* DRY HAS ITS OWN TOKEN, not `--electric`. That blue means "the pipeline detected this" everywhere
   in the app, and a dry wall is not a detection — sharing it meant Dry could not change colour
   without every detected element changing with it. */
.t-dry   { --t: var(--dry); }
.t-wet   { --t: var(--wet); }
.t-brace { --t: var(--brace); }
.t-fire  { --t: var(--fire); }
.t-it    { --t: var(--it); }
#areas-list .stat .s-value.t-dry,
#areas-list .stat .s-value.t-wet,
#areas-list .stat .s-value.t-brace,
#areas-list .stat .s-value.t-fire,
#areas-list .stat .s-value.t-it { color: var(--t); }

#scene-areas { display: flex; flex-direction: column; overflow: hidden; }

.area-rows { display: flex; flex-direction: column; gap: 2px; margin: 8px 0; }
.area-row {
  display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 6px;
  border: 1px solid transparent; cursor: pointer; font-size: 11.5px; flex-wrap: wrap;
}
.area-row:hover { background: var(--ink-2); }

/* the area's NUMBER, drawn as it is on the plan — the two are the same label and have to look it */
.ar-num {
  font-family: var(--font-m); font-size: 10.5px; color: var(--txt-0);
  min-width: 20px; height: 18px; padding: 0 5px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--hair-strong); background: var(--ink-1);
}
.ar-size { font-family: var(--font-m); color: var(--txt-1); min-width: 58px; }
.ar-pick { display: inline-flex; align-items: center; gap: 5px; margin-left: auto; }
.ar-trade { font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--txt-2); }
/* DRY IS AN ANSWER, NOT A BLANK. Both states of this control wear their own board type — the same
   colour that type wears on the drawing beside it and in the figures below — so reading down the
   column shows which answer each area has been given rather than only which ones are wet. */
.ar-select {
  font-family: var(--font-d); font-size: 10.5px; color: var(--dry);
  background: var(--ink-1); border: 1px solid var(--dry); border-radius: 5px; padding: 2px 4px;
}
.ar-select:focus { outline: none; border-color: var(--electric); }
.ar-select.wet { border-color: var(--wet); color: var(--wet); }
.ar-select.fire { border-color: var(--fire); color: var(--fire); }
/* THE HEIGHT IS NOT A BOARD TYPE, so it does not wear a board type's colour: it is a measurement
   the room is worked to, and grey is what the other five colours are read against. */
.ar-select.height, .ar-read.height {
  color: var(--txt-1); border-color: var(--hair-strong);
}
/* …UNTIL SOMEBODY MOVES IT OFF THE STOREY'S. A height that is not the one this floor is worked to
   is a decision, and the only one in the row that cannot be seen by reading the control: every
   other answer here names itself, while 2400 and 2700 look alike down a column. Electric is what
   this app uses for a figure a reviewer has set, so the changed rows are findable at a glance. */
.ar-select.height.changed, .ar-read.height.changed {
  color: var(--electric); border-color: var(--electric);
}
/* THE COLUMN NAMES, once at the top of the list. Sized and lit to be read as headings over the
   controls under them rather than as a caption somebody left on. */
.area-row.area-head {
  padding-bottom: 2px; pointer-events: none;
  font-family: var(--font-m); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--txt-1);
}
.area-row.area-head .ar-pick { justify-content: center; }
/* …and no disc over the numbers. The header's first cell holds the column open so the headings sit
   over their own controls; drawing the area number's badge there would put an area on the row that
   names the columns. */
.area-row.area-head .ar-num { border-color: transparent; background: none; }
/* Walls held wet by the ceiling above them: the answer is not this control's to give. It keeps
   the teal that says wet and dims, so a reviewer reading down the column can see which of
   the two answers is the one being made. */
.ar-select.locked { opacity: 0.6; cursor: not-allowed; }
/* a confirmed floor: the answers are a record, so they read rather than open */
.ar-read { font-family: var(--font-m); font-size: 10.5px; color: var(--dry); }
.ar-read.wet { color: var(--wet); }
.ar-read.fire { color: var(--fire); }
/* a stored mark whose point no longer lands in any area — see areasstage.js. It wears the flag
   colour because that is exactly what it is: something nobody has ruled on since the walls moved. */
.area-row.orphan {
  border-color: color-mix(in oklab, var(--flag) 35%, transparent);
  background: color-mix(in oklab, var(--flag) 7%, transparent);
  cursor: default;
}
.area-row.orphan .ar-num { border-color: var(--flag); color: var(--flag); }
.ar-lost { font-size: 10.5px; line-height: 1.4; color: var(--flag); flex: 1; }
/* said where the figures would be, when the storey has no stud height to measure walls with */
.area-nofact { font-size: 10.5px; line-height: 1.4; color: var(--flag); margin: 6px 2px 0; }

/* the areas over the sheet */
/* DRY IS GREY HERE TOO. These drew in `--electric`, which is the colour of a pipeline detection —
   so every dry area, its outline, its disc and its face labels came out in the detection blue and
   changing the board type's colour did nothing to them. */
.areas-overlay .ar-shape {
  fill: color-mix(in oklab, var(--dry) 10%, transparent);
  /* A HEAVY OUTLINE, because the fill cannot do this job. Colour here means wet or dry and nothing
     else, so two rooms side by side are the same colour and only their shared edge tells them
     apart — over line-work already drawn in blue, a hairline does not. Non-scaling, so the boundary
     stays the same weight at every magnification. */
  stroke: var(--dry); stroke-width: 1.6; vector-effect: non-scaling-stroke;
}
/* the FILL answers for the ceiling and the OUTLINE for the walls — see areasstage.paintAreas */
.areas-overlay .ar-shape.wet-fill { fill: color-mix(in oklab, var(--wet) 13%, transparent); }
/* A FIRE CEILING, IN THE FIRE BOARD'S OWN COLOUR — and with no wave over it. The wave means water;
   on a Fyreline ceiling it would say something untrue. The colour carries it alone, which is what
   the three wall exceptions do on their faces too. */
.areas-overlay .ar-shape.fire-fill { fill: color-mix(in oklab, var(--fire) 13%, transparent); }
.areas-overlay .ar-shape.wet-edge { stroke: var(--wet); }
/* …and the wet CEILING carries a wave over its wash, so the two answers never rely on one channel */
.areas-overlay .ar-hatch { fill: url(#ar-wet-wave); stroke: none; pointer-events: none; }
.areas-overlay .ar-wave { fill: none; stroke: var(--wet); stroke-width: 0.3; opacity: 0.75; }
.areas-overlay .ar-disc { fill: var(--ink-0); stroke: var(--dry); stroke-width: 1.4;
  vector-effect: non-scaling-stroke; }
.areas-overlay .ar-disc.wet { stroke: var(--wet); }
.areas-overlay .ar-disc.fire { stroke: var(--fire); }
.areas-overlay .ar-disc-num {
  fill: var(--txt-0); font-family: var(--font-m); font-weight: 700;
  text-anchor: middle; pointer-events: none;
}
/* THE MARKED RUN along a face, in its type's colour, and the band a reviewer points at.
   The hit band is invisible and wide: the chip is too small to aim at and a line has no width, so
   the target is the whole run of wall, on its own side of it. */
/* the SAME weight as the area outline it replaces — see RUN_INSET_PT. Anything heavier or lighter
   leaves the blue Dry line showing along one edge of a wall that is not dry. */
.areas-overlay .ar-run {
  stroke: var(--t); stroke-width: 1.6; stroke-linecap: butt;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* the stud positions the handle snaps to, dotted along the run — darker than the band so they read
   as marks ON it rather than as part of it */
/* HALF THE BAND'S WIDTH: a mark ON the run, not a bead threaded onto it */
.areas-overlay .ar-stud {
  stroke: color-mix(in oklab, var(--t) 45%, #000);
  stroke-width: 0.8; stroke-linecap: round;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* the backing that makes the open controls one hover region — see showFaceControls */
.areas-overlay .ar-shield { fill: transparent; pointer-events: all; }
/* the two halves of a part-marked face's label outline */
.areas-overlay .ar-stop { stop-color: var(--t); }
.areas-overlay .ar-hit {
  stroke: transparent; stroke-width: 9; vector-effect: non-scaling-stroke;
  cursor: pointer; pointer-events: stroke;
}
.areas-overlay .ar-hit:hover { stroke: color-mix(in oklab, var(--txt-0) 14%, transparent); }
/* an area set wet in the panel has answered for every one of its faces — no target, no hover */
.areas-overlay .ar-hit.off { pointer-events: none; }

/* the type picker — the four types this face is NOT */
.areas-overlay .ar-chip { cursor: pointer; }
/* the same weight of outline and the same face as the {area}:{face} label, because the chip is the
   same kind of object: a small boxed annotation on the drawing, belonging to the face it sits on */
.areas-overlay .ar-chip rect {
  fill: var(--ink-0); stroke: var(--t); stroke-width: 0.5; vector-effect: non-scaling-stroke;
}
.areas-overlay .ar-chip text {
  fill: var(--t); font-family: var(--font-m); font-weight: 700; text-anchor: middle;
  pointer-events: none;
}
.areas-overlay .ar-chip:hover rect { fill: var(--t); }
.areas-overlay .ar-chip:hover text { fill: var(--ink-0); }

/* the end handles — drag inward to record only part of the face */
.areas-overlay .ar-handle {
  fill: var(--ink-0); stroke: var(--txt-0); stroke-width: 1.4;
  vector-effect: non-scaling-stroke; cursor: grab;
}
.areas-overlay .ar-handle:hover { fill: var(--txt-0); }

/* the wall FACES, labelled {area}:{face} — the spike's top view. The chip sits ON the face it names,
   so a number can never be read against the wrong wall. */
.areas-overlay .ar-face rect {
  fill: var(--ink-0); stroke: var(--dry); stroke-width: 0.5;
  vector-effect: non-scaling-stroke; opacity: 0.92;
}
.areas-overlay .ar-face text {
  fill: var(--txt-0); font-family: var(--font-m); font-weight: 700;
  text-anchor: middle; pointer-events: none;
}
.areas-overlay .ar-face.wet rect { stroke: var(--wet); }
.areas-overlay .ar-face.wet text { fill: var(--wet); }
/* …and a face marked in its OWN right outranks the area's answer — it is the more specific one */
.areas-overlay .ar-face:not(.t-dry) rect { stroke: var(--t); }
.areas-overlay .ar-face:not(.t-dry) text { fill: var(--t); }
/* the leaders from the face label down to each type chip — a hairline, because it is a pointer and
   not a thing to read: it exists so the row is unmistakably THIS face's and not the next wall's */
.areas-overlay .ar-lead polyline {
  stroke: var(--txt-0); stroke-width: 0.4; vector-effect: non-scaling-stroke;
  opacity: 0.55; pointer-events: none; fill: none;
}

/* ---- the area under the pointer, and its two repairs ---------------------------------------
   The fill DOUBLES and the disc grows: the tools hang off that disc, so it has to read as the
   thing being addressed rather than as one label among ten. */
.areas-overlay .ar-shape.hot { fill: color-mix(in oklab, var(--dry) 20%, transparent); }
.areas-overlay .ar-shape.hot.wet-fill { fill: color-mix(in oklab, var(--wet) 26%, transparent); }
.areas-overlay .ar-shape.hot.fire-fill { fill: color-mix(in oklab, var(--fire) 26%, transparent); }
.areas-overlay .ar-tool.t-split { --t: var(--cut); }
.areas-overlay .ar-tool.t-undo { --t: var(--keep); }
/* merge is not offered on the disc; its colour and cursor stay so the tool is whole */
.areas-overlay .ar-tool.t-merge { --t: var(--keep); }
.areas-overlay .ar-cursor.merge { stroke: var(--keep); stroke-width: 1.8; stroke-linecap: round; }
.areas-overlay .ar-tool.on rect { fill: var(--t); }
.areas-overlay .ar-tool.on text { fill: var(--ink-0); }
/* WHILE A TOOL IS UP the wall-face targets are out of the pointer's way entirely. The hit band is
   a nine-point stroke lying over the rooms and a split STARTS on a wall face, so leaving it live
   meant the band swallowed the one click the split needed. */
.areas-overlay.in-mode .ar-hit,
.areas-overlay.in-mode .ar-handle,
.areas-overlay.in-mode .ar-face { pointer-events: none; }
.areas-overlay.in-mode .ar-shape { cursor: crosshair; }

/* what the reviewer is holding: a red dash for Split, a green cross for Merge */
.areas-overlay .ar-cursor { fill: none; pointer-events: none; vector-effect: non-scaling-stroke; }
.areas-overlay .ar-cursor.split {
  stroke: var(--cut); stroke-width: 1.8; stroke-dasharray: 4 3; stroke-linecap: round;
}


/* ═══════════════════════════════════════════════════════════════════ Boards ═══
   The floor seen from a corner, and what it is lined in. Colours come from the board type's own
   token (`--t` above), so a wall on the render and its line in the panel are the same colour by
   construction rather than by two lists agreeing. */

/* THE PANE IS THE FRAME. `.wb-main` is a flex column, and a flex item's default `min-height: auto`
   lets its content push it taller than the box it is in — which put the floor and the view cube
   below the bottom of the window. The scene fits the frame; it never sets it. */
#scene-boards { display: flex; flex-direction: column; overflow: hidden; }
.bd-body-view { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; }
.bd-stack { position: absolute; inset: 0; }
.iso-scene { width: 100%; height: 100%; display: block; }
.iso-cam { transition: transform 260ms cubic-bezier(.2, .7, .2, 1); }
/* THE PLAN ON THE GROUND, at full strength — the walls above it are what gives way, not the
   drawing. Inert, so a pointer aiming at a room hits the room. */
.iso-floor { opacity: 1; pointer-events: none; }

/* A WALL IS A SOLID THING and reads as one: its own colour, a darker edge so two walls meeting at
   a corner do not merge into one shape. `paint-order` keeps the stroke inside the fill's colour. */
/* A WALL IS GLASS, not a slab. At 60% the plan reads through the room and so does whatever stands
   behind it — the drawing is a glass model of the floor, which is what makes the sheet underneath
   worth having at full strength. */
/* A SURFACE IN THIS VIEW takes the drawing colour, not the colour a figure is read in — see the
   tokens. Walls, ceilings and the sheet outlines on them alike: they are one drawing, and a ceiling
   lighter than the walls under it would read as a different board.
   Only the two that differ are listed; a type whose read colour works as a surface has one colour
   and needs no second. */
.iso-w.t-dry, .iso-c.t-dry, .iso-b.t-dry, .iso-band.t-dry { --t: var(--dry-solid); }
.iso-w.t-brace, .iso-c.t-brace, .iso-b.t-brace, .iso-band.t-brace { --t: var(--brace-solid); }
.iso-w.t-it, .iso-c.t-it, .iso-b.t-it, .iso-band.t-it { --t: var(--it-solid); }
.iso-w {
  fill: color-mix(in oklab, var(--t) 78%, var(--ink-0));
  stroke: color-mix(in oklab, var(--t) 40%, var(--ink-0));
  stroke-width: 1.2;
  opacity: .6;
  vector-effect: non-scaling-stroke;
  transition: opacity 140ms ease, fill 140ms ease;
}
/* A CEILING IS A LID, so it is see-through: at stud height it sits over the room it belongs to,
   and an opaque one would turn the whole view into a plan of roofs. */
.iso-c {
  fill: color-mix(in oklab, var(--t) 60%, transparent);
  stroke: color-mix(in oklab, var(--t) 75%, transparent);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  /* THE LID IS A PICTURE. It is pointed at through a still copy of itself (see boardsstage.js),
     because a target that moves out from under the pointer flickers without end. */
  pointer-events: none;
  transition: opacity 140ms ease, transform 260ms cubic-bezier(.2, .7, .2, 1);
}
/* THE ROOM UNDER THE POINTER HAS ITS LID LIFTED — half a storey, ceiling and the sheets on it
   together, and back down when the pointer moves on. Height only moves a point up the screen in
   this projection, so the rise is a translation and animates. */
.iso-c.lit, .iso-b.on-ceil.lit { transform: translateY(var(--lift, 0)); }
.iso-c-hit { fill: transparent; stroke: none; cursor: pointer; }
/* A WALL LINED TWICE wears both products, as bands of each across its face. The fill comes from a
   pattern set inline (see boardsstage.js), so nothing here may set `fill` on it — the stroke is
   neutral because no single type owns the wall. */
.iso-w.striped { stroke: color-mix(in oklab, var(--txt-2) 55%, transparent); }
/* EVERY SHEET, OUTLINED. Drawn over the wall or ceiling it is on and nowhere else, so the drawing
   shows the boards that were priced rather than a surface in a colour. Unfilled, because the fill
   underneath is what says which board it is. */
.iso-b {
  fill: none; stroke: color-mix(in oklab, var(--t) 80%, #000); stroke-width: 0.9;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
/* …and a wall's sheets hold the same strength as the wall they are on, so the lines do not come
   forward as the wall goes back. */
.iso-b:not(.on-ceil) { opacity: .6; }
/* A CEILING'S SHEETS carry the same darkened board colour as a wall's; only the weight differs,
   because a ceiling is seen through and its joints would otherwise fade into the lid. */
.iso-b.on-ceil {
  stroke-width: 1.1;
  transition: opacity 140ms ease, transform 260ms cubic-bezier(.2, .7, .2, 1);
}
.iso-b.dim { opacity: 0.22; }
/* …AND THE OUTLINES WITH THEM. `off` was being set on every sheet outline and no rule acted on
   it, so switching a board type off left its boards drawn over the room. */
.iso-w.off, .iso-c.off, .iso-b.off, .iso-c-hit.off { display: none; }
.iso-w.dim, .iso-c.dim { opacity: 0.22; }
/* HOVERED: forward to 90% and up to the full-strength colour, both — the room being pointed at is
   the one being read. */
.iso-w.lit, .iso-b:not(.on-ceil).lit { opacity: .9; }
.iso-w.lit:not(.striped) { fill: var(--t); }
.iso-w.striped.lit { stroke: var(--txt-0); stroke-width: 1.8; }
.iso-c.lit { fill: color-mix(in oklab, var(--t) 85%, transparent); }

/* THE SURFACES ARE THE TARGETS — see boardsstage.js. Nothing else is aimable, so the sheet
   outlines and the plan on the floor cannot come between a pointer and the room it is over. */
.iso-w, .iso-c { cursor: pointer; }

/* the four corners, drawn as the top of the cube they turn */
.bd-cube {
  position: absolute; right: 16px; top: 14px; width: 84px; opacity: .85;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.bd-cube-label {
  font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--txt-2);
}
.bd-cube:hover { opacity: 1; }
.bd-cube-svg { width: 72px; height: 72px; overflow: visible; }
.cube-face {
  fill: color-mix(in oklab, var(--txt-2) 14%, transparent);
  stroke: var(--hair-strong); stroke-width: .03; cursor: pointer;
}
.cube-face:hover { fill: color-mix(in oklab, var(--electric) 26%, transparent); }
.cube-face.on { fill: color-mix(in oklab, var(--electric) 46%, transparent); }
.cube-edge { fill: none; stroke: var(--txt-2); stroke-width: .045; pointer-events: none; }


/* the two view filters, in the header beside the page nav */
.bd-filters { display: flex; align-items: center; gap: 18px; margin-left: auto; }
.bd-filter { display: inline-flex; align-items: center; gap: 8px; }
.bd-filter-label {
  font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--txt-2);
}

/* one segmented control, used by the filters and by the selection preference */
.bd-segd {
  display: inline-flex; border: 1px solid var(--hair); border-radius: 7px; overflow: hidden;
  background: var(--ink-1);
}
.bd-segb {
  font-family: var(--font-d); font-size: 10.5px; color: var(--txt-2);
  padding: 3px 9px; border: 0; background: transparent; cursor: pointer; white-space: nowrap;
}
.bd-segb + .bd-segb { border-left: 1px solid var(--hair); }
.bd-segb:hover { color: var(--txt-0); }
.bd-segb.on { color: var(--ink-0); background: var(--electric); }

/* ── the side panel ── */
/* THE CONTROL UNDER ITS LABEL, not beside it — three segments and a heading do not fit a panel
   column on one line without one of them being squeezed. */
.bd-pref { display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
           margin: 10px 0 4px; }
.bd-pref-label {
  font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--txt-2);
}
/* THE ICON ALONE, beside the order it re-prices. A word here competed with the sub-section title
   for a line that already carries the total. */
.bd-recompute {
  width: 20px; height: 20px; line-height: 1; font-size: 12px; color: var(--txt-2);
  background: transparent; border: 1px solid var(--hair); border-radius: 5px;
  cursor: pointer; padding: 0;
}
.bd-recompute:hover:not(:disabled) { color: var(--txt-0); border-color: var(--electric); }
.bd-recompute:disabled { opacity: .5; cursor: default; }
.bd-recompute.working { color: var(--electric); border-color: var(--electric); }
/* the mark itself: inline-block so it can be turned at all, and centred so it turns on the spot */
.bd-spin { display: inline-block; line-height: 1; }
.bd-recompute.working .bd-spin { animation: bd-spin 900ms linear infinite; }
@keyframes bd-spin { to { transform: rotate(360deg); } }
.bd-sec { margin-top: 12px; }
.bd-sec-head {
  display: flex; align-items: center; gap: 8px; padding-bottom: 5px;
  border-bottom: 1px solid var(--hair);
}
.bd-sec-name { font-size: 11px; letter-spacing: .04em; color: var(--txt-1); }
.bd-empty { font-size: 10.5px; color: var(--txt-2); padding: 6px 0; }
/* EVERY LINE KEEPS ITS TYPE'S COLOUR even though the trade's lines are one list — a delivery is one
   docket and several products, and the colour is what says which. */
.bd-line {
  display: flex; align-items: baseline; gap: 10px; padding: 3px 0; color: var(--t);
}
.bd-prod { font-family: var(--font-m); font-size: 10.5px; opacity: .92; }
/* THE COUNTS MAKE A COLUMN, which is the point of putting them last: they sit on the panel's right
   edge whatever the product beside them is called, and a delivery can be checked off down it. */
.bd-qty {
  margin-left: auto; font-family: var(--font-m); font-size: 11.5px; min-width: 34px;
  text-align: right;
}
.bd-flag {
  font-size: 10px; line-height: 1.35; color: var(--flag); padding: 4px 0 0;
}
.bd-customise {
  margin-top: 8px; font-family: var(--font-d); font-size: 10.5px; color: var(--txt-1);
  background: var(--ink-1); border: 1px solid var(--hair-strong); border-radius: 6px;
  padding: 4px 12px; cursor: pointer;
}
.bd-customise:hover { color: var(--txt-0); border-color: var(--electric); }

/* ── the customise dialogue ── */
.bd-modal {
  position: fixed; inset: 0; z-index: 60; display: grid; place-items: center;
  background: color-mix(in oklab, var(--ink-0) 72%, transparent);
}
.bd-modal.hidden { display: none; }
.bd-card {
  width: min(660px, 94vw); max-height: 88vh; display: flex; flex-direction: column;
  background: var(--ink-1); border: 1px solid var(--hair-strong); border-radius: 12px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .5);
}
.bd-top {
  display: flex; align-items: center; padding: 14px 16px 10px; border-bottom: 1px solid var(--hair);
}
.bd-heading { font-size: 13px; letter-spacing: .03em; color: var(--txt-0); }
.bd-top .review-modal-x { margin-left: auto; }
.bd-body { overflow: auto; padding: 6px 16px 12px; }
.bd-foot { border-top: 1px solid var(--hair); padding: 12px 16px; }

.bd-mrow { padding: 12px 0; border-bottom: 1px solid var(--hair); }
.bd-mrow.folded { padding: 7px 0; }
.bd-mrow-head { display: flex; align-items: center; gap: 10px; }
.bd-mrow-name { font-size: 11.5px; letter-spacing: .04em; color: var(--t); }
.bd-mrow-prod { font-size: 10px; color: var(--txt-2); }
/* ONE GRID FOR THE HEADING ROW AND EVERY PRODUCT ROW. Given a grid each, the two sized their own
   columns and no heading sat over the thing it names: the heading row's quantity column is as wide
   as the word QTY, a product row's is as wide as a stepper, so the two disagreed by the difference
   and PRODUCT drifted with them. `display: contents` drops the row boxes and lets every cell share
   one set of tracks, which is also what keeps them lined up when a stepper or a heading changes
   size. The row gap replaces the padding those dropped boxes carried. */
.bd-table {
  margin: 8px 0 6px;
  display: grid; grid-template-columns: 26px minmax(0, 1fr) auto; gap: 6px 10px;
  align-items: center;
}
/* WHAT THE BOARD IS, THEN HOW MANY OF IT — the same reading order as the side panel, so the two
   lists of one order are checked the same way round. The product takes the width it needs; the
   quantity takes what a stepper is wide and holds the right-hand edge, with the ✕ at the far end
   of the row from the stepper's +. */
.bd-th, .bd-tr { display: contents; }
/* THE FIGURES ARE READ UP THE RIGHT-HAND EDGE — the board, the count and the sums they make all
   settle against it, so a column is scanned down rather than hunted across.
   A HEADING SITS OVER THE MIDDLE OF WHAT IT NAMES. Aligned to an edge instead, each one drifted to
   an end of its column — PRODUCT stranded far left of a right-aligned dropdown, QTY out past the
   stepper — and a heading nowhere near its column labels nothing. */
.bd-th > :nth-child(2), .bd-th > :nth-child(3) { justify-content: center; text-align: center; }
.bd-step { justify-content: flex-end; text-align: right; }
/* THE HEADINGS ARE STILL STYLED FROM HERE. A box that is `display: contents` has no box of its
   own, but it is still in the tree its children inherit from — what it cannot carry any more is
   the padding, which the grid's row gap now provides. */
.bd-th { font-size: 9px; letter-spacing: .08em; text-transform: uppercase; color: var(--txt-2); }
.bd-step { display: flex; align-items: center; gap: 4px; }
.bd-pm {
  width: 20px; height: 20px; border-radius: 5px; border: 1px solid var(--hair-strong);
  background: var(--ink-0); color: var(--txt-1); cursor: pointer; line-height: 1;
}
.bd-pm:hover { border-color: var(--electric); color: var(--txt-0); }
.bd-num {
  width: 46px; text-align: center; font-family: var(--font-m); font-size: 11px;
  color: var(--txt-0); background: var(--ink-0); border: 1px solid var(--hair);
  border-radius: 5px; padding: 2px 4px;
}
.bd-sizesel {
  font-family: var(--font-m); font-size: 10.5px; color: var(--txt-0); background: var(--ink-0);
  border: 1px solid var(--hair); border-radius: 5px; padding: 2px 4px; width: 100%;
  text-align: right;
}

/* A FIGURE INSIDE A TYPE'S SECTION WEARS THAT TYPE'S COLOUR, exactly as the same figure does in the
   side panel — the quantity, the board it names, and the three numbers that section adds up to.
   `--t` is the type's READ colour, which is what all of these are: text on
   a dark ground, not a filled surface. Scoped to `.bd-mrow`, so the totals in the footer stay
   neutral: they are the sum ACROSS the types and belong to none of them. */
.bd-mrow .bd-num,
.bd-mrow .bd-sizesel,
.bd-mrow .bd-msum .bd-stat-v { color: var(--t); }
.bd-del {
  width: 22px; height: 22px; border: 0; background: transparent; color: var(--txt-2);
  cursor: pointer; border-radius: 5px;
}
.bd-del:hover { color: var(--flag); background: color-mix(in oklab, var(--flag) 12%, transparent); }
.bd-add {
  display: block; margin-left: auto; width: fit-content;
  font-family: var(--font-d); font-size: 10px; color: var(--txt-2); background: transparent;
  border: 1px dashed var(--hair-strong); border-radius: 6px; padding: 3px 10px; cursor: pointer;
}
.bd-add:hover:not(:disabled) { color: var(--txt-0); border-color: var(--electric); }
.bd-add:disabled { opacity: .4; cursor: not-allowed; }

.bd-msum, .bd-totals { display: flex; gap: 26px; margin-top: 8px; flex-wrap: wrap; }
/* A SUMMARY SITS UNDER THE COLUMN IT SUMS. The quantities are on the right-hand edge now, so every
   figure they add up to is read from there — a type's own three, and the footer's totals across
   every type, in one line down the right of the dialogue with the buttons under them. */
.bd-msum, .bd-totals { justify-content: flex-end; text-align: right; }
.bd-stat-l {
  font-size: 9px; letter-spacing: .08em; text-transform: uppercase; color: var(--txt-2);
}
.bd-stat-v { font-family: var(--font-m); font-size: 12px; color: var(--txt-0); margin-top: 2px; }
.bd-totals { margin-bottom: 12px; }
.bd-acts { display: flex; gap: 10px; justify-content: flex-end; }
.bd-btn {
  font-family: var(--font-d); font-size: 11px; color: var(--txt-1); background: var(--ink-0);
  border: 1px solid var(--hair-strong); border-radius: 7px; padding: 6px 16px; cursor: pointer;
}
.bd-btn:hover:not(:disabled) { color: var(--txt-0); border-color: var(--electric); }
.bd-btn.primary { color: var(--ink-0); background: var(--electric); border-color: var(--electric); }
.bd-btn:disabled { opacity: .5; cursor: default; }

/* THE EXTERNAL PERIMETER WALL on the Walls surface (js/wallsstage.js `drawPerimeterWall`).
   Deep purple against the partitions' electric blue, so a run that is already walled reads as one
   colour or the other and a run that is not reads as neither. The palette's own deep purple, but
   under its own name: `--it-solid` is Noiseline in the Boards view, and these are not the same
   fact about a wall.
   Width comes from the drawing, not from here — it is the wall's true 110 mm at the sheet's own
   scale, which nothing in a stylesheet can know. */
.ov-extwall-run {
  fill: none;
  stroke: var(--extwall);
  stroke-width: var(--extwall-pt);
  stroke-linejoin: miter;
  opacity: 0.7;
}
