:root {
  --bg: #0f1216;
  --panel: #171b22;
  --panel-2: #1e242d;
  --line: #2a323d;
  --text: #e6edf3;
  --muted: #8b98a9;
  --accent: #4c9aff;
  --ok: #3fb950;
  --warn: #d29922;
  --err: #f85149;
  /* Named once, reused everywhere it appeared as a bare hex before (active table/list
     rows — .job-row, .condition-row, .sheet) — same color, just no longer a magic
     number duplicated six separate times across the file. */
  --active-bg: #223049;
  color-scheme: dark;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
/* Every hover/active state change in this app used to snap instantly — this is the
   single highest-leverage "feels considered" fix: a fast, consistent transition on the
   properties that actually change color/shadow on interaction. Transitions only affect
   HOW a change animates, never the resting-state appearance, so this can't alter any
   existing layout. */
button, input, select, textarea,
.tool-btn, .btn, .chevron-btn, .toggle, .jf-node, .job-row, .condition-row, .sheet,
.cat-tab, .jobdetail-tab, .cat-row-clickable, .folder-card, .project-card, .planset-row,
.status-select, .cat-chip {
  transition: background-color .15s ease, border-color .15s ease, color .15s ease,
    box-shadow .15s ease, opacity .15s ease, filter .15s ease;
}
/* One shared focus treatment for every text input/select/textarea in the app, instead of
   the ~15 near-identical ":focus { outline:none; border-color:var(--accent) }" blocks
   scattered through this file each independently deciding how focus looks. Those still
   set border-color (same value, no conflict) — this just layers a soft glow on top of
   all of them uniformly, so focus reads as one deliberate, consistent design decision.
   :not([type=checkbox]) — a checkbox's own accent-color ring is a better fit than a glow
   built for a rectangular field. */
input:not([type="checkbox"]):focus, select:focus, textarea:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
/* Thin, dark, unobtrusive scrollbars — the OS-default scrollbar (wide, light-chrome on
   most platforms) is the single most visible "this wasn't designed as a dark app" tell
   in a UI this scroll-heavy (sheet list, conditions list, every modal, every catalog
   table). Firefox + Chromium/WebKit both covered. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--line); border-radius: 20px; border: 2px solid transparent; background-clip: padding-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: padding-box; }
body {
  font: 14px/1.4 -apple-system, Segoe UI, Roboto, sans-serif;
  background: var(--bg); color: var(--text);
  display: flex; flex-direction: column; height: 100vh; overflow: hidden;
}
/* Wraps rather than clipping: ~20 controls need ~1600px on a project page, so on anything
   narrower than a wide desktop monitor (a 13" laptop at 1440, or a remote-desktop session
   showing a laptop's resolution) the right-hand end — Log out included — used to spill off
   the edge with no way to reach it. min-height instead of a hard height so a wrapped second
   row is actually visible; on a wide screen nothing wraps and this looks identical to before. */
#topbar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 0 14px; min-height: 48px; background: var(--panel);
  border-bottom: 1px solid var(--line); flex: none;
}
.brand { font-weight: 600; letter-spacing: .2px; }
.brand span { color: var(--muted); font-weight: 400; }
/* Holds the full PDF filename + page/dpi/processed stats — real filenames run long enough
   ("…SCHEMATIC DESIGN_07_ARCHITECTURAL.pdf") that unconstrained it wrapped to three lines
   and bled out of the bar over the toolbar below. Truncate; the full text is on hover. */
.project-meta {
  color: var(--muted); font-size: 12.5px;
  max-width: 260px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* "⋯ More" dropdown — holds the occasional tools (Bid summary / Catalogs / Colors / Users)
   so they cost one button's width in the bar instead of four. The menu is anchored to its
   own button rather than the bar, so it lands in the right place even after the topbar
   wraps to a second row on a narrow screen. */
.topbar-more { position: relative; flex: none; }
.topbar-more-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 60;
  display: flex; flex-direction: column; gap: 4px; padding: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0,0,0,.5); min-width: 180px;
}
.topbar-more-menu[hidden] { display: none; }
.topbar-more-menu .toggle { text-align: left; width: 100%; }
.topbar-more-btn-open { color: var(--accent); border-color: var(--accent); }
.topbar-context { display: flex; align-items: center; }
.back-link { color: var(--accent); text-decoration: none; font-size: 13px; }
.back-link:hover { text-decoration: underline; }
.spacer { flex: 1; }
.user-info { color: var(--muted); font-size: 12.5px; }
.upload-btn {
  background: var(--accent); color: #fff; padding: 7px 12px; border-radius: 6px;
  cursor: pointer; font-size: 13px; font-weight: 500; border: none; font-family: inherit;
}
.upload-btn:hover { filter: brightness(1.08); }
.toggle {
  padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px;
  cursor: pointer; font-size: 12.5px; color: var(--muted); user-select: none;
  background: transparent; font-family: inherit;
}
.toggle:hover { color: var(--text); border-color: var(--muted); }
.revision-switch {
  padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px;
  cursor: pointer; font-size: 12.5px; color: var(--muted); background: transparent; font-family: inherit; max-width: 220px;
}
.revision-diff-overlay { pointer-events: none; }
.topbar-sheet-switcher { display: flex; align-items: center; gap: 4px; }
.topbar-sheet-switcher[hidden] { display: none; } /* [hidden] and the class rule above tie on specificity — this must come after to win, same gotcha as .scale-controls */
.topbar-sheet-select {
  padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px;
  cursor: pointer; font-size: 12.5px; color: var(--text); background: var(--panel-2);
  font-family: inherit; max-width: 260px;
}
.toggle.on { color: var(--accent); border-color: var(--accent); }

/* --- Home / workspace view --- */
#home {
  flex: 1; display: none; flex-direction: column; min-height: 0; overflow-y: auto;
  padding: 20px 28px;
}
.breadcrumb { font-size: 13px; color: var(--muted); margin-bottom: 16px; }
.breadcrumb a { color: var(--muted); text-decoration: none; }
.breadcrumb a:last-child { color: var(--text); font-weight: 600; }
.breadcrumb a:hover { color: var(--accent); }
.crumb-sep { margin: 0 4px; }
.home-actions { display: flex; gap: 10px; margin-bottom: 20px; }
.folder-list, .project-list { display: flex; flex-wrap: wrap; gap: 12px; }
.folder-list:not(:empty) { margin-bottom: 22px; }
.folder-card, .project-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  padding: 12px 14px; cursor: pointer; min-width: 160px;
}
.folder-card:hover, .project-card:hover { border-color: var(--accent); background: var(--panel-2); }
.project-name { font-weight: 600; font-size: 13.5px; margin-bottom: 4px; }
.project-sub { color: var(--muted); font-size: 11.5px; }
.empty-hint { color: var(--muted); font-size: 13px; }

/* --- "Where is this used" catalog reverse lookup --- */
.cat-usage { margin: 4px 0 10px; font-size: 12.5px; }
.cat-usage summary { cursor: pointer; color: var(--muted); }
.cat-usage summary:hover { color: var(--text); }
.cat-usage-job { padding: 6px 0 6px 14px; border-left: 2px solid var(--line); margin-top: 6px; }
.cat-usage-job b { font-size: 12.5px; }
.cat-usage-conds { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.cat-usage-cond {
  display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 5px; padding: 2px 7px; text-decoration: none;
}
.cat-usage-cond:hover { color: var(--text); border-color: var(--accent); }

/* --- Jobs workspace --- */
.jobs-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
.jobs-header h1 { font-size: 20px; margin: 0; }
.jobs-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.jobs-search {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 7px 10px; color: var(--text); font-size: 13px; font-family: inherit; min-width: 240px;
}
.jobs-search:focus { outline: none; border-color: var(--accent); }
.jobs-filter {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 7px 10px; color: var(--text); font-size: 13px; font-family: inherit; cursor: pointer;
}
/* Folder tree sidebar (OST roadmap #5) — All jobs / My jobs / per-estimator / Deleted
   Bids / Templates, replacing the old flat table + "Group by estimator" toggle. */
.jobs-shell { display: flex; gap: 20px; align-items: flex-start; }
.jobs-sidebar { flex: none; width: 200px; display: flex; flex-direction: column; gap: 1px; }
.jobs-main { flex: 1; min-width: 0; }
.jf-node {
  padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 13px;
  color: var(--muted); display: flex; justify-content: space-between; align-items: center;
}
.jf-node:hover { background: var(--panel-2); color: var(--text); }
.jf-node.active { background: var(--panel-2); color: var(--accent); font-weight: 600; }
.jf-section {
  padding: 10px 10px 4px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); opacity: .7;
}
.jf-count {
  color: var(--muted); font-weight: 500; font-size: 11px;
  background: var(--panel); border-radius: 10px; padding: 0 7px; min-width: 18px; text-align: center;
}
.jf-node.active .jf-count { color: var(--accent); }
.jf-row-actions { display: flex; gap: 6px; justify-content: flex-end; }

.jobs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.jobs-table thead th {
  text-align: left; color: var(--muted); font-weight: 500; font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .03em; padding: 8px 12px; border-bottom: 1px solid var(--line);
}
.jobs-table tbody td { padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.job-row { cursor: pointer; }
.job-row:hover td { background: var(--panel-2); }
.jobs-table th.sortable { cursor: pointer; user-select: none; }
.jobs-table th.sortable:hover { color: var(--text); }
.jobs-table th.sorted { color: var(--accent); }
.jobs-table tfoot .jobs-foot td {
  border-top: 2px solid var(--line); border-bottom: none; color: var(--muted);
  font-size: 12px; padding-top: 10px;
}
.jobs-table tfoot .jobs-foot .job-total { color: var(--text); }
/* Inline status dropdown in a table row — styled to read like the badge it replaces. */
.status-select {
  border: 1px solid var(--line); border-radius: 20px; padding: 2px 6px; font-size: 11px;
  font-weight: 600; font-family: inherit; cursor: pointer; color: var(--text);
  background: var(--panel-2); max-width: 110px;
}
.status-select:focus { outline: none; border-color: var(--accent); }
.status-select.s-active     { color: #7fd4ff; }
.status-select.s-bidding    { color: var(--warn); }
.status-select.s-submitted  { color: #c9a5ff; }
.status-select.s-won        { color: var(--ok); }
.status-select.s-lost       { color: var(--err); }
.status-select.s-archived   { color: var(--muted); }
.job-num { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.job-name { font-weight: 600; }
.job-sub { color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.job-sub .ingesting { color: var(--warn); }
.job-total { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
.job-updated { color: var(--muted); font-size: 11.5px; white-space: nowrap; }

.status-badge {
  display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; white-space: nowrap;
}
.status-badge.s-active     { color: #7fd4ff; background: #10314a; }
.status-badge.s-bidding    { color: var(--warn); background: #3a2f12; }
.status-badge.s-submitted  { color: #c9a5ff; background: #2c1f45; }
.status-badge.s-won        { color: var(--ok); background: #12331c; }
.status-badge.s-lost       { color: var(--err); background: #3a1616; }
.status-badge.s-archived   { color: var(--muted); background: var(--panel-2); }

.job-lock-badge {
  display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600;
  white-space: nowrap; color: var(--warn); background: #3a2f12; cursor: help;
}

/* Job detail */
.jobdetail-top { margin-bottom: 10px; }
.jobdetail-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 22px; flex-wrap: wrap; }
.jobdetail-title { font-size: 20px; font-weight: 600; }
.jd-num { color: var(--muted); font-weight: 500; }
.jobdetail-meta { color: var(--muted); font-size: 12.5px; margin-top: 6px; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.jobdetail-notes { color: var(--text); font-size: 12.5px; margin-top: 8px; white-space: pre-wrap; max-width: 640px; padding: 8px 10px; background: var(--panel-2); border-radius: 6px; border: 1px solid var(--line); }
.jobdetail-total { margin-top: 8px; font-size: 13px; }
.jobdetail-wrap { margin-top: 8px; font-size: 12.5px; }
.jobdetail-wrap label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.jobdetail-wrap input[type="number"] { padding: 2px 4px; }
.modal .field textarea { resize: vertical; min-height: 40px; }
.jobdetail-actions { display: flex; gap: 8px; }
.tool-btn.danger, .btn.danger { color: var(--err); border-color: #5a2020; }
.tool-btn.danger:hover, .btn.danger:hover { background: #3a1616; }

/* Job detail tabs — Plan sets / Estimate / Info. Same visual language as .cat-tabs in the
   catalogs modal so the app has one tab idiom, not two. Wraps, so a future Alternates /
   Buyout / Change Orders tab doesn't push the strip off a laptop screen. */
.jobdetail-tabs {
  display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px;
  border-bottom: 1px solid var(--line); padding-bottom: 10px;
}
.jobdetail-tab {
  padding: 7px 14px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2);
  color: var(--muted); font-size: 13px; cursor: pointer; font-family: inherit;
}
.jobdetail-tab:hover { color: var(--text); border-color: var(--accent); }
.jobdetail-tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.jobtab-count {
  font-size: 11px; background: rgba(0,0,0,.25); border-radius: 9px; padding: 1px 6px; margin-left: 2px;
}
.jobdetail-tabpanel { min-height: 120px; }
/* Info tab: label/value pairs rather than the old one-line "a · b · c" run-on. */
.jobinfo-list {
  display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px;
  margin: 0 0 12px; font-size: 13px; max-width: 640px;
}
.jobinfo-list dt { color: var(--muted); }
.jobinfo-list dd { margin: 0; color: var(--text); }
.planset-section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.planset-section-head h2 { font-size: 15px; margin: 0; }
.disc-group { margin-bottom: 18px; }
.disc-head { font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); margin-bottom: 6px; }
.planset-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px; margin-bottom: 6px;
}
.planset-name { font-weight: 600; font-size: 13.5px; }
.rev-tag { font-size: 11px; color: var(--accent); border: 1px solid var(--line); border-radius: 4px; padding: 0 5px; margin-left: 6px; }
.planset-sub { color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.planset-actions { display: flex; gap: 6px; align-items: center; }

/* Admin: Users panel */
.users-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.users-table thead th {
  text-align: left; color: var(--muted); font-weight: 500; font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .03em; padding: 6px 10px; border-bottom: 1px solid var(--line);
}
.users-table tbody td { padding: 8px 10px; border-bottom: 1px solid var(--line); }
.role-badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; color: var(--muted); background: var(--panel-2); }
.role-badge.admin { color: var(--accent); background: #10314a; }
.you-tag { color: var(--muted); font-size: 11px; font-weight: 500; }

/* --- Estimating catalogs (cost codes, materials, wages, crews, labor, equipment, suppliers) --- */
.cat-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; border-bottom: 1px solid var(--line); padding-bottom: 10px; }
.cat-tab {
  padding: 6px 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2);
  color: var(--muted); font-size: 12.5px; cursor: pointer; font-family: inherit;
}
.cat-tab:hover { color: var(--text); border-color: var(--accent); }
.cat-tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.cat-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.cat-table thead th {
  text-align: left; color: var(--muted); font-weight: 500; font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .03em; padding: 6px 10px; border-bottom: 1px solid var(--line);
}
.cat-table tbody td { padding: 7px 10px; border-bottom: 1px solid var(--line); }
.cat-table th.sortable { cursor: pointer; user-select: none; }
.cat-table th.sortable:hover { color: var(--text); }
.cat-table th.sorted { color: var(--accent); }
.cat-table tr.wage-main-row td { border-bottom: none; }
.cat-table tr.wage-meta-row td { padding: 0 10px 7px; border-bottom: 1px solid var(--line); font-size: 11px; color: var(--muted); }
.cat-row-inactive { opacity: .45; }
.cat-row-clickable { cursor: pointer; }
.cat-row-clickable:hover td { background: var(--panel-2); }
.cat-subactions { display: flex; gap: 8px; margin-top: 10px; }
.cat-chiprow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12.5px; }
.cat-chip {
  display: inline-flex; align-items: center; gap: 2px; padding: 3px 4px 3px 10px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 14px;
}
.cat-chip .chevron-btn { padding: 2px 5px; }
.cat-chip-inactive { opacity: .5; }
.wage-grid-table th, .wage-grid-table td { white-space: nowrap; padding: 5px 6px; }
.wage-grid-table input[type="number"] {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 4px;
  padding: 4px 5px; color: var(--text); font-size: 12px; font-family: inherit;
}
.wage-grid-table input[type="number"]:focus { outline: none; border-color: var(--accent); }
.items-grid-table th, .items-grid-table td { white-space: nowrap; padding: 5px 6px; }
.items-grid-table input[type="text"], .items-grid-table input[type="number"], .items-grid-table select {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 4px;
  padding: 4px 5px; color: var(--text); font-size: 12px; font-family: inherit;
}
.items-grid-table input:focus, .items-grid-table select:focus { outline: none; border-color: var(--accent); }
/* Repeatable row patterns for wage-rate components and crew members (same add/remove-row
   shape conditions.js uses for a condition's heights list). A wage-rate row has 7 fields
   (name/kind/amount/base/insurance/wrap%/remove) — too many for one line on anything but a
   very wide screen, so it WRAPS onto multiple lines (name gets its own line; the rest flow
   below) rather than requiring horizontal scroll to see fields that got clipped off-screen. */
.wr-row, .cr-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
.wr-row .wr-name { flex: 1 1 100%; min-width: 160px; }
.wr-row .wr-kind { width: 110px; flex: none; }
.wr-row .wr-amount { width: 90px; flex: none; }
.wr-row .wr-wrapamount { width: 80px; flex: none; }
.cr-row .cr-class { flex: 1; }
.cr-row .cr-qty { width: 70px; }
.wr-row input[type="text"], .wr-row input[type="number"], .wr-row select,
.cr-row select, .cr-row input {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 6px 8px; color: var(--text); font-size: 13px; font-family: inherit;
}
.wr-legend {
  display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 8px 10px; margin: 6px 0;
}
.wr-legend b { color: var(--text); }
.wr-legend-dot, .wr-legend-check { color: var(--accent); font-weight: bold; }
.wr-isbase-label {
  display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted);
  white-space: nowrap; flex: none; cursor: pointer;
  border: 1px solid var(--line); border-radius: 6px; padding: 5px 8px;
  background: var(--panel-2);
}
.wr-isbase-label input { cursor: pointer; accent-color: var(--accent); width: 14px; height: 14px; }
.wr-isbase-label:has(input:checked) {
  border-color: var(--accent); background: color-mix(in srgb, var(--accent) 18%, var(--panel-2));
  color: var(--text);
}
.wr-baserow {
  background: color-mix(in srgb, var(--accent) 10%, var(--panel-2));
  border: 1px solid var(--accent); border-radius: 6px; padding: 8px 10px;
}
.wr-base-tag {
  font-size: 12px; font-weight: bold; color: var(--accent); flex: none; white-space: nowrap;
}
.wr-row select:disabled { opacity: 0.7; cursor: default; }
.wr-remove, .cr-remove { cursor: pointer; color: var(--muted); padding: 2px 6px; flex: none; }
.wr-remove:hover, .cr-remove:hover { color: var(--err); }
#wr-results { font-size: 13px; background: var(--panel-2); border-radius: 6px; padding: 10px 12px; }

/* Job modal: two fields side by side */
.field-row { display: flex; gap: 12px; }
.field-row .field { flex: 1; }
.field select {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 8px 10px; color: var(--text); font-size: 13.5px; font-family: inherit;
}
.field select:focus { outline: none; border-color: var(--accent); }

/* Modal list-chooser (revision pickers) — vertical stack of full-width choice buttons. */
.choose-list { display: flex; flex-direction: column; gap: 8px; margin: 12px 0; }
.choose-option { text-align: left; }

/* --- Login page --- */
.login-body {
  display: flex; align-items: center; justify-content: center; height: 100vh;
  background: var(--bg); color: var(--text); margin: 0;
  font: 14px/1.4 -apple-system, Segoe UI, Roboto, sans-serif;
}
.login-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 28px 30px; width: 320px; display: flex; flex-direction: column; gap: 12px;
}
.login-card h1 { font-size: 18px; margin: 0 0 4px; }
.login-error { color: var(--err); font-size: 12.5px; min-height: 1em; }
.field { display: flex; flex-direction: column; gap: 5px; font-size: 12.5px; color: var(--muted); }
.req-mark { color: var(--err); font-weight: 700; }
.field input {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 8px 10px; color: var(--text); font-size: 13.5px; font-family: inherit;
}
.field input:focus { outline: none; border-color: var(--accent); }
.field textarea {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 8px 10px; color: var(--text); font-size: 13.5px; font-family: inherit; resize: vertical;
}
.field textarea:focus { outline: none; border-color: var(--accent); }
.checkbox-row { display: flex; align-items: center; gap: 7px; flex-direction: row; margin-top: 4px; }
.checkbox-row input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--accent); }
.field-hint { font-size: 11.5px; color: var(--muted); margin: 2px 0 0; line-height: 1.4; }
.login-submit { margin-top: 6px; text-align: center; }
.switch-link { color: var(--muted); font-size: 12.5px; text-align: center; text-decoration: none; }
.switch-link:hover { color: var(--accent); }

/* #main is now a column: the toolbar and the contextual selection bar span the full
   application width (OST docks its toolbars above the panels, not inside the image
   window), and #main-row below them is the old horizontal dock-left / viewer / dock-right
   arrangement. main.js sets an inline display:flex on #main, so only the direction and the
   inner row need declaring here. */
#main { flex: 1; display: none; flex-direction: column; min-height: 0; position: relative; }
#main-row { flex: 1 1 auto; display: flex; min-height: 0; }

/* --- Dockable panels (Sheets, Conditions) ---
   Each panel (see panels.js) is either docked into #dock-left / #dock-right — a plain
   flex column that collapses to nothing when empty, so the viewer fills whatever space
   isn't claimed — or floating: pulled into #floating-layer and absolutely positioned/
   sized from its saved state, sitting on top of the viewer like the quick-link popup
   already does. Two panels docked to the same side just stack (flex: 1 1 0 each). */
/* 330. This went 320 → 380 to buy 16 more characters of a partition name, and that was
   the wrong trade: on a takeoff tool the PLAN is the work surface, and two 380px zones
   left it 840px of a 1600px window — 53%, where OST gives its image window ~74%. The
   description's second line (which the active row now unclamps to) is what rescues a
   100-character name, not another 50px of width: at 330 the compact row still shows ~60
   characters and the selected row still shows all 102. The resizer below moves it in one
   drag if a particular job wants the width back. */
.dock-zone {
  position: relative;
  display: flex; flex-direction: column; width: 330px; flex: none;
  background: var(--panel); min-height: 0;
}
/* :empty no longer works as "has no panel" — the zone permanently holds a resize grip
   (initDockResizers). Presence of an actual .dock-panel child is the real test. */
.dock-zone:not(:has(.dock-panel)) { display: none; }
/* While any panel is being dragged, show empty zones as a thin strip so there's
   something to actually see and aim for — otherwise a hidden (width:0) drop target
   would be invisible until the exact instant you release the pointer over it. */
body.dragging-panel .dock-zone:not(:has(.dock-panel)) { display: flex; width: 14px; background: var(--panel-2); }
/* Drag the zone's INNER edge (the one facing the plan) to trade panel width against
   viewer width — a 100-character partition name needs more than a hard-coded 320px. */
.dock-zone-resize {
  position: absolute; top: 0; bottom: 0; width: 6px; z-index: 5;
  cursor: col-resize; touch-action: none;
}
#dock-left .dock-zone-resize { right: -2px; }
#dock-right .dock-zone-resize { left: -2px; }
.dock-zone-resize:hover { background: var(--accent); opacity: .5; }

/* Auto-hide (panels.js applyDockAutoHide): the column shrinks to a rail and its panels
   become an overlay that flies out over the plan on hover. The width is forced with
   !important because the resize grip writes an inline width onto the same element. */
.dock-zone-pin {
  position: absolute; top: 6px; z-index: 7; width: 16px; height: 22px; padding: 0;
  border: 1px solid var(--line); border-radius: 4px; background: var(--panel-2);
  color: var(--muted); font-size: 11px; line-height: 1; cursor: pointer; font-family: inherit;
}
.dock-zone-pin:hover { color: var(--accent); border-color: var(--accent); }
#dock-left .dock-zone-pin { right: 4px; }
#dock-right .dock-zone-pin { left: 4px; }
.dock-zone.auto-hide { width: 13px !important; background: var(--panel-2); overflow: visible; z-index: 58; }
.dock-zone.auto-hide .dock-zone-resize { display: none; }
.dock-zone.auto-hide .dock-zone-pin { top: 50%; transform: translateY(-50%); right: -1px; left: auto; }
#dock-right.dock-zone.auto-hide .dock-zone-pin { left: -1px; right: auto; }
/* The flyout panels stay in the zone's normal flex flow — they are simply wider than the
   13px rail and allowed to overflow it. Absolute positioning was the obvious alternative
   and it breaks the moment two panels share a column: both would land at top:0 and one
   would bury the other, where flex still stacks them. Hidden by visibility rather than
   display so the sheet list keeps its layout box and its scroll position between hovers. */
.dock-zone.auto-hide .dock-panel {
  width: 320px; visibility: hidden; flex: 1 1 0;
  box-shadow: 0 6px 28px rgba(0,0,0,.55); border: 1px solid var(--line);
}
#dock-right.auto-hide .dock-panel { margin-left: -307px; }
/* :hover on the zone covers the flyout too — the panels are descendants, overflowing or not.
   A 13px target sounds thin and isn't: the rail is flush with the window's own edge, so
   throwing the cursor left always lands on it, and once the flyout is up it keeps itself up. */
.dock-zone.auto-hide:hover .dock-panel { visibility: visible; }
body.dock-hint-left #dock-left,
body.dock-hint-right #dock-right { outline: 2px solid var(--accent); outline-offset: -2px; }

#floating-layer { position: fixed; inset: 0; pointer-events: none; z-index: 55; }

.dock-panel {
  display: flex; flex-direction: column; flex: 1 1 0; min-height: 0; overflow: hidden;
}
/* [hidden] and the class rule above tie on specificity and the class rule wins by
   cascade order — without this, main.js hiding the panels on the home view (where
   initPanel never runs and they're still bare <body> children) would do nothing.
   Same recurring gotcha as .scale-controls / .topbar-sheet-switcher. */
.dock-panel[hidden] { display: none; }
.dock-zone .dock-panel + .dock-panel { border-top: 1px solid var(--line); }
.dock-panel.floating {
  position: absolute; pointer-events: auto; border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0,0,0,.5); background: var(--panel); z-index: 56;
}
.dock-panel.dragging { opacity: .85; z-index: 70; }
/* 4px of padding, not 8: this bar and the control strip beneath it sit between the top of
   the panel and the first condition, and every pixel of them is a pixel the list doesn't
   get. The panel is one of two things on screen an estimator reads all day; its title is
   not. */
.dock-panel-header {
  display: flex; align-items: center; gap: 3px; padding: 4px 8px; cursor: grab;
  background: var(--panel-2); border-bottom: 1px solid var(--line); flex: none;
  user-select: none; touch-action: none;
}
.dock-panel-header:active { cursor: grabbing; }
.dock-panel-title { flex: 1; font-size: 11.5px; font-weight: 600; color: var(--muted); }
.dock-panel-header .chevron-btn { padding: 0 5px; height: 20px; line-height: 18px; }
.dock-panel-header .tool-btn { padding: 1px 8px; height: 20px; font-size: 11.5px; }
.dock-panel-body { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.dock-panel-resize {
  position: absolute; right: 0; bottom: 0; width: 14px; height: 14px; cursor: nwse-resize; display: none;
}
.dock-panel.floating .dock-panel-resize { display: block; }

.progress {
  padding: 10px 12px; border-bottom: 1px solid var(--line); font-size: 12.5px; color: var(--muted);
}
.progress .bar { height: 6px; background: var(--panel-2); border-radius: 4px; margin-top: 8px; overflow: hidden; }
.progress .bar > i { display: block; height: 100%; background: var(--accent); width: 0; transition: width .3s; }
.chevron-btn {
  background: none; border: none; color: var(--muted); cursor: pointer; font-size: 13px;
  padding: 2px 6px; line-height: 1; border-radius: 4px;
}
.chevron-btn:hover { color: var(--text); background: var(--panel-2); }
.chevron-btn.active { color: var(--accent); background: var(--panel-2); }
.sheetlist { overflow-y: auto; flex: 1 1 auto; min-height: 100px; }
/* Collapsed view: one row for whatever sheet is actually open, instead of the full
   scrollable list — toggled by #sheetlist-toggle. Reuses the exact same .sheet row
   markup/styling as the full list (viewer.js's buildSheetRow builds both), so there's
   nothing extra to keep visually in sync here. */
.sheetlist-current { display: none; flex: none; }
#sheets-panel.sheets-collapsed .sheetlist { display: none; }
#sheets-panel.sheets-collapsed .sheetlist-current { display: block; }

/* Pop-out window (?panel=1): just the conditions panel, filling the whole window —
   everything plan/sheet-related that boot() still initializes underneath (OSD viewer,
   toolbar, sheets panel) stays hidden rather than removed, so the pop-out is the same
   code path as the main window, not a second implementation to keep in sync. Dragging
   only makes sense within the main window (main.js skips initPanel() here entirely),
   so this forces the conditions panel to just fill the screen instead of whatever
   dock/float state it happened to have.  */
/* !important on #main specifically: main.js unconditionally sets an INLINE
   style="display:flex" on it (regardless of panel mode), which otherwise beats any
   stylesheet rule no matter the selector specificity. */
body.panel-mode #main { display: none !important; }
body.panel-mode #sheets-panel,
body.panel-mode #topbar-sheet-switcher,
body.panel-mode #topbar-conditions-switcher,
body.panel-mode #topbar-context,
body.panel-mode #revision-switch,
body.panel-mode #add-revision-btn,
body.panel-mode #ai-compare-btn,
body.panel-mode #carry-forward-btn,
body.panel-mode #project-meta,
body.panel-mode .search-box,
body.panel-mode #user-info,
body.panel-mode #topbar-more,
body.panel-mode #logout-btn { display: none; }
body.panel-mode #conditions-panel {
  position: static; width: auto; flex: 1 1 auto; min-height: 0;
  border: none; border-radius: 0; box-shadow: none;
}
.conditions-header {
  display: flex; align-items: center; justify-content: space-between; padding: 10px 12px;
  border-bottom: 1px solid var(--line); font-size: 12.5px; font-weight: 600; color: var(--muted); flex: none;
}
/* One 26px strip. Chrome above the column header used to run 137px of a 903px panel —
   title row, page-jump, filter, group-by, density, sort, and a wrapping row of layer
   pills — i.e. 31% of the list's own viewport spent on controls, in a window where only
   22 of 66 conditions fit. Everything that isn't the filter and the grouping either moved
   into the ⋮ menu or collapsed into the 👁 button. */
.conditions-controls { display: flex; gap: 4px; align-items: center; padding: 3px 6px; border-bottom: 1px solid var(--line); }
.conditions-controls[hidden] { display: none; }
.condition-search {
  flex: 1; min-width: 0; background: var(--panel-2); border: 1px solid var(--line); border-radius: 4px;
  padding: 2px 7px; color: var(--text); font-size: 11.5px; font-family: inherit; height: 20px;
}
.condition-search:focus { outline: none; border-color: var(--accent); }
.conditions-controls .cond-mini-select { padding: 1px 4px; font-size: 11px; height: 20px; }
.conditions-controls .chevron-btn { padding: 0 5px; font-size: 12px; height: 20px; line-height: 18px; }
.conditions-list { overflow-y: auto; flex: 1; }

/* --- Condition groups (OST's "grouped by Type by default" + Condition Folders) ---
   Sticky headers so the group you're scrolled into is always named, a count and a summed
   quantity so a folded group is still worth reading, and (in Folder mode) the header
   doubles as the drop target for filing a condition into it. */
/* 17px, not the 28.9px it was — a group header used to be exactly as tall as a data row,
   so the eight of them ate 231px of a 742px viewport (31%) to say seven words. Everything
   on the header sits on the caret's single 15px baseline: no vertical padding, no chip
   that grows the line, a smaller label than the rows it introduces. That alone is worth
   three more conditions on screen, and it also stops the header reading as a row. */
.cond-group-head {
  position: sticky; top: var(--colhead-h, 19px); z-index: 2;
  display: flex; align-items: center; gap: 5px; padding: 0 10px; cursor: pointer;
  height: 17px; line-height: 16px;
  background: var(--panel-2); border-bottom: 1px solid var(--line);
  font-size: 10px; font-weight: 700; color: var(--muted); user-select: none;
  text-transform: uppercase; letter-spacing: .3px;
}
.cond-group-head:hover { color: var(--text); }
.cond-group-caret { width: 8px; flex: none; font-size: 8px; }
.cond-group-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Still a chip so the count can't be misread as part of the summed total next to it — the
   fixed height on .cond-group-head is what keeps its padding from growing the row. */
.cond-group-count {
  flex: none; background: var(--line); color: var(--text); border-radius: 6px;
  padding: 0 5px; font-size: 9.5px; font-weight: 600; line-height: 13px; letter-spacing: 0;
}
.cond-group-total { flex: none; color: var(--text); font-size: 10px; font-variant-numeric: tabular-nums; letter-spacing: 0; }
.cond-group-sheet { flex: none; color: var(--accent); font-size: 9.5px; font-variant-numeric: tabular-nums; letter-spacing: 0; }
.cond-group-icon { flex: none; font-size: 10px; }
.cond-group.collapsed .cond-group-rows { display: none; }
.cond-group.drop-target .cond-group-head { background: var(--active-bg); box-shadow: inset 0 0 0 1px var(--accent); }
.condition-row.dragging { opacity: .45; }
/* Nested folder levels (a layer of "Walls/Interior/Rated" is three deep) indent by their
   own depth, headers and rows alike, so the tree reads as a tree. */
.cond-group-head { padding-left: calc(10px + var(--depth, 0) * 11px); }
.cond-group .condition-row { padding-left: calc(10px + var(--depth, 0) * 11px); }

/* --- The condition grid ---
   Header (#cond-colhead) and every row share the same track list via --cond-grid, set by
   render() from whichever of No./Height/Quantity are switched on. The last two tracks are
   a RESERVED action gutter (⚙ assembly + ⋯ menu): the previous hover-in cluster was
   absolutely positioned over the quantity and the ⚙ button, so elementFromPoint at the
   gear's own center hit ✕ Delete and the assembly editor was unreachable by mouse.

   The header is sticky INSIDE .conditions-list rather than a sibling above it. As a
   sibling it was laid out over the panel's full width while the rows lost the scrollbar's
   10px, so HEIGHT/QUANTITY printed their labels a scrollbar clear of their own numbers.
   Sharing the scroller's content box (and the rows' exact 10px side padding) means the
   two grids resolve to identical track positions at every panel width, with no JS.
   Group indent only ever eats into the 1fr Name track, so the right-aligned numeric
   columns stay aligned at every nesting depth too. */
.cond-colhead {
  position: sticky; top: 0; z-index: 3;
  display: grid; grid-template-columns: var(--cond-grid); gap: 6px; align-items: center;
  padding: 3px 10px; border-bottom: 1px solid var(--line); background: var(--panel-2);
  font-size: 9.5px; line-height: 12px; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .2px; user-select: none;
}
.cond-col { cursor: pointer; overflow: hidden; white-space: nowrap; }
.cond-col:hover { color: var(--text); }
.cond-col.r { text-align: right; }
.cond-col.sorted { color: var(--accent); }
.cond-sort-caret { font-size: 7px; margin-left: 2px; }
.cond-col-menu { cursor: pointer; text-align: center; }
.cond-col-menu:hover { color: var(--text); }

.condition-row {
  display: grid; grid-template-columns: var(--cond-grid); gap: 6px; align-items: center;
  padding: 4px 10px; border-bottom: 1px solid var(--line); cursor: pointer;
}
/* Compact was 38.4px a row, then 29.6px, and is now 25px — 66 conditions × 4.6px is a
   whole extra screenful. Our row legitimately carries two lines (the code chip line and
   the full-width description), so every pixel comes out of leading and padding rather
   than out of a line: 1px of vertical padding and an explicit 12.5px/10.5px pair of line
   boxes, which is the smallest that still leaves the descenders room at these sizes.
   Comfortable keeps the roomy 13px/1.25 sizing for anyone who wants it. */
.conditions-list.dense .condition-row { row-gap: 0; padding-top: 1px; padding-bottom: 1px; }
/* .condition-name is a baseline-aligned flex row, so it takes its leading from whatever
   line-height it INHERITS, not from its own font size — leaving the panel's 1.4 body
   leading on this cell is what made the tallest grid item 14.1px instead of 12.5px and
   cost ~1.6px on every one of the 66 rows. */
.conditions-list.dense .condition-info { line-height: 12.5px; }
.conditions-list.dense .condition-total { gap: 0; }
.conditions-list.dense .condition-swatch { width: 10px; height: 10px; border-radius: 2px; }
.conditions-list.dense .condition-name { font-size: 12px; line-height: 12.5px; }
/* border:0, not a transparent border — a transparent 1px border still occupies its 1px top
   and bottom, which made this chip the tallest thing on the row (14.1px against a 12.5px
   line) and set the height of all 66 of them. */
.conditions-list.dense .cond-code { font-size: 11px; border: 0; padding: 0 2px; line-height: 12.5px; }
.conditions-list.dense .cond-desc-full { font-size: 10px; line-height: 10.5px; }
.conditions-list.dense .cond-no,
.conditions-list.dense .cond-num { font-size: 10.5px; line-height: 12.5px; }
.conditions-list.dense .condition-assembly,
.conditions-list.dense .cond-more { line-height: 12.5px; }
/* The row you're actually working on is the one whose name you need in full, and there is
   only ever one of it — so the second description line is paid for once instead of 66
   times: the active/selected row unclamps, which fits all 102 characters of the longest
   partition name on the set, and the other 65 stay at one dense line. That's
   comfortable-mode legibility at compact-mode density, and it's why the list can be 25px
   a row without going back to showing ten characters of a name.
   Deliberately keyed to selection and NOT to :hover — a row that grows under the pointer
   shoves every row beneath it downward, so scanning the list by moving the mouse down it
   would make the list crawl away from you. Hover already shows the whole name in the
   row's title tooltip, which costs no layout at all.
   max-height rather than -webkit-line-clamp: this element is a grid item (grid-column:
   1/-1), and a grid item blockifies its display, so `display:-webkit-box` resolves to
   flow-root and the clamp silently does nothing — it was measured computing to
   `display: flow-root` on the live row. A max-height in whole line-boxes is the honest
   way to say "up to four lines" here, and unlike the clamp it leaves room for the last
   line's descenders instead of shaving a pixel off them. Four lines covers every
   partition name on the set (the 102-character worst case wraps to two at this width). */
.conditions-list.dense .condition-row.active .cond-desc-full,
.conditions-list.dense .condition-row.selected .cond-desc-full {
  /* Looser leading than the list's 10.5px, because this is the row being read rather than
     scanned — two pixels of row height is nothing when it is spent once. max-height is in
     whole line-boxes: four lines covers every name on the set (the 102-character worst
     case wraps to two at this width) without letting a pathological one push the list. */
  white-space: normal; max-height: 46px; line-height: 11.5px;
}
.condition-row:hover { background: var(--panel-2); }
.condition-row.active { background: var(--active-bg); }
.condition-row.selected { background: color-mix(in srgb, var(--accent) 22%, transparent); box-shadow: inset 2px 0 0 var(--accent); }
.condition-swatch { width: 14px; height: 14px; border-radius: 3px; flex: none; border: 1px solid rgba(255,255,255,.25); }
/* A point-count condition's swatch is the mark itself (see countSwatchSvg) — no chip
   border or fill, or the symbol would sit inside a box the plan doesn't draw. */
.condition-swatch.swatch-glyph { border: none; border-radius: 0; background: none; display: block; }
.condition-swatch.swatch-glyph svg { width: 14px; height: 14px; display: block; }
/* Second visual dimension for conditions that collide on a bank colour (assignSwatchPatterns
   hands each member of a same-colour group its own index). 20 colours × 6 patterns is 120
   tellable-apart swatches, so two identically-red rows are no longer indistinguishable —
   which is the whole job of the colour column. Patterns are overlaid on the inline
   background-color, so they read on every hue. */
.condition-swatch.sw-p1 { background-image: repeating-linear-gradient(45deg, rgba(0,0,0,.55) 0 2px, transparent 2px 4px); }
.condition-swatch.sw-p2 { background-image: repeating-linear-gradient(-45deg, rgba(255,255,255,.75) 0 2px, transparent 2px 4px); }
.condition-swatch.sw-p3 { background-image: repeating-linear-gradient(0deg, rgba(0,0,0,.55) 0 2px, transparent 2px 4px); }
.condition-swatch.sw-p4 { background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.75) 0 2px, transparent 2px 4px); }
.condition-swatch.sw-p5 { background-image: radial-gradient(rgba(0,0,0,.65) 1.1px, transparent 1.2px); background-size: 4px 4px; }
/* Seven is the largest same-colour group on a real 66-condition set, so the bank needs
   eight patterns (plain + these) for every swatch on such a job to be one of a kind. */
.condition-swatch.sw-p6 {
  background-image: repeating-linear-gradient(0deg, rgba(0,0,0,.5) 0 1.5px, transparent 1.5px 4px),
                    repeating-linear-gradient(90deg, rgba(0,0,0,.5) 0 1.5px, transparent 1.5px 4px);
}
.condition-swatch.sw-p7 { background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.8) 0 3px, transparent 3px 7px); }
.cond-no { font-size: 10.5px; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.cond-height { font-size: 11.5px; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cond-height:hover { color: var(--accent); }
.cond-height.unset { color: var(--muted); }
.cond-height-input {
  width: 100%; min-width: 0; text-align: right; font-size: 11px; font-family: inherit;
  background: var(--panel); border: 1px solid var(--accent); border-radius: 4px;
  color: var(--text); padding: 1px 3px; -moz-appearance: textfield;
}
.cond-height-input::-webkit-outer-spin-button,
.cond-height-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.condition-info { min-width: 0; }
.condition-name { display: flex; align-items: baseline; gap: 5px; min-width: 0; font-size: 13px; font-weight: 600; }
/* Comfortable wraps the code chip and its flags onto as many lines as they need; compact
   is deliberately one line, since the row height IS the feature there. */
.conditions-list:not(.dense) .condition-name { flex-wrap: wrap; align-items: center; }
.cond-desc-inline { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cond-meta { margin-left: auto; flex: none; font-size: 10px; font-weight: 500; color: var(--muted); }
/* The description gets its own grid row spanning every column, so it reads across the
   whole panel instead of competing with No./Height/Quantity for the name column. */
.cond-desc-full {
  grid-column: 1 / -1; font-size: 10.5px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: -1px;
}
/* Long partition names lead with the code the estimator searches for ("C91BV: 5/8+5/8+…").
   The code gets a chip of its own so it can never be the thing that's ellipsized, and the
   description gets two wrapped lines below instead of ten characters of one. */
.cond-code {
  flex: none; background: var(--panel-2); border: 1px solid var(--line); border-radius: 4px;
  padding: 0 4px; font-size: 11.5px; letter-spacing: .2px; white-space: nowrap;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.condition-row.active .cond-code { border-color: var(--accent); }
/* Comfortable earns its extra height by showing MORE, never less: the description wraps
   here, which fits all 102 characters of the longest partition name on the set, where
   compact ellipsizes one line at ~62. A density control that cost characters as well as
   rows (the old comfortable showed 21 of 102 in a 74px row) is not a density control.
   max-height, not -webkit-line-clamp, for the same reason as the compact rule above: this
   is a grid item, so its display blockifies and the clamp never took effect. */
.conditions-list:not(.dense) .cond-desc-full {
  white-space: normal; max-height: 52px;
}
.condition-row.cond-hidden .condition-info { opacity: .55; }
.condition-sub { font-size: 10.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.condition-height-line { opacity: .85; }
.condition-total { font-size: 11.5px; font-weight: 600; white-space: nowrap; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; font-variant-numeric: tabular-nums; }
.cond-unit { font-size: 9.5px; font-weight: 500; opacity: .6; }
.condition-mark-count { font-size: 10px; font-weight: 500; color: var(--muted); background: var(--panel-2); padding: 0 5px; border-radius: 8px; }
/* Six always-visible icon buttons ate ~126px of a 320px row, which is why a 100-character
   partition name showed about ten characters of itself. Two remain, in tracks of their
   own: ⚙ (its badge is information — how many assembly components the condition carries)
   and ⋯, which opens the same menu as right-clicking the row. */
.condition-assembly, .cond-more {
  cursor: pointer; opacity: .55; font-size: 12px; text-align: center; position: relative;
}
.condition-assembly:hover, .cond-more:hover { opacity: 1; }
.cond-more { font-weight: 700; letter-spacing: -1px; }
.condition-assembly.has-assembly { opacity: 1; }
.condition-assembly .asm-badge {
  position: absolute; top: -3px; right: -3px; background: var(--accent); color: #fff;
  font-size: 8.5px; font-weight: 700; min-width: 12px; height: 12px; line-height: 12px;
  text-align: center; border-radius: 6px; padding: 0 2px;
}

/* Right-click menu for a condition row and for the column header ("Show Columns"). Above
   floating panels (z 56) and the modal backdrop is irrelevant — a menu never coexists
   with one, since opening a modal closes it. */
.cond-menu {
  position: fixed; z-index: 200; min-width: 170px; padding: 4px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 10px 28px rgba(0,0,0,.55);
}
.cond-menu-header {
  padding: 5px 8px 6px; font-size: 10.5px; font-weight: 700; color: var(--muted);
  border-bottom: 1px solid var(--line); margin-bottom: 3px;
  max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cond-menu-item {
  display: block; width: 100%; text-align: left; padding: 5px 9px; border: none;
  background: none; color: var(--text); font-size: 12.5px; font-family: inherit;
  border-radius: 5px; cursor: pointer; white-space: nowrap;
}
.cond-menu-item:hover { background: var(--active-bg); }
.cond-menu-item.danger { color: var(--err); }
.cond-menu-check { display: inline-block; width: 13px; color: var(--accent); }
.cond-menu-sep { height: 1px; background: var(--line); margin: 4px 2px; }

/* The group-by picker sits in a control row with a filter box and two icon buttons, so it
   gets a hard cap — otherwise "No grouping" sets its intrinsic width and squeezes the
   filter out. */
.cond-mini-select {
  flex: none; max-width: 96px; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 6px; padding: 5px 4px; color: var(--text); font-size: 11.5px; font-family: inherit;
}
.cond-mini-select:focus { outline: none; border-color: var(--accent); }

/* --- Assembly editor (Condition Detail) ---
   Clean two-tier layout: a readable item grid (name / basis / cost chips / qty), one
   row expanded at a time into a detail panel that only shows the cost sides the item
   actually uses — a labor-only item has no material field at all. */
.asm-rows { margin: 8px 0 4px; }
.asm-rows input, .asm-rows select {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 5px 6px; color: var(--text); font-size: 12px; font-family: inherit; min-width: 0;
}
.asm-rows input:focus, .asm-rows select:focus { outline: none; border-color: var(--accent); }
.asm-head, .asm-item-line {
  display: grid;
  grid-template-columns: 16px minmax(150px, 2fr) 90px minmax(170px, 3fr) 90px 22px;
  gap: 8px; align-items: center;
}
.asm-head { font-size: 10.5px; color: var(--muted); font-weight: 600; padding: 2px 0 6px; text-transform: uppercase; letter-spacing: .3px; }
.asm-head .r { text-align: right; }
.asm-item { border-bottom: 1px solid var(--line); }
.asm-item-line { padding: 5px 0; cursor: pointer; }
.asm-item-line:hover { background: color-mix(in srgb, var(--active-bg) 40%, transparent); }
.asm-item.open { background: color-mix(in srgb, var(--active-bg) 25%, transparent); border-radius: 6px; }
.asm-chev { color: var(--muted); font-size: 10px; text-align: center; }
.asm-row-info {
  font-size: 11px; color: var(--muted); padding: 0 0 5px 24px; margin-top: -3px;
  display: flex; gap: 10px; flex-wrap: wrap; font-variant-numeric: tabular-nums;
}
.asm-row-info b { color: var(--text); font-weight: 600; }
.asm-basis-sum { font-size: 11.5px; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.asm-sides-sum { display: flex; gap: 5px; flex-wrap: wrap; min-width: 0; }
.asm-chip {
  font-size: 11px; padding: 1px 7px; border-radius: 9px; border: 1px solid var(--line);
  background: var(--panel-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px;
}
.asm-chip b { font-size: 10px; opacity: .75; margin-right: 2px; }
.asm-chip-m { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.asm-chip-l { border-color: color-mix(in srgb, var(--ok, #4caf7d) 45%, var(--line)); }
.asm-chip-e { border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); }
.asm-chip-s { border-color: color-mix(in srgb, var(--err) 35%, var(--line)); }
.asm-chip-none { color: var(--warn); font-style: italic; }
.asm-chip-cost { font-variant-numeric: tabular-nums; font-weight: 600; margin-left: 2px; }
.asm-chip-warn { border-color: var(--err) !important; background: color-mix(in srgb, var(--err) 15%, var(--panel-2)); }
.asm-chip-warn .asm-chip-cost { color: var(--err); }
.asm-chip-zero { border-color: var(--warn) !important; background: color-mix(in srgb, var(--warn) 15%, var(--panel-2)); }
.asm-chip-zero .asm-chip-cost { color: var(--warn); }
.asm-qty { font-size: 12px; font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.asm-remove { cursor: pointer; opacity: .6; text-align: center; }
.asm-remove:hover { opacity: 1; color: var(--err); }
.asm-detail { padding: 4px 8px 10px 24px; }
.asm-detail-qtyrow { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.asm-detail-qtyrow label { display: flex; flex-direction: column; gap: 3px; font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .3px; }
.asm-detail-qtyrow input { width: 90px; }
.asm-detail-qtyrow select { max-width: 190px; }
.asm-detail-sides { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-start; }
.asm-side {
  border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; background: var(--panel-2);
  display: flex; flex-direction: column; gap: 6px; min-width: 230px;
}
.asm-side-head { display: flex; justify-content: space-between; align-items: center; font-size: 11px; text-transform: uppercase; letter-spacing: .3px; color: var(--muted); }
.asm-side-head b { color: var(--text); }
.asm-side-x { cursor: pointer; opacity: .5; }
.asm-side-x:hover { opacity: 1; color: var(--err); }
.asm-side-manual { display: flex; gap: 6px; align-items: center; font-size: 11.5px; color: var(--muted); }
.asm-side-manual input { width: 80px; }
.asm-side-note { font-size: 11px; color: var(--muted); }
.asm-side-note a { color: var(--accent); }
.asm-side-cost { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; margin: -2px 0 2px; }
.asm-side-cost-warn { color: var(--err); }
.asm-side-cost-zero { color: var(--warn); }
.asm-side-explain { font-size: 10.5px; color: var(--muted); line-height: 1.35; margin: -4px 0 2px; }
.asm-side-adds { display: flex; gap: 6px; flex-wrap: wrap; align-self: center; }
.asm-side-adds .btn { font-size: 11.5px; padding: 4px 9px; opacity: .8; }
.asm-side-adds .btn:hover { opacity: 1; }
.asm-qty-panel {
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line)); border-radius: 8px;
  padding: 10px 12px; margin-bottom: 8px; background: color-mix(in srgb, var(--accent) 6%, var(--panel-2));
}
.asm-qty-panel-head { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
.asm-qty-panel-fields { display: flex; gap: 16px; flex-wrap: wrap; }
.asm-qty-panel-fields label { display: flex; flex-direction: column; gap: 3px; font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .3px; }
.asm-qty-panel-fields input { width: 110px; }
.asm-addbar { display: flex; gap: 8px; align-items: center; margin: 4px 0 6px; }
.asm-addpicker { flex: 1; }
.asm-addpicker input { width: 100%; }
.asm-picker { position: relative; display: inline-block; }
.asm-picker input {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 6px 8px; color: var(--text); font-size: 13px; font-family: inherit; width: 230px;
}
.asm-picker input:focus { outline: none; border-color: var(--accent); }
.asm-picker-results {
  position: absolute; top: 100%; left: 0; z-index: 45; width: 360px; max-height: 260px; overflow-y: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; margin-top: 4px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .25);
}
.asm-picker-group { padding: 5px 10px 2px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); }
.asm-hit-unit { color: var(--muted); font-size: 11px; }
.asm-toolbar { display: flex; gap: 8px; margin-top: 8px; align-items: center; }
.asm-copy { display: flex; gap: 6px; align-items: center; }
.asm-copy select {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 5px 7px; color: var(--text); font-size: 13px; font-family: inherit; max-width: 220px;
}
.asm-lib { position: relative; }
.asm-lib input {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 5px 7px; color: var(--text); font-size: 13px; font-family: inherit; width: 240px;
}
.asm-lib input:focus { outline: none; border-color: var(--accent); }
.asm-lib-results {
  position: absolute; top: 100%; left: 0; z-index: 40; width: 340px; max-height: 260px; overflow-y: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; margin-top: 4px;
}
.asm-lib-hit { padding: 6px 10px; font-size: 13px; cursor: pointer; border-bottom: 1px solid var(--line); }
.asm-lib-hit:last-child { border-bottom: none; }
.asm-lib-hit:hover { background: var(--active-bg); }
.asm-lib-none { color: var(--muted); font-style: italic; cursor: default; }
.asm-lib-flag1 { color: var(--warn); font-size: 11px; }
.asm-lib-flag2 { color: var(--err); font-size: 11px; letter-spacing: -2px; }
.asm-side-browse { font-size: 11px; padding: 5px 8px; margin-left: 4px; }
.asm-picker-browse { width: 420px; max-height: 340px; }
.asm-browse-hint { padding: 5px 10px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); border-bottom: 1px solid var(--line); }
.asm-browse-hit { padding: 6px 10px; }
.asm-browse-name { font-size: 13px; }
.asm-browse-detail { font-size: 11px; color: var(--muted); margin-top: 1px; }
.asm-savelib { margin-left: auto; }
.asm-savelib input {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 5px 7px; color: var(--text); font-size: 13px; font-family: inherit; width: 170px;
}
.asm-savelib input:focus { outline: none; border-color: var(--accent); }
.asm-bulk { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 10px; }
.asm-bulk summary { cursor: pointer; font-size: 13px; color: var(--muted); }
.asm-bulk summary:hover { color: var(--text); }
.asm-bulk-list {
  margin-top: 8px; max-height: 160px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: 6px; padding: 6px 10px; background: var(--panel-2);
}
.asm-bulk-item { display: block; font-size: 13px; padding: 3px 0; }
.asm-bulk-item input { margin-right: 6px; }
.asm-bulk-haz { color: var(--warn); font-size: 11.5px; }
.condition-notes-flag, .cat-notes-flag { font-size: 11px; cursor: help; }
.condition-review-flag { font-size: 11px; cursor: help; color: #e8a33d; }

.sheet {
  display: flex; gap: 9px; padding: 8px 12px; border-bottom: 1px solid var(--line);
  cursor: pointer; align-items: center;
}
.sheet:hover { background: var(--panel-2); }
.sheet.active { background: var(--active-bg); }
.sheet.has-takeoff { border-left: 3px solid var(--accent); padding-left: 9px; background: rgba(76, 154, 255, 0.07); }
.sheet.has-takeoff.active { background: var(--active-bg); } /* active still wins over the plain has-takeoff tint */
.sheet.has-takeoff:hover { background: var(--panel-2); }
.sheet .thumb {
  width: 54px; height: 40px; flex: none; background: #0b0e12; border: 1px solid var(--line);
  border-radius: 3px; object-fit: cover; object-position: top left;
}
.sheet .thumb.placeholder { display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 10px; }
.sheet .info { min-width: 0; flex: 1; }
.sheet .num { font-weight: 600; font-size: 13px; }
.sheet .name { color: var(--muted); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sheet .badge { font-size: 10px; padding: 1px 6px; border-radius: 10px; }
.sheet.flagged { border-left: 3px solid var(--warn); padding-left: 9px; }
.sheet.flagged.has-takeoff { border-left-color: var(--accent); } /* takeoff highlight wins the border; the flag emoji still shows */
.sheet-flag-btn {
  flex: none; background: none; border: none; cursor: pointer; font-size: 13px; line-height: 1;
  padding: 2px 4px; border-radius: 4px; opacity: .35; font-family: inherit;
}
.sheet-flag-btn.set { opacity: 1; }
.sheet.sheet-hidden { opacity: .45; }
.sheetlist-hidden-toggle {
  display: block; width: 100%; text-align: center; padding: 8px; margin-top: 2px;
  background: none; border: none; border-top: 1px solid var(--line); color: var(--muted);
  font-size: 12px; cursor: pointer; font-family: inherit;
}
.sheetlist-hidden-toggle:hover { color: var(--text); background: var(--panel-2); }
.sheet-flag-btn:hover { opacity: 1; background: var(--panel-2); }
.sheet-note-mark { font-size: 10px; cursor: help; }
.sheet-flag-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.flag-choice {
  padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px; cursor: pointer;
  background: var(--panel-2); color: var(--text); font-size: 12.5px; font-family: inherit;
}
.flag-choice:hover { border-color: var(--muted); }
.flag-choice.selected { border-color: var(--accent); background: var(--active-bg); }
.badge.pending { color: var(--muted); background: var(--panel-2); }
.badge.processing { color: var(--warn); background: #3a2f12; }
.badge.ready { color: var(--ok); background: #12331c; }
.badge.failed { color: var(--err); background: #3a1616; }

#viewer-wrap { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #0b0e12; }
#viewer-stage { flex: 1; position: relative; min-height: 0; }
#viewer { position: absolute; inset: 0; }
#overlay { position: absolute; inset: 0; pointer-events: none; }
.empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--muted); }
.zoombox-btn {
  position: absolute; top: 6px; left: 148px; z-index: 10; width: 32px; height: 32px;
  border-radius: 4px; border: 1px solid rgba(255,255,255,.3); background: rgba(0,0,0,.5);
  color: #fff; font-size: 16px; line-height: 1; cursor: pointer; font-family: inherit;
}
.zoombox-btn:hover { background: rgba(0,0,0,.7); }
.zoombox-btn.active { background: var(--accent); border-color: var(--accent); }
/* Prev/next SHEET (not zoom/pan) — fixed to the viewer's edges so they're reachable at
   any zoom level without leaving the plan to hunt for the sheet list/topbar switcher. */
.page-nav-btn {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 10;
  width: 36px; height: 56px; border-radius: 6px; border: 1px solid rgba(255,255,255,.3);
  background: rgba(0,0,0,.45); color: #fff; font-size: 22px; line-height: 1; cursor: pointer;
  font-family: inherit; display: flex; align-items: center; justify-content: center;
}
.page-nav-btn:hover:not(:disabled) { background: rgba(0,0,0,.7); }
.page-nav-btn:disabled { opacity: .25; cursor: default; }
.page-nav-prev { left: 6px; }
.page-nav-next { right: 6px; }
.zoombox-rect {
  position: absolute; z-index: 25; border: 1.5px dashed var(--accent);
  background: rgba(88,166,255,.15); pointer-events: none;
}
/* The hover quantity readout, in .viewer-corner-bl above the sheet caption. It exists so
   the plan itself doesn't have to carry a permanent tag per measurement — OST's Image
   Window draws no text at all and puts the numbers in the docked grid; this is the same
   bargain at cursor speed. Never interactive: it appears under the pointer's attention,
   not under the pointer. */
.hover-readout {
  display: flex; align-items: center; gap: 8px;
  background: rgba(15, 18, 22, .92); border: 1px solid var(--line); border-radius: 8px;
  padding: 6px 11px; font-size: 12.5px; max-width: 100%; pointer-events: none !important;
}
.hover-readout .condition-swatch { width: 13px; height: 13px; border-radius: 3px; flex: none; }
.hover-readout-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 28ch; }
.hover-readout-qty { color: var(--text); font-weight: 700; white-space: nowrap; }
.hover-readout-meta { color: var(--muted); white-space: nowrap; }

/* Sits in .viewer-corner-bl — not positioned itself. */
.sheet-caption {
  background: rgba(0,0,0,.6);
  padding: 6px 10px; border-radius: 6px; font-size: 12px; color: var(--text);
  border: 1px solid var(--line); pointer-events: none;
  max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Image-adjust popover: anchored top-right under the toolbar's Image button. */
.image-adjust-panel {
  position: absolute; top: 8px; right: 12px; z-index: 30; width: 240px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  padding: 12px; box-shadow: 0 6px 20px rgba(0,0,0,.5); font-size: 12.5px;
}
.image-adjust-panel[hidden] { display: none; }
.image-adjust-panel .ia-row { margin-bottom: 10px; }
.image-adjust-panel .ia-row label { display: flex; justify-content: space-between; color: var(--muted); margin-bottom: 4px; }
.image-adjust-panel input[type="range"] { width: 100%; accent-color: var(--accent); }
.image-adjust-panel .ia-checks { display: flex; flex-direction: column; gap: 6px; margin: 10px 0; }
.image-adjust-panel .ia-checks label { display: flex; align-items: center; gap: 7px; color: var(--text); }
.image-adjust-panel .ia-checks input { accent-color: var(--accent); }
.image-adjust-panel .ia-presets { display: flex; gap: 6px; align-items: center; border-top: 1px solid var(--line); padding-top: 10px; }
.image-adjust-panel .ia-presets .spacer { flex: 1; }
.autocount-bar {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 16px; z-index: 30;
  display: flex; align-items: center; gap: 10px; max-width: 90%;
  background: var(--panel); border: 1px solid var(--accent); border-radius: 8px;
  padding: 8px 12px; box-shadow: 0 6px 20px rgba(0,0,0,.5); font-size: 13px;
}
.autocount-bar .spacer { width: 8px; flex: none; }
.autocount-bar[hidden] { display: none; }
.fs-hl-size { display: inline-flex; gap: 2px; }
.fs-hl-size .tool-btn { min-width: 26px; text-align: center; font-weight: 600; }
.autocount-cond-wrap { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); }
.autocount-cond-wrap[hidden] { display: none; }
.autocount-cond-wrap select {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 4px 6px; color: var(--text); font-size: 12px; font-family: inherit; max-width: 180px;
}
.autocount-cond-wrap select:focus { outline: none; border-color: var(--accent); }
.autocount-all-wrap { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); white-space: nowrap; cursor: pointer; }
.autocount-all-wrap[hidden] { display: none; }
.autocount-all-wrap input { accent-color: var(--accent); cursor: pointer; }

/* Find-similar all-sheets results modal */
.fs-rows { display: flex; flex-direction: column; gap: 4px; max-height: 46vh; overflow-y: auto; margin: 10px 0; }
.fs-row {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: 6px; cursor: pointer; background: var(--panel-2);
}
.fs-row:hover { border-color: var(--accent); }
.fs-num { font-size: 13px; font-weight: 500; }
.fs-src { color: var(--muted); font-weight: 400; font-size: 11.5px; }
.fs-count { font-size: 12px; font-weight: 600; color: var(--accent); font-variant-numeric: tabular-nums; }
.fs-actions { display: flex; align-items: center; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.fs-actions .spacer { flex: 1; }

/* --- Toolbar (tools + calibration) --- */
.toolbar {
  display: flex; align-items: center; gap: 8px; row-gap: 6px; flex-wrap: wrap; padding: 8px 12px;
  background: var(--panel); border-bottom: 1px solid var(--line); flex: none;
}
.tool-btn {
  padding: 6px 11px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--panel-2); color: var(--text); font-size: 12.5px; cursor: pointer; font-family: inherit;
}
/* ICONS, not words, in the top toolbar. Measured at 1600x1000 the bar was 141px tall —
   three wrapping rows of word-buttons ("Select / Pan / Linear / Continuous / Area / Cutout
   / Flood Fill / Count / Measure / Legend / Markup") — and the plan stage was left 50.5% of
   the window. OST puts the same tools in ONE row of icons and gives the image window the
   dominant share. Every button keeps its full title= tooltip, and the word itself is still
   in the DOM (.tb-word) for screen readers and for the dropdown menus, where there is room
   for it and where the tools are ones you meet rarely enough to need naming. */
.toolbar .tb-word { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.tb-group-menu .tb-word { position: static; width: auto; height: auto; clip-path: none; }
.toolbar > .tool-btn, .toolbar .tb-group-btn { padding: 5px 8px; line-height: 0; }
.tb-i { width: 18px; height: 18px; display: block; }
.tb-group-btn { display: inline-flex; align-items: center; gap: 3px; }
.tb-caret { font-size: 9px; line-height: 1; color: var(--muted); }
.tb-group-menu .tb-i { display: inline-block; vertical-align: -4px; margin-right: 6px; }
.tool-btn:hover { border-color: var(--accent); }
.tool-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.tool-btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.tool-btn.primary:hover { filter: brightness(1.08); }
.tool-btn:disabled { opacity: .35; cursor: default; border-color: var(--line); }
.tool-btn:disabled:hover { border-color: var(--line); }
.toolbar-sep { width: 1px; height: 20px; background: var(--line); margin: 0 2px; }
.tool-btn.danger { color: var(--err); }
.tool-btn.danger:hover { border-color: var(--err); }

/* Toolbar tool groups (📐 Sheet, 🧭 Navigate) — a trigger plus a dropdown of the
   occasional tools, so the eight drawing tools an estimator actually reaches for stay
   inline. .tb-group-has-active is the trigger's stand-in for a folded-away Snap/Quick
   Links still being switched on (see initToolbarGroups in main.js). */
.tb-group { position: relative; flex: none; }
.tb-group-menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 60;
  display: flex; flex-direction: column; gap: 4px; padding: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0,0,0,.5); min-width: 180px;
}
.tb-group-menu[hidden] { display: none; }
.tb-group-menu .tool-btn { text-align: left; width: 100%; }
.tb-group-btn.tb-group-open { border-color: var(--accent); color: var(--accent); }
.tb-group-btn.tb-group-has-active { border-color: var(--accent); }

/* Contextual selection bar — only in the DOM's flow while something is selected (see
   updateSelectionBar in tools.js). Tinted so it reads as "these act on your selection"
   rather than as a second permanent toolbar row. */
.selection-bar {
  display: flex; align-items: center; gap: 8px; row-gap: 6px; flex-wrap: wrap; padding: 6px 12px;
  background: var(--active-bg); border-bottom: 1px solid var(--accent); flex: none;
}
.selection-bar[hidden] { display: none; } /* the class's display:flex otherwise beats [hidden] — same gotcha as .scale-controls */
.selection-bar .spacer { flex: 1; }
.selection-count { font-size: 12px; font-weight: 600; color: var(--accent); white-space: nowrap; }
.area-controls { display: inline-flex; align-items: center; gap: 6px; }
.area-select, .reassign-select {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 6px 8px; color: var(--text); font-size: 12.5px; font-family: inherit;
  max-width: 200px; /* a <select> auto-sizes to its longest option — 66 partition names made this 788px wide */
}
.scale-preset, .label-mode {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 6px 8px; color: var(--text); font-size: 12.5px; font-family: inherit;
}
/* Markup colours live inside the Markup dropdown, where the swatch row is a toolbar
   control rather than a modal field — smaller, and on one line however narrow the menu is. */
.markup-swatch-row { flex-wrap: nowrap; padding: 2px 4px; }
.markup-swatch { width: 20px; height: 20px; border-radius: 4px; }
.scale-status { font-size: 12.5px; padding: 5px 10px; border-radius: 6px; }
.scale-status.uncalibrated { color: var(--warn); background: #3a2f12; }
.scale-status.calibrated { color: var(--ok); background: #12331c; }
.scale-status.mismatch { color: var(--err); background: #3a1616; }
.scale-status.unverified { color: var(--warn); background: #3a2f12; }
/* Scale is auto-detected + auto-verified against a printed dimension for every sheet at
   ingest — this UI is just whether to SHOW that, hidden by default to declutter the
   toolbar. #scale-toggle-btn reuses .chevron-btn. A mismatch (the one state that
   actually matters — an active scale contradicted by a printed dimension) stays visible
   regardless of the collapsed state, so it can't get lost just because the panel's hidden. */
.scale-controls { display: flex; align-items: center; gap: 6px; }
.scale-controls[hidden] { display: none; } /* [hidden] and .scale-controls tie on specificity — this must come after to actually win */
.scale-mismatch-flag {
  font-size: 12px; padding: 4px 8px; border-radius: 6px; color: var(--err); background: #3a1616;
  cursor: pointer; white-space: nowrap;
}
.verify-detail-row { font-size: 13px; color: var(--text); line-height: 1.5; }
.verify-detail-row .mismatch { color: var(--err); font-weight: 600; }
.verify-detail-row .verified { color: var(--ok); font-weight: 600; }

/* --- Condition modal: color bank swatches + shape picker --- */
.swatch-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.swatch {
  width: 24px; height: 24px; border-radius: 5px; cursor: pointer;
  border: 2px solid transparent; padding: 0;
}
.swatch:hover { border-color: var(--muted); }
.swatch.selected { border-color: #fff; outline: 2px solid var(--accent); }
.swatch-row input[type="color"] {
  width: 32px; height: 26px; padding: 0 2px; background: var(--panel-2);
  border: 1px dashed var(--line); border-radius: 5px; cursor: pointer;
}
.shape-row { display: flex; gap: 6px; }
.shape-btn {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 7px 4px 5px; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 6px; cursor: pointer; color: var(--accent); font-family: inherit;
}
.shape-btn span { font-size: 10.5px; color: var(--muted); }
.shape-btn:hover { border-color: var(--muted); }
.shape-btn.selected { border-color: var(--accent); background: var(--active-bg); }
.shape-btn.selected span { color: var(--text); }

/* --- Settings: color bank editor --- */
.bank-wells { display: flex; flex-wrap: wrap; gap: 8px; }
.bank-well {
  width: 40px; height: 32px; padding: 0 2px; background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 6px; cursor: pointer;
}
.modal .actions .spacer { flex: 1; }

/* --- Condition list: unused-on-this-sheet rows sink + gray --- */
.condition-row.inactive { opacity: .45; }

/* --- Isolate banner --- */
/* Amber, centred at the top of the plan, and it stays there for as long as the filter is
   on. Isolating is the one view state that makes the drawing LIE about how much takeoff
   exists, so it gets the loudest chrome in the viewer and a Show-all button right in it. */
.isolate-pill {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%); z-index: 18;
  display: flex; align-items: center; gap: 8px; max-width: 70%;
  padding: 5px 8px 5px 10px; border-radius: 999px;
  background: rgba(38, 28, 6, .92); border: 1px solid #b7791f;
  color: var(--text); font-size: 12px; box-shadow: 0 3px 12px rgba(0,0,0,.45);
}
.isolate-dot { width: 8px; height: 8px; border-radius: 50%; background: #f0b429; flex: 0 0 auto; }
.isolate-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.isolate-clear {
  background: #b7791f; border: none; color: #1b1400; cursor: pointer;
  font: inherit; font-weight: 600; padding: 2px 9px; border-radius: 999px; white-space: nowrap;
}
.isolate-clear:hover { background: #f0b429; }

/* --- On-sheet legend --- */
/* Bottom-right overlay stack — holds the legend and the running tally. Anchored once here
   so the two can't collide, and so the tally is out of the way of the plan (it used to be
   pinned top-left, where it covered content as soon as you zoomed in). Column direction with
   the container anchored at the bottom means the tally stays put at the corner and the legend
   grows upward, rather than the tally jumping when the legend is toggled. */
/* The two bottom corner stacks. Each is a bottom-anchored column, so whatever sits in it
   grows UPWARD and the bottom-most item stays pinned to its corner — nothing jumps when a
   sibling is toggled. The running tally lives in the right stack, and relocates itself to
   the left one when the Legend is open (conditions.js) so the two never share a corner.
   Capped at 46% each so the two stacks can never collide however long their text runs —
   they ellipsize instead. That matters on a ~640px stage, i.e. a laptop with both panels
   docked. */
.viewer-corner-bl,
.viewer-corner-br {
  position: absolute; bottom: 12px; z-index: 16;
  display: flex; flex-direction: column; gap: 8px;
  max-width: 46%;
  pointer-events: none;              /* an empty column must not eat clicks on the plan */
}
.viewer-corner-bl { left: 12px; align-items: flex-start; }
.viewer-corner-br { right: 12px; align-items: flex-end; }
.viewer-corner-bl > *,
.viewer-corner-br > * { pointer-events: auto; max-width: 100%; }

/* The Image Legend is a DOCKED BAND above the plan, not a floating panel on it. As an
   overlay it measured 244 x 427px on a 17-condition sheet — 12.3% of a 940x903 plan
   viewport, opaque, over the upper-left plan quadrant, and the old CSS cap (40% wide by
   62vh) allowed it to reach ~27%. The whole value of a key is that it explains the
   drawing; one that hides an eighth of the drawing is a net loss. Docked, it costs a strip
   of chrome and no plan pixels at all, and it takes the plan's full width so it holds far
   more conditions in far less height. */
.legend-panel {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 5px 10px; font-size: 12px;
  background: var(--panel-2, rgba(15, 18, 22, .55));
  border-bottom: 1px solid var(--line);
}
.legend-header { display: flex; flex-direction: column; gap: 1px; flex: 0 0 auto; padding-top: 3px; }
.legend-title { font-weight: 700; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; white-space: nowrap; }
.legend-toggle { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 11px; font-family: inherit; padding: 0; white-space: nowrap; text-align: left; }
.legend-toggle:hover { text-decoration: underline; }
.legend-empty { color: var(--muted); font-size: 11.5px; padding: 5px 0; }
/* Chips wrap across the band's full width and scroll vertically past two rows, so a
   66-condition set is a two-row strip you can scroll rather than a column down the plan. */
.legend-rows {
  display: flex; flex-flow: row wrap; align-content: flex-start;
  gap: 2px 4px; flex: 1 1 auto; min-width: 0;
  max-height: 54px; overflow-y: auto;
}
/* Doubles as the active-condition picker in the main window, now that the full
   conditions list is popup-only — click a row to make it the one drawing tools feed
   into (tools.js reads getActiveCondition()), same as clicking a row in the sidebar
   list already did. */
.legend-row { display: flex; align-items: center; gap: 6px; padding: 2px 6px; border-radius: 4px; cursor: pointer; max-width: 260px; }
.legend-rows .legend-name { flex: 0 1 auto; max-width: 160px; }
/* The condition's "No." — the same number the plan's quantity labels lead with, so a tag
   reading "12 · 44.0 LF" can be traced back to a chip without clicking anything. */
.legend-no { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 11px; min-width: 14px; text-align: right; }
.legend-row:hover { background: rgba(255,255,255,.06); }
.legend-row.active { background: var(--active-bg); }
.legend-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.legend-qty { color: var(--muted); white-space: nowrap; }
/* A hidden condition's row still lists a quantity that is NOT drawn on the sheet — the
   struck swatch and dimmed row are what stop the legend from lying about the drawing. */
.legend-row.hidden-cond { opacity: .45; }
.legend-row.hidden-cond .condition-swatch { position: relative; }
.legend-row.hidden-cond .condition-swatch::after {
  content: ""; position: absolute; inset: -1px;
  background: linear-gradient(to bottom right, transparent 43%, #fff 43%, #fff 57%, transparent 57%);
}
.legend-hidden-tag { color: var(--muted); font-size: 11px; }
/* Pattern + Spacing sit on one row, matching OST's Appearance block. */
.pattern-row { display: flex; gap: 8px; align-items: center; }
.pattern-row select { flex: 1; min-width: 0; }
.pattern-spacing-label { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 12px; white-space: nowrap; }
.pattern-spacing-label input { width: 62px; }

/* Active-condition running tally — pinned top-left of the plan, out of the way of the
   bottom-corner sheet caption and legend. Live per-condition quantity as you take off. */
/* Sits in .viewer-corner-br (bottom-right) — not positioned itself. Kept click-through so
   it never intercepts a draw/pan gesture, even though its container allows pointer events. */
.active-tally {
  display: flex; align-items: center; gap: 8px;
  background: rgba(15, 18, 22, .9); border: 1px solid var(--line); border-radius: 8px;
  padding: 6px 11px; font-size: 12.5px; max-width: 100%; pointer-events: none !important;
}
.active-tally .condition-swatch { width: 13px; height: 13px; border-radius: 3px; flex: none; }
.active-tally-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 26ch; }
.active-tally-nums { display: flex; gap: 10px; color: var(--muted); white-space: nowrap; }
.active-tally-num b { color: var(--text); font-weight: 700; }
/* Live material/labor $ — QuickBid/Edge-style cost feedback while drawing (fed from the
   real pricing engine, see computeConditionCost in server/jobEstimate.js). Only appears
   once the condition has an assembly configured; a soft accent tint on the $ figures
   keeps it visually distinct from the plain quantity numbers to its left. */
.active-tally-cost { padding-left: 10px; border-left: 1px solid var(--line); }
.active-tally-cost b { color: var(--accent); }

/* Keyboard-shortcut editor (settings → ⌨ Keyboard shortcuts) */
.shortcut-list { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
.shortcut-row { display: flex; align-items: center; gap: 10px; }
.shortcut-label { flex: 1; font-size: 13px; }
.shortcut-key { min-width: 78px; text-align: center; font-weight: 700; }
.shortcut-key.capturing { border-color: var(--accent); color: var(--accent); }
.shortcut-clear {
  background: none; border: none; color: var(--muted); cursor: pointer; font-size: 13px;
  padding: 4px 6px; border-radius: 4px;
}
.shortcut-clear:hover { color: var(--err); background: var(--panel-2); }

/* Copy-conditions-from-another-plan-set picker */
.copy-cond-list { max-height: 46vh; overflow: auto; margin: 8px 0; }
.copy-cond-row { display: flex; align-items: center; gap: 8px; padding: 5px 4px; border-radius: 4px; cursor: pointer; }
.copy-cond-row:hover { background: rgba(255,255,255,.05); }
.copy-cond-row .condition-swatch { width: 13px; height: 13px; border-radius: 3px; flex: none; }
.copy-cond-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.copy-cond-meta { color: var(--muted); font-size: 11.5px; white-space: nowrap; }
.copy-dupe { color: var(--warn, #f0a020); }
.copy-selectall { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--muted); cursor: pointer; }
.modal-hint { color: var(--muted); font-size: 12.5px; line-height: 1.5; margin: 0 0 10px; }

/* --- Quick-link preview popup: an independent mini deep-zoom viewer, so you can read
   a referenced detail without losing your place on the sheet you're taking off --- */
/* Right-click menu on a quick-link hotspot */
.quicklink-menu {
  position: fixed; z-index: 60; background: var(--panel); border: 1px solid var(--line);
  border-radius: 6px; box-shadow: 0 6px 20px rgba(0,0,0,.5); padding: 4px; min-width: 180px;
}
.quicklink-menu button {
  display: block; width: 100%; text-align: left; background: none; border: none; color: var(--text);
  font-size: 13px; font-family: inherit; padding: 7px 10px; border-radius: 4px; cursor: pointer;
}
.quicklink-menu button:hover { background: var(--panel-2); color: var(--err); }

.quicklink-popup {
  position: absolute; top: 12px; right: 12px; z-index: 30;
  width: 46%; height: 58%; min-width: 340px; min-height: 260px;
  /* % of the stage, not vw — same as above; min-width is what actually bounds this one. */
  max-width: calc(100% - 24px); max-height: calc(100% - 24px);
  background: var(--panel); border: 1px solid var(--accent); border-radius: 10px;
  box-shadow: 0 8px 28px rgba(0,0,0,.55);
  display: flex; flex-direction: column; overflow: hidden;
  /* Resizing is driven by the 4 corner handles below (JS drag, quicklinks.js) rather
     than native CSS `resize` — that property only ever gives a single bottom-right
     handle, no way to grab any of the other 3 corners. */
}
.quicklink-resize-handle { position: absolute; width: 16px; height: 16px; z-index: 40; }
.quicklink-resize-handle.nw { top: 0; left: 0; cursor: nwse-resize; }
.quicklink-resize-handle.ne { top: 0; right: 0; cursor: nesw-resize; }
.quicklink-resize-handle.sw { bottom: 0; left: 0; cursor: nesw-resize; }
.quicklink-resize-handle.se { bottom: 0; right: 0; cursor: nwse-resize; }
.quicklink-resize-handle:hover { background: rgba(255,255,255,.12); }
/* The base rule's `display: flex` has the same specificity as the browser's built-in
   `[hidden] { display: none }` and, as an author-stylesheet rule, wins that tie by
   cascade order — so without this override the popup rendered visible unconditionally
   from page load, regardless of the `hidden` attribute ever being set. */
.quicklink-popup[hidden] { display: none; }
.quicklink-popup-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 12px; border-bottom: 1px solid var(--line); font-weight: 600; font-size: 13px; flex: none;
}
.quicklink-popup-header #quicklink-popup-close { cursor: pointer; opacity: .7; }
.quicklink-popup-header #quicklink-popup-close:hover { opacity: 1; color: var(--err); }
.quicklink-popup-viewer { flex: 1; position: relative; background: #0b0e12; min-height: 0; }
.quicklink-popup-actions { padding: 8px 12px; border-top: 1px solid var(--line); display: flex; justify-content: flex-end; flex: none; }

/* --- Sheet-text search — lives in the toolbar now, not the sidebar --- */
.search-box { flex: none; }
.search-box input {
  width: 220px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 6px 10px; color: var(--text); font-size: 12.5px; font-family: inherit;
}
.search-box input:focus { outline: none; border-color: var(--accent); }
.search-panel { left: auto; right: 12px; top: 90px; } /* below the navigator minimap */
.search-snippet {
  padding: 6px 10px; border-top: 1px solid var(--line); color: var(--muted);
  font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- Locate panel (floating over the viewer) --- */
.locate-panel {
  position: absolute; top: 12px; left: 12px; z-index: 20; width: 220px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  font-size: 12.5px; box-shadow: 0 4px 16px rgba(0,0,0,.4);
}
.locate-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 10px; border-bottom: 1px solid var(--line); font-weight: 600;
}
.locate-header #locate-close { cursor: pointer; opacity: .7; padding: 0 2px; }
.locate-header #locate-close:hover { opacity: 1; color: var(--err); }
.locate-nav {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  padding: 8px 10px;
}
.locate-nav #locate-count { color: var(--muted); white-space: nowrap; }
.locate-pages { border-top: 1px solid var(--line); max-height: 180px; overflow-y: auto; }
.locate-page-row { padding: 6px 10px; cursor: pointer; color: var(--muted); }
.locate-page-row:hover { background: var(--panel-2); color: var(--text); }
.locate-page-row.current { color: var(--accent); font-weight: 600; }

/* --- Modal --- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,.55);
  display: flex; align-items: center; justify-content: center; z-index: 50;
}
.modal-backdrop[hidden] { display: none; }
.modal {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 22px 24px; min-width: 300px; max-width: 420px;
  max-height: 88vh; overflow-y: auto;
}
.modal.wide { max-width: min(1100px, 92vw); width: 92vw; }
/* Genuine viewport-filling surface (the 3D view's canvas) — no padding/radius/scroll to
   fight orbit-drag gestures with, unlike .wide which is still a centered dialog box. */
.modal.full {
  width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh;
  min-width: 0; border-radius: 0; padding: 0; overflow: hidden;
}
.view3d-controls {
  position: absolute; top: 14px; right: 14px; z-index: 1;
  display: flex; gap: 8px;
}

/* --- Notice (replaces native alert() — see web/js/notice.js for why) --- */
.notice-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,.55);
  display: flex; align-items: center; justify-content: center; z-index: 300;
}
.notice-backdrop[hidden] { display: none; }
.notice-box {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 20px 22px; min-width: 280px; max-width: 440px; max-height: 80vh; overflow-y: auto;
}
.notice-title { font-weight: 700; font-size: 15px; margin-bottom: 8px; }
.notice-message { font-size: 13.5px; line-height: 1.5; white-space: pre-wrap; color: var(--text); }
.notice-box .field { margin-top: 12px; }
.notice-input {
  width: 100%; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 8px 10px; color: var(--text); font-size: 13.5px; font-family: inherit; box-sizing: border-box;
}
.notice-input:focus { outline: none; border-color: var(--accent); }
.notice-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.notice-actions .btn {
  padding: 7px 16px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--panel-2); color: var(--text); font-size: 13px; cursor: pointer; font-family: inherit;
}
.notice-actions .btn:hover { border-color: var(--accent); }
.notice-actions .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.notice-actions .btn.primary:hover { filter: brightness(1.08); }
.notice-actions .btn.primary.danger { background: var(--err); border-color: var(--err); color: #fff; }
.notice-actions .btn.primary.danger:hover { filter: brightness(1.08); }

/* Says which plan set of the bid the numbers below cover — the Rollup is scoped to one
   plan set and used to say so nowhere, so a report off a three-set job read as the bid. */
.rollup-ident { font-size: 12px; color: var(--muted); margin: -6px 0 12px; }
.rollup-controls { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.rollup-controls label { font-size: 12.5px; color: var(--muted); }
.rollup-controls { flex-wrap: wrap; }
.rollup-controls select {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 7px 10px; color: var(--text); font-size: 13px; font-family: inherit;
}
/* The scope picker carries full sheet numbers + names, the others one short word each. */
#rollup-scope { min-width: 260px; }
.rollup-controls .rollup-check { display: flex; align-items: center; gap: 5px; cursor: pointer; white-space: nowrap; }
.rollup-controls .spacer { flex: 1; }
.rollup-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.rollup-table th {
  text-align: left; font-size: 11.5px; color: var(--muted); font-weight: 600;
  padding: 6px 10px; border-bottom: 1px solid var(--line);
}
.rollup-table td { padding: 7px 10px; border-bottom: 1px solid var(--line); }
.rollup-table tr.rollup-row-inactive { opacity: .4; }
.rollup-table .rollup-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rollup-table tfoot td { font-weight: 700; border-top: 2px solid var(--line); border-bottom: none; }
.rollup-notes { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
/* ONE LINE PER CONDITION. The name plus a 1-3 line note used to wrap inside the cell, which
   made this table 4,900px tall for 66 conditions — a median row of 67px against the ~25px
   Quick Bid's Conditions tab fits 44 conditions on one screen at. The name is
   middle-truncated (full text on hover and on every printed/exported copy) and the note is
   behind the 🗒. */
.rollup-cond { display: flex; align-items: center; gap: 4px; min-width: 0; }
.rollup-cond-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rollup-note-toggle { flex: none; border: 0; background: none; color: var(--muted); cursor: pointer;
                      font-size: 11px; padding: 0 2px; line-height: 1; }
.rollup-note-toggle:hover, .rollup-note-toggle.on { color: var(--accent); }
.rollup-area-toggle { margin-right: 4px; vertical-align: middle; }
.rollup-table tr.rollup-area-row td { padding: 4px 10px; border-bottom: none; color: var(--muted); font-size: 12.5px; background: var(--panel-2); }
.rollup-table tr.rollup-area-row .rollup-area-name { padding-left: 22px; }
.rollup-price-input {
  width: 84px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 4px 7px; color: var(--text); font-size: 13px; font-family: inherit;
  text-align: right; font-variant-numeric: tabular-nums;
}
.rollup-price-input:focus { border-color: var(--accent); }
/* Columns that support the read rather than carry it (type, height, hours) sit back so the
   quantity/money columns lead the eye — QB's own report leans on the same hierarchy. */
.rollup-table .rollup-dim { color: var(--muted); }
/* Group header + group total, per QB's Bid Summary: each group opens with its own banded
   header and closes with a total row before the next one starts. */
.rollup-table tr.rollup-group-row td {
  background: var(--panel-2); font-weight: 700; font-size: 12px; letter-spacing: .03em;
  text-transform: uppercase; color: var(--text); padding: 9px 10px 6px;
  border-bottom: 1px solid var(--line);
}
.rollup-group-count { color: var(--muted); font-weight: 500; margin-left: 8px; text-transform: none; letter-spacing: 0; }
.rollup-table tr.rollup-subtotal-row td {
  font-weight: 700; border-top: 1px solid var(--line); border-bottom: 2px solid var(--line);
}
/* Quantity taken off but never priced — not $0.00, which reads as "this is free". */
.rollup-unpriced { color: #e8a33d; font-weight: 600; font-size: 12px; }
.empty-hint.warn { color: #e8a33d; margin-top: 8px; }

/* The condition's own takeoff symbol on its summary row — same glyph the plan print's
   legend draws, so a row here and a mark on the sheet are visibly the same condition. */
.rollup-glyph { display: inline-flex; line-height: 0; vertical-align: middle; }
.rollup-glyph svg { width: 18px; height: 18px; }
/* ⚑ beside a condition whose price is standing on a material line nobody converted, or
   whose material computes to zero. Sits next to the takeoff symbol on purpose: this is a
   fact about the row, and it now rides the row onto the print and the spreadsheet too. */
.rollup-flag { margin-left: 4px; font-weight: 700; cursor: help; vertical-align: middle; }
.rollup-flag.unit { color: #e8a33d; }
.rollup-flag.zero { color: var(--muted); }
/* The Reports window — one row per report, filed under QB's own [General] Type= headings. */
.reports-table td { vertical-align: middle; }
.reports-table td b { font-size: 13.5px; }
/* Second grouping level (Section → Area): indented under its parent, and its subtotal
   deliberately lighter than the parent group's so the nesting reads at a glance. */
.rollup-table tr.rollup-subgroup-row td { background: var(--panel-2); font-weight: 600; padding-left: 22px; font-size: 12.5px; }
.rollup-table tr.rollup-subsubtotal-row td { font-weight: 600; border-top: 1px solid var(--line); color: var(--muted); }
.reports-table td:first-child { width: 28px; text-align: center; }
/* The audience presets — one click for "who is this copy for", which is the choice that
   decides whether the basis-check machinery prints at all. */
.rp-presets { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px;
              padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.rp-presets .bid-sec-title { margin: 0; }
/* THE AUDIENCE SWITCH, as a segmented control rather than three push buttons. The three
   presets used to render as identical .btn elements with no aria-pressed and the same
   computed background, so a dialog sitting on the internal QC settings looked exactly like
   one sitting on the issue settings. Selection has to be visible or the switch may as well
   not exist — which is how a batch went out as the QC copy by accident. */
.aud-seg { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 6px 0 12px;
           padding-bottom: 10px; border-bottom: 1px solid var(--line); }
.aud-seg-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.aud-seg-btns { display: flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; flex-wrap: wrap; }
.aud-seg-btn { display: block; text-align: left; padding: 6px 12px; background: var(--panel-2); color: var(--muted);
               border: 0; border-right: 1px solid var(--line); font: inherit; cursor: pointer; }
.aud-seg-btn:last-child { border-right: 0; }
.aud-seg-btn b { display: block; font-size: 12.5px; }
.aud-seg-btn span { display: block; font-size: 11px; opacity: .8; }
.aud-seg-btn:hover { background: var(--line); color: var(--text); }
.aud-seg-btn[aria-pressed="true"] { background: var(--accent); color: #fff; }
.aud-seg-btn[aria-pressed="true"] span { opacity: .92; }
/* Custom is a state, not a choice — it lights up when the tick boxes stop matching any
   preset and cannot be clicked into. */
.aud-seg-custom { cursor: default; opacity: .55; }
.aud-seg-custom[aria-pressed="true"] { background: var(--warn); color: #1a1205; opacity: 1; }
/* The Height warning, raised where it can still be acted on rather than printed on the
   finished quote as an admission. */
.rp-warn { color: var(--err); font-size: 11.5px; line-height: 1.35; margin: 2px 0 6px 22px; }
/* QB's Divisions/Sections filter tree in the report options dialog. */
.rp-tree { max-height: 220px; overflow: auto; border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; margin-bottom: 8px; }
.rp-div + .rp-div { margin-top: 6px; }
.je-print-check.rp-sec { padding-left: 18px; }

/* The purchasing list's disclosure blocks — what it can't order and what it left out. Kept
   visually as loud as the order table itself: this is the half of the document a reader
   would otherwise never know was missing. */
.pl-block { margin-top: 16px; }
.pl-block h3 { font-size: 12.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin: 0 0 6px; }
.pl-block.warn h3 { color: #e8a33d; }
.pl-recon { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: baseline; margin-top: 14px; padding-top: 8px;
  border-top: 1px solid var(--line); font-size: 12.5px; color: var(--muted); }
.pl-recon b { color: var(--text); }

/* --- Job-wide quick price tab (mirrors the rollup table's own conventions) --- */
.jqp-toggle { margin-right: 4px; vertical-align: middle; }
.jqp-varies { font-size: 11px; font-weight: 700; color: #e8a33d; white-space: nowrap; }
.rollup-table tr.jqp-sub-row td { padding: 4px 10px; border-bottom: none; color: var(--muted); font-size: 12.5px; background: var(--panel-2); }
.jqp-sub-name { padding-left: 22px; }
.jqp-bulk { display: flex; align-items: center; gap: 6px; justify-content: flex-end; }
.jqp-price-input, .jqp-bulk-input {
  width: 84px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 4px 7px; color: var(--text); font-size: 13px; font-family: inherit;
  text-align: right; font-variant-numeric: tabular-nums;
}
.jqp-price-input:focus, .jqp-bulk-input:focus { border-color: var(--accent); }
.jqp-apply-all { font-size: 11.5px; padding: 4px 8px; white-space: nowrap; }

/* --- Manual estimate tab (catalog picker + line items) --- */
.mep-picker { margin-bottom: 16px; }
.mep-picker-types { display: flex; gap: 6px; margin-bottom: 8px; }
.mep-type-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.mep-search { width: 100%; max-width: 360px; }
.mep-results {
  margin-top: 6px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2);
  max-height: 260px; overflow-y: auto;
}
.mep-result {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 10px; font-size: 13px; border-bottom: 1px solid var(--line);
}
.mep-result:last-child { border-bottom: none; }
.mep-result-disabled { color: var(--muted); font-style: italic; }
.mep-result-unit { color: var(--muted); font-size: 11.5px; margin-left: 6px; }
.mep-rate-per {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 3px 6px; color: var(--text); font-size: 12.5px; font-family: inherit;
}
.mep-qty-input { width: 84px; }

/* --- Estimate window mode (?estimate=1&job=…) — the second-monitor live estimate --- */
.estimate-window-root { padding: 14px 20px 40px; max-width: 1250px; margin: 0 auto; overflow-y: auto; height: calc(100vh - 48px); }
body.estimate-mode .search-box { display: none; }
/* The pop-out is a bid grid on a second monitor and nothing else, so it gets the whole
   window. "Takeoff | Alice Estimator | ⋯ More | Log out" is 48px of app chrome for a
   window that navigates nowhere — there is no project to switch, no sheet to search, and
   the way out is the window's own close button. Together with the head strip the actions
   menu replaced, this is ~95px of the 312px that used to sit above condition row one. */
body.estimate-mode #topbar { display: none; }
body.estimate-mode .estimate-window-root { padding-top: 6px; height: 100vh; }
body.estimate-mode .estimate-card { padding: 4px 18px 16px; }
/* The pop-out exists to BE the bid grid on a second monitor, so it gets that monitor. The
   1250px cap is right for the estimate inside the workspace (where the card sits in a
   column of prose-width panels) and wrong here: on a 1500px window it left 270px empty
   down the right-hand side while the Condition column showed a quarter of a wall makeup,
   and on a 2560px monitor it wasted more than half the screen. Only the grid widens —
   the markup / buildup / totals tables below are narrow tables of percentages that read
   worse stretched, so they keep a cap of their own.
   The `margin: 0` matters as much as the cap: body is a column flexbox here, and an auto
   cross-axis margin suppresses the stretch that would otherwise fill the row — with the
   cap gone but the margin left in, the root still sized to its own content and stopped
   ~290px short of the window edge. */
body.estimate-mode .estimate-window-root { max-width: none; margin: 0; }
body.estimate-mode .je-markup-table,
body.estimate-mode .je-buildup-table,
body.estimate-mode .je-totals-table { max-width: 1000px; }
/* The width the grid gained here used to be handed to the Condition column and then taken
   straight back by a `max-width: min(58ch, 30vw)` on the description inside it: the 58ch
   term bound at every viewport above ~1354px, so the column stood at 638px with 406px of
   text in it at 1600, and at 1065px with the same 406px at 2560 — the wide monitor the
   comment promised never got the benefit, and the widest name on this job (626px, which
   fits either column outright) ellipsised at both. There is no cap here now: the
   description is a flex item with min-width:0 (see .je-cond-name-inner) so it takes the
   column it is given and shrinks with it, which is the behaviour the cap was approximating
   badly. */

/* --- Estimate screen conditions grid (the QB-style condition list) --- */
.je-cond-row { cursor: pointer; }
.je-cond-row:hover td { background: var(--panel-2); }
.je-cond-row.je-cond-unpriced td { background: rgba(232, 163, 61, .07); }
.je-unpriced-tag { font-size: 11px; font-weight: 700; color: var(--warn); white-space: nowrap; }
.je-match-tag { font-size: 11px; font-weight: 700; color: var(--accent); white-space: nowrap; }
.je-manual-tag { font-size: 10.5px; color: var(--muted); cursor: help; }
/* A row whose own numbers say it can't be right (web/js/costSanity.js). Amber rather than
   red: the row is not broken and the bid still totals — it is a "go and look at this"
   before the number leaves the building, in the same register as the ⚡ review flag it
   sits beside. Both the mark and the unit price carry it, so the row reads as suspect
   whether you're scanning the Mark column or the money. */
.je-sanity-mark { font-size: 11px; font-weight: 700; color: var(--warn); cursor: help; }
.je-price-suspect { color: var(--warn); }
.je-price-suspect input { color: var(--warn); }
.je-info-missing { color: var(--warn); }
.btn-link { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 12px; padding: 0; text-decoration: underline; }
.btn-link:hover { color: var(--text); }
.je-cond-table .condition-swatch { width: 13px; height: 13px; border-radius: 3px; display: inline-block; }

/* --- Bid total bar: the money number, pinned ---
   It used to sit ~2,400px down the page under 66 ungrouped rows; it now leads the card and
   sticks to the top of the scrollport, so "what is this job bid at" is answerable at any
   scroll position. */
.je-bidbar {
  position: sticky; top: 0; z-index: 6; display: flex; align-items: center; flex-wrap: wrap;
  gap: 8px 28px; margin: 0 -18px 3px; padding: 9px 18px;
  background: var(--panel); border-top: 1px solid var(--line); border-bottom: 2px solid var(--accent);
}
.je-bidbar-total { display: flex; align-items: baseline; gap: 10px; }
.je-bidbar-label { font-size: 11px; font-weight: 700; letter-spacing: .6px; color: var(--muted); }
.je-bidbar-value { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
.je-bidbar-target { font-size: 12px; color: var(--muted); }
.je-bidbar-target.je-over { color: var(--err); }
.je-bidbar-target.je-under { color: var(--ok); }
.je-bidbar-parts { display: flex; gap: 22px; flex-wrap: wrap; }
.je-bidbar-part { display: flex; flex-direction: column; font-size: 12px; color: var(--muted); }
.je-bidbar-part b { font-size: 14px; color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; }
/* The job-identity strip lives inside the bar's right end now rather than in its own
   ~50px row beneath it — see jobInfoBarHtml. Two stacked lines of 12px sit inside the
   bar's existing height, so it costs nothing. */
.je-bidbar-right { margin-left: auto; text-align: right; display: flex; flex-direction: column; gap: 2px; }
.je-bidbar-counts { font-size: 12px; color: var(--muted); }
.je-bidbar-info { font-size: 11.5px; color: var(--muted); display: flex; align-items: center; gap: 5px; justify-content: flex-end; }
.je-bidbar-ident { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- Bid grid: grouping/filter controls + collapsible subtotal rows --- */
/* One line, always. Five filters plus a search box plus three buttons wrapped to a second
   line the moment Section joined them, and a wrapped filter row costs 34px of the same
   height budget the bid bar and the sticky header are already spending — it is height
   taken from ROWS, which is the only thing on this screen an estimator is actually
   reading. The selects are capped rather than shortened: "09 21 16 Gypsum Board
   Assemblies" stays the option text (and the tooltip), it just doesn't get to set the
   control's width. */
.je-grid-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 5px; font-size: 12px; color: var(--muted); }
.je-grid-controls label { display: flex; align-items: center; gap: 5px; min-width: 0; }
.je-grid-controls select { max-width: 132px; }
/* .jobs-search carries a 240px min-width for the jobs home, where it is the only control
   on its row. Here it is one of nine, and at 1280px those 90 extra pixels were enough to
   wrap this row onto three lines — 32px taken from the grid so a name filter could be
   wider than any condition code you would type into it. */
.je-grid-search { width: 150px; min-width: 0; flex: none; }
.je-fold-btns { display: flex; gap: 3px; }
.je-fold-btns .btn { padding: 3px 7px; font-size: 13px; line-height: 1; }
.je-grid-hidden { margin-left: auto; }
.je-cond-table .je-cond-no { width: 38px; text-align: right; color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
/* One line per condition, OST-tight (~25px rows): a partition name is the whole wall
   makeup spelled out and wraps to three lines, which is what turned 66 rows into a page
   of scrolling. The code stays in bold at the left; the full text is the cell's tooltip. */
/* Fixed layout: the colgroup jobEstimate.js emits gives every column but Condition an
   explicit width, so Condition is the one auto column and absorbs whatever the window has
   left. Content can no longer widen a column, which is the only way the description gets
   the space it is given instead of the table growing past the card to fit the longest
   name on the job. */
.je-cond-table { table-layout: fixed; }
.je-cond-table td { padding: 3px 8px; overflow: hidden; text-overflow: ellipsis; }
.je-cond-table .je-cond-name { white-space: nowrap; }
/* The description is the ONLY thing in this cell allowed to shrink; the bold code, the
   plan-set hint and the ⚠ tag are flex:none, so the ellipsis always lands on the wall
   makeup and never on the code that identifies the row. min-width:0 on both the row and
   the description is what lets the table shrink this column below its text — without it
   an auto-layout table sizes the column to the longest name on the job and pushes the
   money columns, the reason the screen exists, off the right edge.
   There is deliberately NO min-width here. It used to carry one, and it was inert: under
   table-layout:fixed a cell's min-width is ignored outright — the column widths come from
   the <col> elements and the table's own width, nothing else — so the rule read as a floor
   the layout never honoured, and at an 800px window this column still computed to 0px.
   The floor now lives on the table's min-width, which fixed layout must obey; see the
   comment above the <table> in jobEstimate.js. */
.je-cond-name-inner { display: flex; align-items: baseline; gap: 5px; min-width: 0; overflow: hidden; }
.je-cond-aside { flex: none; }
/* Drawn as head + tail (see splitDescTail): the head absorbs the ellipsis, the tail is
   flex:none so the extent clause that ends the name — the part that separates C91B from
   C92R — is on screen at every column width instead of being the first thing cut. */
.je-cond-desc {
  color: var(--muted); display: flex; flex: 1 1 auto; min-width: 0;
  overflow: hidden; white-space: nowrap;
}
.je-cond-desc-head { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* The tail does not shrink: when the column is too narrow for both, the head is the half
   worth losing — it is the wall makeup, which every C-series row on the job repeats
   verbatim, while the bold code beside it already identifies the row. max-width caps the
   pathological case (a column narrower than the clause itself) so the tail ellipsises
   rather than shouldering the head out of the cell entirely.
   pre, not nowrap: the boundary space belongs to the tail, and collapsing it would join
   "…Blanket+5/8" to "From Raised Floor" with no gap when the whole name does fit. */
.je-cond-desc-tail {
  flex: 0 0 auto; max-width: 100%; white-space: pre;
  overflow: hidden; text-overflow: ellipsis;
}
/* OST's Estimate Tab draws ~13px rows and fits a small job — conditions, group bars and
   the Totals row — inside a 430px window. Ours is Quick Bid's 25px, which is the right
   default for a grid you type into, and the wrong one for the moment you want to see the
   whole bid at once. Compact is that moment: ~18px, everything still hit-testable, toggled
   from ▦ Columns and remembered. */
.je-cond-compact td { padding: 0 8px; font-size: 12px; line-height: 18px; }
.je-cond-compact .je-group-row td { line-height: 14px; }
.je-cond-compact .je-grid-input { padding: 0 4px; }
.je-cond-table .je-unit-price { color: var(--text); }
/* The unit travels with the number (QB carries no Unit column — see quickbid-structure.md
   §2). Muted and a size down so a column of quantities still reads as a column of numbers:
   what an estimator scans down is the figures, and "LF" repeated 64 times must not compete
   with them for the eye the way the standalone column did. */
.je-qty-unit { color: var(--muted); font-size: 11px; }
/* Colour chip + both status glyphs (▤ assembly, ⚡ needs review) in one 54px column. */
.je-mark-cell { white-space: nowrap; padding-left: 6px !important; padding-right: 4px !important; }
.je-mark-cell .je-item-mark { color: var(--muted); font-size: 10px; margin-left: 3px; opacity: .75; }
.je-mark-cell .je-match-tag { font-size: 11px; margin-left: 2px; }
.je-group-row { cursor: pointer; background: var(--panel); }
/* 17px, the height the takeoff-side Conditions Window already proved out for its own group
   headers — a group row is a label and two numbers, not a condition. At the 30px it used
   to be, this job's ten group rows in a two-level tree ate 130px of the scrollport, which
   is five conditions' worth of reading spent on the headings above them. */
.je-group-row td {
  font-weight: 600; font-size: 11.5px; line-height: 15px; padding: 1px 10px;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.je-group-row:hover td { background: var(--panel-2); }
/* Second level (code family inside a scope) reads as a sub-total, not a peer of the
   scope heading — same weight distinction QB's Bid Summary draws between Section and Area. */
.je-group-row.je-group-d1 td { font-weight: 500; font-size: 12px; background: transparent; border-top: none; }
.je-group-row.je-group-d0 td { background: var(--panel); }
.je-group-caret { color: var(--muted); display: inline-block; width: 12px; }
.je-cond-table tfoot .je-foot-shown td { font-weight: 600; border-top: 1px solid var(--line); color: var(--muted); }

/* --- The header row stays put ---
   Deliberately NOT wrapped in .table-scroll: that sets overflow-x, which promotes
   overflow-y to auto and traps a sticky <thead> inside a box that never scrolls
   vertically — the header would compute as sticky and still scroll away. With the
   wrapper transparent to overflow, the page is the scrollport and the header pins
   directly beneath the (also sticky) bid-total bar, whose measured height jobEstimate.js
   publishes as --je-bidbar-h. Past ~600px of scroll this screen used to be four columns
   of unlabelled em-dashes under a bar carrying the total but not the column names. */
.je-grid-scroll { overflow: visible; }
.je-cond-table thead th {
  position: sticky; top: var(--je-bidbar-h, 62px); z-index: 4; background: var(--panel);
  box-shadow: inset 0 -1px 0 var(--line), 0 1px 0 var(--line); white-space: nowrap; user-select: none;
}
.je-cond-table thead th.je-th-sort { cursor: pointer; }
.je-cond-table thead th.je-th-sort:hover { color: var(--text); }
.je-cond-table thead th.je-th-active { color: var(--accent); }
.je-sort-mark { font-size: 9px; margin-left: 3px; }
/* Grab strip on the header's right edge. Sits inside the cell so it can't be covered by
   the next column, and swallows its own click so resizing never also re-sorts. */
.je-col-resize { position: absolute; top: 0; right: 0; width: 6px; height: 100%; cursor: col-resize; }
.je-col-resize:hover { background: var(--accent); opacity: .5; }
.je-col-sel { width: 26px; text-align: center; }
.je-col-sel input { cursor: pointer; margin: 0; }
.je-cond-row.je-cond-selected td { background: var(--active-bg); }

/* Typed unit price and typed Qty — OST 13.01's editable cost cell, on the grid's single
   Unit Price column (per brian: no per-row Material/Labor/Sub breakdown on this screen)
   and on the quantity column OST says an estimator fills in for every condition they have
   not drawn. A DRAWN quantity is still never editable — real takeoff outranks a typed
   number everywhere, so an editor over it would be a control that changes nothing — but a
   DERIVED unit price now is: typing over it records an override that outranks the assembly
   without touching it (see unitPriceCell), which is what "shave this wall to hit the
   number" has always meant in every estimating program worth the name.
   An editable cell at rest is a NUMBER, not a box — both bars draw it that way and only
   become an input under the caret. This grid was drawing 124 bordered boxes on load, all
   of them empty, which is what made it read as a form rather than as a bid. The green left
   edge is QB's "you may type here" colour language kept at 2px on the cell itself, so the
   affordance survives without the 124 boxes. A drawn Qty has no edge at all — the four
   cells on this job that are genuinely not yours to type in look it. Every Unit Price cell
   now carries one, because every one of them now IS typable; "which of these rows needs
   me" is answered a row at a time instead, by the faint $/unit ghost where no price exists
   and the amber tint on a row with takeoff and nothing to charge for it. */
.je-editable { cursor: text; box-shadow: inset 2px 0 0 var(--ok); }
.je-editable:hover { background: var(--panel-2); }
.je-cell-ghost { color: var(--muted); opacity: .4; }
.je-cost-cell, .je-qty-cell { white-space: nowrap; }
/* A price the estimator typed over one the program derived reads BLUE, which is OST's own
   convention for a typed value (13.01's priced sample renders its hand-entered Qty1 in blue
   against the derived figures around it). Without it the two are the same glyphs in the
   same column and nobody can tell, three days later, which walls they shaved. */
.je-cond-table .je-price-typed { color: var(--accent); font-weight: 600; }
.je-cond-table .je-price-typed .je-manual-tag { color: var(--accent); opacity: .8; }
.je-cost-input {
  width: 68px; background: var(--panel); border: 1px solid var(--line); border-left: 2px solid var(--ok);
  border-radius: 4px; padding: 1px 4px; color: var(--text); font-size: 11.5px; font-family: inherit;
  text-align: right; font-variant-numeric: tabular-nums;
}
.je-qty-input {
  width: 64px; background: var(--panel); border: 1px solid var(--line); border-left: 2px solid var(--ok);
  border-radius: 4px; padding: 1px 4px; color: var(--text); font-size: 11.5px; font-family: inherit;
  text-align: right; font-variant-numeric: tabular-nums;
}
.je-qty-input::placeholder { color: var(--muted); opacity: .45; }
.je-cost-input:focus, .je-qty-input:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(93, 156, 236, .25);
}
.je-cost-input::placeholder { color: var(--muted); opacity: .55; }
/* In-flight save. Deliberately not `disabled`: the estimator has already tabbed on and a
   disabled box that repaints under them reads as the grid fighting back. */
.je-grid-input.je-input-saving { opacity: .55; }
/* Hide the spinners — they steal 16px from a 64px cell and nobody nudges a wall's LF by
   one with a mouse. */
.je-grid-input::-webkit-outer-spin-button, .je-grid-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.je-grid-input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.je-section-cell { font-size: 11.5px; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.je-section-cell.je-section-set { color: var(--text); }
.je-blend-note { font-size: 10px; cursor: help; }
.je-area-cell { font-size: 12px; color: var(--muted); white-space: nowrap; }
.je-scope-cell { font-size: 12px; color: var(--muted); white-space: nowrap; }
.je-scope-cell.je-scope-set { color: var(--text); }

/* Print/report options — QuickBid's three-column "Modify Options" dialog. */
.je-print-opts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 20px; margin-bottom: 8px; }
.je-print-check { display: flex; align-items: center; gap: 6px; font-size: 12.5px; padding: 2px 0; cursor: pointer; }
.je-print-check input { margin: 0; }

/* --- Estimate worksheet --- */
.est-settings { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
.est-setting { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
.est-pct-wrap { display: flex; align-items: center; gap: 3px; }
.est-setting input {
  width: 72px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 5px 7px; color: var(--text); font-size: 13px; font-family: inherit; text-align: right;
}
.est-setting input:focus { outline: none; border-color: var(--accent); }
.est-pct { color: var(--muted); }
.est-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 12px; }
.est-table th { text-align: left; font-size: 11px; color: var(--muted); font-weight: 600; padding: 5px 8px; border-bottom: 1px solid var(--line); }
.est-table td { padding: 6px 8px; border-bottom: 1px solid var(--line); }
.est-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.est-line.expandable { cursor: pointer; }
.est-line.expandable:hover { background: var(--panel-2); }
.est-caret { color: var(--muted); font-size: 10px; margin-right: 2px; }
.est-basis { font-size: 10.5px; color: var(--muted); background: var(--panel-2); padding: 0 6px; border-radius: 8px; margin-left: 4px; }
.est-unpriced { font-size: 10.5px; color: var(--warn); margin-left: 4px; }
.est-line.is-unpriced { opacity: .7; }
.est-comp-row td { color: var(--muted); font-size: 12px; background: rgba(0,0,0,.15); border-bottom: 1px solid var(--line); }
.est-comp-name { padding-left: 22px !important; }
.est-warn { color: var(--warn); margin: -4px 0 14px; }
.est-buildup { max-width: 460px; margin-left: auto; }
.est-buildup td { border-bottom: none; padding: 4px 8px; }
.est-buildup tr.est-subtotal td { border-top: 1px solid var(--line); padding-top: 8px; }
.est-buildup tr.est-grand td { border-top: 2px solid var(--accent); font-size: 15px; font-weight: 700; padding-top: 8px; color: var(--text); }

/* --- Bid summary --- */
.bid-edit { margin-bottom: 8px; }
.bid-alt-row { display: grid; grid-template-columns: 1fr 130px 24px; gap: 8px; align-items: center; margin-bottom: 6px; }
.bid-alt-row input {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 6px 8px; color: var(--text); font-size: 13px; font-family: inherit;
}
.bid-alt-row input:focus { outline: none; border-color: var(--accent); }
.bid-alt-amount { text-align: right; }
.bid-alt-remove { cursor: pointer; opacity: .6; text-align: center; }
.bid-alt-remove:hover { opacity: 1; color: var(--err); }
.bid-preview-card { background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; margin-top: 8px; }
.bid-sec-title { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); font-weight: 700; margin: 14px 0 6px; }
.bid-sec-title:first-child { margin-top: 0; }
.bid-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.bid-table td { padding: 4px 6px; border-bottom: 1px solid var(--line); }
.bid-table .r { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.bid-buildup tr.sub td { border-top: 1px solid var(--muted); font-weight: 600; }
.bid-buildup tr.grand td { border-top: 2px solid var(--accent); font-size: 15px; font-weight: 700; padding-top: 8px; }
.bid-list { margin: 4px 0; padding-left: 20px; font-size: 13px; }
.bid-notes-block { font-size: 13px; color: var(--text); }
.bid-table th {
  text-align: left; color: var(--muted); font-weight: 500; font-size: 11px;
  text-transform: uppercase; letter-spacing: .03em; padding: 4px 6px; border-bottom: 1px solid var(--line);
}

/* --- Job-level estimate (Phase E coversheet) --- */
.estimate-card { background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; padding: 16px 18px; margin-bottom: 22px; }
.jp-form { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 12px; }
.jp-form .jp-wide { grid-column: 1 / -1; }
.estimate-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
.estimate-head h2 { font-size: 16px; margin: 0; }
.estimate-actions { display: flex; gap: 8px; align-items: center; }
.estimate-warn { color: var(--warn); }
.carry-dup-warn { color: var(--warn); border: 1px solid var(--warn); border-radius: 6px; padding: 8px 10px; }
/* Unpriced-component banner: a bid whose totals are silently missing labor is the most
   dangerous number the coversheet can show — this must read as an error, not a hint. */
.je-skipped-warn { border: 2px solid var(--err); background: #3a1616; color: #ffb3ad; border-radius: 6px; padding: 10px 14px; margin: 12px 0; font-size: 13px; }
.je-skipped-warn .je-skipped-head { font-weight: 700; margin-bottom: 4px; }
.je-skipped-warn ul { margin: 4px 0 0; padding-left: 20px; }
.je-skipped-warn li { margin: 2px 0; }
.je-skip-fixes { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0 6px; }
.je-skip-fix {
  background: #ffb3ad; color: #3a1616; border: none; font-weight: 600; font-size: 12px;
  padding: 5px 10px; border-radius: 5px; cursor: pointer;
}
.je-skip-fix:hover { background: #ffcac6; }
.je-markup-table input, .je-settings-row input { width: 80px; }
.je-markup-table td { padding: 5px 10px; }
.je-settings-row { display: flex; gap: 20px; flex-wrap: wrap; font-size: 13px; margin-bottom: 6px; }
.je-settings-row label { display: flex; align-items: center; gap: 6px; }
.je-hint { color: var(--muted); font-size: 11px; }
.je-bond-label { gap: 8px !important; }
.je-buildup-table tbody tr:hover td { background: var(--panel); }
.je-totals-table { margin-top: 4px; }
.je-target-row td { color: var(--muted); font-size: 12px; }
.je-target-row .je-over { color: var(--err); }
.je-target-row .je-under { color: var(--ok); }
.je-lines-title { display: flex; align-items: center; justify-content: space-between; }
.je-lines-table { margin-top: 4px; }
.je-cond-head td { font-weight: 600; color: var(--text); padding-top: 10px; }
.je-line-indent { padding-left: 18px !important; color: var(--muted); }
.je-line-dates { color: var(--muted); font-size: 11px; text-align: center; }
.je-area-toggle { margin-right: 4px; vertical-align: middle; }
.je-area-rows td { padding: 0 10px 6px 10px !important; border-bottom: 1px solid var(--line); }
.je-area-breakdown-table { width: 100%; background: var(--panel-2); border-radius: 6px; }
.je-area-breakdown-table td { padding: 3px 10px; color: var(--muted); font-size: 12.5px; }

.sheet-manage-table tr.sm-row-hidden { opacity: .5; }
.sm-scale-cell { display: flex; flex-direction: column; gap: 2px; }
.sm-scale-select { font-size: 12px; }
.sm-scale-status { font-size: 11px; color: var(--muted); }
.sheet-manage-table .sm-repeats-input { width: 60px; }
/* Wide data tables (6-col cost buildup, line items) mustn't force the whole card to scroll
   sideways on a narrow screen — only the table itself scrolls, same pattern .asm-rows uses. */
.table-scroll { overflow-x: auto; }

.ai-compare-narrative { font-size: 13.5px; line-height: 1.55; margin-bottom: 16px; white-space: pre-wrap; }
.ai-compare-sheets { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; margin-bottom: 12px; }
.ai-compare-sheet {
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--panel-2);
}
.ai-compare-sheet:hover { border-color: var(--accent); }
.ai-compare-sheet-title { font-weight: 600; font-size: 13px; margin-bottom: 3px; }
.ai-compare-sheet-summary { font-size: 12.5px; color: var(--text); }
.ai-compare-sheet-focus { font-size: 12px; color: var(--accent); margin-top: 4px; }

.conditions-header > div { display: flex; gap: 6px; }

.sched-rows { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; margin: 12px 0; }
.sched-row {
  display: grid; grid-template-columns: auto auto 1fr 90px 90px; grid-template-rows: auto auto;
  align-items: center; gap: 4px 8px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
}
.sched-row .sched-desc { grid-column: 2 / -1; font-size: 12px; color: var(--muted); }
.sched-row input[type="text"], .sched-row input[type="number"] {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 5px 8px; color: var(--text); font-size: 13px; font-family: inherit;
}
.sched-already { color: var(--err); }
/* Plan-usage badges from the schedule-read grounding pass: green = tag verified on
   plan sheets; amber = printed on the schedule's type matrix but used nowhere (starts
   unchecked); red = tag isn't even in the schedule sheet's own text (likely AI misread). */
.sched-usage { font-size: 11px; font-weight: 700; white-space: nowrap; border-radius: 4px; padding: 1px 6px; }
.sched-usage.good { color: #37b26c; background: rgba(55, 178, 108, .12); }
.sched-usage.warn { color: #e8a33d; background: rgba(232, 163, 61, .13); }
.sched-usage.bad { color: var(--err); background: rgba(224, 82, 82, .13); }
.sched-missed {
  font-size: 12.5px; color: #e8a33d; background: rgba(232, 163, 61, .1);
  border: 1px solid rgba(232, 163, 61, .35); border-radius: 8px; padding: 8px 12px; margin: 8px 0;
}
/* General/typical construction notes found on the schedule sheet (sealant beads, bracing,
   bridging…) that apply broadly across wall types rather than one specific tag's row. */
.sched-general-notes {
  font-size: 12.5px; background: rgba(76, 154, 255, .08);
  border: 1px solid var(--line); border-left: 4px solid var(--accent); border-radius: 8px;
  padding: 8px 12px; margin: 8px 0;
}
.sched-general-notes-title { font-weight: 700; margin-bottom: 2px; }
.sched-general-notes ul { margin: 0; padding-left: 20px; }
.sched-general-notes li { margin: 3px 0; }

.deleted-row {
  display: flex; align-items: center; gap: 8px; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 8px; margin-bottom: 8px;
}
.deleted-name { flex: 1; }
.deleted-when { font-size: 12px; color: var(--muted); }
.sched-row-links { grid-column: 2 / -1; display: flex; gap: 14px; }
.sched-row .sched-source-link, .sched-row .sched-plancheck-link {
  font-size: 11.5px; color: var(--accent); cursor: pointer; width: fit-content;
}
.sched-row .sched-source-link:hover, .sched-row .sched-plancheck-link:hover { text-decoration: underline; }
.cond-review-banner { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 4px 0 10px; }
.cond-review-banner .empty-hint { margin: 0; flex: 1; min-width: 200px; }
.cond-source-btn { font-size: 12.5px; border-color: var(--accent); flex: none; }
.cond-notes-big { min-height: 110px; }

/* --- AI assistant chat panel --- */
#assistant-panel .dock-panel-body { display: flex; flex-direction: column; }
.assistant-messages { flex: 1; overflow-y: auto; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
.assistant-hint { padding: 4px 2px; }
.assistant-msg { max-width: 92%; border-radius: 10px; padding: 7px 10px; font-size: 13px; line-height: 1.45; }
.assistant-msg-user { align-self: flex-end; background: color-mix(in srgb, var(--accent) 22%, var(--panel-2)); }
.assistant-msg-assistant { align-self: flex-start; background: var(--panel-2); border: 1px solid var(--line); }
.assistant-msg-error { border-color: var(--err); color: var(--err); }
.assistant-msg-pending { color: var(--muted); font-style: italic; }
.assistant-msg-sheets { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
.assistant-sheet-chip {
  font-size: 11px; padding: 2px 8px; border-radius: 9px; border: 1px solid var(--line);
  background: var(--panel); color: var(--text); cursor: pointer; font-family: inherit;
}
.assistant-sheet-chip:hover { border-color: var(--accent); color: var(--accent); }
.assistant-inputrow { display: flex; gap: 6px; padding: 8px 10px; border-top: 1px solid var(--line); align-items: flex-end; }
.assistant-inputrow textarea {
  flex: 1; resize: none; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 7px 9px; color: var(--text); font-size: 13px; font-family: inherit;
}
.assistant-inputrow textarea:focus { outline: none; border-color: var(--accent); }
/* Embedded plan-check viewer — an inline panel INSIDE the wizard modal (not a floating
   popup like quick-links use) since only one modal can be open at a time in this app. */
.sched-plancheck {
  margin: 10px 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
  background: var(--panel-2);
}
.sched-plancheck-header {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  border-bottom: 1px solid var(--line); font-size: 13px;
}
.sched-plancheck-header #sched-plancheck-count { font-size: 12px; color: var(--muted); white-space: nowrap; }
.sched-plancheck-viewer { width: 100%; height: 320px; background: #111; }
.sched-reread-link { color: var(--accent); cursor: pointer; }
.sched-reread-link:hover { text-decoration: underline; }

.carry-rows { display: flex; flex-direction: column; gap: 8px; max-height: 50vh; overflow-y: auto; margin: 12px 0; }
.carry-row {
  display: grid; grid-template-columns: auto auto 1fr auto; grid-template-rows: auto auto;
  align-items: center; gap: 4px 8px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
}
.carry-row .carry-desc { grid-column: 2 / -1; font-size: 12px; color: var(--muted); }
.carry-name { font-weight: 500; }
.carry-count { font-size: 12px; color: var(--muted); white-space: nowrap; }
.change-scan-section { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 12px; }
.change-scan-head { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); font-weight: 700; margin-bottom: 6px; }
.change-scan-rows { display: flex; flex-direction: column; gap: 4px; max-height: 40vh; overflow-y: auto; }
.change-scan-row {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: 6px; cursor: pointer; background: var(--panel-2);
}
.change-scan-row:hover { border-color: var(--accent); }
.change-scan-num { font-size: 13px; font-weight: 500; }
.change-scan-pct { font-size: 11.5px; color: var(--warn); white-space: nowrap; }
.modal h2 { margin: 0 0 14px; font-size: 16px; }
.modal .field { margin-bottom: 12px; }
.modal .field label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 5px; }
.modal .field input, .modal .field select, .modal .field textarea {
  width: 100%; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 8px 10px; color: var(--text); font-size: 13.5px; font-family: inherit; resize: vertical;
}
.modal .field input:focus, .modal .field select:focus, .modal .field textarea:focus { outline: none; border-color: var(--accent); }
.modal .field select:disabled { opacity: .55; cursor: not-allowed; }
.heights-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.height-row { display: flex; gap: 6px; align-items: center; }
.height-row .height-label { flex: 1.4; }
.height-row .height-ft { flex: 1; }
.height-row .height-remove { flex: none; cursor: pointer; opacity: .7; padding: 4px; }
.height-row .height-remove:hover { opacity: 1; color: var(--err); }
.modal .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.modal .btn {
  padding: 7px 14px; border-radius: 6px; font-size: 13px; cursor: pointer;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--text); font-family: inherit;
}
.modal .btn:hover { border-color: var(--accent); }
.modal .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.modal .btn.primary:hover { filter: brightness(1.08); }
