/* ============================================================
   components.css — reusable UI primitives
   ============================================================ */
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
  font-family:var(--font);font-size:var(--fs-base);color:var(--text);
  background:var(--bg);line-height:1.5;-webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
h1,h2,h3,h4{margin:0;font-weight:var(--fw-bold);letter-spacing:-.01em;color:var(--text)}
a{color:var(--brand-600);text-decoration:none}
button{font-family:inherit}
::selection{background:var(--brand-200);color:var(--text)}

/* sane default size for inline icons — context rules (.btn, .kpi, .nav…) override.
   Without this, an SVG with only a viewBox expands to fill its container. */
svg{width:16px;height:16px;flex:none;vertical-align:-2px}

/* scrollbars */
*{scrollbar-width:thin;scrollbar-color:var(--border-strong) transparent}
*::-webkit-scrollbar{width:10px;height:10px}
*::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:99px;border:3px solid transparent;background-clip:content-box}
*::-webkit-scrollbar-thumb:hover{background:var(--text-3)}

/* ---- buttons ---- */
.btn{
  display:inline-flex;align-items:center;gap:var(--sp-2);justify-content:center;
  padding:9px 15px;border-radius:var(--r-sm);border:1px solid var(--border-strong);
  background:var(--surface);color:var(--text);font-size:var(--fs-sm);font-weight:var(--fw-semibold);
  cursor:pointer;transition:all .15s ease;white-space:nowrap;line-height:1;
}
.btn:hover{background:var(--surface-2);border-color:var(--text-3)}
.btn:active{transform:translateY(1px)}
.btn:focus-visible{outline:none;box-shadow:var(--ring)}
.btn svg{width:16px;height:16px;flex:none}
/* --brand-fill, not --brand-600: this is a FILL under white text, and the ink ramp
   lightens in dark mode. See the FILL vs INK note in tokens.css. */
.btn-primary{background:var(--brand-fill);border-color:var(--brand-fill);color:var(--on-brand)}
.btn-primary:hover{background:var(--brand-fill-hover);border-color:var(--brand-fill-hover)}
.btn-ghost{background:transparent;border-color:transparent}
.btn-ghost:hover{background:var(--surface-3)}
.btn-danger{color:var(--danger-500);border-color:transparent;background:transparent}
.btn-danger:hover{background:var(--danger-50)}
.btn-sm{padding:6px 10px;font-size:var(--fs-xs)}
.btn-icon{padding:8px;border-radius:var(--r-sm)}
.btn-icon svg{width:17px;height:17px}
.btn:disabled{opacity:.5;cursor:not-allowed}

/* ---- cards ---- */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow-sm)}
.card-pad{padding:var(--sp-6)}

/* ---- inputs ---- */
.field{display:flex;flex-direction:column;gap:6px}
.field label{font-size:var(--fs-xs);font-weight:var(--fw-semibold);color:var(--text-2);text-transform:uppercase;letter-spacing:.04em}
.input,.select,textarea.input{
  width:100%;padding:9px 12px;border:1px solid var(--border-strong);border-radius:var(--r-sm);
  background:var(--surface);color:var(--text);font-size:var(--fs-base);font-family:inherit;transition:all .15s;
}
.input:focus,.select:focus,textarea.input:focus{outline:none;border-color:var(--brand-500);box-shadow:var(--ring)}
.input::placeholder{color:var(--text-3)}
.input-num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--mono);font-size:var(--fs-sm)}
/* Chevron is baked into the data-URI as a fixed hex per theme (a native <select>
   can't host a mask or ::after, so a mask+currentColor trick doesn't apply here)
   — the old single light-mode grey (#8b91a7) never got a dark counterpart and
   fell to ~2.9:1 on the dark surface. */
.select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23636a7e' stroke-width='3'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:30px;cursor:pointer}
[data-theme="dark"] .select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a93b6' stroke-width='3'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");}

/* ---- pills / badges ---- */
.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:var(--r-full);font-size:var(--fs-xs);font-weight:var(--fw-semibold);background:var(--surface-3);color:var(--text-2)}
.pill-brand{background:var(--brand-50);color:var(--brand-600)}
.pill-ok{background:var(--ok-50);color:var(--ok-700)}
.pill-warn{background:var(--warn-50);color:var(--warn-700)}
.pill-danger{background:var(--danger-50);color:var(--danger-700)}
/* An auto take-off suggestion is never a measurement — it has to read differently
   from a number a human typed, at a glance, on both the input and its unit caption.
   Three confidence classes, colour-graded so the estimator sees at a glance which
   quantities to review before a bid: LOW (routing assumption, amber-red),
   MEDIUM (count/geometry-derived, amber), HIGH (drawing-measured, green). */
.pill-est{background:var(--warn-50);color:var(--warn-700);padding:1px 6px;text-transform:uppercase;letter-spacing:.04em}
.pill-est.est-low{background:var(--danger-50);color:var(--danger-700)}
.pill-est.est-high{background:var(--ok-50);color:var(--ok-700)}
.inline-input.is-estimated{background:var(--warn-50);border-color:var(--warn-700);color:var(--warn-700);font-style:italic}
.inline-input.is-estimated.est-low{background:var(--danger-50);border-color:var(--danger-700);color:var(--danger-700)}
.inline-input.is-estimated.est-high{background:var(--ok-50);border-color:var(--ok-700);color:var(--ok-700);font-style:normal}
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok-500);flex:none;vertical-align:1px}

/* ---- validation badge (Confidence / Engineering Reference / Version / Last Verified) ---- */
.metabadge{display:inline-block}
.metabadge summary{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border-radius:var(--r-full);font-size:var(--fs-xs);font-weight:var(--fw-semibold);background:var(--surface-3);color:var(--text-2);cursor:pointer;list-style:none}
.metabadge summary::-webkit-details-marker{display:none}
.metabadge summary:hover{background:var(--surface-2)}
.metabadge .metabody{margin-top:8px;padding:10px 12px;border:1px solid var(--border);border-radius:var(--r-sm);background:var(--surface);font-size:var(--fs-xs);line-height:1.7;max-width:340px}
.metabadge .metabody .muted{color:var(--text-3)}

/* ---- explain calculation panel ---- */
.explain{margin-top:4px}
.explain summary{cursor:pointer;font-size:var(--fs-xs);color:var(--text-3);list-style:none;display:inline-flex;align-items:center;gap:4px}
.explain summary::-webkit-details-marker{display:none}
.explain summary:hover{color:var(--brand-600)}
.explain summary .chev{display:inline-block;transition:transform .15s;font-size:10px}
.explain[open] summary .chev{transform:rotate(90deg)}
.explain .expbody{margin-top:8px;padding:12px 14px;border:1px solid var(--border);border-radius:var(--r-sm);background:var(--surface-2);font-size:var(--fs-xs);line-height:1.8}
.explain .expbody .row{display:flex;justify-content:space-between;gap:12px;padding:3px 0}
.explain .expbody .formula{font-family:var(--mono);background:var(--surface-3);padding:8px 10px;border-radius:6px;margin:4px 0 8px;word-break:break-word}

/* ---- toggle / segmented ---- */
.segmented{display:inline-flex;background:var(--surface-3);border-radius:var(--r-sm);padding:3px;gap:2px}
.segmented button{border:none;background:transparent;color:var(--text-2);padding:6px 13px;border-radius:7px;font-size:var(--fs-sm);font-weight:var(--fw-semibold);cursor:pointer;transition:all .15s}
.segmented button.active{background:var(--surface);color:var(--text);box-shadow:var(--shadow-sm)}

.switch{position:relative;width:40px;height:23px;flex:none}
.switch input{opacity:0;width:0;height:0}
.switch .slider{position:absolute;inset:0;background:var(--border-strong);border-radius:99px;transition:.2s;cursor:pointer}
.switch .slider::before{content:"";position:absolute;height:17px;width:17px;left:3px;top:3px;background:#fff;border-radius:50%;transition:.2s;box-shadow:var(--shadow-sm)}
.switch input:checked + .slider{background:var(--brand-fill)}
.switch input:checked + .slider::before{transform:translateX(17px)}

input.checkbox{width:18px;height:18px;flex:none;accent-color:var(--brand-600);cursor:pointer;margin:0}

/* ---- material picker (Add material modal: Library/Recent/Favorites rows) ---- */
.pick-list{display:flex;flex-direction:column;gap:2px;max-height:400px;overflow:auto;margin:0 -4px}
.pick-row{display:flex;align-items:center;gap:10px;padding:9px 8px;border-radius:var(--r-sm)}
.pick-row:hover{background:var(--surface-2)}

/* ---- dependency advisory chip ("consider adding…") ---- */
.advisory-chip{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:var(--r-md);background:var(--warn-50);color:var(--warn-700);font-size:var(--fs-sm)}
.advisory-chip svg{flex:none;width:16px;height:16px}
@keyframes row-flash{0%,100%{background:transparent}30%{background:var(--warn-50)}}
.row-flash{animation:row-flash 1.6s ease}

/* ---- tables ---- */
.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface);transition:box-shadow .15s}
/* Scroll-shadow affordance: a thin browser scrollbar alone is easy to miss on a wide
   table (Trade Database, Materials, Multiplier overrides, ...), so the Unit Price or
   Actions column can silently sit off-screen with no clue there's more. .has-more-x /
   .has-more-x-left are toggled in app.js (after every render + on actual scroll) based
   on real scrollWidth/scrollLeft — box-shadow paints on the wrap's own box, never the
   scrolling content, so it stays correctly pinned regardless of scroll position. */
.table-wrap.has-more-x{box-shadow:inset -14px 0 10px -10px rgba(0,0,0,.16)}
.table-wrap.has-more-x-left{box-shadow:inset 14px 0 10px -10px rgba(0,0,0,.16)}
.table-wrap.has-more-x.has-more-x-left{box-shadow:inset -14px 0 10px -10px rgba(0,0,0,.16),inset 14px 0 10px -10px rgba(0,0,0,.16)}
table.data{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--fs-sm)}
table.data thead th{
  position:sticky;top:0;z-index:2;background:var(--surface-2);text-align:left;
  padding:10px 12px;font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.04em;
  color:var(--text-3);font-weight:var(--fw-bold);border-bottom:1px solid var(--border);white-space:nowrap;
}
table.data tbody td{padding:9px 12px;border-bottom:1px solid var(--border);color:var(--text-2);vertical-align:middle}
/* A tbody <th scope="row"> (the element editor's Mark cell — AT-meaningful as
   the row's label) must look exactly like a td; nothing above styles tbody th,
   so without this it would fall back to the browser's bold/centered default. */
table.data tbody th{padding:9px 12px;border-bottom:1px solid var(--border);color:var(--text-2);vertical-align:middle;font-weight:inherit;text-align:inherit;background:none}
table.data tbody tr:hover{background:var(--surface-2)}
table.data tbody tr.row-clickable{cursor:pointer}
table.data tbody tr:last-child td{border-bottom:none}
table.data .num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--mono);color:var(--text)}
/* Key/value variant — the material story's fact tables. No thead to stick to, a
   narrow label column, and no row-hover: these are facts to read, not rows to pick. */
table.data--kv tbody th{width:1%;white-space:nowrap;padding-right:var(--sp-4);color:var(--text-2);font-weight:var(--fw-med)}
table.data--kv tbody td{color:var(--text)}
table.data--kv tbody tr:hover{background:none}
table.data .strong{font-weight:var(--fw-bold);color:var(--text)}
/* Section group-headers (Materials/Multipliers/Trade DB) stick below the
   already-sticky thead so the section context survives scrolling a long
   grouped table — approximates the header's rendered height (~38px). */
.row-sub{background:var(--surface-3)!important;font-weight:var(--fw-bold);color:var(--text);position:sticky;top:38px;z-index:1}
.cell-name{font-weight:var(--fw-semibold);color:var(--text)}
/* A material name that opens its own story. Underline-on-hover rather than a
   permanent link colour: in a table of 600 names, 600 blue links is noise, but a
   name that does nothing on click is a dead end. */
.cell-link{font:inherit;color:inherit;background:none;border:0;padding:0;text-align:left;cursor:pointer;border-radius:var(--r-xs)}
.cell-link:hover{text-decoration:underline;text-decoration-color:var(--brand-fill);text-underline-offset:2px}
.cell-link:focus-visible{outline:none;box-shadow:var(--ring)}
/* The BOM name cell holds a name, an Explain disclosure, a brand input and
   (on tiles) a size select. Without min-width:0 the grid/table refuses to
   shrink it and its children spill into the Unit column — the overlap the
   owner reported. break-word stops a long unbroken SKU doing the same. */
.cell-name{min-width:0;overflow-wrap:anywhere}
.cell-name .inline-input{max-width:100%;box-sizing:border-box}
/* A persistent border (not transparent) so an editable cell reads as editable
   at rest, not as a static number — and on row hover it must stand OUT, not
   blend in: tbody tr:hover shares --surface-2 with this input's old resting
   background, so the one visual cue a new user had for "this is a field"
   disappeared exactly when they moved the mouse toward it. */
.inline-input{width:90px;padding:5px 8px;border:1px solid var(--field-line);border-radius:6px;background:var(--field-tint);text-align:right;font-family:var(--mono);font-size:var(--fs-sm);color:var(--text);transition:border-color .12s,box-shadow .12s}
.inline-input:hover{border-color:var(--field-focus)}
table.data tbody tr:hover .inline-input{border-color:var(--field-focus);background:var(--field-tint)}
.inline-input:focus{outline:none;border-color:var(--field-focus);background:var(--surface);box-shadow:0 0 0 3px color-mix(in srgb,var(--field-focus) 28%,transparent)}

/* FILLABLE FIELDS ARE OLIVE — one hue, one meaning: "you type here".
   Scoped to real data entry only. Search boxes, filter selects and the report's text
   settings deliberately keep the neutral treatment: if every input on the screen were
   olive, olive would stop meaning anything. The states that override this (estimated,
   missing-price, edited, invalid) still win, because those carry a louder message than
   "fillable" and are defined after this block. */
.input-grid .input-num,
.field .input-num{border-color:var(--field-line);background:var(--field-tint)}
.input-grid .input-num:hover,
.field .input-num:hover{border-color:var(--field-focus)}
.input-grid .input-num:focus,
.field .input-num:focus{border-color:var(--field-focus);background:var(--surface);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--field-focus) 28%,transparent)}

/* ---- modal ---- */
.overlay{position:fixed;inset:0;background:rgba(10,12,26,.5);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;z-index:100;padding:var(--sp-4);animation:fade .15s ease}
.modal{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow-pop);width:100%;max-width:560px;max-height:88vh;overflow:auto;animation:pop .18s cubic-bezier(.2,.9,.3,1.2)}
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:var(--sp-5) var(--sp-6);border-bottom:1px solid var(--border)}
.modal-body{padding:var(--sp-6)}
.modal-foot{display:flex;justify-content:flex-end;gap:var(--sp-3);padding:var(--sp-4) var(--sp-6);border-top:1px solid var(--border);background:var(--surface-2);position:sticky;bottom:0}
@keyframes fade{from{opacity:0}to{opacity:1}}
@keyframes pop{from{opacity:0;transform:scale(.96) translateY(8px)}to{opacity:1;transform:none}}

/* ---- toast ---- */
.toasts{position:fixed;bottom:24px;right:24px;display:flex;flex-direction:column;gap:10px;z-index:200}
.toast{display:flex;align-items:center;gap:10px;background:var(--text);color:var(--text-inv);padding:11px 16px;border-radius:var(--r-md);box-shadow:var(--shadow-lg);font-size:var(--fs-sm);font-weight:var(--fw-semibold);animation:slideup .25s ease}
.toast.ok{background:var(--ok-500);color:#fff}
.toast.warn{background:var(--warn-500);color:#fff}
.toast-action{margin-left:4px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.32);color:inherit;font:inherit;font-weight:var(--fw-bold);padding:3px 10px;border-radius:var(--r-sm);cursor:pointer;white-space:nowrap}
.toast-action:hover{background:rgba(255,255,255,.28)}
@keyframes slideup{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}

/* ---- misc ---- */
.muted{color:var(--text-3)}
.t2{color:var(--text-2)}
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
.divider{height:1px;background:var(--border);margin:var(--sp-5) 0}
.empty{text-align:center;padding:var(--sp-12) var(--sp-6);color:var(--text-3)}
.empty svg{width:46px;height:46px;opacity:.4;margin-bottom:var(--sp-3)}
.skel{background:linear-gradient(90deg,var(--surface-2),var(--surface-3),var(--surface-2));background-size:200% 100%;animation:shimmer 1.2s infinite;border-radius:var(--r-sm)}
@keyframes shimmer{to{background-position:-200% 0}}

/* ============================================================
   POLISH LAYER — craft details (focus, touch targets, motion, hierarchy)
   ============================================================ */
body{line-height:var(--lh)}
h1,h2,h3,h4{line-height:var(--lh-tight)}

/* Accessible focus — one consistent, visible ring on every interactive element.
   Mouse users don't see it (:focus-visible); keyboard users always do. */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,
textarea:focus-visible,[tabindex]:focus-visible,label.switch:focus-within,
.trade-card:focus-visible,.tli:focus-visible{outline:none;box-shadow:var(--ring);border-radius:var(--r-sm)}

/* Comfortable, consistent control sizing (touch-friendly) */
.btn{min-height:40px;padding:10px 16px;border-radius:var(--r-md)}
.btn-sm{min-height:32px;padding:7px 12px}
.btn-icon{min-height:40px;min-width:40px;padding:10px}
.btn-icon.btn-sm{min-height:32px;min-width:32px;padding:7px}
.input,.select,textarea.input{min-height:40px;padding:10px 13px;border-radius:var(--r-md)}
.inline-input{min-height:32px}

/* Touch targets: a BOM row packs a checkbox, brand/qty/price/labor inputs and a
   delete button side by side at 18-32px — fine for a mouse, a frequent mis-tap
   on the tablet this tool actually targets (and for delete, a destructive one).
   Raised only for coarse (touch) pointers so desktop density is untouched. */
@media (pointer:coarse){
  /* PROJ-3 — height alone still left a 32px-wide destructive target; width matters
     just as much when the finger is gloved. */
  .inline-input,.btn-icon.btn-sm{min-height:44px}
  .btn-icon.btn-sm{min-width:44px}
  input.checkbox{width:22px;height:22px}
  .chk-tap{min-width:44px;min-height:44px}
}
.chk-tap{display:inline-flex;align-items:center;justify-content:center;cursor:pointer}

/* ---- favorite-material star (BOM rows + picker) ---- */
.btn-fav{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;margin-right:2px;border:none;background:transparent;color:var(--text-3);cursor:pointer;border-radius:var(--r-xs);vertical-align:-4px}
.btn-fav svg{width:15px;height:15px}
.btn-fav:hover{color:var(--warn-600);background:var(--surface-2)}
.btn-fav.on{color:var(--warn-600)}
.btn-fav:focus-visible{outline:none;box-shadow:var(--ring)}

/* Buttons: subtler default, clearer primary, smooth press */
.btn{transition:background .15s,border-color .15s,box-shadow .15s,transform .06s}
.btn-primary{box-shadow:0 1px 2px rgba(79,70,229,.25)}
.btn-primary:hover{box-shadow:0 2px 8px rgba(79,70,229,.35)}

/* Inputs: calmer resting border, clear focus */
.input,.select,textarea.input{border-color:var(--border-strong);background:var(--surface-2)}
.input:hover,.select:hover,textarea.input:hover{border-color:var(--text-3)}
.input:focus,.select:focus,textarea.input:focus{background:var(--surface);border-color:var(--brand-500)}

/* Cards: hairline + soft elevation, gentle hover lift where interactive */
.card{border-radius:var(--r-lg)}
.kpi{border-radius:var(--r-lg);transition:box-shadow .18s,transform .18s,border-color .18s}

/* Pills a touch larger for legibility */
.pill{padding:4px 10px;font-weight:var(--fw-semibold)}

/* Section helper text */
.help{font-size:var(--fs-xs);color:var(--text-3);line-height:1.5}
.kbd{font-family:var(--mono);font-size:11px;background:var(--surface-3);border:1px solid var(--border);border-bottom-width:2px;border-radius:5px;padding:1px 6px;color:var(--text-2)}

/* Tables: a hair more breathing room, calmer zebra on hover */
table.data tbody td{padding:10px 14px}
table.data tbody th{padding:10px 14px}
table.data thead th{padding:11px 14px}

/* Respect reduced-motion preferences */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}
}

/* ============================================================
   UTILITY LAYER — the small, most-repeated shapes views were
   re-typing inline (view-head/panel-head flex rows, spacing, the
   checkbox/actions table columns) get one named class instead.
   ============================================================ */
.row{display:flex;align-items:center;gap:var(--sp-3)}
.row-between{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);flex-wrap:wrap}
.wrap{flex-wrap:wrap}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-4)}
.mt-1{margin-top:var(--sp-1)} .mt-2{margin-top:var(--sp-2)} .mt-3{margin-top:var(--sp-3)} .mt-4{margin-top:var(--sp-4)}
.mb-1{margin-bottom:var(--sp-1)} .mb-2{margin-bottom:var(--sp-2)} .mb-3{margin-bottom:var(--sp-3)} .mb-4{margin-bottom:var(--sp-4)}

/* view-head: with or without a trailing action, one class either way */
.view-head{margin-bottom:var(--sp-6)}
.view-head.has-actions{display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:var(--sp-3)}

/* table structural columns — were hand-typed inline (style="width:34px" etc.)
   at every call site; one width per role now. */
.col-check{width:36px} .col-actions{width:40px} .col-num-sm{width:130px}
.table-empty td{text-align:center;padding:var(--sp-8);color:var(--text-3)}

/* pill size variant — the "custom"/"edited" tag was font-size:9px inline,
   repeated across six views */
.pill-xs{padding:2px 7px;font-size:var(--fs-xs)}

/* status dot — color driven by a class instead of inline style="background:..."
   built from a JS color map (CONF_COLOR) at every call site */
.dot-ok{background:var(--ok-500)} .dot-warn{background:var(--warn-500)}
.dot-danger{background:var(--danger-500)} .dot-info{background:var(--info-500)}
.dot-hollow{background:transparent;border:1.5px solid var(--border-strong)}

/* left-aligned text variant of .inline-input — width:95%/120px and
   text-align:left were re-declared per call site instead of named once */
.inline-input--text{width:95%;text-align:left;font-family:inherit}

/* emphasized grand-total row — was inline style="background:var(--brand-50)!important"
   on the Trades total row only; Dashboard's equivalent row looked different */
.row-total{background:var(--brand-50)!important}

/* ---- toast: danger variant (a real failure, vs. warn's soft validation nudge) ---- */
.toast.danger{background:var(--danger-500);color:#fff}

/* ---- live-value pulse: "same number, re-costed" — the tier switch and any
   quiet re-render that changes a headline figure gets a brief color pulse
   instead of silently updating, so the eye tracks that something moved ---- */
@keyframes tick{0%{color:var(--brand-600)}100%{color:inherit}}
.tick{animation:tick .5s ease}
@media (prefers-reduced-motion:reduce){.tick{animation:none}}

/* press feedback on the two big clickable-but-not-<button> rows, matching
   .btn:active's language */
.tli:active,.trade-card:active{transform:translateY(1px)}

/* Labor-detail expand chevron (Multipliers — Labor Catalog folded in) */
.btn-icon svg{transition:transform .15s}
.btn-icon.rotated svg{transform:rotate(180deg)}

/* ---- Phase 5: global-scope banner, breadcrumb, footer nudge, switcher, palette ---- */
.global-banner{display:flex;align-items:center;gap:8px;background:var(--warn-50);color:var(--warn-700);font-size:var(--fs-sm);font-weight:var(--fw-medium);padding:8px var(--sp-6);border-bottom:1px solid var(--border)}
.global-banner svg{flex:none;width:16px;height:16px}

.title{display:flex;align-items:center;gap:6px;min-width:0;overflow:hidden}
.crumb{font-weight:var(--fw-bold);letter-spacing:-.01em;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.crumb-sep{color:var(--text-3);flex:none}
.crumb-project{background:none;border:none;padding:2px 6px;margin:-2px -6px;border-radius:var(--r-sm);cursor:pointer;font:inherit}
.crumb-project:hover{background:var(--surface-3)}

.footer-nudge{display:flex;align-items:center;gap:8px;font-size:var(--fs-xs);color:var(--text-2);text-decoration:none;cursor:pointer;width:100%}
.footer-nudge:hover{color:var(--brand-600)}
.footer-nudge svg{flex:none;width:15px;height:15px}

.switcher-chevron{margin-left:auto;display:inline-flex}
.switcher-pop{z-index:80;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:1px;max-height:340px;overflow:auto}
.switcher-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:var(--r-sm);border:none;background:none;text-align:left;font-size:var(--fs-sm);color:var(--text-2);cursor:pointer;text-decoration:none;width:100%}
.switcher-item:hover{background:var(--surface-3);color:var(--text)}
.switcher-item.current{color:var(--brand-600);font-weight:var(--fw-semibold)}
.switcher-item svg{width:15px;height:15px;flex:none}
.switcher-all{border-top:1px solid var(--border);margin-top:4px;padding-top:9px;color:var(--text-3)}

.palette-item{display:block;width:100%;text-align:left;padding:9px 10px;border-radius:var(--r-sm);border:none;background:none;font-size:var(--fs-sm);color:var(--text);cursor:pointer}
.palette-item:hover,.palette-item:focus-visible{background:var(--surface-3);outline:none}

/* ---- Compact density (data-density="compact" on <html>) -------------------
   The professional-estimating-grid mode: tighter row heights, tighter card and
   panel padding, smaller section headings — roughly 40% more BOM rows per
   screen with no information removed. Toggled from the topbar, persisted. */
[data-density="compact"] table.data tbody td,
[data-density="compact"] table.data tbody th{padding:4px 10px}
[data-density="compact"] table.data thead th{padding:6px 10px}
[data-density="compact"] table.data{font-size:var(--fs-xs)}
[data-density="compact"] .card-pad{padding:var(--sp-4)}
[data-density="compact"] .panel-head{padding:var(--sp-3) var(--sp-4)}
[data-density="compact"] .view-head{margin-bottom:var(--sp-3)}
[data-density="compact"] .view-head h1{font-size:var(--fs-lg)}
[data-density="compact"] .view-head p{font-size:var(--fs-xs)}
[data-density="compact"] .inline-input{padding:2px 6px;font-size:var(--fs-xs)}
[data-density="compact"] .section-label{margin:8px 0 6px}
[data-density="compact"] .input-grid{gap:8px}
[data-density="compact"] .field label{margin-bottom:2px;font-size:var(--fs-2xs)}
[data-density="compact"] .input{padding:5px 8px;font-size:var(--fs-xs)}
[data-density="compact"] .pill{padding:2px 7px}

/* ============================================================
   §4 TABLE SYSTEM — one pattern for every data table in the app.
   Declared once here; views opt in by markup convention only.
   ============================================================ */

/* .table-wrap--pinned: THE modifier for any table taller than a viewport. Making the
   wrap the scrollport is what actually engages the existing sticky thead/.row-sub —
   inside a plain wrap they have no scroll container to stick to. */
.table-wrap--pinned{max-height:calc(100vh - var(--topbar-h) - 180px);overflow:auto}
/* Running totals stay visible while scrolling a long BOM (TRD-3). */
.table-wrap--pinned tfoot td,.table-wrap--pinned tfoot th{
  position:sticky;bottom:0;background:var(--surface-2);z-index:2;border-top:1px solid var(--border)}

/* Sortable header: a real button so it is keyboard-reachable and screen-reader-announced. */
table.data th .th-sort{
  display:inline-flex;align-items:center;gap:4px;border:none;background:none;padding:0;
  font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;cursor:pointer}
table.data th .th-sort:hover{color:var(--text)}
table.data th .th-sort:focus-visible{outline:none;box-shadow:var(--ring);border-radius:4px}
table.data th .chev{opacity:0;font-size:10px;transition:opacity .15s}
table.data th[aria-sort] .chev{opacity:1}
table.data th[aria-sort="ascending"] .chev{transform:rotate(180deg)}

/* Sticky action column — keeps Edit/Delete reachable while a wide table scrolls (MAT-8). */
table.data .col-actions{position:sticky;right:0;background:var(--surface);z-index:1}
table.data thead .col-actions{background:var(--surface-2)}
table.data tbody tr:hover .col-actions{background:var(--surface-2)}
table.data .col-check{width:34px}

/* Empty state lives on the <tr> (MAT-1) — the pre-existing `.table-empty td` rule
   never matched while the class sat on the <td>. Selector is qualified to outrank
   `table.data tbody td{padding:10px 14px}` above, which otherwise wins on specificity
   and flattens the breathing room this state exists to give. */
table.data tbody tr.table-empty td{text-align:center;padding:var(--sp-8);color:var(--text-3)}
table.data tbody tr.table-empty td .btn{margin-left:6px;vertical-align:baseline}
table.data tbody tr.table-empty:hover{background:transparent}

/* A pill that navigates must look like it does (TRD-8). */
.pill-link{border:none;cursor:pointer;transition:background .15s,color .15s}
.pill-link:hover{background:var(--brand-50);color:var(--brand-600)}
.pill-link:focus-visible{outline:none;box-shadow:var(--ring)}
.pill-link::after{content:"›";margin-left:2px;opacity:.6}

/* Invalid numeric entry — colour is never the only signal (WCAG 2.2 AA): the border
   thickens and aria-invalid carries it to assistive tech. */
.inline-input[aria-invalid="true"],.input[aria-invalid="true"]{
  border-color:var(--danger-500);border-width:2px;background:var(--danger-50)}
/* A line with a real quantity but no price — amber, matching the est/measured language. */
.inline-input.is-missing-price{border-color:var(--warn-700);background:var(--warn-50)}

/* Keyboard-shortcut chips (SHELL-2). */
kbd{display:inline-block;padding:1px 6px;border:1px solid var(--border-strong);border-bottom-width:2px;
  border-radius:5px;background:var(--surface-2);font-family:var(--mono);font-size:var(--fs-2xs);color:var(--text-2)}

[data-density="compact"] .table-wrap--pinned{max-height:calc(100vh - var(--topbar-h) - 140px)}
/* SHELL-1 — keyboard-driven palette selection. Focus stays in the input, so the
   active row needs its own visible state (not :focus). */
.palette-item.is-active{background:var(--brand-50);color:var(--brand-600);font-weight:var(--fw-semibold)}
/* MULT-6 — a cell that deviates from the shipped workbook default says so itself,
   instead of a single row-level dot meaning "one of these five numbers changed". */
.inline-input.is-edited{border-color:var(--warn-700);background:var(--warn-50)}

/* DASH-2 — the attention band: one fixed slot above the cost grid holding everything
   that needs a decision, so conditional warnings never reflow the money tiles. */
.attention-band{display:flex;flex-direction:column;gap:8px;margin-bottom:16px;padding:12px 14px;
  border:1px solid var(--warn-500);background:var(--warn-50);border-radius:var(--r-lg)}
.attention-band.is-clear{border-color:var(--ok-500);background:var(--ok-50);color:var(--ok-700)}
.attention-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;color:var(--warn-700)}
.attention-row.is-danger{color:var(--danger-700)}
.attention-progress{height:6px;background:var(--surface-3);border-radius:99px;overflow:hidden}
.attention-progress>div{height:100%;background:var(--ok-500);border-radius:99px;transition:width .3s}
[data-theme="dark"] .attention-band{background:transparent}
/* SCHED-1 — without this the browser claims the gesture for panning and the drag
   never reaches the handler on touch devices. */
td.m[data-task]{touch-action:none}

/* §4 / Phase 3.1 — a table that sits inside a .panel already has the panel's border;
   repeating style="border:none" at five call sites was the most-duplicated inline
   style in the app. One modifier, same result. */
.table-wrap--flush{border:none}

/* Measurement convention under a quantity input. These areas are typed by hand and
   the engine never derives them, so whether a wall is measured gross or net of
   openings was previously unstated anywhere — a silent 17.6% swing on CHB, mortar,
   plaster and paint at once. The caption is the rule; the full reasoning is in the
   input's title. Small and quiet: it must be readable without competing with the
   field label. */
.measure-hint{font-size:var(--fs-2xs);line-height:1.35;color:var(--text-3);margin-top:3px}

/* ============================================================
   §6 TRADE SUMMARY BAR + DISCLOSURE SECTIONS
   The owner's brief: a trade opens showing only its seven headline facts, and
   every working surface sits behind one identical expand control. Before this the
   editor rendered the quantities grid, the labour panel AND a seventy-row BOM all
   expanded at once — three competing surfaces with no hierarchy, which is what
   read as "developer-built".
   ONE pattern, reused everywhere: .disclose > summary.disclose-head + .disclose-body.
   Nothing here introduces a new colour, radius or shadow — it spends the tokens
   that already exist.
   ============================================================ */

/* --- the seven facts, always visible, never collapsed --- */
.tsum{display:grid;grid-template-columns:repeat(auto-fit,minmax(104px,1fr));gap:0;
  border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin:var(--sp-4) 0 0}
.tsum-cell{padding:var(--sp-3) var(--sp-4) var(--sp-3) 0;border-right:1px solid var(--border);min-width:0}
.tsum-cell:last-child{border-right:none}
.tsum-k{display:block;font-size:var(--fs-2xs);font-weight:var(--fw-bold);letter-spacing:.08em;
  text-transform:uppercase;color:var(--text-3);margin-bottom:3px;white-space:nowrap}
.tsum-v{font-size:var(--fs-md);font-weight:var(--fw-bold);color:var(--text);line-height:1.2;
  font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.tsum-v .sub{font-size:var(--fs-2xs);font-weight:var(--fw-regular);color:var(--text-3);margin-left:4px}
/* The money cell is the one thing an executive looks for first. */
.tsum-cell--lead .tsum-v{font-size:var(--fs-xl);letter-spacing:-.01em}

/* --- the single disclosure pattern --- */
.disclose{border:1px solid var(--border);border-radius:var(--r-md);background:var(--surface);
  margin-top:var(--sp-3);overflow:hidden}
.disclose + .disclose{margin-top:var(--sp-2)}
.disclose > summary.disclose-head{
  display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);
  cursor:pointer;list-style:none;user-select:none;min-height:44px;   /* coarse-pointer target */
  transition:background .15s ease}
.disclose > summary.disclose-head::-webkit-details-marker{display:none}
.disclose > summary.disclose-head:hover{background:var(--surface-2)}
.disclose > summary.disclose-head:focus-visible{outline:none;box-shadow:var(--ring)}
.disclose-chev{flex:none;width:16px;height:16px;display:grid;place-items:center;color:var(--text-3);
  transition:transform .18s cubic-bezier(.4,0,.2,1);font-size:11px}
.disclose[open] > summary .disclose-chev{transform:rotate(90deg)}
.disclose-title{font-size:var(--fs-sm);font-weight:var(--fw-bold);color:var(--text);letter-spacing:.01em}
.disclose-meta{margin-left:auto;display:flex;align-items:center;gap:var(--sp-2);
  font-size:var(--fs-xs);color:var(--text-3);font-variant-numeric:tabular-nums;text-align:right}
.disclose-body{padding:0 var(--sp-4) var(--sp-4);border-top:1px solid var(--border)}
.disclose-body--flush{padding:0;border-top:1px solid var(--border)}
/* An expanded section is the one the user is working in — lift it a hair so the
   others recede, rather than fencing each one in a heavier card. */
.disclose[open]{box-shadow:var(--shadow-sm)}
@media (prefers-reduced-motion:reduce){
  .disclose-chev,.disclose > summary.disclose-head{transition:none}
}
.disclose-bar{display:flex;align-items:center;gap:var(--sp-2);margin-top:var(--sp-4)}
.disclose-bar .spacer{margin-left:auto}

/* --- Workflow rail: numbered steps with derived state (app.js workflowState) --- */
.nav a.nav-step{gap:var(--sp-3)}
.nav a .step-n{flex:none;width:20px;height:20px;border-radius:var(--r-full);display:grid;place-items:center;
  font-size:var(--fs-2xs);font-weight:var(--fw-bold);font-variant-numeric:tabular-nums;
  border:1px solid var(--border-strong);color:var(--text-3);background:var(--surface);transition:all .15s ease}
/* Done recedes; current is the only thing competing for attention. State is carried
   by fill AND glyph (✓ vs number), so it survives greyscale and colour-blindness. */
.nav a.is-done .step-n{background:var(--ok-50);border-color:var(--ok-500);color:var(--ok-700)}
.nav a.is-current .step-n{background:var(--brand-fill);border-color:var(--brand-fill);color:var(--on-brand);
  box-shadow:0 0 0 3px var(--brand-100)}
.nav a.is-current > span:not(.step-n):not(.count){font-weight:var(--fw-bold);color:var(--text)}
/* Dark theme remaps --brand-600 to a LIGHT indigo (#a5b4fc), so the white glyph the
   light theme uses drops to ~1.9:1 on it. Flip to dark ink on the light fill. */
/* The dark-ink override this rule used to carry is gone — it was compensating for
   --brand-600 lightening under the fill, and --on-brand now solves that for every
   brand-filled control at once. Left in place it FOUGHT the new token, putting dark
   ink back on the now-dark fill at 3.26:1. Only the focus glow remains. */
[data-theme="dark"] .nav a.is-current .step-n{box-shadow:0 0 0 3px rgba(99,102,241,.22)}
[data-theme="dark"] .nav a.is-done .step-n{background:rgba(5,150,105,.16);border-color:var(--ok-500);color:#6ee7b7}
@media (prefers-reduced-motion:reduce){ .nav a .step-n{transition:none} }
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* Focus ring for EVERY disclosure trigger, not just .disclose-head. The app has ten
   native <details><summary> (explain-calculation, workbook notes, advanced overrides,
   measured runs, derived pills, reinforcement, plan basis, MEP calibration…) and none
   of them had a focus indicator anywhere — a keyboard user tabbing through a trade had
   no idea which section they were on. The default UA outline is suppressed by the
   app's own reset, so this has to be restored explicitly. */
summary:focus-visible{outline:none;box-shadow:var(--ring);border-radius:var(--r-sm)}
summary{outline:none}

/* --- Required / Recommended / Optional + the on-demand ⓘ (items 5 and 2) --- */
.field label{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.need{font-size:var(--fs-2xs);font-weight:var(--fw-bold);letter-spacing:.04em;text-transform:uppercase;
  padding:1px 6px;border-radius:var(--r-full);white-space:nowrap}
/* Only "required AND still blank" earns colour — see field-missing below. An unfilled
   grid where every badge shouts is a grid where no badge is read. */
.need-required{background:var(--surface-3);color:var(--text-2)}
.need-recommended{background:var(--surface-3);color:var(--text-3)}
.need-optional{background:transparent;color:var(--text-3);font-weight:var(--fw-medium)}
/* The one state that is actually actionable: a value real priced lines depend on, empty. */
.field-missing .need-required{background:var(--warn-50);color:var(--warn-700)}
.field-missing .input{border-color:var(--warn-700);background:var(--warn-50)}
.field-missing .input:focus{background:var(--surface)}
[data-theme="dark"] .field-missing .input{background:rgba(217,119,6,.10)}

.info-btn{width:16px;height:16px;flex:none;border-radius:var(--r-full);border:1px solid var(--border-strong);
  background:var(--surface);color:var(--text-3);font-size:10px;font-weight:var(--fw-bold);font-family:var(--font);
  line-height:1;cursor:pointer;display:grid;place-items:center;padding:0;transition:all .13s;text-transform:none;letter-spacing:0}
.info-btn:hover{border-color:var(--brand-500);color:var(--brand-600);background:var(--brand-50)}
.info-btn:focus-visible{outline:none;box-shadow:var(--ring)}
/* Coarse pointers get a real target without the badge row growing on desktop. */
@media (pointer:coarse){ .info-btn{width:24px;height:24px;font-size:12px} }

/* Per-period earned bar on Progress Billing. Sits under the figure it describes, so the
   number stays the precise value and the bar only carries the at-a-glance shape. */
.billbar{height:3px;border-radius:2px;background:var(--surface-3);margin-top:4px;overflow:hidden}
.billbar > span{display:block;height:100%;background:var(--ok-500);border-radius:2px}

/* ---- COSTING TIER COLOUR-CODING ----
   Prevents the worst mistake this app enables: issuing a Direct-cost document (your own
   cost, zero markup) to a client. The tier is stamped on <html data-tier> by render(), so
   these rules can colour the shell from one place. Colour is never the ONLY signal —
   the switcher still shows the tier by name, and the strip below spells it out in words. */
:root{--tier:var(--tier-client);--tier-tint:var(--tier-client-tint)}
[data-tier="direct"]{--tier:var(--tier-direct);--tier-tint:var(--tier-direct-tint)}
[data-tier="internal"]{--tier:var(--tier-internal);--tier-tint:var(--tier-internal-tint)}
[data-tier="client"]{--tier:var(--tier-client);--tier-tint:var(--tier-client-tint)}

/* A hairline of tier colour under the topbar: always in peripheral vision, never in the way. */
/* The active segment of the tier switcher takes the tier's own colour, so the control that
   sets the tier and the tier itself can never disagree. */

/* The internal-only warning strip. Shown for direct and internal, never for client —
   see app.js: the point is that a NON-client tier announces itself. */
.tier-strip{display:flex;align-items:center;gap:10px;padding:7px var(--sp-6);
  background:var(--tier-tint);border-bottom:1px solid var(--tier);
  font-size:var(--fs-xs);font-weight:var(--fw-semibold);color:var(--tier)}
.tier-strip svg{width:15px;height:15px;flex:none}
.tier-strip b{font-weight:var(--fw-bold)}

/* Locked-project banner. Green, not red: the state is CORRECT and protective, not an
   error. Sits under the topbar alongside the tier strip. */
.lock-banner{display:flex;align-items:center;gap:10px;padding:7px var(--sp-6);
  background:var(--ok-50);border-bottom:1px solid var(--ok-500);
  font-size:var(--fs-xs);color:var(--ok-700)}
.lock-banner svg{width:15px;height:15px;flex:none}
/* Frozen fields read as frozen rather than merely inert — without this a locked input
   looks identical to an editable one until you try to type in it. */
[data-locked="1"] .inline-input,[data-locked="1"] .input-grid .input-num{
  background:var(--surface-2);border-color:var(--border);color:var(--text-3);cursor:not-allowed}
