/* ── Reset & base ──────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* ── Theme ────────────────────────────────────────────────────────────────────
   Two palettes, one set of names. Everything visual goes through these; a
   color literal anywhere else is a bug, because it can only be right in one
   theme and will be wrong in the other.

   :root is DARK and is the default. :root[data-theme="light"] overrides it.
   The attribute is set on <html> before first paint, so the page never flashes
   the wrong theme on load.

   The accent gradient stays saturated in BOTH themes, which is why --on-accent
   is white in both. Washing the accent out is what made the price hero
   unreadable in the first light mock -- the hero is meant to be the one loud
   thing on the page. */
:root {
    --bg-deep:    #0c0e17;
    --bg-panel:   #12141f;
    --bg-glass:   rgba(22, 25, 40, 0.72);
    --bg-surface: #1b1e30;
    --accent-a:   #5b8cff;
    --accent-b:   #8a5bff;
    --text:       #e7eaf6;
    --muted:      #838cb0;
    --border:     rgba(255, 255, 255, 0.09);
    --danger:     #f2555f;
    --topbar:     52px;

    /* These were literals until the light theme arrived, and a literal is
       exactly the thing that cannot follow a theme. */
    --hover:      rgba(255,255,255,.08);
    --hover-soft: rgba(255,255,255,.06);
    --shadow:     rgba(0,0,0,.35);
    --shadow-sm:  rgba(0,0,0,.4);
    --shadow-lg:  rgba(0,0,0,.5);
    --on-accent:  #fff;
    --good:       #2ec859;
    --warn:       #ffd764;
    --warn-bg:    rgba(20, 22, 34, .9);
    --overlay-bg: rgba(12,14,23,.9);
    --topbar-bg2: rgba(18,20,31,.85);
    --canvas-text-shadow: rgba(0,0,0,.9);
    --marker:     #fff;
}

:root[data-theme="light"] {
    --bg-deep:    #f4f6fb;
    --bg-panel:   #ffffff;
    --bg-glass:   rgba(255, 255, 255, 0.86);
    --bg-surface: #eceff7;
    --accent-a:   #2b5fd9;
    --accent-b:   #6d3fc4;
    --text:       #161821;
    --muted:      #5a6280;
    --border:     rgba(16, 20, 40, 0.14);
    --danger:     #cf3b45;

    --hover:      rgba(16,20,40,.07);
    --hover-soft: rgba(16,20,40,.05);
    --shadow:     rgba(16,20,40,.13);
    --shadow-sm:  rgba(16,20,40,.16);
    --shadow-lg:  rgba(16,20,40,.20);
    --on-accent:  #fff;
    --good:       #1a9c4a;
    --warn:       #8a6100;
    --warn-bg:    rgba(255,255,255,.94);
    --overlay-bg: rgba(255,255,255,.94);
    --topbar-bg2: rgba(255,255,255,.92);
    --canvas-text-shadow: rgba(255,255,255,.9);
    --marker:     #333;
}

html, body {
    height: 100%;
    background: var(--bg-deep);
    color: var(--text);
    font-family: 'Inter', system-ui, -apple-system, sans-serif;
    font-size: 13px;
    overflow: hidden;
}

.hidden { display: none !important; }

/* ── Topbar ────────────────────────────────────────────────────────────────── */
.topbar {
    height: var(--topbar);
    background: linear-gradient(180deg, var(--bg-panel), var(--topbar-bg2));
    border-bottom: 1px solid var(--border);
    display: flex; align-items: center;
    padding: 0 20px; gap: 12px;
}
.brand { display: flex; align-items: center; gap: 9px; }
.brand-mark {
    width: 26px; height: 26px; border-radius: 7px;
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    display: flex; align-items: center; justify-content: center;
    font-size: 11px; font-weight: 800; letter-spacing: .02em; color: var(--on-accent);
    box-shadow: 0 2px 10px rgba(90, 110, 255, .35);
}
.brand-name { font-size: 15px; font-weight: 800; letter-spacing: .01em; }
.tagline { color: var(--muted); font-size: 12px; padding-left: 12px; border-left: 1px solid var(--border); }

.unit-toggle {
    margin-left: auto;
    display: flex; background: var(--bg-surface);
    border: 1px solid var(--border); border-radius: 8px; padding: 2px; gap: 2px;
}
.unit-btn {
    background: transparent; border: none; color: var(--muted);
    font-family: inherit; font-size: 11.5px; font-weight: 700;
    padding: 5px 10px; border-radius: 6px; cursor: pointer;
    transition: background .15s, color .15s;
}
.unit-btn:hover { color: var(--text); }
.unit-btn.active { background: linear-gradient(135deg, var(--accent-a), var(--accent-b)); color: var(--on-accent); }

.quote-cta {
    display: flex; align-items: center; gap: 7px;
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    color: var(--on-accent); border: none; border-radius: 8px;
    padding: 7px 14px; font-size: 12.5px; font-weight: 700;
    font-family: inherit; cursor: pointer;
    box-shadow: 0 2px 12px rgba(90, 110, 255, .3);
    transition: opacity .15s, transform .15s;
}
.quote-cta:hover:not(:disabled) { opacity: .9; transform: translateY(-1px); }
.quote-cta:disabled { opacity: .4; cursor: not-allowed; box-shadow: none; }
/* Secondary style -- sits next to "Estimate Cost" only for multi-body files,
   so it shouldn't visually compete with that primary CTA. */
.batch-quote-cta {
    background: var(--bg-surface); color: var(--text);
    border: 1px solid var(--border); box-shadow: none;
}
.batch-quote-cta:hover:not(:disabled) { background: var(--hover); opacity: 1; }

.admin-topbar-link {
    color: var(--muted); font-size: 11.5px; font-weight: 600; text-decoration: none;
    padding: 6px 8px; border-radius: 6px; transition: color .15s, background .15s;
}
.admin-topbar-link:hover { color: var(--text); background: var(--hover-soft); }


/* ── Topbar "More" menu ────────────────────────────────────────────────────
   How To, Database, Answers Needed and Admin behind one button. Inside the
   menu the links stack, so .admin-topbar-link's inline padding becomes a row
   and needs to fill the width rather than hug its text. */
.topbar-more { position: relative; }
.topbar-more-btn {
    display: flex; align-items: center; gap: 4px;
    color: var(--muted); font-size: 11.5px; font-weight: 600;
    font-family: inherit;
    background: none; border: none; cursor: pointer;
    padding: 6px 8px; border-radius: 6px;
    transition: color .15s, background .15s;
}
.topbar-more-btn:hover,
.topbar-more.open .topbar-more-btn { color: var(--text); background: var(--hover-soft); }
.topbar-more-btn svg { transition: transform .18s; }
.topbar-more.open .topbar-more-btn svg { transform: rotate(180deg); }

.topbar-more-menu {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
    min-width: 168px;
    display: flex; flex-direction: column; gap: 2px;
    padding: 5px;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 9px;
    box-shadow: 0 8px 24px var(--shadow);
}
.topbar-more-menu .admin-topbar-link { display: block; padding: 7px 10px; }

/* ── Upload Panel ──────────────────────────────────────────────────────────── */
#upload-panel {
    height: calc(100vh - var(--topbar));
    overflow-y: auto;
    display: flex; align-items: center; justify-content: center;
    padding: 32px 20px;
    background:
        radial-gradient(ellipse 900px 500px at 50% 30%, rgba(91,140,255,.10), transparent 60%),
        radial-gradient(ellipse 700px 500px at 80% 80%, rgba(138,91,255,.08), transparent 60%);
}
/* Wide enough for the drop zone and the sample/batch column to sit side by
   side. At 480px everything stacked and the samples fell below the fold on a
   720px-tall laptop -- measured, not assumed. The warning and intro keep their
   own narrower max-width, because a 900px line of prose is unreadable. */
.upload-center { width: min(900px, 100%); }
.upload-center > .landing-intro { max-width: 640px; margin-left: auto; margin-right: auto; }

.landing-actions { display: flex; gap: 16px; align-items: stretch; }
.landing-actions > #drop-zone { flex: 1 1 0; }
.landing-side { flex: 1 1 0; display: flex; flex-direction: column; justify-content: center; }
/* The batch button carries its own top margin for the stacked case. */
.landing-side > .batch-open-btn { margin-top: 0; }
.landing-side > .sample-row { margin-top: 14px; }
/* Three full-width rows rather than 2+1 wrapping: in a half-width column the
   flex-basis leaves a ragged orphan, and each label gets its own line anyway. */
.landing-side .sample-btns { flex-direction: column; }
/* flex-basis 150px is a WIDTH in the default row direction; in a column it
   becomes a height and grow stretches each button to fill, which made the
   three of them 489px tall. */
.landing-side .sample-btn { flex: 0 0 auto; }
.landing-side .sample-lead { text-align: left; }
/* Stacked layout and the phone-sized trims live at the END of this file --
   they lost to the base #drop-zone and .landing-intro rules on equal
   specificity when they sat here. See "Landing page, narrow screens". */

.landing-intro { margin-bottom: 28px; }
.landing-headline { font-size: 24px; font-weight: 800; letter-spacing: -.01em; line-height: 1.25; margin-bottom: 8px; }
.landing-subhead { color: var(--muted); font-size: 13.5px; line-height: 1.55; margin-bottom: 18px; }
.landing-bullets { list-style: none; display: flex; flex-direction: column; gap: 9px; margin-bottom: 18px; }
.landing-bullets li {
    position: relative; padding-left: 18px; font-size: 12.5px; color: var(--muted); line-height: 1.5;
}
.landing-bullets li::before {
    content: ''; position: absolute; left: 0; top: 6px;
    width: 7px; height: 7px; border-radius: 50%;
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
}
.landing-bullets li b { color: var(--text); font-weight: 700; }
.landing-fine { font-size: 11px; color: var(--muted); opacity: .8; line-height: 1.6; }

#drop-zone {
    border: 1.5px dashed var(--border);
    border-radius: 18px;
    padding: 60px 40px;
    text-align: center; cursor: pointer;
    background: var(--bg-glass);
    backdrop-filter: blur(10px);
    transition: border-color .2s, background .2s, transform .2s;
    display: block;
}
#drop-zone:hover, #drop-zone.drag-over {
    border-color: var(--accent-a);
    background: rgba(91, 140, 255, .07);
    transform: translateY(-2px);
}
.drop-icon      { color: var(--accent-a); margin-bottom: 16px; display: flex; justify-content: center; }
.drop-primary   { font-size: 18px; font-weight: 700; margin-bottom: 5px; }
.drop-secondary { color: var(--muted); margin-bottom: 18px; }
.drop-hint      { font-size: 11px; color: var(--muted); opacity: .75; letter-spacing: .02em; }

/* ── Viewer Panel ──────────────────────────────────────────────────────────── */
#viewer-panel { height: calc(100vh - var(--topbar)); }
#viewer { position: relative; width: 100%; height: 100%; overflow: hidden; background: var(--bg-deep); }
#viewer canvas { display: block; width: 100% !important; height: 100% !important; }
#viewer.measuring canvas { cursor: crosshair; }

/* ── Floating toolbar ──────────────────────────────────────────────────────── */
#toolbar {
    position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
    display: flex; align-items: center; gap: 3px;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 13px;
    padding: 5px;
    z-index: 10;
    box-shadow: 0 8px 24px var(--shadow);
}
.tool-btn {
    width: 34px; height: 34px;
    border: none; border-radius: 9px;
    background: transparent; color: var(--muted);
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: background .15s, color .15s;
}
.tool-btn:hover { background: var(--bg-surface); color: var(--text); }
.tool-btn.active { background: linear-gradient(135deg, var(--accent-a), var(--accent-b)); color: var(--on-accent); }
.tool-sep { width: 1px; height: 20px; background: var(--border); margin: 0 3px; }

/* ── Axis indicator ────────────────────────────────────────────────────────── */
#axis-indicator {
    position: absolute; top: 16px; right: 16px;
    width: 96px; height: 96px; pointer-events: none; z-index: 5;
    border-radius: 12px; overflow: hidden;
    background: var(--bg-glass);
    backdrop-filter: blur(10px);
    border: 1px solid var(--border);
}
.axis-label {
    position: absolute; font-size: 10px; font-weight: 700; line-height: 1;
    transform: translate(-50%, -50%); pointer-events: none;
    text-shadow: 0 0 5px var(--canvas-text-shadow);
    color: var(--text);
}

/* ── Contextual tool panel (Section / Measure) ────────────────────────────── */
#tool-panel {
    position: absolute; top: 66px; left: 50%; transform: translateX(-50%);
    display: flex; align-items: center; gap: 10px;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 8px 12px;
    z-index: 10;
    box-shadow: 0 8px 24px var(--shadow);
}
.tp-axis-group { display: flex; gap: 2px; background: var(--bg-surface); border-radius: 8px; padding: 2px; }
.tp-axis-btn {
    width: 24px; height: 24px; border: none; border-radius: 6px;
    background: transparent; color: var(--muted);
    font-size: 11px; font-weight: 700; cursor: pointer;
    transition: background .15s, color .15s;
}
.tp-axis-btn:hover { color: var(--text); }
.tp-axis-btn.active { background: linear-gradient(135deg, var(--accent-a), var(--accent-b)); color: var(--on-accent); }
.tp-axis-btn.wide { width: auto; padding: 0 11px; }

#section-slider {
    width: 140px; accent-color: var(--accent-a);
    background: transparent; cursor: pointer;
}

.tp-icon-btn {
    display: flex; align-items: center; justify-content: center; gap: 5px;
    height: 26px; padding: 0 10px; border: none; border-radius: 7px;
    background: var(--bg-surface); color: var(--muted);
    font-size: 11.5px; font-weight: 600; cursor: pointer;
    transition: background .15s, color .15s;
}
.tp-icon-btn:hover { background: var(--hover); color: var(--text); }
.tp-icon-btn.primary { background: linear-gradient(135deg, var(--accent-a), var(--accent-b)); color: var(--on-accent); }
.tp-icon-btn.primary:hover { opacity: .9; }
.tp-icon-btn:disabled { opacity: .5; cursor: not-allowed; }

.tp-label { font-size: 12px; color: var(--muted); white-space: nowrap; }

#measure-hint { font-size: 12px; color: var(--muted); line-height: 1.4; max-width: 250px; }

/* ── Measurement overlays ──────────────────────────────────────────────────── */
/* Sits on the tip of the pull-direction arrow. Names which tool's axis it is
   showing, because the DFM tool and the estimator keep separate pull
   directions and an unlabelled arrow would be ambiguous between them. */
.pull-arrow-label {
    position: absolute; z-index: 6; pointer-events: none;
    transform: translate(-50%, -50%);
    padding: 2px 7px; border-radius: 999px;
    font-size: 11px; font-weight: 700; white-space: nowrap;
    color: #fff; background: rgba(176, 124, 255, .92);
    box-shadow: 0 2px 8px var(--shadow);
}

.measure-label {
    position: absolute; transform: translate(-50%, -50%);
    background: var(--warn-bg);
    border: 1px solid rgba(255, 215, 100, .4);
    color: var(--warn);
    font-size: 11.5px; font-weight: 700; font-variant-numeric: tabular-nums;
    padding: 3px 8px; border-radius: 6px;
    white-space: nowrap; pointer-events: none;
    z-index: 8;
}

/* ── Zoom controls ─────────────────────────────────────────────────────────── */
/* LEFT edge, not right (BACKLOG item 62). The right edge carries the axis
   indicator at the top, the analysis card along the bottom, and these sat
   between them -- so with the card open they were boxed in against it, and on
   a shorter viewport they overlapped it. The left edge holds only the info
   card, which is anchored to the bottom, so mid-height here is clear. */
#zoom-controls {
    position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
    display: flex; flex-direction: column; gap: 5px; z-index: 5;
    background: var(--bg-glass);
    backdrop-filter: blur(10px);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 5px;
}
.zoom-btn {
    width: 30px; height: 30px;
    background: transparent; color: var(--muted);
    border: none; border-radius: 8px;
    font-size: 17px; line-height: 1; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: background .15s, color .15s;
}
.zoom-btn:hover { background: var(--bg-surface); color: var(--text); }

/* ── Model info card ───────────────────────────────────────────────────────── */
#info-card {
    position: absolute; bottom: 16px; left: 16px;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 13px;
    padding: 12px 15px;
    z-index: 5; min-width: 190px;
    box-shadow: 0 8px 24px var(--shadow);
}
#info-filename {
    font-size: 12.5px; font-weight: 700;
    margin-bottom: 8px; padding-bottom: 8px;
    border-bottom: 1px solid var(--border);
    word-break: break-all; line-height: 1.4;
}
.info-row {
    display: flex; justify-content: space-between; align-items: center;
    gap: 16px; font-size: 11.5px; padding: 2.5px 0;
}
.info-row span { color: var(--muted); }
.info-row b { font-weight: 600; font-variant-numeric: tabular-nums; }
.info-row b.info-bad  { color: var(--danger); }
.info-row b.info-good { color: var(--good); }

/* ── Analysis results (draft / wall thickness) ────────────────────────────── */
#analysis-card {
    position: absolute; bottom: 16px; right: 16px;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 13px;
    padding: 12px 15px;
    z-index: 5; min-width: 190px;
    box-shadow: 0 8px 24px var(--shadow);

    /* max-width is the whole of BACKLOG item 62's first half. There was a
       min-width and no max, and the undercut result puts several sentences of
       prose in #analysis-extra -- so the card stretched to fit the longest
       line and measured 918x342 in a 1600x948 viewport, covering 27.6% of the
       part it exists to annotate. Capped, the prose wraps instead.
       max-height because the wrapped card is tall, and a card taller than the
       viewport cannot be read at all. */
    max-width: 300px;
    /* Bounded, then SCROLLED -- not truncated, and nothing hidden behind a
       disclosure. The undercut result carries content that is there on
       purpose: the slide/lifter proposal, the "an estimate, not a tool design"
       caveat, and the warning about regions that cannot be withdrawn in any
       direction. Tucking those away to save space would quietly weaken
       guards that were added deliberately.
       Unbounded it renders 457px tall, which still covers 37% of the part
       once you zoom in to look at the painted regions -- which is the whole
       workflow. min() so it also respects a short viewport. */
    max-height: min(300px, calc(100% - 32px));
    overflow-y: auto; overscroll-behavior: contain;
}
/* Collapsed: keep the title and the headline number, drop everything else.
   Uncovering the model must not cost you the finding. */
#analysis-card.collapsed { max-height: none; overflow: visible; }
#analysis-card.collapsed #analysis-gradient-wrap,
#analysis-card.collapsed #analysis-gradient-labels,
#analysis-card.collapsed #analysis-extra { display: none !important; }
#analysis-card.collapsed .analysis-header { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
#analysis-card.collapsed #analysis-nominal { margin-bottom: 0; margin-top: 6px; }
#analysis-collapse-btn {
    display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
}
#analysis-collapse-btn svg { transition: transform .18s; display: block; }
#analysis-card.collapsed #analysis-collapse-btn svg { transform: rotate(-90deg); }
.analysis-header {
    display: flex; align-items: center; gap: 6px;
    margin-bottom: 8px; padding-bottom: 8px;
    border-bottom: 1px solid var(--border);
}
/* Pushes both buttons to the right, whatever the title's length. The header
   was justify-content: space-between with exactly two children, which put the
   collapse button in the middle the moment a third arrived. */
#analysis-title { flex: 1 1 auto; min-width: 0; }
#analysis-title { font-size: 12.5px; font-weight: 700; }
.analysis-header button {
    width: 20px; height: 20px; border: none; background: transparent;
    color: var(--muted); font-size: 15px; line-height: 1; cursor: pointer;
    border-radius: 5px; transition: background .15s, color .15s;
}
.analysis-header button:hover { background: var(--bg-surface); color: var(--text); }
#analysis-nominal {
    display: block; font-size: 11px; font-weight: 600; color: var(--text);
    margin-bottom: 8px; font-variant-numeric: tabular-nums;
}
.legend-gradient-wrap { position: relative; margin-bottom: 5px; }
.legend-gradient-bar { height: 8px; border-radius: 3px; }
#analysis-nominal-tick {
    position: absolute; top: -3px; width: 2px; height: 14px;
    background: var(--marker); border-radius: 1px; transform: translateX(-1px);
    box-shadow: 0 0 0 1px var(--shadow-sm);
}
.legend-gradient-labels {
    display: flex; justify-content: space-between;
    font-size: 10px; color: var(--muted); font-variant-numeric: tabular-nums;
}

#analysis-extra {
    display: flex; flex-direction: column; gap: 6px;
    margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
    font-size: 11px; color: var(--muted);
}
#analysis-extra div { display: flex; align-items: center; gap: 6px; }
/* A legend entry is a flex row so its swatch lines up. A sentence is not:
   as a flex row every <b> inside it becomes a flex ITEM, so a bold number
   broke out of its sentence onto its own line with a 6px gap either side.
   Visible in the undercut card as "would need:  3  slides" long before the
   cavity/core notes made whole paragraphs of it. */
#analysis-extra .slide-note { display: block; }
#analysis-extra .swatch {
    display: inline-block; width: 14px; height: 3px; border-radius: 2px; flex-shrink: 0;
}

#analysis-tooltip {
    position: fixed; z-index: 20; pointer-events: none;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 7px;
    padding: 4px 9px;
    font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
    box-shadow: 0 6px 16px var(--shadow);
}

/* ── Back to batch (viewer, opened from a batch-table row) ───────────────────── */
.viewer-back-to-batch {
    position: absolute; top: 16px; left: 16px; z-index: 6;
    display: flex; align-items: center; gap: 6px;
    background: var(--bg-glass); backdrop-filter: blur(12px);
    border: 1px solid var(--border); border-radius: 10px;
    padding: 9px 14px; color: var(--text); font-family: inherit;
    font-size: 12.5px; font-weight: 700; cursor: pointer;
    box-shadow: 0 8px 24px var(--shadow);
    transition: background .15s;
}
.viewer-back-to-batch:hover { background: var(--hover); }
/* Body tree normally sits top-left too -- push it down so it doesn't collide
   with this button when both are showing. */
.viewer-back-to-batch:not(.hidden) ~ #body-tree { top: 62px; max-height: calc(100% - 78px); }

/* ── Body tree (multi-body files) ──────────────────────────────────────────── */
#body-tree {
    position: absolute; top: 16px; left: 16px;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 13px;
    padding: 10px 8px;
    z-index: 5; min-width: 170px; max-width: 240px;
    max-height: calc(100% - 32px); overflow-y: auto;
    box-shadow: 0 8px 24px var(--shadow);
}
.body-tree-header {
    font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); padding: 2px 7px 8px;
}
.body-row {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 7px; border-radius: 8px; cursor: pointer;
    font-size: 12.5px; transition: background .15s;
}
.body-row:hover { background: var(--bg-surface); }
.body-row .body-eye {
    display: flex; flex-shrink: 0; color: var(--accent-a);
}
.body-row.body-hidden { color: var(--muted); }
.body-row.body-hidden .body-eye { color: var(--muted); opacity: .5; }
.body-row .body-name {
    flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.body-opacity-btn {
    display: flex; flex-shrink: 0; align-items: center; justify-content: center;
    width: 22px; height: 22px; border: none; border-radius: 6px;
    background: transparent; color: var(--muted); cursor: pointer;
    transition: background .15s, color .15s;
}
.body-opacity-btn:hover { background: var(--hover); color: var(--text); }
.body-opacity-btn.active { color: var(--accent-a); }
.body-opacity-btn:disabled { opacity: .35; cursor: not-allowed; pointer-events: none; }

/* ── Loading overlay ───────────────────────────────────────────────────────── */
#loading-overlay {
    position: absolute; inset: 0;
    background: var(--overlay-bg);
    backdrop-filter: blur(4px);
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 16px; z-index: 20;
}
.spinner {
    width: 38px; height: 38px;
    border: 3px solid var(--border);
    border-top-color: var(--accent-a);
    border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#loading-progress { color: var(--muted); font-size: 13px; }

/* ── Error toast ───────────────────────────────────────────────────────────── */
.error-toast {
    position: absolute; bottom: 16px; right: 16px;
    background: var(--danger); color: var(--on-accent);
    padding: 11px 15px; border-radius: 10px;
    font-size: 13px; z-index: 30; max-width: 340px; line-height: 1.4;
    box-shadow: 0 8px 24px rgba(242, 85, 95, .3);
}

/* ── Cost Estimator panel ──────────────────────────────────────────────────── */
/* Every estimator panel scrolls. Matched by id suffix rather than an
   explicit list -- a new process's panel picks this up automatically, which
   an explicit list did not (both die casting and thermoforming shipped
   unscrollable because their id was never added here). */
[id$="estimator-panel"] { height: calc(100vh - var(--topbar)); overflow-y: auto; background: var(--bg-deep); }
.est-header {
    display: flex; align-items: center; gap: 14px;
    padding: 14px 24px; border-bottom: 1px solid var(--border);
    background: var(--bg-panel); position: sticky; top: 0; z-index: 2;
}
.est-back-btn {
    display: flex; align-items: center; gap: 6px;
    background: var(--bg-surface); border: 1px solid var(--border); color: var(--text);
    font-family: inherit; font-size: 12.5px; font-weight: 600;
    padding: 7px 12px; border-radius: 8px; cursor: pointer; transition: background .15s;
}
.est-back-btn:hover { background: var(--hover); }
.est-title { font-size: 15px; font-weight: 800; }
.est-filename { margin-left: auto; color: var(--muted); font-size: 12px; }

.est-body { max-width: 1100px; margin: 0 auto; padding: 24px; display: flex; flex-direction: column; gap: 20px; }
.est-disclaimer {
    display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
    background: rgba(245,166,35,.1); border: 1px solid rgba(245,166,35,.35);
    border-radius: 10px; padding: 11px 16px;
    font-size: 12.5px; color: var(--text);
}
.est-disclaimer span { flex: 1; min-width: 220px; }
.est-quote-btn {
    flex-shrink: 0; padding: 7px 14px; border: none; border-radius: 8px;
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    color: var(--on-accent); font-family: inherit; font-size: 12.5px; font-weight: 700;
    cursor: pointer; transition: opacity .15s, transform .15s;
}
.est-quote-btn:hover:not(:disabled) { opacity: .9; transform: translateY(-1px); }
.est-quote-btn:disabled { opacity: .4; cursor: not-allowed; }
.est-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.est-card { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; }
.est-card h3 {
    font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
    color: var(--accent-a); margin-bottom: 10px;
}
.est-hint { font-size: 11.5px; color: var(--muted); line-height: 1.5; margin-bottom: 12px; }
/* Qualifier inside a field label, for inputs that are not required. */
.est-optional { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--muted); }

.est-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
.est-field-row.three { grid-template-columns: 1fr 1fr 1fr; }
.est-field-row:last-child { margin-bottom: 0; }
.est-field label {
    display: block; font-size: 11px; font-weight: 700; color: var(--muted);
    text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px;
}
.est-field label em { font-style: normal; opacity: .7; text-transform: none; letter-spacing: 0; }
.est-field input, .est-field select {
    width: 100%; background: var(--bg-surface); border: 1px solid var(--border);
    color: var(--text); font-family: inherit; font-size: 13px;
    padding: 8px 10px; border-radius: 8px; outline: none; transition: border-color .15s;
}
.est-field input:focus, .est-field select:focus { border-color: var(--accent-a); }
.est-field select { cursor: pointer; }
.est-field input[readonly] { opacity: .7; cursor: default; }
.est-ifield { display: flex; align-items: center; gap: 6px; }
.est-ifield input { flex: 1; min-width: 0; }
.est-pull-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.est-pull-row .est-hint { margin-bottom: 0; }
.est-ubadge {
    font-size: 11px; font-weight: 700; color: var(--muted);
    background: var(--bg-surface); border: 1px solid var(--border);
    padding: 8px; border-radius: 7px; white-space: nowrap;
}

/* The "?" beside a cost field, and the panel it opens.
   Borrows .est-ubadge's colours and border so it reads as part of the existing
   vocabulary, at pill radius and tighter padding because it sits in a label
   rather than beside an input. cursor:help is already this app's signal that
   there is an explanation here. */
.field-help-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 15px; height: 15px; margin-left: 5px; padding: 0;
    vertical-align: middle;
    font-size: 10px; font-weight: 700; line-height: 1;
    font-family: inherit; text-transform: none; letter-spacing: 0;
    color: var(--muted);
    background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: 999px; cursor: help;
    transition: color .12s, border-color .12s, background .12s;
}
/* The glyph is content, not a text node. The button sits inside a <label>, and
   label.textContent is read by other code -- the drawing card builds
   "Secondary op: <label> x N" from it -- so a "?" in the DOM text would leak
   into unrelated UI. aria-label carries the meaning for assistive tech. */
.field-help-btn::before { content: '?'; }
.field-help-btn:hover,
.field-help-btn.open {
    color: var(--accent-a); border-color: var(--accent-a);
    background: rgba(91, 140, 255, .10);
}

/* Appended to <body> by viewer.js, not into a panel: [id$="estimator-panel"] is
   overflow-y:auto and CSS will not give one axis `visible` while the other is
   not, so a child of a panel would be clipped at its edge. Living outside the
   panel is what avoids that -- not this `fixed`, which is here so the
   coordinates mean the same thing however the page is scrolled. No ancestor
   sets transform, filter, backdrop-filter, will-change, contain or perspective,
   so `fixed` really does resolve against the viewport here.
   z-index 40 sits above #analysis-tooltip (20) and below .dwg-card (59) and
   .modal-overlay (100) -- a modal still covers it. */
#field-help {
    position: fixed; z-index: 40;
    width: 330px; max-width: calc(100vw - 24px);
    max-height: 62vh; overflow-y: auto;
    padding: 13px 14px 11px;
    background: var(--bg-glass);
    backdrop-filter: blur(12px);
    border: 1px solid var(--border);
    border-radius: 9px;
    box-shadow: 0 10px 28px var(--shadow);
    font-size: 12px; line-height: 1.55; color: var(--text);
    text-transform: none; letter-spacing: 0;
}
#field-help .fh-head {
    font-size: 11px; font-weight: 800; color: var(--text);
    text-transform: uppercase; letter-spacing: .05em;
    margin-bottom: 7px;
}
#field-help .fh-unit {
    font-weight: 700; color: var(--muted); text-transform: none; letter-spacing: 0;
}
#field-help .fh-what { margin: 0 0 9px; }
#field-help .fh-row {
    display: grid; grid-template-columns: 88px 1fr; gap: 8px;
    padding: 7px 0; border-top: 1px solid var(--border);
}
#field-help .fh-key {
    font-size: 10px; font-weight: 700; color: var(--muted);
    text-transform: uppercase; letter-spacing: .05em; padding-top: 1px;
}
#field-help .fh-formula span:last-child {
    font-variant-numeric: tabular-nums; color: var(--text);
}
#field-help .fh-prov-model  span:last-child,
#field-help .fh-prov-rate   span:last-child { color: var(--text); }
#field-help .fh-prov-typed  span:last-child { color: var(--accent-a); }
#field-help .fh-prov-empty  span:last-child,
#field-help .fh-prov-manual span:last-child { color: var(--muted); }
#field-help .fh-moves {
    margin: 9px 0 0; color: var(--muted); font-size: 11.5px;
}
#field-help .fh-scope {
    margin: 7px 0 0; color: var(--muted); font-size: 11px; font-style: italic;
}
#field-help .fh-unsourced {
    margin: 9px 0 0; padding: 6px 8px;
    background: rgba(91, 140, 255, .08);
    border-radius: 6px; color: var(--muted); font-size: 11px;
}
#field-help .fh-close {
    margin-top: 11px; width: 100%; padding: 6px;
    font-size: 11px; font-weight: 700; font-family: inherit;
    color: var(--muted);
    background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: 7px; cursor: pointer;
}
#field-help .fh-close:hover { color: var(--text); border-color: var(--accent-a); }

.est-advanced { grid-column: 1 / -1; }
/* These cards hold real inputs -- rates, and the secondary-operation counts --
   and a 10px muted chevron was not enough to say so: reported 2026-09-29 as
   "not clear at first glance". The summary is now a bar that looks pressable,
   with an explicit Show/Hide on the right, so there is no doubt something is
   folded away underneath. */
.est-advanced summary {
    cursor: pointer; list-style: none; font-size: 12px; font-weight: 700;
    text-transform: uppercase; letter-spacing: .06em; color: var(--accent-a);
    display: flex; align-items: center; gap: 8px;
    padding: 9px 12px; margin: -2px -4px 0;
    border: 1px solid var(--border); border-radius: 8px;
    background: var(--hover-soft);
    transition: background .15s, border-color .15s;
}
.est-advanced summary:hover {
    background: var(--hover); border-color: var(--accent-a);
}
.est-advanced summary::-webkit-details-marker { display: none; }
.est-advanced summary::before {
    content: '▸'; font-size: 13px; color: var(--accent-a);
    transition: transform .15s; display: inline-block; line-height: 1;
}
.est-advanced[open] summary::before { transform: rotate(90deg); }
/* Says what the click will do, not merely that one is possible. */
.est-advanced summary::after {
    content: 'Show'; margin-left: auto;
    font-size: 10px; font-weight: 700; letter-spacing: .08em;
    color: var(--muted); border: 1px solid var(--border);
    border-radius: 999px; padding: 2px 9px;
}
.est-advanced[open] summary::after { content: 'Hide'; }
.est-advanced[open] > *:not(summary) { margin-top: 12px; }
.est-adv-hint { color: var(--muted); font-weight: 500; text-transform: none; letter-spacing: 0; font-size: 11.5px; }

.est-results { display: grid; grid-template-columns: 220px 160px 1fr; gap: 16px; }
.est-price-hero {
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    border-radius: 12px; padding: 20px; color: var(--on-accent);
    display: flex; flex-direction: column; justify-content: center;
}
.est-price-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: rgba(255,255,255,.8); }
.est-price-value { font-size: 1.5rem; font-weight: 800; margin: 4px 0; font-variant-numeric: tabular-nums; line-height: 1.2; }

.est-stats-card {
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: 12px;
    padding: 16px; display: flex; flex-direction: column; justify-content: center; gap: 14px;
}
.est-stat { display: flex; flex-direction: column; gap: 3px; }
.est-stat-label { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.est-stat-value { font-size: 1.15rem; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--text); }

.est-matrix-wrap { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; }
.est-matrix-label { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin-bottom: 10px; }
.est-matrix { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.est-matrix th {
    text-align: left; font-weight: 700; color: var(--muted); font-size: 10.5px;
    text-transform: uppercase; letter-spacing: .04em; padding: 4px 8px;
}
.est-matrix td { padding: 7px 8px; border-top: 1px solid var(--border); font-variant-numeric: tabular-nums; }
.est-matrix .est-tm-row { color: var(--muted); font-weight: 600; }
.est-matrix .est-tm-selected { background: rgba(91,140,255,.14); color: var(--accent-a); font-weight: 800; border-radius: 6px; }
.est-matrix-note { font-size: 10.5px; color: var(--muted); margin-top: 10px; line-height: 1.5; }

/* ── Cost breakdown ("show the math") ────────────────────────────────────── */
.est-bd { display: flex; flex-direction: column; }
.est-bd-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 6px 0; font-size: 12.5px; border-top: 1px solid var(--border);
}
.est-bd-row:first-child { border-top: none; }
.est-bd-row span { color: var(--muted); }
.est-bd-row b { font-variant-numeric: tabular-nums; font-weight: 600; }
.est-bd-row.est-bd-subtotal { border-top: 1px solid var(--border-h); }
.est-bd-row.est-bd-subtotal span, .est-bd-row.est-bd-subtotal b { color: var(--text); font-weight: 700; }
.est-bd-row.est-bd-total {
    margin-top: 6px; padding-top: 12px; border-top: 2px solid var(--accent-a);
}
.est-bd-row.est-bd-total span, .est-bd-row.est-bd-total b { color: var(--accent-a); font-weight: 800; font-size: 14px; }
.est-bd-empty { color: var(--muted); font-size: 12.5px; padding: 8px 0; }

/* ── Volume tiers ──────────────────────────────────────────────────────────── */
.est-tiers { display: flex; flex-direction: column; }
.est-tier-row {
    display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 10px;
    padding: 8px 0; font-size: 12.5px; border-top: 1px solid var(--border);
}
.est-tier-row:first-child { border-top: none; }
.est-tier-row span { color: var(--muted); }
.est-tier-row span em { font-style: normal; opacity: .75; }
.est-tier-row b { font-variant-numeric: tabular-nums; font-weight: 700; }
.est-tier-row .est-tier-tooling { font-size: 10.5px; text-align: right; white-space: nowrap; }
.est-tier-row.est-tier-current { background: rgba(91,140,255,.1); border-radius: 8px; padding-left: 8px; padding-right: 8px; margin: 0 -8px; }
.est-tier-row.est-tier-current span, .est-tier-row.est-tier-current b { color: var(--accent-a); }

/* ── Cavitation optimizer chart ───────────────────────────────────────────── */

/* ── Batch process ─────────────────────────────────────────────────────────── */
.batch-open-btn {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    width: 100%; margin-top: 12px; padding: 10px 14px;
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: 10px;
    color: var(--text); font-family: inherit; font-size: 12.5px; font-weight: 600;
    cursor: pointer; transition: background .15s;
}
.batch-open-btn:hover { background: var(--hover); }

/* Sample models on the landing page. Three across on a desktop, stacking on a
   phone -- the buttons carry two lines of text, so a fixed row would overflow
   rather than wrap. */
.sample-row { margin-top: 18px; }
.sample-lead { margin: 0 0 8px; color: var(--muted); font-size: 12px; text-align: center; }
.sample-btns { display: flex; gap: 8px; flex-wrap: wrap; }
.sample-btn {
    flex: 1 1 150px; display: flex; flex-direction: column; gap: 2px;
    padding: 9px 12px; text-align: left;
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: 10px;
    color: var(--text); font-family: inherit; cursor: pointer;
    transition: background .15s, border-color .15s;
}
.sample-btn:hover { background: var(--hover); border-color: var(--accent); }
.sample-btn:disabled { opacity: .5; cursor: default; }
.sample-name { font-size: 12.5px; font-weight: 600; }
.sample-meta { font-size: 11px; color: var(--muted); }

#batch-panel { height: calc(100vh - var(--topbar)); overflow-y: auto; background: var(--bg-deep); }

.batch-folder-row { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.batch-select-folder-btn {
    flex-shrink: 0; padding: 8px 14px; border-radius: 8px; border: 1px solid var(--border);
    background: var(--bg-surface); color: var(--text); font-family: inherit;
    font-size: 12.5px; font-weight: 700; cursor: pointer; transition: background .15s;
}
.batch-select-folder-btn:hover { background: var(--hover); }
.batch-folder-summary { color: var(--muted); font-size: 12px; }

.batch-progress-wrap { display: flex; align-items: center; gap: 12px; }
.batch-progress-track {
    flex: 1; height: 8px; border-radius: 4px; background: var(--bg-surface);
    border: 1px solid var(--border); overflow: hidden;
}
.batch-progress-fill {
    height: 100%; width: 0%; background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    transition: width .2s;
}
.batch-progress-label { color: var(--muted); font-size: 12px; white-space: nowrap; }

.batch-results-header { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.batch-results-header h3 { margin: 0; }
.batch-results-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.batch-results-summary { color: var(--muted); font-size: 12px; }

.batch-table-wrap { overflow-x: auto; }
.batch-table { width: 100%; border-collapse: collapse; font-size: 12.5px; white-space: nowrap; }
.batch-table th {
    text-align: left; padding: 8px; color: var(--muted); font-weight: 700;
    text-transform: uppercase; letter-spacing: .04em; font-size: 10.5px;
    border-bottom: 1px solid var(--border);
}
.batch-table td { padding: 8px; border-top: 1px solid var(--border); font-variant-numeric: tabular-nums; vertical-align: middle; }
.batch-table td.batch-td-model { font-variant-numeric: normal; white-space: normal; max-width: 220px; }
.batch-td-clickable { cursor: pointer; color: var(--accent-a); text-decoration: underline; text-decoration-color: transparent; transition: text-decoration-color .15s; }
.batch-td-clickable:hover { text-decoration-color: var(--accent-a); }
/* A whole clickable ROW, as opposed to the single clickable cell above:
   it highlights rather than pretending each cell is a link. */
.batch-row-clickable { cursor: pointer; }
.batch-row-clickable:hover td { background: var(--bg-surface); }
.batch-row-clickable td:first-child { color: var(--accent-a); font-weight: 600; }
.batch-thumb {
    width: 64px; height: 48px; object-fit: contain; background: var(--bg-surface); border-radius: 6px; display: block;
    cursor: zoom-in; transition: transform .15s ease, box-shadow .15s ease;
}
.batch-thumb:hover {
    position: relative; z-index: 20; transform: scale(3.5);
    box-shadow: 0 8px 24px var(--shadow-lg);
}
.batch-table select, .batch-table input[type=number] {
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px;
    color: var(--text); font-family: inherit; font-size: 12px; padding: 5px 6px;
}
.batch-table input[type=number] { width: 80px; }
.batch-undercut-yes { color: var(--danger); font-weight: 700; }
.batch-undercut-no  { color: var(--good); font-weight: 700; }
.batch-row-error td { color: var(--danger); font-style: italic; white-space: normal; }

@media (max-width: 900px) {
    .est-grid { grid-template-columns: 1fr; }
    .est-results { grid-template-columns: 1fr; }
}

/* ── Print (PDF export via window.print) ──────────────────────────────────── */
.print-only { display: none; }
/* ── Multi-body fork on the process picker ──────────────────────────────── */
.pp-multibody {
    background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: 10px; padding: 12px 14px; margin-bottom: 14px;
}
.pp-mb-head { display: flex; flex-direction: column; gap: 3px; margin-bottom: 10px; }
.pp-mb-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pp-mb-controls label { font-size: 12px; font-weight: 600; color: var(--muted); }
.pp-mb-controls select { min-width: 190px; }
.pp-mb-or { font-size: 12px; color: var(--muted); }

/* ── Body rename + color ────────────────────────────────────────────────── */
.body-swatch {
    width: 12px; height: 12px; border-radius: 3px; flex: none;
    border: 1px solid var(--border); cursor: pointer;
}
.body-swatch:hover { outline: 2px solid var(--accent-a); outline-offset: 1px; }
.body-rename-input {
    width: 100%; font: inherit; color: var(--text);
    background: var(--bg-surface); border: 1px solid var(--accent-a);
    border-radius: 4px; padding: 1px 4px;
}
.body-palette {
    position: absolute; z-index: 20; margin-top: 4px;
    display: grid; grid-template-columns: repeat(4, 16px); gap: 5px;
    padding: 7px; border-radius: 8px;
    background: var(--bg-panel); border: 1px solid var(--border);
    box-shadow: 0 8px 24px var(--shadow);
}
.body-palette button {
    width: 16px; height: 16px; border-radius: 4px; padding: 0;
    border: 1px solid var(--border); cursor: pointer;
}
.body-palette button:hover { outline: 2px solid var(--accent-a); outline-offset: 1px; }
.body-row { position: relative; }

/* ── Drawing facts card ──────────────────────────────────────────────────── */
/* Reviewing a drawing splits the window under the header: the drawing in the
   left two-thirds, what was read from it in the right third (user's layout,
   2026-10-02). --dwg-top is the header's height, set from script. */
.dwg-card {
    position: fixed; right: 0; top: var(--dwg-top, 64px); bottom: 0; z-index: 60;
    width: 33.333vw; overflow: auto;
    background: var(--bg-panel); border-left: 1px solid var(--border);
    box-shadow: -8px 0 24px var(--shadow);
    padding: 16px 20px; font-size: 13px;
}
.dwg-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.dwg-close { background: none; border: none; color: var(--muted); font-size: 20px; line-height: 1; cursor: pointer; }
.dwg-close:hover { color: var(--text); }
.dwg-row { display: flex; align-items: center; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--border); }
.dwg-row:last-child { border-bottom: none; }
.dwg-row .k { color: var(--muted); min-width: 92px; font-size: 12px; }
.dwg-row .v { font-weight: 600; flex: 1; }
.dwg-apply {
    font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    color: var(--on-accent); border: none; border-radius: 6px; padding: 3px 9px;
}
.dwg-apply[disabled] { opacity: .45; cursor: default; }
.dwg-row .v.dwg-warn { color: var(--warn); font-weight: 600; }
.dwg-row .dwg-how { color: var(--muted); font-weight: 400; font-size: 12px; }
.dwg-head-actions { display: flex; align-items: center; gap: 10px; }
.dwg-link { background: none; border: none; color: var(--accent-a); font: inherit; font-size: 12px; cursor: pointer; padding: 0; }
.dwg-link:hover { text-decoration: underline; }
.dwg-pick { flex-direction: column; align-items: stretch; }
.dwg-pick .dwg-pick-head { display: flex; align-items: center; gap: 8px; }
.dwg-inputs { display: flex; align-items: flex-end; gap: 8px; margin-top: 8px; }
.dwg-in { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: var(--muted); flex: 1; min-width: 0; }
.dwg-in input {
    font: inherit; font-size: 13px; font-weight: 600; width: 100%; padding: 5px 7px;
    color: var(--text); background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px;
}
.dwg-in input:focus { outline: none; border-color: var(--accent-a); }
.dwg-unit { font-size: 12px; color: var(--muted); padding-bottom: 7px; }
.dwg-slots { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12px; }
.dwg-slots .v { font-weight: 600; flex: 1; }
.dwg-amber { color: var(--warn); font-size: 12px; margin-top: 5px; line-height: 1.45; }
.dwg-flags { margin: 6px 0 0; padding-left: 18px; font-size: 12px; line-height: 1.6; }
.dwg-all-notes { margin-top: 8px; font-size: 12px; }
.dwg-all-notes summary { cursor: pointer; color: var(--muted); }
.dwg-note-list { list-style: none; margin: 6px 0 0; padding: 0; }
.dwg-note-item { display: flex; gap: 6px; padding: 4px 0; border-bottom: 1px solid var(--border); line-height: 1.45; }
.dwg-note-item:hover { background: var(--bg-hover, rgba(0, 0, 0, .04)); }
.dwg-note-n { color: var(--muted); min-width: 20px; }
.dwg-tags i { font-style: normal; font-size: 11px; margin-left: 4px; padding: 0 5px; border-radius: 8px;
              border: 1px solid var(--border); color: var(--muted); white-space: nowrap; }
/* The optional AI reading, beside the rules reading */
.dwg-ai .dwg-pick-head { align-items: center; }
.dwg-ai-result { margin-top: 6px; padding-left: 8px; border-left: 2px solid var(--accent-a); }
.dwg-ai-tag { font-style: normal; font-size: 10px; font-weight: 700; padding: 0 4px; border-radius: 6px;
              border: 1px solid var(--accent-a); color: var(--accent-a); }
.dwg-ai-quote { display: block; margin-top: 2px; font-size: 11px; font-weight: 400; color: var(--muted); font-style: italic; }
/* The admin page's AI status */
.admin-ai-table { border-collapse: collapse; margin-top: 10px; font-size: 13px; }
.admin-ai-table th { text-align: left; vertical-align: top; padding: 5px 16px 5px 0; color: var(--muted); font-weight: 500; }
.admin-ai-table td { padding: 5px 0; }
.admin-ai-on { color: var(--good); font-weight: 700; }
.admin-ai-off { color: var(--muted); font-weight: 700; }

/* The drawing beside the card. Highlights are positioned in FRACTIONS of the
   page, so they stay on their word at any zoom. */
.dwg-preview {
    position: fixed; left: 0; top: var(--dwg-top, 64px); bottom: 0; z-index: 59;
    width: 66.667vw; display: flex; flex-direction: column;
    background: var(--bg-panel); padding: 12px 16px; font-size: 13px;
}
.dwg-preview-scroll { flex: 1; min-height: 0; overflow: auto; border-radius: 6px; background: #fff; }
/* Fitted to the pane by script (--dwg-fit-w); zoom widens it and scrolls. */
.dwg-preview-stage { position: relative; width: var(--dwg-fit-w, 100%); margin: 0 auto; cursor: zoom-in; }
.dwg-preview-stage.zoomed { width: calc(var(--dwg-fit-w, 100%) * 2.4); cursor: zoom-out; }
.dwg-preview-stage img { display: block; width: 100%; height: auto; }
.dwg-mark {
    position: absolute; pointer-events: none; border: 2px solid var(--accent-a);
    border-radius: 3px; box-shadow: 0 0 0 3px rgba(0, 120, 255, .18);
    margin: -3px 0 0 -3px; padding: 1px;
}
.dwg-mark.held { border-color: var(--good, #1f9d55); box-shadow: 0 0 0 3px rgba(31, 157, 85, .2); }
@media (max-width: 900px) { .dwg-preview { width: 100vw; bottom: auto; height: calc(50vh - var(--dwg-top, 64px)); } }
.dwg-note { color: var(--muted); font-size: 12px; line-height: 1.5; }
@media (max-width: 900px) { .dwg-card { left: 0; width: auto; top: 50vh; } }

/* ── Docked viewer ───────────────────────────────────────────────────────────
   The estimator panels used to HIDE the model outright, so the part vanished at
   the exact moment you started describing it. Docked, it stays on screen beside
   the form.

   The dock is a narrow column, so the viewer's floating furniture -- toolbar,
   body tree, info card -- is hidden inside it. Those are sized for a full-width
   canvas and overlap each other badly at 30vw; the model, the axis gizmo and
   zoom are what you actually need while typing. The full set comes back on
   "Back to Viewer".

   Below 1200px there is no room for two columns, so the dock is off and the old
   full-screen behavior applies unchanged. */
body.viewer-docked #viewer-panel {
    display: block !important;
    position: fixed;
    top: calc(var(--topbar) + 12px);
    right: 14px;
    width: clamp(300px, 31vw, 520px);
    height: calc(100vh - var(--topbar) - 26px);
    z-index: 40;
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
    background: var(--bg-panel);
    box-shadow: 0 8px 28px var(--shadow);
}
body.viewer-docked #toolbar,
body.viewer-docked #body-tree,
body.viewer-docked #info-card,
body.viewer-docked .viewer-back-to-batch { display: none !important; }

/* The HEADER needs the same clearance as the body. Margining only .est-body
   left the filename and the Save PDF button sitting UNDER a fixed, z-indexed
   canvas -- they still rendered, so it looked fine in a screenshot, but the
   canvas swallowed the clicks. test_estimator_overhaul caught it by trying to
   press a button that was no longer reachable; my own dock test did not,
   because it measured .est-body and nothing else. */
body.viewer-docked .est-body,
body.viewer-docked .est-header { margin-right: clamp(316px, 33vw, 540px); max-width: none; }

/* Collapse, for when the form needs the width back. */
.dock-collapse {
    position: absolute; top: 8px; left: 8px; z-index: 6;
    width: 26px; height: 26px; padding: 0;
    display: none; align-items: center; justify-content: center;
    background: var(--bg-glass); color: var(--muted);
    border: 1px solid var(--border); border-radius: 7px; cursor: pointer;
}
body.viewer-docked .dock-collapse { display: flex; }
.dock-collapse:hover { background: var(--hover); color: var(--text); }

body.viewer-docked.dock-collapsed #viewer-panel { width: 46px; height: 42px; }
body.viewer-docked.dock-collapsed #viewer,
body.viewer-docked.dock-collapsed #zoom-controls,
body.viewer-docked.dock-collapsed #axis-indicator { display: none !important; }
body.viewer-docked.dock-collapsed .est-body { margin-right: 0; }

@media (max-width: 1199px) {
    body.viewer-docked #viewer-panel { display: none !important; }
    body.viewer-docked .est-body,
    body.viewer-docked .est-header { margin-right: 0; }
}

@media print {
    body.viewer-docked #viewer-panel { display: none !important; }
    body.viewer-docked .est-body,
    body.viewer-docked .est-header { margin-right: 0 !important; }
}

/* ── Theme toggle ───────────────────────────────────────────────────────── */
.theme-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 28px; padding: 0;
    background: transparent; color: var(--muted);
    border: 1px solid var(--border); border-radius: 8px;
    cursor: pointer; transition: background .15s, color .15s;
}
.theme-toggle:hover { background: var(--hover); color: var(--text); }
/* Show the icon for the theme you would switch TO, not the one you are in --
   a sun while already light reads as "you are in light mode" to half of people
   and "click for light mode" to the other half. */
.theme-icon-light { display: none; }
:root[data-theme="light"] .theme-icon-light { display: block; }
:root[data-theme="light"] .theme-icon-dark  { display: none; }

@media print {
    .topbar, .est-back-btn, #est-pdf-btn, .est-quote-btn, #upload-panel, #viewer-panel { display: none !important; }
    /* A batch prints as its report and nothing else. Driven by a body class
       set at Save-PDF time rather than inferred from what happens to be
       visible: the batch panel's sections use the same .est-card class as the
       estimator panels, so hiding them by class would take the single-part
       report with them. */
    body.printing-batch #batch-panel,
    body.printing-batch #print-header { display: none !important; }
    .est-header { display: none !important; }
    .field-help-btn, #field-help { display: none !important; }
    /* All seven panels. This named #estimator-panel alone, so the other six
       printed on their dark background -- invisible while only injection
       molding had a Save PDF button, and wrong the moment the others got one. */
    body,
    #estimator-panel, #machining-estimator-panel, #diecasting-estimator-panel,
    #thermoforming-estimator-panel, #sheetmetal-estimator-panel,
    #additive-estimator-panel, #investmentcasting-estimator-panel {
        background: #fff !important; color: #111 !important;
        height: auto !important; overflow: visible !important;
    }
    .print-only { display: block; margin-bottom: 18px; }
    .print-title { font-size: 20px; font-weight: 800; }
    .print-meta { font-size: 12px; color: #555; margin-top: 4px; }

    /* The part, as the user had it framed. Capped by height rather than width
       so a long flat part and a tall one both leave room for the numbers
       underneath -- a quote sheet whose first page is entirely picture is
       worse than one with no picture at all. */
    .print-shot {
        display: block;
        max-width: 100%;
        max-height: 88mm;
        margin: 12px auto 0;
        border: 1px solid #ccc;
        border-radius: 4px;
        background: #fff;
        break-inside: avoid;
    }
    .print-shot.hidden { display: none !important; }

    .est-body { max-width: 100% !important; padding: 0 !important; }
    .est-card, .est-matrix-wrap, .est-stats-card, .est-disclaimer {
        background: #fff !important; border: 1px solid #ccc !important; box-shadow: none !important;
        break-inside: avoid;
    }
    .est-price-hero { background: #eef3ff !important; color: #111 !important; border: 1px solid #ccc !important; break-inside: avoid; }
    /* The calibration note is white-on-gradient on screen. The printed hero is
       a pale blue, so the same white is invisible -- it printed as a blank gap
       under the price on the first PDF that carried it. It is exactly the line
       that should survive onto a sheet someone forwards, so it gets a printed
       color of its own rather than being hidden. */
    .est-price-cal { color: #33406b !important; }
    .est-price-cal a { color: #1a4fd6 !important; }
    .est-disclaimer { color: #333 !important; }
    .est-price-label { color: #444 !important; }
    .est-field input, .est-field select { background: #fff !important; border: 1px solid #ccc !important; color: #111 !important; }
    .est-ubadge { background: #f4f4f4 !important; border: 1px solid #ccc !important; color: #555 !important; }
    .est-hint, .est-bd-row span, .est-tier-row span, .est-matrix-note, .est-stat-label, .est-pull-row .est-hint { color: #555 !important; }
    .est-bd-row b, .est-tier-row b, .est-stat-value, .est-matrix td { color: #111 !important; }
    .est-bd-row, .est-tier-row, .est-matrix td { border-color: #ddd !important; }
    .est-bd-row.est-bd-total { border-top-color: #1a4fd6 !important; }
    .est-bd-row.est-bd-total span, .est-bd-row.est-bd-total b { color: #1a4fd6 !important; }
    .est-tier-row.est-tier-current { background: #eef3ff !important; }
    .est-tier-row.est-tier-current span, .est-tier-row.est-tier-current b { color: #1a4fd6 !important; }
    .est-matrix .est-tm-selected { background: #eef3ff !important; color: #1a4fd6 !important; }
    .tp-axis-btn.active { background: #1a4fd6 !important; color: #fff !important; }
}

/* ── Request Quote modal ───────────────────────────────────────────────────── */
.modal-overlay {
    position: fixed; inset: 0; z-index: 100;
    background: rgba(8, 9, 15, .65);
    backdrop-filter: blur(6px);
    display: flex; align-items: center; justify-content: center;
    padding: 24px;
}
.modal-panel {
    width: 100%; max-width: 480px; max-height: calc(100vh - 48px);
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 16px;
    display: flex; flex-direction: column;
    box-shadow: 0 24px 64px rgba(0,0,0,.5);
    overflow: hidden;
}
.modal-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 16px 18px; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.modal-title { font-size: 15px; font-weight: 800; }
.modal-close {
    width: 26px; height: 26px; border: none; background: transparent;
    color: var(--muted); font-size: 18px; line-height: 1; cursor: pointer;
    border-radius: 6px; transition: background .15s, color .15s;
}
.modal-close:hover { background: var(--bg-surface); color: var(--text); }
.modal-body { padding: 18px; overflow-y: auto; }
.modal-sub { color: var(--muted); font-size: 12.5px; line-height: 1.6; margin-bottom: 16px; }

/* ── Process picker ───────────────────────────────────────────────────────── */
/* Wider than the other modals: this one holds a card per manufacturing
   process, and a single flex row of them stopped fitting once there were
   more than about four (they overflowed the panel and the last few were
   clipped off-screen entirely). A wider panel plus an auto-fitting grid
   wraps them into rows instead, and keeps working as more are added. */
#process-picker-modal .modal-panel { max-width: 760px; }
.process-picker-choices {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
    gap: 12px;
}
.process-choice-btn {
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px;
    padding: 18px 12px; cursor: pointer; color: var(--text); font-family: inherit;
    transition: border-color .15s, background .15s, transform .15s;
}
.process-choice-btn:hover { border-color: var(--accent-a); background: rgba(91,140,255,.08); transform: translateY(-2px); }
.process-choice-icon { color: var(--accent-a); }
.process-choice-label { font-size: 13.5px; font-weight: 800; }
.process-choice-desc { font-size: 10.5px; color: var(--muted); text-align: center; line-height: 1.4; }

/* ── "Help Me Decide" process-suggestion flow ─────────────────────────────── */
.pp-help-link {
    display: block; width: 100%; margin-top: 14px; padding: 8px; text-align: center;
    background: transparent; border: none; color: var(--accent-a);
    font-family: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
    border-radius: 8px; transition: background .15s;
}
.pp-help-link:hover { background: rgba(91,140,255,.08); }
.pp-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 4px; }

.pp-result-row {
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px;
    padding: 14px 16px; margin-bottom: 10px;
}
.pp-result-row.pp-result-best { border-color: var(--accent-a); background: rgba(91,140,255,.08); }
.pp-result-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.pp-result-label { font-size: 13.5px; font-weight: 800; }
.pp-result-badge {
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b)); color: #fff;
    font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    padding: 2px 8px; border-radius: 999px;
}
.pp-result-score { margin-left: auto; color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.pp-result-reasons { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.pp-result-reasons li {
    position: relative; padding-left: 14px; font-size: 11.5px; color: var(--muted); line-height: 1.5;
}
/* The picker's working state. Same idea as the upload overlay: say what is
   happening and roughly how put out we are about being asked. */
.pp-working { padding: 34px 10px 26px; text-align: center; }
.pp-working .spinner { margin: 0 auto 18px; }
.pp-working-stage { font-size: 13.5px; font-weight: 600; color: var(--text); }
.pp-working-snark {
    margin: 10px auto 0; max-width: 400px;
    font-size: 12px; line-height: 1.5; color: var(--muted);
    font-style: italic; opacity: .85; min-height: 2.6em;
}

/* Optional requirements, collapsed by default. The whole point is that the
   fastest path to an estimate does not go through it, so it must read as an
   offer rather than a field waiting to be filled. */
.pp-optional {
    border: 1px solid var(--border); border-radius: 9px;
    padding: 10px 13px; margin-bottom: 16px;
}
.pp-optional > summary {
    cursor: pointer; font-size: 12.5px; font-weight: 600; color: var(--text);
    list-style: none; display: flex; align-items: center; gap: 9px;
}
.pp-optional > summary::-webkit-details-marker { display: none; }
.pp-optional > summary::before {
    content: '+'; font-weight: 700; color: var(--muted);
    width: 14px; text-align: center; flex: none;
}
.pp-optional[open] > summary::before { content: '\2212'; }
.pp-optional[open] > summary { margin-bottom: 10px; }
.pp-optional-tag {
    font-size: 10.5px; font-weight: 600; color: var(--muted);
    text-transform: uppercase; letter-spacing: .04em;
}

.pp-result-reasons li::before { content: '—'; position: absolute; left: 0; }
/* Cost is what the ranking is actually on, so it outranks the fit score
   visually. The tooling note sits with it because a low piece price backed by
   a $20k die is a different proposition from the same price with no tooling,
   and that difference is the whole point of ranking on landed cost. */
.pp-result-cost { margin: 0 0 10px; }
.pp-result-price {
    display: block; font-size: 17px; font-weight: 700; color: var(--text);
    font-variant-numeric: tabular-nums;
}
.pp-result-costnote { display: block; font-size: 11px; color: var(--muted); margin-top: 2px; }
/* A process that survived screening but still tripped a caution -- producible,
   but off the published guidelines. Amber to match the panel banner's own
   caution state, so the two read as the same class of remark. */
.pp-result-caution {
    margin: 0 0 12px; padding: 8px 10px; border-radius: 7px;
    background: rgba(245,166,35,.10); border: 1px solid rgba(245,166,35,.32);
    font-size: 11.5px; color: var(--text); line-height: 1.5;
    display: grid; gap: 7px;
}
.pp-caution-fix { display: block; color: var(--muted); margin-top: 1px; }
/* Non-breaking space: a trailing plain space inside content is collapsed. */
.pp-caution-fix::before { content: "\2192\00a0"; }
.pp-use-btn { width: 100%; }

/* Break-even volumes. Sits under the ranked list because it answers the
   follow-up question -- not "which process" but "at what quantity does this
   answer change" -- and it belongs next to the answer it qualifies. */
#pp-crossover {
    margin: 16px 0 4px; padding: 12px 14px; border-radius: 9px;
    background: var(--bg-surface); border: 1px solid var(--border);
}
.pp-crossover-head {
    font-size: 10.5px; font-weight: 700; text-transform: uppercase;
    letter-spacing: .05em; color: var(--muted); margin-bottom: 8px;
}
.pp-crossover-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.pp-crossover-list li { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.pp-crossover-list b { color: var(--text); font-weight: 600; }
.pp-crossover-note { font-size: 11px; color: var(--muted); margin: 9px 0 0; line-height: 1.45; }

.pp-ineligible-heading {
    font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
    color: var(--muted); margin: 16px 0 8px;
}
.pp-ineligible-row {
    display: flex; align-items: baseline; gap: 10px; padding: 6px 0;
    font-size: 12px; color: var(--muted); border-top: 1px solid var(--border);
}
.pp-ineligible-row span:first-child { color: var(--text); font-weight: 700; flex-shrink: 0; }
.pp-ineligible-reason { font-size: 11.5px; }

/* ── Machining estimator caveat banner ────────────────────────────────────── */
.mach-caveat {
    background: rgba(245,166,35,.1); border: 1px solid rgba(245,166,35,.35);
    border-radius: 10px; padding: 11px 16px; font-size: 12.5px; color: var(--text);
    line-height: 1.5;
}

/* The landing banner is injected as RAW HTML (_DEFAULT_BANNER in app.py, or
   whatever CADEVAL_BANNER an operator writes), so links inside it carry no
   class of ours and fall back to the browser default #0000EE. Against the dark
   banner that measures 2.05:1 where WCAG AA wants 4.5 -- measured 2026-09-27,
   and it reads as very nearly black on brown.
   
   Styled by descendant selector rather than by adding a class, because the
   markup belongs to whoever writes the banner and we cannot require them to
   know our class names. */
.mach-caveat a {
    color: var(--accent-a);
    text-decoration: underline;
    text-underline-offset: 2px;
}
.mach-caveat a:hover { color: var(--text); }

/* The waiting-room commentary. Deliberately quieter than the progress line
   above it -- it's there to reassure, not to compete with the real status. */
#loading-snark {
    margin-top: 10px; max-width: 380px; text-align: center;
    font-size: 12px; line-height: 1.5; color: var(--muted); opacity: .85;
    font-style: italic;
    animation: snark-in .4s ease-out;
}
@keyframes snark-in { from { opacity: 0; transform: translateY(4px); } to { opacity: .85; transform: none; } }
@media (prefers-reduced-motion: reduce) { #loading-snark { animation: none; } }

/* ── Process-fit warning ──────────────────────────────────────────────────
   Deliberately heavier than .mach-caveat above. A caveat says "watch out for
   this while you read the number"; this says "this process may not be able to
   make this part at all", which is a different order of problem and has to
   read that way at a glance. The estimate below it still calculates -- the
   warning informs, it never blocks. */
.fit-warn {
    border-radius: 10px; padding: 13px 16px; line-height: 1.5;
    border: 1px solid rgba(242,85,95,.45); background: rgba(242,85,95,.10);
    border-left: 4px solid var(--danger);
}
.fit-warn.is-caution {
    border-color: rgba(245,166,35,.45); background: rgba(245,166,35,.10);
    border-left-color: #f5a623;
}
.fit-warn-head {
    display: flex; align-items: center; gap: 8px;
    font-size: 13.5px; font-weight: 700; color: var(--text); margin-bottom: 8px;
}
.fit-warn-head svg { flex: none; color: var(--danger); }
.fit-warn.is-caution .fit-warn-head svg { color: #f5a623; }
.fit-warn-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; }
.fit-warn-list li { font-size: 12.5px; color: var(--text); }
.fit-warn-fix { display: block; color: var(--muted); margin-top: 2px; }
.fit-warn-fix::before { content: "→ "; }
.fit-warn-alt {
    margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--border);
    font-size: 12.5px; color: var(--muted);
}
.fit-warn-alt b { color: var(--text); font-weight: 600; }

/* Picker tile + batch row flags -- same signal, much less room for words. */
.process-choice-btn.is-unfit { opacity: .62; }
.process-choice-flag {
    display: block; margin-top: 6px; font-size: 10.5px; line-height: 1.35;
    color: var(--danger); font-weight: 600;
}
.batch-fit-flag {
    display: inline-flex; vertical-align: middle; margin-left: 5px;
    color: var(--danger); cursor: help;
}
.batch-fit-flag.is-caution { color: #f5a623; }

.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-group { margin-bottom: 14px; }
.form-group label {
    display: block; font-size: 11px; font-weight: 700; color: var(--muted);
    text-transform: uppercase; letter-spacing: .06em; margin-bottom: 6px;
}
.form-group .req { color: var(--danger); }
.form-group input, .form-group select, .form-group textarea {
    width: 100%; background: var(--bg-surface); border: 1px solid var(--border);
    color: var(--text); font-family: inherit; font-size: 13px;
    padding: 9px 11px; border-radius: 8px; outline: none;
    transition: border-color .15s;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
    border-color: var(--accent-a);
}
.form-group textarea { resize: vertical; min-height: 64px; line-height: 1.5; }
.form-group select { cursor: pointer; }

.form-error {
    background: rgba(242, 85, 95, .1); border: 1px solid rgba(242, 85, 95, .3);
    color: #ff9aa2; font-size: 12px; padding: 9px 11px; border-radius: 8px;
    margin-bottom: 12px;
}

.modal-submit {
    width: 100%; padding: 11px; border: none; border-radius: 9px;
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    color: #fff; font-family: inherit; font-size: 13.5px; font-weight: 700;
    cursor: pointer; transition: opacity .15s, transform .15s;
}
.modal-submit:hover:not(:disabled) { opacity: .9; transform: translateY(-1px); }
.modal-submit:disabled { opacity: .5; cursor: not-allowed; }

.modal-success { text-align: center; padding: 24px 8px; }
.success-icon {
    width: 52px; height: 52px; margin: 0 auto 14px;
    border-radius: 50%; background: rgba(46, 200, 89, .12);
    border: 2px solid #2ec859; color: #2ec859;
    display: flex; align-items: center; justify-content: center; font-size: 22px;
}
.success-title { font-size: 15px; font-weight: 800; margin-bottom: 5px; }
.success-sub { color: var(--muted); font-size: 12.5px; }

@media (max-width: 520px) {
    .form-row { grid-template-columns: 1fr; }
    .tagline { display: none; }
}

/* ── Admin: rate-table editor ─────────────────────────────────────────────── */
/* The base html,body rule above is pinned to the viewport with overflow:hidden
   for the single-page viewer app -- these are plain, separately-navigated
   pages, so both the html and body boxes need to be let go of that pin, not
   just body, or the page clips at the viewport edge with no scrollbar. */
.admin-html, .admin-body { height: auto; min-height: 100%; }
/* Only the OUTER box scrolls. With overflow:auto on body as well, body became
   the scroll container that position:sticky measures against -- a box that
   never scrolls -- so the rate editor's floating Save bar never floated. */
.admin-html { overflow: auto; }
.admin-body { overflow: visible; }

.admin-login-wrap {
    min-height: calc(100vh - var(--topbar));
    display: flex; align-items: center; justify-content: center; padding: 24px;
}
.admin-login-card { width: 100%; max-width: 340px; }
.admin-login-card .est-field { margin-bottom: 14px; }
.admin-login-error {
    background: rgba(242,85,95,.1); border: 1px solid rgba(242,85,95,.35);
    color: var(--danger); border-radius: 8px; padding: 9px 12px;
    font-size: 12px; margin-bottom: 14px;
}

/* Process selector sits above the table tabs -- see the comment in
   admin_rates.html for why the tab bar is two levels now. */
.admin-group-tabs {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    margin-bottom: 10px;
}
.admin-group-tab {
    background: var(--bg-surface); border: 1px solid var(--border); color: var(--muted);
    font-family: inherit; font-size: 12.5px; font-weight: 700;
    padding: 9px 15px; border-radius: 9px; cursor: pointer;
    transition: background .15s, color .15s, border-color .15s; white-space: nowrap;
}
.admin-group-tab:hover { color: var(--text); border-color: var(--border-h, rgba(255,255,255,.18)); }
.admin-group-tab.active {
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    color: #fff; border-color: transparent;
    box-shadow: 0 2px 12px rgba(90, 110, 255, .3);
}

.admin-tabs {
    display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: 10px;
    padding: 4px; margin-bottom: 4px;
}
.admin-tab {
    background: transparent; border: none; color: var(--muted);
    font-family: inherit; font-size: 11.5px; font-weight: 700;
    padding: 7px 12px; border-radius: 7px; cursor: pointer;
    transition: background .15s, color .15s; white-space: nowrap;
}
.admin-tab:hover { color: var(--text); }
.admin-tab.active { background: linear-gradient(135deg, var(--accent-a), var(--accent-b)); color: #fff; }
.admin-tab-divider { width: 1px; align-self: stretch; background: var(--border); margin: 4px 2px; }

/* Each rate table carries when it was last edited and where its numbers came
   from. These are point-in-time snapshots of markets that move -- resin
   shifted ~23% in one quarter -- so a stale table should look stale rather
   than presenting an old price as confidently as a current one. */
.admin-card-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.admin-card-head h3 { margin: 0; }
.admin-reviewed {
    margin-left: auto; font-size: 11px; font-weight: 700; letter-spacing: .04em;
    color: var(--muted); background: var(--bg-surface); border: 1px solid var(--border);
    padding: 3px 9px; border-radius: 20px; white-space: nowrap; cursor: help;
}
.admin-reviewed.is-stale {
    color: #f0b429; border-color: rgba(240,180,41,.35); background: rgba(240,180,41,.10);
}
.admin-source {
    font-size: 11.5px; line-height: 1.55; color: var(--muted);
    margin: 8px 0 0; max-width: 96ch;
}
.admin-source.is-unsourced { color: #f0b429; }

.admin-table-actions { display: flex; align-items: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
/* In the rate editor the Save bar rides along the bottom of the window while
   its table is on screen -- the long tables (materials, constants) otherwise
   need a scroll to the very end to save. Sticky within its own card, so it
   never floats over a different table. */
.admin-tab-card .admin-table-actions {
    position: sticky; bottom: 0; z-index: 3;
    margin: 14px -18px -16px; padding: 10px 18px 12px;
    background: var(--bg-panel); border-top: 1px solid var(--border);
    border-radius: 0 0 12px 12px; box-shadow: 0 -6px 14px -8px var(--shadow, rgba(0, 0, 0, .35));
}
.admin-add-btn {
    background: var(--bg-surface); border: 1px solid var(--border); color: var(--text);
    font-family: inherit; font-size: 12px; font-weight: 600;
    padding: 7px 12px; border-radius: 8px; cursor: pointer; transition: background .15s;
}
.admin-add-btn:hover { background: rgba(255,255,255,.08); }
.admin-save-btn {
    padding: 7px 14px; border: none; border-radius: 8px;
    background: linear-gradient(135deg, var(--accent-a), var(--accent-b));
    color: #fff; font-family: inherit; font-size: 12.5px; font-weight: 700;
    cursor: pointer; transition: opacity .15s, transform .15s;
}
.admin-save-btn:hover:not(:disabled) { opacity: .9; transform: translateY(-1px); }
.admin-del-btn {
    background: transparent; border: 1px solid var(--border); color: var(--danger);
    font-family: inherit; font-size: 11.5px; font-weight: 600;
    padding: 5px 9px; border-radius: 6px; cursor: pointer; transition: background .15s;
}
.admin-del-btn:hover { background: rgba(242,85,95,.12); }
.admin-msg { font-size: 12px; }
.admin-msg.ok { color: #2ec859; }
.admin-msg.err { color: var(--danger); }
.admin-table input[type=text] {
    background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px;
    color: var(--text); font-family: inherit; font-size: 12px; padding: 5px 6px; width: 100px;
}
/* The constants tables' "code" and "name" columns hold much longer strings
   (e.g. "tolerance_mult_precision", "Tooling cost multiplier — Polished
   finish") than the original material/region tables' short codes/names. */
.admin-table td:nth-child(1) input[type=text] { width: 190px; }
.admin-table td:nth-child(2) input[type=text] { width: 280px; }

/* ── Rate sensitivity report ──────────────────────────────────────────────── */
/* Wider than the other modals for the same reason the process picker is: it
   holds a six-column table, and wrapping a rate's name across two lines makes
   a list meant for scanning much harder to scan. */
#sens-modal .modal-panel { max-width: 900px; }

.sens-summary {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
    gap: 10px; margin-bottom: 16px;
}
.sens-stat {
    background: var(--bg-surface); border: 1px solid var(--border);
    border-radius: 10px; padding: 10px 12px;
    display: flex; flex-direction: column; gap: 2px;
}
.sens-stat-val { font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; }
.sens-stat-lbl { color: var(--muted); font-size: 11px; line-height: 1.35; }

.sens-table-wrap { overflow-x: auto; max-height: 46vh; overflow-y: auto; }
.sens-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.sens-table th {
    text-align: left; padding: 8px; color: var(--muted); font-weight: 700;
    text-transform: uppercase; letter-spacing: .04em; font-size: 10.5px;
    border-bottom: 1px solid var(--border);
    /* The list runs to forty rows inside a scrolling box -- without this the
       reader loses track of which column is dollars and which is percent. */
    position: sticky; top: 0; background: var(--bg-panel); z-index: 1;
}
.sens-table td {
    padding: 7px 8px; border-top: 1px solid var(--border); vertical-align: middle;
}
.sens-table th.num, .sens-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.sens-table td.sens-tbl, .sens-table td.sens-rev { color: var(--muted); font-size: 11.5px; white-space: nowrap; }
.sens-table tbody tr:hover td { background: var(--bg-surface); }

/* A rate whose two directions disagree sits on a step in a lookup table, so
   its exact value changes the answer categorically rather than
   proportionally. Different enough from "this one is big" to be worth its own
   mark rather than another number. */
.sens-step {
    display: inline-block; margin-left: 6px; padding: 1px 6px;
    border-radius: 999px; font-size: 9.5px; font-weight: 800;
    text-transform: uppercase; letter-spacing: .05em;
    background: rgba(242, 85, 95, .14); color: var(--danger);
    cursor: help;
}
.sens-foot { color: var(--muted); font-size: 11.5px; line-height: 1.6; margin: 12px 0 14px; }

/* ── Design sensitivity ("what would make this cheaper") ──────────────────── */
/* Four columns on one line, because the reader is comparing rows: the lever,
   the change, what it saves per part, what it saves a year. Anything that
   wraps breaks the comparison. */
.ds-list { display: flex; flex-direction: column; }
.ds-row {
    display: grid;
    grid-template-columns: 92px 1fr auto auto;
    align-items: baseline; gap: 12px;
    padding: 8px 0; border-top: 1px solid var(--border);
    font-size: 12.5px;
}
.ds-row:first-child { border-top: none; }
.ds-group {
    color: var(--muted); font-size: 10.5px; font-weight: 700;
    text-transform: uppercase; letter-spacing: .04em;
}
.ds-change { color: var(--text); }
.ds-delta, .ds-annual { font-variant-numeric: tabular-nums; text-align: right; }
.ds-delta { font-weight: 800; }
.ds-annual { color: var(--muted); font-size: 11.5px; min-width: 92px; }
/* Green for a saving is the whole point of the block, so it is the one place
   here that gets a color. Everything that costs more stays neutral rather
   than red -- tightening a tolerance is a legitimate choice, not an error. */
.ds-row.is-saving .ds-delta { color: #2ec859; }

@media (max-width: 640px) {
    .ds-row { grid-template-columns: 1fr auto; row-gap: 2px; }
    .ds-group { grid-column: 1 / -1; }
    .ds-annual { grid-column: 2; }
}

/* ── Ranking sensitivity: rates that change the answer, not just the price ── */
/* A rate that flips the recommendation gets the accent and sorts to the top.
   Everything else stays neutral -- if every row were emphasised, none would
   be. */
.sens-table tr.is-flipper td { background: rgba(91,140,255,.06); }
.sens-flip-cell { white-space: nowrap; }
.sens-flip {
    display: inline-block; padding: 2px 7px;
    border-radius: 999px; font-size: 10.5px; font-weight: 800;
    background: rgba(91,140,255,.16); color: var(--accent-a);
    cursor: help;
}
.sens-noflip { color: var(--muted); }
/* A thin winning margin is the one summary number that changes how the rest
   of the table should be read, so it is allowed to shout. */
.sens-stat.is-thin { border-color: var(--danger); }
.sens-stat.is-thin .sens-stat-val { color: var(--danger); }

.pp-crossover-band { color: var(--muted); font-size: 11.5px; }

/* Piece price and tooling are different kinds of money, so they get different
   weights: the recurring per-part figure leads, the one-time tooling figure
   sits beside it in the muted column. Neither is ever summed with the other. */
.ds-unit { color: var(--muted); font-weight: 600; font-size: 10.5px; }
.ds-annual { text-align: right; }

/* ── "Answers Needed" page ───────────────────────────────────────────────────
   Its own table style rather than reusing .est-matrix: this one is read, not
   scanned for numbers, so it wants left-aligned prose cells with room to
   breathe rather than tight numeric columns. */
.answers-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
.answers-table th {
    text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); font-weight: 600; padding: 0 12px 8px 0; border-bottom: 1px solid var(--border-h);
}
.answers-table td {
    padding: 12px 12px 12px 0; vertical-align: top; font-size: 13px; line-height: 1.55;
    border-bottom: 1px solid var(--border-h);
}
.answers-table tr:last-child td { border-bottom: none; }
.answers-table td:first-child { width: 24%; min-width: 150px; }
.an-val { white-space: nowrap; font-variant-numeric: tabular-nums; font-weight: 600; }
.an-note { color: var(--muted); font-size: 12px; }
.an-count {
    float: right; font-size: 11.5px; font-weight: 500; color: var(--muted);
    text-transform: none; letter-spacing: 0;
}
@media (max-width: 720px) {
    .answers-table td:first-child { width: auto; min-width: 0; }
    .answers-table, .answers-table tbody, .answers-table tr, .answers-table td, .answers-table th { display: block; }
    .answers-table th { display: none; }
    .answers-table td { border-bottom: none; padding: 4px 0; }
    .answers-table tr { border-bottom: 1px solid var(--border-h); padding: 10px 0; }
}

/* ── Landing page, narrow screens ──────────────────────────────────────────────
   Deliberately at the end of the file: these override base rules of the SAME
   specificity (#drop-zone, .landing-intro), so source order is what makes them
   win. Placed next to .upload-center they silently did nothing, and the samples
   stayed 62px below the fold on a phone.

   Goal is that the drop zone, Batch Process and the sample models are all
   reachable without scrolling. The warning banner stays first and uncut. */
@media (max-width: 760px) {
    .landing-actions { flex-direction: column; gap: 12px; }
    .landing-side > .sample-row { margin-top: 10px; }
    .landing-intro { margin-bottom: 18px; }
    /* The drop zone is the biggest single block on the page. Its icon is
       decorative, and on a phone it costs more than it says. */
    #drop-zone { padding: 26px 20px; }
    #drop-zone .drop-icon { display: none; }
    .drop-secondary { margin-bottom: 12px; }
}

/* Short phone viewports (a 640-664px-tall browser window, i.e. a small handset
   with its address bar showing). Tall phones keep the roomier layout above --
   the max-height is there so this only bites where it has to.

   Nothing is removed, only tightened: the warning banner is the largest block
   on the page at 212px, and it stays in full. */
@media (max-width: 760px) and (max-height: 820px) {
    .upload-center > .mach-caveat { padding: 8px 12px; font-size: 11.5px; line-height: 1.45; }
    .landing-intro { margin-bottom: 12px; }
    .landing-fine { font-size: 10.5px; line-height: 1.5; }
    #drop-zone { padding: 14px 18px; }
    .drop-primary { font-size: 16px; }
    .drop-secondary { margin-bottom: 8px; font-size: 12px; }
    .landing-actions { gap: 10px; }
    .sample-btn { padding: 6px 11px; }
    .sample-btns { gap: 6px; }
    .sample-lead { margin-bottom: 5px; }
    .landing-side > .sample-row { margin-top: 8px; }
}

/* ── How To page ──────────────────────────────────────────────────────────────
   Sticky contents on the left, article on the right, stacking under 900px.
   The theme-paired screenshots are the only unusual part: each figure carries
   a light and a dark image and CSS picks one, because a screenshot taken in one
   theme looks broken in the other and the captures are scripted anyway. */
.howto-layout {
    display: flex; gap: 40px; align-items: flex-start;
    max-width: 1180px; margin: 0 auto; padding: 28px 24px 80px;
}
.howto-toc {
    position: sticky; top: calc(var(--topbar) + 18px);
    flex: 0 0 218px; font-size: 12.5px;
}
.howto-toc-title {
    font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
    color: var(--muted); margin-bottom: 10px;
}
.howto-toc ol { list-style: none; counter-reset: toc; display: flex; flex-direction: column; gap: 2px; }
.howto-toc li { counter-increment: toc; }
.howto-toc a {
    display: block; padding: 5px 10px; border-radius: 7px; border-left: 2px solid transparent;
    color: var(--muted); text-decoration: none; line-height: 1.35;
}
.howto-toc a::before { content: counter(toc) ". "; opacity: .55; }
.howto-toc a:hover { background: var(--hover); color: var(--text); }
.howto-toc a.howto-todo { opacity: .55; font-style: italic; }

.howto-main { flex: 1 1 0; min-width: 0; max-width: 760px; }
.howto-h1 { font-size: 27px; font-weight: 800; letter-spacing: -.01em; margin-bottom: 10px; }
.howto-lede { color: var(--muted); font-size: 14px; line-height: 1.65; margin-bottom: 26px; }
.howto-main section { margin-bottom: 40px; scroll-margin-top: calc(var(--topbar) + 18px); }
.howto-main h2 {
    font-size: 17px; font-weight: 700; margin-bottom: 12px;
    padding-bottom: 8px; border-bottom: 1px solid var(--border);
}
.howto-main p { font-size: 13.5px; line-height: 1.7; margin-bottom: 13px; }
.howto-main a { color: var(--accent-a); }
.howto-main code {
    font-size: 12px; padding: 1px 5px; border-radius: 4px;
    background: var(--bg-surface); border: 1px solid var(--border);
}
.howto-note {
    border-left: 3px solid var(--accent-a); padding: 10px 14px;
    background: var(--bg-surface); border-radius: 0 8px 8px 0;
}
.howto-main h3 {
    font-size: 14px; font-weight: 700; margin: 20px 0 9px;
}
.howto-list { list-style: none; display: flex; flex-direction: column; gap: 11px; margin: 0 0 14px; }
.howto-list li {
    position: relative; padding-left: 16px;
    font-size: 13.5px; line-height: 1.7;
}
.howto-list li::before {
    content: ''; position: absolute; left: 0; top: 9px;
    width: 5px; height: 5px; border-radius: 50%; background: var(--accent-a);
}

.howto-fig { margin: 16px 0 18px; }
.howto-fig img {
    /* width:auto, NOT 100%. These are element screenshots at their natural
       size; stretching a 480px-wide capture across a 760px column just blurs
       it. This caps at the column and never upscales. */
    display: block; width: auto; max-width: 100%; height: auto;
    border: 1px solid var(--border); border-radius: 10px; box-shadow: var(--shadow);
}
.howto-fig-narrow img { max-width: 380px; }
.howto-fig figcaption {
    margin-top: 9px; font-size: 12px; line-height: 1.6; color: var(--muted);
}
/* Show the screenshot that matches the active theme.

   EVERY selector here is qualified with `img`. `.howto-fig img` above sets
   display:block at specificity (0,1,1), so a bare `.shot-dark` (0,1,0) LOSES to
   it and both images render. That only shows up when the viewer has made no
   theme choice -- an explicit [data-theme] selector scores (0,2,0) and wins --
   so it is invisible to any test that sets the theme before looking. */
.howto-fig img.shot-dark { display: none; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .howto-fig img.shot-light { display: none; }
    :root:not([data-theme="light"]) .howto-fig img.shot-dark  { display: block; }
}
:root[data-theme="dark"] .howto-fig img.shot-light { display: none; }
:root[data-theme="dark"] .howto-fig img.shot-dark  { display: block; }
:root[data-theme="light"] .howto-fig img.shot-light { display: block; }
:root[data-theme="light"] .howto-fig img.shot-dark  { display: none; }

@media (max-width: 900px) {
    .howto-layout { flex-direction: column; gap: 22px; padding: 20px 16px 60px; }
    .howto-toc {
        position: static; flex: none; width: 100%;
        border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px;
        background: var(--bg-surface);
    }
    .howto-toc ol { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
    .howto-fig-narrow img { max-width: 100%; }
}

/* ── Slide / lifter proposal ──────────────────────────────────────────────────
   Sits under the undercut result. Styled as a suggestion rather than a fact on
   purpose: it proposes tooling worth thousands per side action, and a person
   presses Apply. */
.slide-proposal {
    margin-top: 10px; padding: 10px 12px;
    border: 1px solid var(--border); border-left: 3px solid var(--accent-a);
    border-radius: 0 8px 8px 0; background: var(--bg-surface);
    display: flex; flex-direction: column; gap: 7px;
}
.slide-proposal > div:first-child { font-size: 13px; }
.slide-note { font-size: 11px; line-height: 1.5; color: var(--muted); }
.slide-warn { color: var(--warn); margin-top: 8px; }
.slide-proposal .dwg-apply { align-self: flex-start; }

/* ── Pull-direction comparison ────────────────────────────────────────────── */
.axis-table { margin-top: 9px; display: flex; flex-direction: column; gap: 3px; font-size: 11.5px; }
.axis-row {
    display: grid; grid-template-columns: 22px 1fr 1.4fr 1fr 1fr; gap: 8px; align-items: center;
    padding: 5px 8px; border-radius: 6px; background: var(--bg-surface); color: var(--muted);
}
.axis-row.axis-best { background: var(--hover); color: var(--text); }
.axis-name { font-weight: 800; }
.axis-mark { font-size: 10.5px; opacity: .8; text-align: right; }
.axis-advice { margin-top: 8px; }

/* ── Process comparison: where the money goes ────────────────────────────── */
.pp-breakdown { margin: 7px 0 4px; }
.pp-bd-bar {
    display: flex; height: 7px; border-radius: 4px; overflow: hidden;
    background: var(--bg-surface);
}
.pp-bd-bar > span { display: block; height: 100%; }
.pp-bd-material { background: var(--accent-a); }
.pp-bd-process  { background: var(--accent-b, #7c5cff); }
.pp-bd-setup    { background: var(--muted); }
.pp-bd-tooling  { background: var(--warn); }
.pp-bd-margin   { background: var(--border); }
.pp-bd-legend { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 5px; font-size: 10.5px; color: var(--muted); }
.pp-bd-key { display: inline-flex; align-items: center; gap: 4px; }
.pp-bd-key i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.pp-intent-note {
    display: inline-block; margin-top: 6px; padding: 2px 7px; border-radius: 4px;
    font-size: 10px; letter-spacing: .02em; color: var(--muted);
    background: var(--bg-surface); border: 1px solid var(--border); cursor: help;
}
.pp-ineligible-hint {
    font-size: 10.5px; line-height: 1.55; color: var(--muted);
    margin-top: 6px; padding-left: 8px; border-left: 2px solid var(--accent-a);
}

/* Calibration state, shown right on the price rather than only in the page
   banner. BACKLOG item 43 §4: a confident-looking single number travels
   onward to whoever the reader quotes, and at that point nothing around it
   says the rates behind it are shipped defaults. Quiet on purpose -- it is a
   provenance note, not a warning, and it disappears once the rates carry no
   UNSOURCED marker. */
.est-price-cal {
    margin-top: 8px;
    font-size: 11px;
    line-height: 1.45;
    color: rgba(255, 255, 255, 0.82);
    font-weight: 500;
}
.est-price-cal a { color: #fff; text-decoration: underline; }

/* ── Batch totals and duplicate parts (BACKLOG item 59) ──────────────────── */
.batch-totals-row td {
    border-top: 2px solid var(--accent-a);
    padding-top: 10px; font-weight: 700; background: var(--hover-soft);
}
.batch-totals-label { text-align: right; color: var(--muted); font-weight: 600; }
.batch-totals-value { white-space: nowrap; }

/* Reads "dup 2/4". Deliberately not a color-only signal -- the count is the
   information, and which row of the group it is matters when checking for
   left/right-hand pairs. */
.batch-dup-badge {
    display: inline-block; margin-left: 6px; padding: 1px 6px;
    font-size: 10px; font-weight: 700; letter-spacing: .02em;
    border-radius: 999px; background: var(--hover);
    color: var(--muted); border: 1px solid var(--border);
    cursor: help;
}
.batch-tool-shared { color: var(--muted); font-style: italic; cursor: help; }

.batch-dup-label { display: block; font-weight: 700; margin-bottom: 4px; cursor: pointer; }
.batch-dup-label input { margin-right: 6px; }
.batch-dup-text { display: block; font-size: 12px; line-height: 1.5; }

/* ── Batch report (BACKLOG item 60) ──────────────────────────────────────────
   A FLOWING grid, not one page. Decided 2026-09-29: six parts fit a page and
   thirty do not, and the only way to honor "one page" at thirty is to shrink
   the type past legibility. So it flows, and the totals sit on the first page
   where someone skimming will look. */
#batch-report { display: none; }
@media print {
    body.printing-batch #batch-report { display: block; }

    .batch-report-totals {
        display: flex; gap: 32px; margin: 14px 0 6px; padding: 10px 14px;
        border: 1px solid #ccc; border-radius: 6px; background: #eef3ff;
        break-inside: avoid;
    }
    .batch-report-totals div { display: flex; flex-direction: column; }
    .batch-report-totals span { font-size: 10px; color: #444; text-transform: uppercase;
                                letter-spacing: .04em; }
    .batch-report-totals b { font-size: 17px; color: #111; }
    .batch-report-note { font-size: 10.5px; color: #555; margin: 6px 0 10px; }

    .batch-report-grid {
        display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px;
    }
    /* Each card stays whole. A part split across a page break is the one thing
       that makes a batch sheet hard to read. */
    .batch-report-card {
        border: 1px solid #ccc; border-radius: 6px; padding: 8px;
        break-inside: avoid; page-break-inside: avoid;
    }
    .batch-report-card img {
        width: 100%; height: auto; display: block; margin-bottom: 6px;
        border: 1px solid #e2e2e2; border-radius: 4px; background: #fff;
    }
    .batch-report-name {
        font-size: 11px; font-weight: 700; color: #111; line-height: 1.3;
        margin-bottom: 5px; word-break: break-word;
    }
    .batch-report-dup {
        display: inline-block; margin-left: 5px; padding: 0 4px;
        font-size: 9px; font-weight: 700; border: 1px solid #bbb;
        border-radius: 999px; color: #555;
    }
    .batch-report-card dl {
        display: grid; grid-template-columns: auto 1fr; gap: 1px 8px;
        margin: 0 0 6px; font-size: 10px;
    }
    .batch-report-card dt { color: #666; }
    .batch-report-card dd { margin: 0; color: #111; text-align: right; }
    .batch-report-prices {
        display: grid; grid-template-columns: auto 1fr; gap: 1px 8px;
        border-top: 1px solid #ddd; padding-top: 5px; font-size: 10px;
    }
    .batch-report-prices span { color: #666; }
    .batch-report-prices b { text-align: right; color: #1a4fd6; font-size: 11.5px; }
}

/* ── Recording what a quote was actually FOR (BACKLOG items 32, 42) ───────── */
.quote-detail-panel {
    display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-end;
    padding: 10px 6px;
}
.quote-detail-panel label {
    display: flex; flex-direction: column; gap: 3px;
    font-size: 11px; color: var(--muted); font-weight: 600;
}
.quote-detail-hint { font-size: 11px; color: var(--muted); max-width: 32rem; line-height: 1.5; }

/* A quote recorded against a different revision than the one estimated. Flagged
   rather than corrected: it is a real thing that happens, and the point is that
   it is visible BEFORE the pair reaches an average where it would read as model
   error rather than as the design having moved. */
.rev-mismatch {
    border-color: var(--warn) !important;
    background: rgba(245, 166, 35, .12) !important;
    font-weight: 700;
}

/* ── Secondary operations (BACKLOG item 11) ──────────────────────────────── */
.secondary-ops {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 10px 16px; padding-top: 4px;
}
.secondary-op { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.secondary-op label {
    font-size: 11.5px; color: var(--text); line-height: 1.35; cursor: help;
}
.secondary-op input { max-width: 4.5rem; }
/* The unit is load-bearing: "8" means eight inserts, eight joints, eight
   features or eight painted parts depending on the row, and the difference is
   the whole cost. Parsed from the operation's own description (see
   secondaryOpUnit) so it cannot drift from the text a shop reads in the rate
   editor. */
.secondary-op-unit {
    font-size: 10px; color: var(--muted); white-space: nowrap;
    text-transform: lowercase; letter-spacing: .02em; min-width: 4.2rem;
}
.secondary-op-note {
    grid-column: 1 / -1; font-size: 11px; color: var(--muted); line-height: 1.5;
    border-top: 1px solid var(--border); padding-top: 8px; margin-top: 2px;
}
.secondary-op-note a { color: var(--accent-a); }
/* Drawing & other costs: priced by the user, one line per thing a drawing asks for */
.manual-costs { grid-column: 1 / -1; border-top: 1px solid var(--border); padding-top: 8px; }
.manual-costs-head { display: flex; justify-content: space-between; align-items: baseline;
    font-size: 11.5px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
.manual-costs-cols, .manual-cost { display: grid; grid-template-columns: 1fr 5rem 5rem 1.6rem; gap: 6px; align-items: center; }
.manual-costs-head .manual-costs-cols { flex: 0 0 calc(10rem + 1.6rem + 12px); grid-template-columns: 5rem 5rem 1.6rem;
    font-size: 10px; font-weight: 400; color: var(--muted); }
.manual-cost { margin-bottom: 6px; }
.manual-cost input { min-width: 0; }
.manual-cost-del { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 15px; line-height: 1; }
.manual-cost-del:hover { color: var(--danger); }
.manual-cost-note { grid-column: 1 / -1; font-size: 10.5px; color: var(--muted); line-height: 1.4; margin-top: -2px; }
.manual-cost-add { font-size: 11px; background: none; border: 1px dashed var(--border); color: var(--accent-a);
    border-radius: 6px; padding: 3px 10px; cursor: pointer; margin: 2px 0 8px; }

/* ── Sandbox rates (BACKLOG item 63) ─────────────────────────────────────────
   The whole risk of this feature is someone quoting from invented rates
   without realising, so sandbox is loud by design: a coloured toggle, an
   undismissable banner, and a tint on the viewport edge that stays visible
   however far the page is scrolled. */
.sandbox-toggle {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 10px; border-radius: 999px; cursor: pointer;
    font-size: 11px; font-weight: 700; letter-spacing: .04em;
    background: transparent; color: var(--muted);
    border: 1px solid var(--border);
    transition: background .15s, color .15s, border-color .15s;
}
.sandbox-toggle:hover { border-color: var(--accent-a); color: var(--text); }
.sandbox-dot {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--good); flex: none;
}
.sandbox-toggle.is-sandbox {
    background: var(--warn); color: #241a00; border-color: var(--warn);
}
.sandbox-toggle.is-sandbox .sandbox-dot { background: #241a00; }

.sandbox-banner {
    position: sticky; top: var(--topbar); z-index: 60;
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    padding: 9px 16px; font-size: 12.5px; line-height: 1.5;
    background: var(--warn); color: #241a00;
    border-bottom: 1px solid rgba(0,0,0,.25);
}
.sandbox-banner button {
    font: inherit; font-size: 11px; font-weight: 700; cursor: pointer;
    padding: 3px 10px; border-radius: 999px;
    background: rgba(0,0,0,.14); color: #241a00;
    border: 1px solid rgba(0,0,0,.3);
}
.sandbox-banner button:hover { background: rgba(0,0,0,.24); }
.sandbox-banner button:disabled { opacity: .6; cursor: default; }

/* Visible no matter where the page is scrolled, and behind everything so it
   cannot swallow a click. */
body.sandbox-mode::after {
    content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 200;
    border: 3px solid var(--warn);
}
.admin-sandbox-flag {
    margin-left: auto; margin-right: 12px;
    padding: 4px 12px; border-radius: 999px;
    background: var(--warn); color: #241a00;
    font-size: 11px; font-weight: 700; letter-spacing: .04em;
}

/* ── Drawing-only estimate (BACKLOG item 61) ──────────────────────────────── */
/* Deliberately quieter than the sample buttons beside it: this is the path for
   someone who cannot use the main one, not a feature to steer people toward.
   The note is part of the control, not a tooltip -- what you DON'T get is the
   whole risk, so it is said before the click. */
.drawing-only-row {
    margin-top: 18px; padding-top: 16px;
    border-top: 1px solid var(--border);
}
.drawing-only-lead {
    font-size: 12px; font-weight: 600; color: var(--muted);
    margin: 0 0 8px;
}
.drawing-only-btn {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 9px 14px; border-radius: 10px;
    border: 1px solid var(--border); background: var(--bg-surface);
    color: var(--text); font-size: 13px; font-weight: 600; cursor: pointer;
    transition: border-color .15s, background .15s;
}
.drawing-only-btn:hover { border-color: var(--accent-a); background: var(--bg-glass); }
.drawing-only-note {
    font-size: 11.5px; line-height: 1.45; color: var(--muted);
    margin: 8px 0 0; max-width: 34em;
}

/* Fields that exist only because there is no model to derive them from. */
.drawing-only-fields { display: none; }
body.drawing-only .drawing-only-fields { display: grid; }
/* A derived field turned editable looks identical to a readonly one otherwise,
   and "why can't I type here" is the whole complaint this mode answers. */
body.drawing-only .drawing-only-editable { border-color: var(--accent-a); }

/* ── Sign-in, account and user pages (BACKLOG item 3, user accounts) ──────── */
.auth-wrap { display: flex; justify-content: center; padding: 48px 16px; }
.auth-card { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 6px; }
.auth-card h3 { margin: 0 0 4px; }
.auth-card label { font-size: 12px; color: var(--muted); margin-top: 8px; }
.auth-card input { padding: 8px 10px; font-size: 14px; }
.auth-submit { margin-top: 14px; padding: 8px; font-size: 14px; }
.auth-error { color: var(--danger); font-size: 12.5px; min-height: 1.2em; margin: 6px 0 0; }
.auth-must { color: var(--warn); font-size: 12.5px; font-weight: 600; margin: 4px 0; }
.auth-inline { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 10px; }
.auth-inline input[type=text] { padding: 6px 8px; min-width: 0; flex: 1 1 160px; }
.auth-check { font-size: 12.5px; display: flex; align-items: center; gap: 4px; }
.auth-temp { font-size: 13px; margin: 10px 0 0; min-height: 1.2em; }
.auth-temp b { font-family: ui-monospace, Consolas, monospace; font-size: 15px; letter-spacing: .04em; }
.users-table { width: 100%; }
.users-actions { display: flex; gap: 6px; justify-content: flex-end; }
.account-name { font-weight: 600; }
.signin-needed { padding: 28px; text-align: center; }

/* ── Each person's own rates (user accounts, phase 2) ─────────────────────── */
.rates-modes { display: flex; gap: 2px; margin-left: 14px; padding: 2px; border: 1px solid var(--border);
    border-radius: 999px; }
.rates-modes a { font-size: 11.5px; padding: 3px 11px; border-radius: 999px; color: var(--muted);
    text-decoration: none; }
.rates-modes a:hover { color: var(--text); background: var(--hover-soft); }
.rates-modes a.active { background: var(--accent-a); color: var(--on-accent); font-weight: 600; }
.rate-mine-dot, .admin-table tr.is-mine td:first-child::before {
    display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-a);
    vertical-align: middle; }
.admin-table tr.is-mine td:first-child { position: relative; }
.admin-table tr.is-mine td:first-child::before { content: ''; position: absolute; left: -2px; top: 50%;
    transform: translateY(-50%); }
.admin-table tr.is-mine input { border-color: var(--accent-a); }
.admin-row-actions { white-space: nowrap; }
.admin-house-btn { font-size: 11px; background: none; border: 1px solid var(--border); color: var(--accent-a);
    border-radius: 6px; padding: 3px 8px; cursor: pointer; margin-left: 4px; }
.admin-house-btn:hover { border-color: var(--accent-a); }
.admin-owned { font-size: 11.5px; color: var(--muted); }
.rates-source-badge { font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 999px;
    border: 1px solid var(--accent-a); color: var(--accent-a); text-decoration: none; white-space: nowrap; }
.rates-source-badge:hover { background: var(--hover-soft); }

/* A region row is a swatch plus a sentence. Without this the sentence's own
   spans become flex items of #analysis-extra div and break apart. */
#analysis-extra .region-row { align-items: baseline; }
#analysis-extra .region-row span:last-child { flex: 1; }
