/*
 * Deep Blue Mosaic
 * Copyright © 2026 Wayne Hawksworth. All rights reserved.
 * Developed by Wayne Hawksworth.
 */

:root {
    /* Theme accent — clean blue. Replaced an earlier green (#4CC575).
       Single source of truth for the app's brand
       accent. CSS uses --hm-accent + variants directly; inline SVG icons
       generated in Core/*Svg.cs emit `currentColor` and inherit the
       resolved tint via their wrapper element's `color` style — so
       changing the values below propagates through borders, fills,
       focus rings, shape previews, and height-strategy icons without
       touching C#. The variants give consistent lighter/darker tints
       for hover halos, soft backgrounds, and pressed/active states.
       Keep the rgba(R,G,B,...) references throughout this file in sync
       with --hm-accent for the box-shadow calls (CSS doesn't yet let
       us derive rgba from a hex var without color-mix). */
    --hm-accent: #3B82F6;
    --hm-accent-hover: #2563EB;
    --hm-accent-soft: #93C5FD;     /* lighter — for soft backgrounds, hover halos */
    --hm-accent-strong: #1D4ED8;   /* darker — for active/pressed states, strong borders */

    /* Bootstrap primary aliases — point Bootstrap's --bs-primary token at the
       theme accent so every Bootstrap-derived "primary" reference (text-primary,
       bg-primary, border-primary, alert-primary, focus-shadow rgb, btn-link
       hover colour, the form-control focus ring's rgb, etc.) picks up the green
       uniformly. Without this, .text-primary / .btn-outline-primary / similar
       classes default to Bootstrap's blue (#0d6efd), which clashes with the
       app's identity. The -rgb variant is the comma-separated form Bootstrap
       uses inside rgba() calls for opacity-tinted variants — keep in sync
       with --hm-accent (rgb 59, 130, 246 — #3B82F6). */
    --bs-primary: var(--hm-accent);
    --bs-primary-rgb: 59, 130, 246;
    --hm-canvas-bg: #0e1012;
    /* Canvas checkerboard — two alternating square tones behind the SVG
       so edge cells (black in dark mode, white in light mode) remain
       distinguishable from the background. Keep both low-contrast: the
       pattern should read as a subtle grid, not compete with cell fills. */
    --hm-checker-a: #1a1d21;
    --hm-checker-b: #24282d;
    /* Plate preview tokens — used by the Estimate / Export plate SVGs and the CSS-only
       cards. `--hm-plate-stroke` is the dashed outline that suggests the build-plate
       area; `--hm-plate-corner` is the colour of the corner-bracket markers. Keep both
       LOW-contrast against the card background so the plate reads as a subtle guide
       rather than competing with the cell polygons for attention. */
    --hm-plate-stroke: rgba(0, 0, 0, 0.18);
    --hm-plate-corner: rgba(0, 0, 0, 0.28);
    --hm-plate-safezone: rgba(196, 139, 0, 0.55);
    /* Cell outline stroke for plate-preview SVGs. Theme-adaptive so dark fills don't
       vanish in dark mode and light fills don't vanish in light mode. Translucent
       so it tints rather than dominates saturated fills. */
    --hm-cell-stroke: rgba(0, 0, 0, 0.6);
    /* Faint hash lines drawn behind the cells in plate-preview SVGs to indicate the
       build-plate area at a glance. Low opacity — visible enough to read as a base
       plate, but not so strong it competes with the cell shapes for attention. */
    --hm-plate-grid: rgba(0, 0, 0, 0.22);
    /* AMS unit icon fill + text. In light mode the body is pale so it doesn't overwhelm
       the small filament-spool circles sitting on top; text is dark for contrast. Dark
       mode flips both below. */
    --hm-ams-body: #e8e9eb;
    --hm-ams-body-stroke: rgba(0, 0, 0, 0.25);
    --hm-ams-text: #2a2d31;
    --hm-ams-spool-stroke: rgba(0, 0, 0, 0.35);
    --hm-ams-empty-stroke: rgba(0, 0, 0, 0.35);

    /* Status / workspace info bar — pale bar with dark text in light mode so
       the bottom strip blends with the rest of the light UI rather than
       slamming a black band onto it. Dark mode override below flips both. */
    --hm-statusbar-bg: #f0f1f3;
    --hm-statusbar-fg: #15181c;
}

html, body { height: 100%; }
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
    -webkit-font-smoothing: antialiased;
    overflow: hidden;
}

/* ================= Theme tokens ================= */
[data-bs-theme="dark"] {
    --bs-body-bg: #121416;
    --bs-body-color: #e6e6e6;
    --bs-border-color: #2a2d31;
    --bs-secondary-bg: #1c1f23;
    --bs-tertiary-bg: #16181b;
    --hm-canvas-bg: #0e1012;
    --hm-checker-a: #1a1d21;
    --hm-checker-b: #24282d;
    /* In dark mode the plate outline needs to be light instead of dark so it reads. */
    --hm-plate-stroke: rgba(255, 255, 255, 0.18);
    --hm-plate-corner: rgba(255, 255, 255, 0.32);
    --hm-plate-safezone: rgba(255, 196, 80, 0.55);
    --hm-cell-stroke: rgba(255, 255, 255, 0.5);
    --hm-plate-grid: rgba(255, 255, 255, 0.2);
    /* Dark AMS body with light text — matches the classic Bambu AMS physical unit. */
    --hm-ams-body: #2a2d31;
    --hm-ams-body-stroke: rgba(255, 255, 255, 0.35);
    --hm-ams-text: #f0f0f0;
    --hm-ams-spool-stroke: rgba(255, 255, 255, 0.55);
    --hm-ams-empty-stroke: rgba(255, 255, 255, 0.55);

    /* Dark theme: dark bar with light text — keeps the bar consistent with
       the rest of the dark UI rather than introducing a stark white band. */
    --hm-statusbar-bg: #1a1d21;
    --hm-statusbar-fg: #f5f5f5;
}
[data-bs-theme="dark"] .card {
    background-color: #1c1f23;
    border-color: #2a2d31;
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
    background-color: #121416;
    border-color: #2a2d31;
    color: #e6e6e6;
}
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus {
    border-color: var(--hm-accent);
    box-shadow: 0 0 0 .2rem rgba(59, 130, 246, .2);
}
[data-bs-theme="dark"] .btn-primary {
    background-color: var(--hm-accent);
    border-color: var(--hm-accent);
    /* White on the medium green — higher legibility than dark text, and the
       conventional primary-button look. Hover state gets a darker green
       (--hm-accent-hover); white still reads well there. */
    color: #fff;
}
[data-bs-theme="dark"] .btn-primary:hover { background-color: var(--hm-accent-hover); border-color: var(--hm-accent-hover); color: #fff; }
[data-bs-theme="dark"] .nav-tabs .nav-link { color: #b3b8be; border-color: transparent; }
[data-bs-theme="dark"] .nav-tabs .nav-link.active { background-color: #1c1f23; color: var(--hm-accent); border-color: #2a2d31 #2a2d31 #1c1f23; }
[data-bs-theme="dark"] .nav-tabs { border-color: #2a2d31; }

[data-bs-theme="light"] {
    --hm-canvas-bg: #f0f0f0;
    --hm-checker-a: #e2e3e5;
    --hm-checker-b: #ededef;
}
[data-bs-theme="light"] body { background: #fafafa; }
[data-bs-theme="light"] .btn-primary {
    background-color: var(--hm-accent);
    border-color: var(--hm-accent);
    color: #fff;
}
[data-bs-theme="light"] .btn-primary:hover { background-color: var(--hm-accent-hover); border-color: var(--hm-accent-hover); color: #fff; }
[data-bs-theme="light"] .nav-tabs .nav-link.active { color: var(--hm-accent-strong); }

/* ================= Outline-primary uniformity ================= */
/* Bootstrap's .btn-outline-primary defaults to the Bootstrap blue (#0d6efd)
   which clashes with the app's green accent. Override the Bootstrap CSS
   variables so every .btn-outline-primary across the app picks up the theme
   green for text, border, hover, and active — Export plate, Add filament,
   Add batch, Upload depth map, etc. all become uniformly green. */
.btn-outline-primary {
    --bs-btn-color: var(--hm-accent);
    --bs-btn-border-color: var(--hm-accent);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: var(--hm-accent);
    --bs-btn-hover-border-color: var(--hm-accent);
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--hm-accent-hover);
    --bs-btn-active-border-color: var(--hm-accent-hover);
    --bs-btn-focus-shadow-rgb: 59, 130, 246;  /* matches --hm-accent rgb */
}
/* ================= End outline-primary ================= */

/* ================= Themed hover for non-primary buttons ================= */
/* Tiered hover (option B): primary buttons go full darker-green; outline-
   secondary buttons (TopBar actions, modal action buttons, canvas toolbar
   icons, palette modal buttons, etc.) pick up a soft green tint on hover so
   the whole app feels on-theme.
   Cancel/dismiss buttons opt OUT via the .hm-btn-cancel marker class — they
   stay Bootstrap-default neutral grey because they shouldn't compete with
   primary actions or look "themed". Add .hm-btn-cancel to any new dismiss
   button to keep it neutral. */
.btn-outline-secondary:not(.hm-btn-cancel):hover,
.btn-outline-secondary:not(.hm-btn-cancel):focus-visible {
    background-color: color-mix(in srgb, var(--hm-accent) 12%, transparent);
    border-color: var(--hm-accent);
    color: var(--hm-accent);
}
/* Active state (mid-press) — slightly stronger tint so the click registers. */
.btn-outline-secondary:not(.hm-btn-cancel):active {
    background-color: color-mix(in srgb, var(--hm-accent) 18%, transparent);
    border-color: var(--hm-accent);
    color: var(--hm-accent);
}
/* ================= End themed hover ================= */

/* ================= Helper-text utility ================= */
/* PROJECT CONVENTION (May 2026): all descriptive / helper info text in
   the app uses this class rather than Bootstrap's .small. Form labels,
   section sub-headers, status messages, and live value displays still
   use .small (0.875em) because they carry their own visual weight;
   helper prose (paragraphs explaining what a control does, calibration
   tips, hint copy) uses .hm-help-text so labels read as primary and the
   help text as supporting context.
   Big enough to stay readable at 1× zoom on typical laptop displays;
   small enough that paragraph-length hints don't dominate the form
   they're describing. Pair with .text-secondary for the muted look. */
.hm-help-text {
    font-size: 0.72rem;
    line-height: 1.35;
}
/* ================= End helper-text utility ================= */

/* ================= Theme-aware warning text =================
   Bootstrap's `.text-warning` resolves to `#ffc107` regardless of theme.
   That bright yellow has poor contrast on the white light-mode body —
   "Pick a filament to start painting." was almost invisible on light
   mode. Override to a theme-aware amber: dark-amber on light bg, lighter
   amber on dark bg. Both pass WCAG AA at 14px+ over the body bg.
   Applied app-wide so every `.text-warning` (status bar, plate chips,
   filament-low warnings, paint-mode hints, etc.) gets the same
   treatment in one place. Alerts (.alert-warning, .badge.bg-warning)
   are unaffected — they use their own bg+fg combo. */
:root,
[data-bs-theme="light"] {
    --hm-warning-text: #b25e00; /* dark amber, ~5.2:1 on white */
}
[data-bs-theme="dark"] {
    --hm-warning-text: #ffb84d; /* light amber, ~7.8:1 on #1a1d21 */
}
.text-warning {
    color: var(--hm-warning-text) !important;
}
/* ================= End theme-aware warning text ================= */

/* ================= Theme-aware info text =================
   Same rationale as the warning override above — Bootstrap's
   `.text-info` is `#0dcaf0`, a pale cyan that's hard to read on white
   (the "X in wishlist" status-bar chip looked washed out in light
   mode). Swap for a deeper teal-blue on light backgrounds; keep the
   bright cyan on dark where it pops nicely. Both pass WCAG AA at the
   small sizes used in chips / status badges. */
:root,
[data-bs-theme="light"] {
    --hm-info-text: #0a6e9e; /* deep teal-blue, ~5.4:1 on white */
}
[data-bs-theme="dark"] {
    --hm-info-text: #4dd6f5; /* bright cyan, ~9:1 on #1a1d21 */
}
.text-info {
    color: var(--hm-info-text) !important;
}
/* ================= End theme-aware info text ================= */

/* ================= Paint-mode crosshair guide =================
   Two dashed lines drawn inside the Preview2D SVG by `attachPaintTool`
   in interop.js — they follow the cursor so the user can see which
   row + column they're hovering over (especially helpful on hex grids
   where it's not always obvious which cell will be painted).
   The JS uses `stroke="currentColor"` so the colour comes from this
   rule — dark grey on light theme, light grey on dark theme. */
.hm-paint-crosshair {
    color: #444;
}
[data-bs-theme="dark"] .hm-paint-crosshair {
    color: #cfd2d6;
}
/* ================= End paint-mode crosshair ================= */

/* ================= Range slider fix ================= */
/* Bootstrap's form-range on dark bg sometimes hides the track. Restore it. */
.form-range { -webkit-appearance: none; appearance: none; height: 20px; background: transparent; padding: 0; }
.form-range:focus { outline: none; }

.form-range::-webkit-slider-runnable-track {
    height: 6px;
    background: var(--bs-border-color);
    border-radius: 3px;
}
.form-range::-moz-range-track {
    height: 6px;
    background: var(--bs-border-color);
    border-radius: 3px;
}
.form-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    margin-top: -5px;
    background: var(--hm-accent);
    border: 2px solid var(--bs-body-bg);
    border-radius: 50%;
    cursor: pointer;
    transition: transform 100ms;
}
.form-range::-webkit-slider-thumb:hover { transform: scale(1.15); }
.form-range::-moz-range-thumb {
    width: 16px;
    height: 16px;
    background: var(--hm-accent);
    border: 2px solid var(--bs-body-bg);
    border-radius: 50%;
    cursor: pointer;
}

/* ================= Layout (FUTURE_IDEAS #17 — new shell) =================
   New shell structure:
       hm-shell-v2 (column flex, full viewport)
         hm-topbar       (slim brand + project actions)
         hm-shell-body   (row flex)
           hm-rail       (vertical workspace selector — VS Code activity bar)
           hm-side-panel (workspace-specific controls)
           hm-canvas     (main preview area)
         hm-statusbar    (slim telemetry strip)

   Legacy .hm-shell / .hm-sidebar / .hm-main / .hm-tabs styles below are
   retained for now — Phase 2 panels still reference .hm-section-title etc.
   They'll be cleaned up once the migration is complete. */
.hm-shell-v2 {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
    background: var(--bs-body-bg);
}
.hm-shell-body {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}

/* ---- Top brand bar ---- */
.hm-topbar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 8px 14px;
    border-bottom: 1px solid var(--bs-border-color);
    background: var(--bs-secondary-bg);
}
.hm-topbar-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 1rem;
    flex: 1 1 auto;
    min-width: 0;
}
/* TopBar project status block — sits between the brand on the left and
   the Project dropdown on the right. Holds the editable project name +
   save-state pill. Hidden on phones (≤ 768 px) via `d-none d-md-flex`
   in markup; project name still surfaces in the page <title> there. */
.hm-topbar-project {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1 1 auto;
    min-width: 0;
    margin: 0 12px;
    overflow: hidden;
}
.hm-topbar-name {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 6px;
    padding: 4px 8px;
    font-weight: 600;
    color: var(--bs-body-color);
    cursor: text;
    text-align: left;
    max-width: 260px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.hm-topbar-name:hover {
    background: var(--bs-tertiary-bg);
    border-color: var(--bs-border-color);
}
.hm-topbar-name-edit-icon {
    opacity: 0;
    font-size: 0.85em;
    color: var(--bs-secondary-color);
    transition: opacity 120ms ease;
}
.hm-topbar-name:hover .hm-topbar-name-edit-icon { opacity: 0.75; }
.hm-topbar-name-input {
    max-width: 260px;
    font-weight: 600;
}

/* Save-state pill — drives the four AutosaveStatus states (Saved /
   Dirty / Saving / Error / Idle hidden). Each state gets its own
   subtle background tint + matching icon colour. The pill is
   conservative on weight so it doesn't out-shout the project name. */
.hm-topbar-savestate {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    font-size: 0.75rem;
    border-radius: 999px;
    white-space: nowrap;
    background: var(--bs-tertiary-bg);
    color: var(--bs-secondary-color);
    border: 1px solid transparent;
}
.hm-topbar-savestate-saved {
    color: var(--bs-success-text-emphasis);
    background: var(--bs-success-bg-subtle);
}
.hm-topbar-savestate-dirty {
    color: var(--bs-secondary-color);
    background: var(--bs-tertiary-bg);
}
/* Explicit Save button in topbar when dirty. */
.hm-topbar-save-btn {
    white-space: nowrap;
    font-size: 0.75rem;
    padding: 4px 12px;
}
.hm-topbar-save-btn-shortcut {
    font-size: 0.62rem;
    font-family: monospace;
    opacity: 0.7;
    margin-left: 4px;
}
.hm-topbar-savestate-saving {
    color: var(--hm-info-text, #0a6e9e);
    background: var(--bs-tertiary-bg);
}
.hm-topbar-savestate-saving .bi-arrow-repeat {
    /* Subtle spin while a save is in flight. */
    animation: hm-spin 1s linear infinite;
}
.hm-topbar-savestate-error {
    color: var(--bs-danger-text-emphasis);
    background: var(--bs-danger-bg-subtle);
}
@keyframes hm-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}
@media (max-width: 1024px) {
    /* Tighten on smaller-desktop / tablet — the pill text wraps awkwardly
       when the topbar narrows. Hide the label + shortcut, keep the icon. */
    .hm-topbar-savestate-text { display: none; }
    .hm-topbar-save-btn-label { display: none; }
    .hm-topbar-save-btn-shortcut { display: none; }
}

/* Project dropdown menu — extends the existing `.hm-action-dropdown`
   styling with a wider min-width so Recent project rows fit. */
.hm-project-menu {
    min-width: 320px;
}

/* User dropdown — narrower than the Project menu (no list of items).
   Header cell has the email + display name + a divider before the
   action items. */
.hm-user-menu {
    min-width: 260px;
}
.hm-user-menu-header {
    padding: 10px 12px 8px 12px;
}

/* Account workspace — full-content view rendered when
   `_activeWorkspace == "account"`. Uses the same outer card pattern as
   the Library workspace; sections are individual `<section class="card">`
   blocks so each can scroll its own contents on phones. */
.hm-account-view {
    padding: 16px;
    max-width: 960px;
    margin: 0 auto;
}
.hm-account-header {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 20px;
}
.hm-account-header-icon {
    font-size: 2.5rem;
    color: var(--hm-accent);
    line-height: 1;
}
.hm-account-header-titles { flex: 1 1 auto; min-width: 0; }
.hm-account-dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 6px 16px;
    margin: 0;
}
.hm-account-dl dt {
    color: var(--bs-secondary-color);
    font-weight: 500;
    font-size: 0.9rem;
}
.hm-account-dl dd {
    margin: 0;
    font-size: 0.95rem;
}
.hm-account-edit-link[disabled] {
    opacity: 0.4;
    cursor: not-allowed;
}
.hm-account-rows {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.hm-account-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 8px 0;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-account-row:last-child { border-bottom: none; }
.hm-account-search {
    max-width: 240px;
}
/* Stage 3d.1 — Recent activity event rows. One per security_events
   row. Compact: icon column + body column, no actions. Subtle dividers
   between rows so a list of 50 stays scannable. Capped scroll-height
   so the card doesn't take the whole viewport. */
.hm-account-events {
    max-height: 360px;
    overflow-y: auto;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
}
.hm-account-event {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 8px 12px;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-account-event:last-child { border-bottom: 0; }
.hm-account-event-icon {
    flex-shrink: 0;
    font-size: 1rem;
    width: 1.25em;
    text-align: center;
    line-height: 1.4;
}
.hm-account-event-body {
    flex: 1 1 auto;
    min-width: 0;
}

/* Stage 3d.2 — Active sessions list. Same shape as
   .hm-account-event but with an actions column on the right. */
.hm-account-sessions {
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
}
.hm-account-session {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-account-session:last-child { border-bottom: 0; }
.hm-account-session-icon {
    flex-shrink: 0;
    color: var(--bs-secondary-color);
    font-size: 1.1rem;
    width: 1.25em;
    text-align: center;
}
.hm-account-session-body {
    flex: 1 1 auto;
    min-width: 0;
}
.hm-account-session-actions {
    flex-shrink: 0;
}

.hm-account-projects {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 10px;
}
.hm-account-project-card {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-tertiary-bg);
    transition: background 100ms ease, border-color 100ms ease;
}
/* Whole-card click target — replaces the per-card "Open" arrow button.
   Cursor + keyboard focus ring make the affordance discoverable; the
   click handler lives on the card div with role="button" / tabindex=0
   for accessibility. Active card has no pointer (it's already open). */
.hm-account-project-card.clickable {
    cursor: pointer;
}
.hm-account-project-card.clickable:hover {
    background: var(--bs-secondary-bg);
    border-color: var(--hm-accent);
}
.hm-account-project-card.clickable:focus-visible {
    outline: 2px solid var(--hm-accent);
    outline-offset: 2px;
}
.hm-account-project-card.active {
    border-color: var(--hm-accent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--hm-accent) 30%, transparent);
}
.hm-account-project-icon {
    color: var(--bs-secondary-color);
    font-size: 1.4rem;
    flex-shrink: 0;
}

/* ── Project-card thumbnail ──────────────────────────────
   Mini SVG preview replaces the generic grid icon when a
   thumbnail is available. Full-size popup on hover. */
.hm-project-thumb-trigger {
    position: relative;
}
.hm-project-thumb-mini {
    width: 36px;
    height: 36px;
    border-radius: 4px;
    overflow: hidden;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
}
.hm-project-thumb-mini svg {
    width: 100%;
    height: 100%;
    display: block;
}
.hm-project-thumb-popup {
    display: none;
    position: absolute;
    left: 100%;
    top: 50%;
    transform: translateY(-50%);
    margin-left: 12px;
    width: 240px;
    z-index: 1050;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-tertiary-bg);
    box-shadow: 0 4px 16px rgba(0,0,0,0.25);
    padding: 8px;
    pointer-events: none;
}
.hm-project-thumb-popup svg {
    width: 100%;
    height: auto;
    display: block;
}
.hm-project-thumb-trigger:hover .hm-project-thumb-popup {
    display: block;
}
/* Mobile: popup below the card instead of to the right */
@media (max-width: 768px) {
    .hm-project-thumb-popup {
        left: 0;
        top: 100%;
        transform: none;
        margin-left: 0;
        margin-top: 8px;
    }
}
.hm-account-project-body {
    flex: 1 1 auto;
    min-width: 0;
}
.hm-project-title-row {
    display: flex;
    align-items: center;
    gap: 6px;
}
.hm-project-delete-btn {
    margin-left: auto;
    flex-shrink: 0;
    background: none;
    border: none;
    padding: 2px 4px;
    font-size: 0.7rem;
    color: var(--bs-secondary-color);
    opacity: 0;
    transition: opacity 120ms ease, color 120ms ease;
    cursor: pointer;
    line-height: 1;
    border-radius: 3px;
}
.hm-account-project-card:hover .hm-project-delete-btn,
.hm-project-delete-btn:focus-visible {
    opacity: 1;
}
.hm-project-delete-btn:hover {
    color: var(--bs-danger);
    opacity: 1;
}
/* ── Project-card metadata grid ────────────────────────
   Two-column grid of icon+value pairs. Each <span> is one
   atomic cell — icon and label never split. Fixed 2-col
   layout keeps cards visually aligned regardless of which
   fields are present. */
.hm-project-meta {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1px 10px;
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
    line-height: 1.5;
    margin-top: 3px;
}
.hm-project-meta > span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hm-project-meta i {
    font-size: 0.65rem;
    opacity: 0.7;
    margin-right: 3px;
    vertical-align: baseline;
}
.hm-project-meta-printed {
    color: var(--bs-success);
}
.hm-project-meta-printed i {
    opacity: 1;
}
/* .hm-account-project-actions removed — delete button now
   lives inline in .hm-project-title-row (May 2026). */
@media (max-width: 540px) {
    .hm-account-view { padding: 8px; }
    .hm-account-search { max-width: 100%; }
    .hm-account-row {
        flex-direction: column;
        align-items: flex-start;
        gap: 6px;
    }
}
.hm-recent-item {
    align-items: center;
}
.hm-recent-item-body {
    flex: 1 1 auto;
    min-width: 0;
}
.hm-recent-item-body .hm-action-dropdown-item-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hm-recent-delete {
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
/* "See all projects…" footer item below the recent rows. Subtle top
   border + slightly muted text so it reads as a navigation
   affordance rather than another recent entry. */
.hm-recent-see-more {
    border-top: 1px solid var(--bs-border-color);
    margin-top: 2px;
    padding-top: 8px;
    color: var(--bs-secondary-color);
}
.hm-recent-see-more .hm-action-dropdown-item-label {
    color: var(--hm-accent);
    font-weight: 500;
}
.hm-recent-delete:hover {
    background: var(--bs-tertiary-bg);
    border-radius: 4px;
}
.hm-recent-item.active .hm-action-dropdown-item-label::after {
    content: " · current";
    color: var(--bs-success-text-emphasis);
    font-weight: 500;
    font-size: 0.85em;
}

/* BETA pill in the top-bar wordmark — earlier draft was 0.55rem with a
   transparent body-bg fill and `text-secondary` muted text. Sat alongside
   "Deep Blue Mosaic" but read as a faint ghost rather than a status flag.
   Bumped to a filled accent-tinted pill at ~0.7rem so it's legible at a
   glance without overpowering the wordmark. Theme-aware via the existing
   --hm-accent token. */
.hm-beta-pill {
    display: inline-flex;
    align-items: center;
    margin-left: 0.5rem;
    padding: 0.18rem 0.5rem;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.06em;
    border-radius: 999px;
    color: #fff;
    background: var(--hm-accent, #4caf50);
    border: 1px solid color-mix(in srgb, var(--hm-accent, #4caf50) 75%, #000);
    text-transform: uppercase;
    white-space: nowrap;
    user-select: none;
}
[data-bs-theme="dark"] .hm-beta-pill {
    /* Slightly less saturated on dark so it doesn't out-shout the brand
       text + hexagon icon. The accent is already legible against the
       darker top-bar bg without needing extra contrast. */
    background: color-mix(in srgb, var(--hm-accent, #4caf50) 88%, #000);
}
/* `.hm-topbar-project-status` rule removed (May 2026) — the class was
   used for an "Image · 828 × 828 px · 900 × 900 mm" status line in the
   TopBar that duplicated info already shown in the Preview2D toolbar
   AND the StatusBar. The TopBar slot is now reserved for ProjectName
   when implemented. */
.hm-topbar-actions {
    display: flex;
    gap: 8px;
    flex: 0 0 auto;
}

/* ---- Activity rail ---- */
/* Wrapper around .hm-rail. Establishes the positioning context for
   the overflow-chevron pseudo-bar that fades in when the rail has
   more items than fit. The wrapper is the same width as the rail
   (68 px) so it slots into the shell flex layout exactly where the
   bare nav used to sit. May 2026. */
.hm-rail-wrapper {
    position: relative;
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    width: 68px;
    min-height: 0;          /* allow the inner nav to flex-shrink + scroll */
}
.hm-rail-wrapper .hm-rail {
    flex: 1 1 auto;
    width: 100%;
}
/* Down-chevron overflow indicator. Hidden by default; faded in by JS
   when the rail's content overflows AND the user isn't already at
   the bottom. pointer-events: none so it never intercepts clicks
   on the rail icons under it. Background gradient sells the
   "there's more below" affordance — fades from transparent at the
   top to the rail's own background at the bottom so the chevron
   appears to peek above the cut-off. May 2026. */
.hm-rail-overflow-chevron {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 28px;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding-bottom: 4px;
    color: var(--bs-secondary-color);
    font-size: 1rem;
    pointer-events: none;
    background: linear-gradient(
        to bottom,
        transparent 0%,
        var(--bs-secondary-bg) 80%);
    opacity: 0;
    transition: opacity 160ms ease;
}
/* Selectors target classes JS attaches to .hm-rail; chevron is the
   immediate next sibling. Visible iff rail has overflow AND is NOT
   currently scrolled to the bottom. */
.hm-rail.has-overflow:not(.at-bottom) + .hm-rail-overflow-chevron {
    opacity: 0.85;
    animation: hm-rail-bob 1.6s ease-in-out infinite;
}
@keyframes hm-rail-bob {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(2px); }
}

.hm-rail {
    flex: 0 0 auto;
    width: 68px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background: var(--bs-secondary-bg);
    border-right: 1px solid var(--bs-border-color);
    padding: 8px 0;
    /* Scroll vertically when the icon stack is taller than the
       viewport (short laptop screens; mobile landscape). Earlier
       `overflow: hidden` clipped the bottom items unreachably.
       Scrollbar fully HIDDEN visually (scrollbar-width: none +
       ::-webkit-scrollbar) — first iteration used `scrollbar-gutter:
       stable` to reserve scrollbar space, but that left a permanent
       gap on the right of the rail on tall viewports (no scrollbar
       needed) and made active-row highlights stop short of the rail
       edge. With the scrollbar fully hidden, the rail width never
       jumps and active highlights extend the full width.
       Scroll still works via wheel / touch / arrow keys. */
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;          /* Firefox */
}
.hm-rail::-webkit-scrollbar {       /* Chromium / WebKit */
    display: none;
}
.hm-rail-group {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
/* Bottom-group divider. The rail uses justify-content: space-between
   so the top + bottom clusters sit pinned to their respective ends
   when there's room. On short viewports the rail switches to scroll
   (overflow-y: auto) — at that point space-between collapses and the
   two clusters touch, with no visual marker between them. The
   border-top here is the marker: invisible-feeling when there's a
   lot of empty space above it (looks like a faint section underline
   above Library), but the visible divider when the clusters stack
   tight under overflow. Subtle colour matches other rail accents. */
.hm-rail-group-bottom {
    border-top: 1px solid var(--bs-border-color);
    padding-top: 8px;
    margin-top: 8px;
}
.hm-rail-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    padding: 6px 4px;
    background: transparent;
    border: none;
    color: var(--bs-secondary-color);
    cursor: pointer;
    /* Left accent strip on hover/active — VS Code style. */
    border-left: 3px solid transparent;
    transition: background 80ms, color 80ms, border-color 80ms;
}
.hm-rail-item i {
    font-size: 1.15rem;
    line-height: 1;
}
.hm-rail-label {
    font-size: 0.58rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    line-height: 1.1;
}
.hm-rail-item:hover:not(:disabled):not(.disabled) {
    background: color-mix(in srgb, var(--hm-accent) 10%, transparent);
    color: var(--bs-body-color);
}
/* Anchor needed so the ::before active-stripe positions correctly. */
.hm-rail-item {
    position: relative;
}
.hm-rail-item.active,
.hm-rail-item.active:hover {
    /* Active state for the SELECTED workspace.
       NO right-edge inset border — user feedback was explicit on this.
       !important to win over the :hover rule when active item is hovered. */
    color: var(--hm-accent) !important;
    background-color: rgba(59, 130, 246, 0.22) !important;
    box-shadow: none !important;
}
.hm-rail-item.active::before {
    /* 4px solid green stripe pinned to the left edge — pseudo-element
       overlay so no other border/box-shadow/border-left rule can hide it. */
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 4px;
    background: var(--hm-accent);
    pointer-events: none;
    z-index: 1;
}
/* Bolden the active rail label so the selected workspace reads as the
   anchor of the user's current focus — works alongside the accent colour
   + left border + background. */
.hm-rail-item.active .hm-rail-label {
    font-weight: 700;
}
.hm-rail-item.active i {
    /* Slight bump on the icon to match the bold label — most Bootstrap
       Icons are constant-weight outlines, but a few (the *-fill variants)
       respect this. The extra weight is also picked up by browsers that
       synthesise bold for missing weights. */
    font-weight: 600;
}
.hm-rail-item:disabled,
.hm-rail-item.disabled {
    opacity: 0.35;
    cursor: not-allowed;
}
/* Icon-only rail variant — used for utility toggles (Theme) where a label
   would be visual noise. Slightly smaller padding to compensate for the
   missing label height so the button sits at a familiar visual weight. */
.hm-rail-item-icon-only {
    padding: 8px 4px;
}
.hm-rail-item-icon-only .hm-rail-label { display: none; }
/* Thin divider used between rail-item groups (e.g. Library/Slicer ↔ Theme). */
.hm-rail-separator {
    height: 1px;
    margin: 6px 12px;
    background: var(--bs-border-color);
}

/* ---- Canvas wrap (canvas + settings drawer overlay + workspace strip) ---- */
.hm-canvas-wrap {
    flex: 1 1 auto;
    min-width: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* ---- Settings drawer (slide-out overlay over the canvas) ---- */
/* Sits absolutely at the left edge of the canvas, slides in/out via the
   `left` property (NOT transform — transform creates a new containing block
   for `position: fixed` descendants, which would trap modals opened from
   inside the drawer to the drawer's bounds). Width matches the old fixed
   side panel — reuses the same SettingsPanel content. */
.hm-settings-drawer {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 360px;
    z-index: 30;
    background: var(--bs-secondary-bg);
    border-right: 1px solid var(--bs-border-color);
    box-shadow: 4px 0 12px -4px rgba(0, 0, 0, 0.18);
    left: -380px;  /* off-screen by drawer width + a bit for the shadow */
    transition: left 180ms ease;
    display: flex;
    flex-direction: column;
}
.hm-settings-drawer.open {
    left: 0;
}
.hm-settings-drawer-header {
    flex: 0 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 12px;
    border-bottom: 1px solid var(--bs-border-color);
    background: var(--bs-tertiary-bg);
}
.hm-settings-drawer-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 1rem;
    padding-bottom: 0;
    /* Reuse .hm-side-panel rules for SettingsPanel content (first-section-title etc.) */
}
/* Apply the same first-section-title rule as .hm-sidebar so the drawer's
   first heading doesn't get a divider above it. */
.hm-settings-drawer-body > .hm-section-title:first-of-type,
.hm-settings-drawer-body > div > .hm-section-title:first-of-type {
    border-top: none;
    padding-top: 0;
}

/* ---- Canvas (main content) ---- */
.hm-canvas {
    flex: 1 1 auto;
    min-width: 0;
    overflow: auto;
    padding: 1rem 1.25rem;
    position: relative;
    display: flex;
    flex-direction: column;
}

/* ---- Workspace info strip (per-workspace contextual telemetry) ---- */
/* Sits between the canvas and the global StatusBar. Same high-contrast colour
   pair as .hm-statusbar so the two bottom strips read as one cohesive unit.
   Hidden when empty so it doesn't add a vertical band of whitespace on
   workspaces that don't need it. */
.hm-workspace-strip {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 0 14px;
    font-size: 0.78rem;
    color: var(--hm-statusbar-fg, #fff);
    border-top: 1px solid var(--bs-border-color);
    background: var(--hm-statusbar-bg, #111);
    min-height: 0;
}
.hm-workspace-strip:empty { display: none; }
.hm-workspace-strip:not(:empty) { min-height: 28px; }

/* ---- Legacy side panel (kept while a few panels still mount via hm-side-panel) ---- */
.hm-side-panel {
    flex: 0 0 auto;
    width: 360px;
    overflow-y: auto;
    border-right: 1px solid var(--bs-border-color);
    background: var(--bs-secondary-bg);
    padding: 1rem;
    padding-bottom: 0;
}

/* ---- Narrow viewport adjustments ---- */
/* Shrink the rail to icons-only (no labels) on narrower screens so the canvas
   gets more room. Rail items keep their tooltips so the meaning is one hover
   away. Settings drawer shrinks to fit the available space. */
@media (max-width: 1100px) {
    .hm-rail-wrapper { width: 48px; }
    .hm-rail { width: 48px; }
    .hm-rail-item { padding: 6px 4px; }
    .hm-rail-label { display: none; }
    .hm-settings-drawer { width: 320px; left: -340px; }
}
@media (max-width: 760px) {
    .hm-settings-drawer { width: 88vw; left: calc(-88vw - 20px); }
}
/* Sub-nav for workspaces that host more than one view (e.g. Project = Crop / 2D).
   Normal flow (not absolute) so the panel below it shifts down to make room —
   prevents the floating buttons from colliding with the panel's own toolbar. */
.hm-canvas-subnav {
    display: flex;
    gap: 6px;
    margin-bottom: 0.75rem;
    flex: 0 0 auto;
}

/* ---- Status bar ---- */
/* High-contrast bottom strip: black-on-white in light mode, white-on-black
   in dark mode, via the inverse-of-body custom properties below. Status text
   needs to be glanceable, not a soft secondary. */
.hm-statusbar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 6px 14px;
    border-top: 1px solid var(--bs-border-color);
    background: var(--hm-statusbar-bg, #111);
    color: var(--hm-statusbar-fg, #fff);
    font-size: 0.78rem;
    min-height: 30px;
}
/* Inside the high-contrast bar, override Bootstrap's text-secondary etc.
   so status text remains legible against the dark/light bg. */
.hm-statusbar .text-secondary { color: inherit !important; opacity: 0.78; }
/* `.text-warning` is now handled globally (theme-aware) at the top of
   this file — no statusbar-specific override needed. */
.hm-statusbar .text-success { color: #5dd07e !important; }
.hm-statusbar .text-info { color: #6cb4d9 !important; }
.hm-statusbar .text-danger { color: #e67c7c !important; }
.hm-statusbar-saving { animation: hm-spin 1s linear infinite; }
.hm-statusbar-section {
    display: flex;
    align-items: center;
    gap: 6px;
}
.hm-statusbar-divider {
    width: 1px;
    height: 16px;
    background: var(--bs-border-color);
}
.hm-statusbar-spacer { flex: 1 1 auto; }
/* ---- Shared page (accept share link) ---- */
.hm-shared-page {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    min-height: 100vh;
    padding: 60px 16px;
    background: var(--bs-body-bg);
}
.hm-shared-card {
    max-width: 480px;
    width: 100%;
    padding: 28px;
    border: 1px solid var(--bs-border-color);
    border-radius: 12px;
    background: var(--bs-tertiary-bg);
}
.hm-shared-card-header {
    display: flex;
    align-items: center;
    gap: 14px;
}
.hm-shared-thumbnail {
    text-align: center;
}
.hm-shared-thumbnail svg {
    max-width: 240px;
    max-height: 180px;
    border-radius: 6px;
}
.hm-shared-message {
    font-size: 0.85rem;
    font-style: italic;
}
/* Share project modal — project preview card */
.hm-share-preview {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-tertiary-bg);
}
.hm-share-preview-thumb {
    width: 64px;
    height: 64px;
    flex-shrink: 0;
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    background: var(--bs-body-bg);
}
.hm-share-preview-thumb svg {
    width: 100%;
    height: 100%;
    display: block;
}
.hm-share-preview-info {
    min-width: 0;
    flex: 1;
}

/* ================= End new shell layout ================= */

/* ================= Layout (legacy — preserved for unmigrated panels) ================= */
.hm-shell {
    display: grid;
    grid-template-columns: 360px 1fr;
    height: 100%;
    overflow: hidden;
}
.hm-sidebar {
    overflow-y: auto;
    height: 100%;
    border-right: 1px solid var(--bs-border-color);
    padding: 1rem;
    /* No bottom padding — the sticky Build button replaces it. */
    padding-bottom: 0;
    background: var(--bs-secondary-bg);
}

/* Sticky Build Mosaic bar — pinned to the bottom of the sidebar's visible area.
   As the user scrolls through the settings, this stays fixed at the bottom so the
   primary call-to-action is always one click away without scrolling. */
.hm-build-sticky {
    position: sticky;
    bottom: 0;
    left: 0;
    right: 0;
    margin: 0 -1rem;  /* extend to sidebar edges */
    padding: 0.75rem 1rem;
    background: var(--bs-secondary-bg);
    border-top: 1px solid var(--bs-border-color);
    /* Soft shadow above the bar so it reads as elevated over scrolled content. */
    box-shadow: 0 -4px 8px -4px rgba(0, 0, 0, 0.08);
    z-index: 5;
}
.hm-main {
    overflow: hidden;
    display: flex;
    flex-direction: column;
    height: 100%;
    padding: 1rem 1.25rem;
    min-height: 0;
}
.hm-tabs { flex: 0 0 auto; margin-bottom: 1rem; }
.hm-tab-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
    padding-bottom: 1rem;
    /* Establish a positioning context so the build overlay (.hm-build-overlay) can
       absolute-pin to the tab body rather than the whole viewport. */
    position: relative;
}

@media (max-width: 960px) {
    body { overflow: auto; }
    .hm-shell { grid-template-columns: 1fr; height: auto; }
    .hm-sidebar { border-right: none; border-bottom: 1px solid var(--bs-border-color); max-height: 70vh; height: auto; }
    .hm-main { overflow: auto; height: auto; }
}

/* ================= Viewer shell (2D + 3D) ================= */
.hm-viewer-shell {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 400px;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    overflow: hidden;
    background: var(--hm-canvas-bg);
}
/* Checkerboard canvas background — subtle alternating squares so edge cells
   (black in dark mode, white in light mode) stay distinguishable from the
   clear zone. Apply to the element that holds the SVG canvas. 20px squares
   scale well at all zoom levels without dominating the cells. */
.hm-canvas-checker {
    background-image:
        conic-gradient(var(--hm-checker-a) 25%, var(--hm-checker-b) 25% 50%,
                       var(--hm-checker-a) 50% 75%, var(--hm-checker-b) 75%);
    background-size: 20px 20px;
}
.hm-viewer-toolbar {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .4rem .75rem;
    background: var(--bs-secondary-bg);
    border-bottom: 1px solid var(--bs-border-color);
    flex: 0 0 auto;
}
/* Info strip inside the viewer toolbar — pipe-divided icon+value sections,
   visually mirroring the bottom StatusBar (.hm-statusbar-section /
   .hm-statusbar-divider) so the two info surfaces feel like the same family.
   Each section is a small icon + value; vertical pipe between them.
   May 2026 UX pass: was previously a single comma-run-on string. */
.hm-viewer-info {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 0.78rem;
    color: var(--bs-secondary-color);
    flex-wrap: wrap;
}
.hm-viewer-info-section {
    display: flex;
    align-items: center;
    gap: 5px;
}
.hm-viewer-info-section > i.bi {
    color: var(--bs-secondary-color);
    opacity: 0.85;
}
.hm-viewer-info-section.text-warning > i.bi,
.hm-viewer-info-section.text-warning > span {
    color: var(--bs-warning) !important;
    opacity: 1;
}
.hm-viewer-info-divider {
    width: 1px;
    height: 14px;
    background: var(--bs-border-color);
    flex: 0 0 auto;
}
/* Vertical separator between toolbar button groups (display toggles |
   editing tools | zoom/layout). Same visual language as the info-bar
   divider but sized to match button height. */
.hm-toolbar-divider {
    width: 1px;
    height: 20px;
    background: var(--bs-border-color);
    flex: 0 0 auto;
    margin: 0 6px;
}
/* Mobile: stack the toolbar so the stats strip sits above the action
   buttons. Without this the buttons cluster squeezes the stats into a
   narrow left column and they wrap one phrase per line. The stats
   strip itself becomes horizontally scrollable so each section keeps
   its `nowrap` (no per-letter wrapping inside a section). */
@media (max-width: 540px) {
    .hm-viewer-toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: 6px;
    }
    .hm-viewer-info {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: thin;
        gap: 8px;
        padding-bottom: 2px;
    }
    .hm-viewer-info-section { white-space: nowrap; flex-shrink: 0; }
    .hm-viewer-info-divider { flex-shrink: 0; }
    /* Right-side button cluster — drop `ms-auto`'s push effect (we're
       in column layout now) and let the buttons sit naturally below. */
    .hm-viewer-toolbar > .ms-auto { margin-left: 0 !important; }
}
.hm-viewer-canvas {
    flex: 1 1 auto;
    min-height: 0;
    position: relative;
    cursor: grab;
    overflow: hidden;
}
.hm-viewer-canvas:active { cursor: grabbing; }

/* Maximised mode — covers the whole viewport */
.hm-viewer-shell.hm-viewer-max {
    position: fixed;
    inset: 0;
    z-index: 1040;
    border-radius: 0;
    border: none;
    min-height: 100vh;
}

/* ================= Misc ================= */
.hm-swatch {
    width: 28px; height: 28px;
    border-radius: 6px;
    /* Dual-ring keyline — same technique as .hm-chip-icon so dark colours stand out on
       dark backgrounds and pale colours stand out on light ones, without a theme-
       specific rule. */
    border: 1px solid rgba(0, 0, 0, 0.5);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
    display: inline-block;
    flex-shrink: 0;
    position: relative;
    overflow: hidden;
}

/* Translucent / Glow filaments — diagonal white-stripe overlay on the
   swatch so the user can see at a glance that the filament prints
   translucent (light passes through) rather than opaque. The base
   colour stays as the inline `background:` style; ::before draws the
   stripes on top via a non-replacing background-image. Designed to
   work on ANY swatch element (.hm-swatch, .hm-filament-picker-swatch,
   .hm-paint-swatch etc.) — the modifier sets its own
   `position: relative` so callers don't need to. May 2026 — added
   when the v8 catalogue surfaced a lot of Translucent / Glow entries. */
.hm-swatch-translucent {
    position: relative;
    overflow: hidden;
}
.hm-swatch-translucent::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image: repeating-linear-gradient(
        45deg,
        rgba(255, 255, 255, 0.55) 0,
        rgba(255, 255, 255, 0.55) 1.5px,
        transparent 1.5px,
        transparent 5px);
    pointer-events: none;
}

.hm-palette-row {
    display: grid;
    grid-template-columns: 40px 1fr 1fr 100px;
    gap: 10px;
    align-items: center;
    padding: 6px 0;
}
.hm-palette-row + .hm-palette-row {
    border-top: 1px solid var(--bs-border-color);
}

.hm-section-title {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--bs-secondary-color);
    margin-top: 1.25rem;
    margin-bottom: 0.5rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--bs-border-color);
}

/* First section title in the sidebar doesn't need a divider — it sits under the header.
   New shell uses .hm-side-panel; same rule applies there. */
.hm-sidebar > .hm-section-title:first-of-type,
.hm-sidebar > form > .hm-section-title:first-of-type,
.hm-side-panel > .hm-section-title:first-of-type,
.hm-side-panel > div > .hm-section-title:first-of-type {
    border-top: none;
    padding-top: 0;
    margin-top: 0;
}

/* Plate cards — responsive grid with consistent card structure.
   Using auto-fill + minmax gives even-width columns that adapt to viewport:
     - XL (≥1400px): ~5-6 cards/row
     - L  (1200px):  ~4 cards/row
     - MD (992px):   ~3 cards/row
     - SM (768px):   ~3 cards/row at minimum card width
     - Phone (<560): 2 cards/row
   All cards in a row share the same height because grid rows auto-size to the tallest. */
.hm-plates-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px;
    margin-top: 8px;
}
@media (min-width: 561px) and (max-width: 720px) {
    .hm-plates-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
}
/* Phones / narrow tablets — 2-column plate cards crop the second
   plate off-screen at typical phone widths. Force a single column
   below 560 px so each plate gets the full content width. The
   2-column rule above only kicks in on bigger tablets. */
@media (max-width: 560px) {
    .hm-plates-grid { grid-template-columns: 1fr; gap: 8px; }
}

.hm-plate-card {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    padding: 8px;
    background: var(--bs-tertiary-bg);
    text-align: center;
    /* Tightened from 6 → 4 px so the title-row + printed tick sit
       closer to the mini preview. Other inter-row gaps still read
       cleanly because the chip rows have their own padding. */
    gap: 4px;
    /* Let cards stretch to fill grid row height so the card bottoms align */
    height: 100%;
}

/* The preview SVG sits at the top of the card */
.hm-plate-card .hm-plate-preview {
    display: flex;
    justify-content: center;
    align-items: center;
    aspect-ratio: 1 / 1;
    width: 100%;
    /* No background fill — the SVG renders a subtle dashed plate outline itself, which
       works on both light and dark themes via the --hm-plate-stroke variable. */
    background: transparent;
    border-radius: 4px;
    overflow: hidden;
}
.hm-plate-card .hm-plate-preview > svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* The info region — structured rows for consistent layout across cards. Each card
   has the same 4 info rows in the same order, so visually they line up between cards
   regardless of content length. */
.hm-plate-card .hm-plate-title {
    font-weight: 600;
    font-size: 0.85rem;
    text-align: left;
    padding: 0;
}
.hm-plate-card .hm-plate-title-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    padding: 0 2px;
}
.hm-plate-card .hm-plate-title-time {
    font-size: 0.78rem;
    color: var(--bs-secondary-color, #6c757d);
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}
.hm-plate-card .hm-plate-title-time i { font-size: 0.85em; }
/* Per-plate "Printed" tick toggle. Used by EstimatePanel + ExportPanel
   plate cards (and any other surface that wants the same affordance).
   Color is set inline by the Razor render to communicate state
   (accent green = printed, amber = stale, secondary = not yet).
   Borderless / transparent — the icon is the entire visual. NOT
   scoped under .hm-plate-card because Export uses
   .hm-plate-export-card; keeping the rule unscoped avoids the
   default <button> border showing through there. May 2026
   plate-printed tracker. */
.hm-plate-printed-toggle {
    background: transparent;
    border: 0;
    padding: 2px;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    transition: transform 80ms ease, opacity 80ms ease;
}
.hm-plate-printed-toggle:hover {
    transform: scale(1.1);
}
.hm-plate-printed-toggle:focus-visible {
    outline: 2px solid var(--hm-accent);
    outline-offset: 2px;
    border-radius: 50%;
}
/* Estimate-card-specific: pushes the toggle to the far right of the
   title row. Export panel doesn't need this — its tick sits in line
   with the heading. */
.hm-plate-card .hm-plate-printed-toggle {
    margin-left: auto;
}

/* Export panel — combined plate-name + tick click target. Whole
   heading area is the toggle so the user can tap either the icon or
   the name to mark printed. Visually matches an h6 heading. May 2026
   plate-printed tracker.
   Hover affordance is a subtle bordered card (background tint +
   1-px ring), NOT a text underline — underlines don't match the
   app's idiom. Ring is implemented via box-shadow so it doesn't
   reflow the surrounding layout when hover starts/ends. Padding +
   matching negative margin keep the heading visually anchored. */
.hm-plate-export-name-toggle {
    background: transparent;
    border: 0;
    padding: 4px 8px;
    margin: -4px -8px;
    border-radius: 6px;
    text-align: left;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    line-height: 1.2;
    transition: background-color 100ms ease, box-shadow 100ms ease;
    /* No colour set here — Razor inline-style sets the icon colour.
       The name itself uses --bs-body-color via the named span. */
}
.hm-plate-export-name-toggle:hover {
    background: var(--bs-tertiary-bg);
    box-shadow: 0 0 0 1px var(--bs-border-color);
}
.hm-plate-export-name-toggle:active {
    background: var(--bs-secondary-bg);
}
.hm-plate-export-name-toggle:focus-visible {
    outline: 2px solid var(--hm-accent);
    outline-offset: 2px;
}
.hm-plate-export-name-toggle i.bi {
    font-size: 1.05rem;
    flex-shrink: 0;
}
.hm-plate-export-name {
    /* Match h6 visual weight from Bootstrap (~ 1rem, semibold). */
    color: var(--bs-body-color);
    font-weight: 600;
    font-size: 1rem;
}
.hm-plate-export-printed-date {
    /* Smaller secondary text to the right of the plate name —
       date(printed) [optional · stale]. */
    color: var(--bs-secondary-color);
    font-size: 0.75rem;
    font-weight: 400;
    margin-left: 4px;
}
/* Labelled variant — used in EstimatePanel where the tick has a
   text description ("Printed" / "Not printed" / "Stale") next to the
   icon. Smaller font + tighter gap so the label reads as a status
   pip rather than a primary control. May 2026. */
.hm-plate-printed-toggle-labelled {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.78rem;
    font-weight: 500;
    line-height: 1;
}
.hm-plate-printed-toggle-labelled i.bi {
    font-size: 1rem;
}
.hm-plate-printed-label {
    /* colour inherits from the parent inline-style, same as the icon */
}
/* Chip base styles — intentionally NOT scoped under .hm-plate-card, because chips are
   also used in the Export panel plate cards (.hm-plate-export-card) and the PlateDetails
   modal. Previously all rules lived under .hm-plate-card which meant chips elsewhere had
   no styling — no background, no padding, no swatch size. If you need different behaviour
   inside a specific card variant, override with a more-specific selector. */
.hm-plate-chip {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 6px;
    border-radius: 4px;
    background: var(--bs-body-bg);
    font-size: 0.72rem;
    color: var(--bs-secondary-color);
    text-align: left;
    line-height: 1.2;
}
.hm-plate-chip.hm-chip-filament {
    color: var(--bs-body-color);
    font-weight: 500;
}
.hm-plate-chip .hm-chip-icon {
    /* Larger + a dual-ring treatment so the swatch reads on ANY background:
         - Outer 1px border in semi-opaque black: keeps pale/white swatches visible
           against a pale card background
         - Inner 1px inset ring in semi-opaque white: keeps dark/black swatches visible
           against a dark card background (e.g. dark mode)
       Together they act like a thin "keyline" that works on both themes without needing
       separate rules. */
    flex: 0 0 18px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 1px solid rgba(0, 0, 0, 0.5);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}
/* Variation for smaller inline chip groups (multi-colour "N colours" row) — keeps the
   row compact. Slightly smaller than the standalone filament chip. */
.hm-plate-chip.hm-chip-filament .hm-chip-icon + .hm-chip-icon,
.hm-plate-chip.hm-chip-filament .hm-chip-icon + .hm-chip-text + .hm-chip-icon {
    margin-left: -2px;  /* overlap when there are many side-by-side */
}
/* Extra-export-only swatches (e.g. the FixedColour base when not in palette).
   Same size as the others but with a dashed outer ring + a small bottom-right
   "+" tag so they're visually distinct from the mosaic-cell filaments. The
   user gets a quick "this one is extra, not part of the cells themselves"
   signal without needing to hover for a tooltip. */
.hm-plate-chip .hm-chip-icon.hm-chip-icon-extra {
    border-style: dashed;
    border-color: rgba(0, 0, 0, 0.65);
    position: relative;
    /* Allow the "+" badge to extend past the swatch's bounding box without
       being clipped — the parent sets `overflow: hidden` on the chip frame
       and we rely on the badge sitting outside the swatch's edge. */
    overflow: visible;
}
.hm-plate-chip .hm-chip-icon.hm-chip-icon-extra::after {
    content: "+";
    position: absolute;
    /* Top-right corner. Negative offsets nudge the badge so it overlaps the
       swatch's edge rather than floating away — matches the visual idiom of
       a small "modifier" badge clipped onto an icon. */
    top: -3px;
    right: -3px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--hm-card-bg, #fff);
    color: var(--hm-text, #222);
    /* Flex centring is the only way to perfectly centre a single glyph inside
       a small circle across browsers — `line-height` sub-pixel rounding made
       the "+" sit visibly off-centre at this size. */
    display: flex;
    align-items: center;
    justify-content: center;
    /* `1` keeps the glyph compact; the box itself does the centring. */
    line-height: 1;
    font-size: 7px;
    font-weight: 700;
    border: 1px solid rgba(0, 0, 0, 0.5);
    /* Sub-pixel optical nudge: the "+" glyph has more visual mass below its
       midline than above, so without this it reads slightly low. -0.5px pulls
       it back to optical centre without disturbing the box's geometric centre. */
    padding-top: 0;
}
.hm-plate-chip .hm-chip-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hm-plate-chips-row {
    display: flex;
    gap: 6px;
    flex-wrap: nowrap;
}
.hm-plate-chips-row > .hm-plate-chip {
    flex: 1 1 50%;
    min-width: 0;
}

.hm-plate-card .hm-plate-actions {
    /* Pinned to bottom of card so "Export plate" buttons align across cards */
    margin-top: auto;
    display: flex;
    gap: 4px;
}
.hm-plate-card .hm-plate-actions .btn {
    flex: 1 1 auto;
}

/* Conditional shape-specific settings panel (shown for PyramidFrustum etc) */
.hm-shape-params {
    padding: 8px 10px;
    margin: 4px 0 10px 0;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    background: color-mix(in srgb, var(--hm-accent) 5%, var(--bs-tertiary-bg));
}

/* ================= Summary cards (sidebar) ================= */
.hm-summary-card {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 10px 12px;
    margin-bottom: 8px;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-tertiary-bg);
    text-align: left;
    color: var(--bs-body-color);
    transition: background 100ms, border-color 100ms;
    cursor: pointer;
}
.hm-summary-card:hover {
    border-color: var(--hm-accent);
    background: color-mix(in srgb, var(--hm-accent) 6%, var(--bs-tertiary-bg));
}
.hm-summary-card:focus-visible {
    outline: 2px solid var(--hm-accent);
    outline-offset: 2px;
}
/* Read-only variant — used in blank-canvas mode where the layout cards still
   display the user's choices but aren't clickable (the modal would let them
   change settings that would invalidate the painted-cell grid). */
.hm-summary-card-static {
    cursor: default;
    opacity: 0.85;
}
.hm-summary-card-static:hover {
    border-color: var(--bs-border-color);
    background: var(--bs-tertiary-bg);
}
.hm-summary-icon {
    flex: 0 0 auto;
    width: 64px;
    height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.hm-summary-icon.hm-summary-palette {
    /* Palette card: let the swatch grid define its own size */
    width: auto;
    height: auto;
    align-self: flex-start;
}
.hm-summary-icon svg { display: block; }
.hm-summary-palette {
    display: grid;
    grid-template-columns: repeat(4, 18px);
    grid-auto-rows: 18px;
    gap: 3px;
    padding: 4px;
    border: 1px solid var(--bs-border-color);
    border-radius: 4px;
    background: color-mix(in srgb, black 15%, var(--bs-tertiary-bg));
    width: fit-content;
}
.hm-summary-swatch {
    display: block;
    width: 18px;
    height: 18px;
    border-radius: 3px;
    border: 1px solid rgba(0,0,0,0.25);
}
.hm-summary-details {
    flex: 1 1 auto;
    min-width: 0;
    line-height: 1.3;
}
/* Compact sub-text for summary cards — smaller than Bootstrap's .small so the card
   title stays the visual anchor and multi-line summaries don't crowd the card. */
.hm-summary-subtext {
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
    line-height: 1.3;
}
.hm-summary-chevron {
    flex: 0 0 auto;
    color: var(--bs-secondary-color);
    font-size: 1.1rem;
}

/* ================= Modal overlay ================= */
.hm-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    z-index: 1050;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
}
.hm-modal-window {
    width: 100%;
    max-height: calc(100vh - 48px);
    display: flex;
    flex-direction: column;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 10px;
    overflow: hidden;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
}
.hm-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    background: var(--bs-secondary-bg);
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-modal-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 16px 18px;
}
.hm-modal-footer {
    padding: 12px 16px;
    background: var(--bs-secondary-bg);
    border-top: 1px solid var(--bs-border-color);
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

/* ================= Shape modal internals ================= */
.hm-shape-modal {
    display: grid;
    grid-template-columns: 260px 1fr;
    gap: 18px;
}
@media (max-width: 768px) {
    .hm-shape-modal { grid-template-columns: 1fr; }
}
.hm-shape-modal-preview {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    padding: 14px;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-tertiary-bg);
    min-width: 0;       /* allow the flex/grid cell to shrink below content intrinsic size */

    /* Sticky-pin the preview column to the top of the scrolling modal body
       so the shape icon + mini-visual + info card stay visible while the
       user scrolls the right-hand controls (May 2026 UX fix). The grid
       alignment matters: align-self: start prevents the cell from
       auto-stretching, which would defeat the sticky positioning.
       max-height + overflow-y mean the preview gets its own inner scroll
       if its content is taller than the viewport. */
    position: sticky;
    top: 0;
    align-self: start;
    max-height: calc(100vh - 180px);
    overflow-y: auto;
    overflow-x: hidden; /* hard-clip any SVG horizontal overflow */
}
@media (max-width: 768px) {
    /* On narrow viewports the modal collapses to a single column — sticky
       positioning would just cover the controls. Drop back to normal flow. */
    .hm-shape-modal-preview {
        position: static;
        max-height: none;
        overflow-y: visible;
    }
}
.hm-shape-modal-preview svg {
    display: block;
    width: 100%;
    height: auto;
    max-width: 220px;   /* keep the SVG inside the padded column */
    max-height: 220px;
}
.hm-shape-modal-summary {
    margin-top: 12px;
    text-align: center;
    line-height: 1.35;
}
.hm-shape-modal-controls {
    min-width: 0;
}
.hm-shape-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
}
@media (max-width: 520px) {
    /* On very narrow viewports, fall back to 2 columns */
    .hm-shape-grid { grid-template-columns: repeat(2, 1fr); }
}
.hm-shape-card {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 8px 4px;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    cursor: pointer;
    transition: background 100ms, border-color 100ms, transform 80ms;
}
.hm-shape-card-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    font-size: 0.8rem;
    opacity: 0.45;
    line-height: 1;
}
.hm-shape-card.selected .hm-shape-card-badge {
    opacity: 1;
    color: var(--hm-accent);
    filter: brightness(0.7);
}
.hm-shape-card:hover {
    border-color: var(--hm-accent);
    background: color-mix(in srgb, var(--hm-accent) 6%, var(--bs-tertiary-bg));
}
.hm-shape-card.selected {
    border-color: var(--hm-accent);
    border-width: 2px;
    background: color-mix(in srgb, var(--hm-accent) 14%, var(--bs-tertiary-bg));
}
.hm-shape-card-thumb {
    width: 60px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Inherited `color` for the embedded shape SVG (which uses
       currentColor by default). Picks up the theme accent so the
       3D-shaded shapes render in green rather than the page's text
       colour. Single source of truth: :root --hm-accent in app.css. */
    color: var(--hm-accent);
}
.hm-shape-card-thumb svg { display: block; }
.hm-shape-card-label {
    font-size: 0.7rem;
    line-height: 1.15;
    text-align: center;
    color: var(--bs-body-color);
}

/* Height-strategy picker — 4 across × 2 rows for 8 strategies */
.hm-strategy-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
}
@media (max-width: 620px) {
    .hm-strategy-grid { grid-template-columns: repeat(2, 1fr); }
}
.hm-strategy-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 6px 4px;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    cursor: pointer;
    transition: background 100ms, border-color 100ms;
}
.hm-strategy-card:hover {
    border-color: var(--hm-accent);
    background: color-mix(in srgb, var(--hm-accent) 6%, var(--bs-tertiary-bg));
}
.hm-strategy-card.selected {
    border-color: var(--hm-accent);
    border-width: 2px;
    background: color-mix(in srgb, var(--hm-accent) 14%, var(--bs-tertiary-bg));
}
.hm-strategy-icon {
    width: 60px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Sets the inherited `color` so accent-tinted parts of the strategy
       icon SVG (which use `currentColor`) pick up the theme accent. The
       icon previously hardcoded #5EC6D8; switching to currentColor + this
       wrapper rule means changing :root --hm-accent in app.css propagates
       through every strategy preview without C# edits. */
    color: var(--hm-accent);
}
.hm-strategy-icon svg { display: block; }
.hm-strategy-label {
    font-size: 0.7rem;
    line-height: 1.15;
    text-align: center;
}

.hm-palette-modal { min-height: 240px; }

/* Discovery swatch grid — colour swatches the user toggles on/off */
/* Discovered colours card inside Palette modal */
.hm-discovery-card {
    background: var(--bs-tertiary-bg);
    border-color: var(--bs-border-color);
}
.hm-discovery-card .card-body {
    padding: 0.75rem 1rem;
}

.hm-discovery-swatch {
    width: 36px;
    height: 36px;
    border-radius: 6px;
    border: 2px solid transparent;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 0.85rem;
    text-shadow: 0 0 3px rgba(0, 0, 0, 0.6);
    transition: border-color 0.15s, box-shadow 0.15s;
    padding: 0;
}
.hm-discovery-swatch:hover {
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.25);
}
.hm-discovery-swatch.selected {
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.35);
}

/* Image setup modal — crop section and static preview fallback */
.hm-image-setup-crop {
    margin-bottom: 0.5rem;
}
.hm-image-setup-crop .hm-crop-container {
    /* Constrain height inside the modal so tall images don't push the form off-screen */
    max-height: 280px;
    display: flex;
    justify-content: center;
}
.hm-image-setup-crop .hm-crop-img {
    max-height: 280px;
    object-fit: contain;
}
.hm-image-setup-preview {
    text-align: center;
    margin-bottom: 0.75rem;
}
.hm-image-setup-preview img {
    max-width: 100%;
    max-height: 200px;
    border-radius: 6px;
    border: 1px solid var(--bs-border-color);
    object-fit: contain;
}

/* Build plate modal — same two-column pattern as shape modal */
.hm-buildplate-modal {
    display: grid;
    grid-template-columns: 300px 1fr;
    gap: 18px;
}
@media (max-width: 768px) {
    .hm-buildplate-modal { grid-template-columns: 1fr; }
}
.hm-buildplate-preview {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    padding: 14px;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-tertiary-bg);
    min-width: 0;
    overflow: hidden;
}
.hm-buildplate-preview svg {
    display: block;
    width: 100%;
    height: auto;
    max-width: 260px;
    max-height: 260px;
}
.hm-buildplate-controls { min-width: 0; }

/* Tiling mode picker */
/* Tiling-mode card layouts (May 2026 final shape):
   - .hm-tile-mode-card by itself = horizontal row (thumb left, body right).
     Used by the Auto-tile control card at the top of PlateLayoutModal.
   - .hm-tile-mode-card-vertical override = thumb on top, label + desc
     centred underneath. Used by the strategy cards (AMS / By colour)
     in a 2-column grid below the Auto-tile card. */
.hm-tile-mode-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
}
.hm-tile-mode-stack {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.hm-tile-mode-card {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    cursor: pointer;
    text-align: left;
    transition: background 100ms, border-color 100ms, opacity 150ms;
}
/* Vertical variant — thumb on top, body below, content centred. Reverts
   to the original "info underneath" layout for narrower 2-column cards. */
.hm-tile-mode-card-vertical {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 8px;
    padding: 14px 10px 12px;
}
/* Disabled state — when AutoTile is OFF, the strategy cards stay visible
   but fade so the user can see the options without being able to click.
   Cursor flips to "not-allowed", interaction (hover/click) suppressed. */
.hm-tile-mode-card[disabled],
.hm-tile-mode-card:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}
.hm-tile-mode-grid-disabled {
    /* Group-level fade to reinforce the disabled state at a glance.
       Combined with per-button opacity it sits at ~0.5 × 0.85 ≈ 0.43 so
       the cards stay readable as outlines but obviously inactive. */
    opacity: 0.85;
}
.hm-tile-mode-card:hover {
    border-color: var(--hm-accent);
    background: color-mix(in srgb, var(--hm-accent) 6%, var(--bs-tertiary-bg));
}
.hm-tile-mode-card.selected {
    border-color: var(--hm-accent);
    border-width: 2px;
    padding: 9px 11px;  /* compensate for thicker border */
    background: color-mix(in srgb, var(--hm-accent) 14%, var(--bs-tertiary-bg));
}
/* Auto-tile card variant — has a toggle inside, so don't react to clicks
   on the card body (only the switch toggles state). Keeps default cursor
   so users don't think the whole card is clickable. */
.hm-tile-mode-card-toggle {
    cursor: default;
}
.hm-tile-mode-card-toggle:hover {
    border-color: var(--bs-border-color);
    background: var(--bs-tertiary-bg);
}
.hm-tile-mode-card-body {
    flex: 1 1 auto;
    min-width: 0;
}
.hm-tile-mode-thumb {
    width: 60px;
    height: 60px;
    flex: 0 0 60px;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Larger thumbnail for the vertical strategy cards — bigger graphic
   carries the visual weight when the card content is centred-stacked. */
.hm-tile-mode-thumb-lg {
    width: 84px;
    height: 84px;
    flex: 0 0 84px;
}
.hm-tile-mode-thumb-lg svg {
    width: 84px;
    height: 84px;
}
.hm-tile-mode-thumb svg { display: block; }
.hm-tile-mode-label {
    font-size: 0.85rem;
    font-weight: 600;
}
.hm-tile-mode-desc {
    font-size: 0.7rem;
    line-height: 1.3;
    color: var(--bs-secondary-color);
}
.hm-tile-estimate {
    padding: 6px 10px;
    background: var(--bs-tertiary-bg);
    border-radius: 4px;
    border-left: 3px solid var(--hm-accent);
}

/* Collapsible filament library accordion */
.hm-library-accordion {
    margin-top: 12px;
}
.hm-library-accordion > .hm-library-summary {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.hm-library-accordion > .hm-library-summary::-webkit-details-marker { display: none; }
.hm-library-accordion > .hm-library-summary::before {
    content: "▸";
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
    transition: transform 120ms;
    display: inline-block;
}
.hm-library-accordion[open] > .hm-library-summary::before {
    transform: rotate(90deg);
}
.hm-library-accordion[open] > .hm-library-summary {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    border-bottom: none;
}
.hm-library-accordion[open] > .card {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}


/* Safe zone compass layout — top, middle (left/centre/right), bottom */
.hm-safezone {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 6px 8px;
    border: 1px dashed var(--bs-border-color);
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
}
.hm-sz-top, .hm-sz-bottom {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
}
.hm-sz-middle {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 6px;
}
.hm-sz-side {
    display: flex;
    align-items: center;
    gap: 6px;
}
.hm-sz-side:last-child { justify-content: flex-end; }
.hm-sz-label {
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
    white-space: nowrap;
}
.hm-sz-num {
    width: 56px;
    text-align: center;
    padding: 2px 4px;
    height: calc(1.5em + 4px);
}
.hm-sz-centre {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 6px 10px;
    border: 1px dashed var(--bs-border-color);
    border-radius: 4px;
    background: color-mix(in srgb, var(--hm-accent) 8%, transparent);
    line-height: 1.2;
    min-width: 92px;
}
/* Older svg rule — kept for any cards outside the new preview wrapper; new cards
   use .hm-plate-preview > svg which wins via specificity. */
.hm-plate-card svg:not(.hm-plate-preview > svg) {
    display: block;
    background: var(--hm-canvas-bg);
    border-radius: 4px;
}

.hm-plate-export-card {
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    padding: 12px;
    background: var(--bs-tertiary-bg);
}

.hm-plate-export-card.hm-plate-over-ams {
    border-color: var(--bs-warning);
    border-width: 2px;
    background: color-mix(in srgb, var(--bs-warning) 6%, var(--bs-tertiary-bg));
}

.hm-colour-toggle {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 3px 6px;
    border-radius: 4px;
    transition: background 120ms;
}
.hm-colour-toggle:hover { background: var(--bs-secondary-bg); }
.hm-colour-toggle input[type="checkbox"] {
    margin: 0 2px 0 0;
    cursor: pointer;
}

/* ================= Print guide ================= */
/* On-screen Filament-legend card uses Bootstrap theme variables so it
   reads as a native panel in either light OR dark mode (was hard-coded
   white background which clashed with dark-mode chrome). The container,
   legend rows, and swatch borders all pick their colours up from the
   active theme — no per-theme override needed. */
.hm-print-legend {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    padding: 12px 16px;
    border-radius: 6px;
    margin-bottom: 16px;
    border: 1px solid var(--bs-border-color);
}
.hm-legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 8px;
    background: var(--bs-tertiary-bg);
    border-radius: 4px;
}
.hm-legend-swatch {
    display: inline-block;
    width: 20px; height: 20px;
    border-radius: 3px;
    border: 1px solid var(--bs-border-color);
    flex-shrink: 0;
}

.hm-print-area {
    /* Theme-aware backdrop — was hard-coded #555 which clashed with light mode.
       --bs-tertiary-bg is light grey in light mode, dark grey in dark mode, so
       the page contrasts with the surround in either theme. */
    background: var(--bs-tertiary-bg);
    padding: 16px;
    border-radius: 6px;
    /* On screen we shrink-to-fit (see screen rules below), so no horizontal
       scroller is needed — clip anything that exceeds, just in case. */
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 0;
    /* Fill remaining vertical space so guide/explode canvases stretch. */
    flex: 1 1 0;
    min-height: 0;
}
/* Guide + exploded modes: stretch children to fill the full card width
   so the interactive canvas (SVG / Three.js) uses all available space.
   Summary / plate-sheets keep centre-aligned page mockups. */
.hm-print-area--guide,
.hm-print-area--exploded {
    align-items: stretch;
}
.hm-print-page {
    background: white;
    color: #111;
    flex: 0 0 auto;
    margin: 0 auto 16px auto;
    padding: 10mm;
    box-shadow: 0 2px 8px rgba(0,0,0,0.35);
    display: flex;
    flex-direction: column;
    page-break-after: always;
}
/* The .hm-print-page emulates a printed page on-screen — should always look
   like white paper with dark ink, regardless of the user's theme. Without
   this, dark-mode Bootstrap variables (--bs-table-bg, --bs-body-color, etc.)
   bleed into table cells inside the page, giving an unreadable dark-on-dark
   "printed page" preview. The override resets the variables to light values
   ONLY inside .hm-print-page subtrees, leaving the rest of the dark-mode UI
   untouched. */
.hm-print-page,
.hm-print-page .card,
.hm-print-page .table {
    --bs-body-bg: #fff;
    --bs-body-color: #111;
    --bs-tertiary-bg: #fff;
    --bs-secondary-bg: #fff;
    --bs-emphasis-color: #111;
    --bs-secondary-color: #555;
    --bs-border-color: #999;
    --bs-table-bg: transparent;
    --bs-table-color: #111;
    --bs-table-striped-bg: rgba(0, 0, 0, 0.04);
    --bs-table-border-color: #ccc;
    --bs-card-bg: #fff;
    --bs-card-color: #111;
    color-scheme: light;
}
.hm-print-page * { color: inherit; }
.hm-print-page .text-secondary { color: #555 !important; }
.hm-print-page .table th { color: #444; }
.hm-print-page .table td { color: #111; }
/* Screen-only shrink-to-fit. The inline width/height (in mm) is what print
   uses; on screen we override with width:100% capped at the natural mm width
   so we never upscale, and use aspect-ratio to keep the page proportions. */
@media screen {
    .hm-print-page {
        width: 100% !important;
        max-width: var(--hm-page-natural-mm, 100%);
        height: auto !important;
        aspect-ratio: var(--hm-page-aspect, auto);
    }
}
.hm-print-page:last-child { page-break-after: auto; }
.hm-print-page-header,
.hm-print-page-footer {
    flex: 0 0 auto;
    color: #333;
    padding: 2mm 0;
    font-size: 10pt;
}

/* Printed-only: hide everything except the print area and legend */
@media print {
    @page {
        margin: 0;
    }
    /* Force light colours regardless of the user's chosen theme. Dark-mode
       Bootstrap variables stay set on `[data-bs-theme="dark"]`, which would
       leak dark backgrounds and light text into the printout otherwise.
       Override the variables back to light values for print, and tell the
       browser to render with a light colour-scheme so form-control / scrollbar
       chrome (rare in print but cheap to handle) also goes light. */
    html { color-scheme: light !important; }
    [data-bs-theme="dark"],
    [data-bs-theme="dark"] body,
    [data-bs-theme="dark"] .card,
    [data-bs-theme="dark"] .table {
        --bs-body-bg: #fff !important;
        --bs-body-color: #111 !important;
        --bs-tertiary-bg: #fff !important;
        --bs-secondary-bg: #fff !important;
        --bs-emphasis-color: #111 !important;
        --bs-secondary-color: #555 !important;
        --bs-border-color: #999 !important;
        --bs-table-bg: transparent !important;
        --bs-table-color: #111 !important;
        --bs-table-border-color: #999 !important;
        --bs-card-bg: #fff !important;
        --bs-card-color: #111 !important;
        background: white !important;
        color: #111 !important;
    }
    html, body {
        height: auto !important;
        overflow: visible !important;
        background: white !important;
        color: #111 !important;
    }
    /* Print isolation strategy (see interop.js `printIsolated`):
       At print time, JS lifts .hm-print-area out of the deeply-nested shell
       chain (.hm-shell-v2 → .hm-shell-body → .hm-canvas-wrap → .hm-canvas →
       ...) and reparents it as a direct child of <body>, then adds the
       `hm-printing-isolated` class to body for the duration. After the print
       dialog closes (afterprint event, or 500ms safety timer) the element is
       restored.
       Pure-CSS approaches kept failing because the parent .hm-canvas is a
       scroll-clipped viewport (`overflow: auto`, height: 100%) and CSS
       overrides for nested layouts are easy to miss layers on. Reparenting
       sidesteps every parent's layout entirely.
       In this isolated state we only need to hide everything else. */
    body.hm-printing-isolated > *:not(.hm-print-area):not(script):not(style) {
        display: none !important;
    }
    body.hm-printing-isolated .hm-print-area {
        background: white !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        display: block !important;
        overflow: visible !important;
        width: 100% !important;
        position: static !important;
    }
    /* Legend is now inside .hm-print-area — give it a page break so
       the first assembly page starts on fresh paper. */
    body.hm-printing-isolated .hm-print-legend {
        page-break-after: always;
        break-after: page;
        border: none !important;
    }
    /* Default (legacy) path — when window.print() is called without isolation
       (fallback if the JS module fails to load): hide non-print chrome by
       class. Won't always render correctly with the nested shell, but it's
       better than nothing. */
    body:not(.hm-printing-isolated) > * { display: none !important; }
    body:not(.hm-printing-isolated) > main,
    body:not(.hm-printing-isolated) > .d-flex { display: block !important; }
    .d-print-none { display: none !important; }
    .hm-print-page {
        box-shadow: none !important;
        margin: 0 !important;
        padding: 10mm !important;
        /* Both legacy (page-break-after) and modern (break-after) so older
           Chromium and modern Firefox both honour the page split. Without
           this, multi-page Plate Sheets / multi-plate selections would
           collapse onto a single physical page in the print output. */
        page-break-after: always !important;
        break-after: page !important;
        /* Plain block layout in print — flex column + tables interact with
           page-break placement in unpredictable ways across browsers. */
        display: block !important;
        overflow: visible !important;
    }
    .hm-print-page:last-child {
        page-break-after: auto !important;
        break-after: auto !important;
    }
    /* Tables inside print pages: don't let them grab the page-break logic
       for themselves. Explicit `auto` overrides browsers' implicit
       `avoid` on table elements. */
    .hm-print-page table,
    .hm-print-page tbody,
    .hm-print-page tr,
    .hm-print-page td,
    .hm-print-page th {
        page-break-inside: auto !important;
        break-inside: auto !important;
    }
    .hm-print-area {
        background: white !important;
        padding: 0 !important;
        border: none !important;
        /* Out of flex layout in print — flex column + align-items: center can
           confuse some browsers' page-break placement. Plain block stacking
           ensures each .hm-print-page child gets its own physical page. */
        display: block !important;
    }
}

/* Summary mode — table-based one-page layout. Three bordered sections so
   Project overview / Filament usage / Plates read as discrete cards. */
.hm-summary-body { color: #111; padding: 0 4mm; flex: 1 1 auto; min-height: 0; }
.hm-summary-body h2,
.hm-summary-body h3 { color: #111; }
.hm-summary-section {
    border: 1px solid #bbb;
    border-radius: 2mm;
    padding: 3mm 4mm 3mm 4mm;
    margin-bottom: 4mm;
    background: #fff;
}
.hm-summary-section:last-child { margin-bottom: 0; }
.hm-summary-section-title {
    margin: 0 0 2mm 0;
    padding-bottom: 1.5mm;
    border-bottom: 1px solid #ddd;
    color: #111;
    display: flex;
    align-items: center;
    gap: 1.5mm;
}
.hm-summary-section-title i { color: #666; font-size: 0.95em; }

.hm-summary-meta th,
.hm-summary-filaments th,
.hm-summary-plates th {
    color: #444;
    font-weight: 600;
    font-size: 8.5pt;
    white-space: nowrap;
    vertical-align: middle;
    padding-top: 7px;
    padding-bottom: 7px;
}
.hm-summary-meta td,
.hm-summary-filaments td,
.hm-summary-plates td {
    color: #111;
    font-size: 9pt;
    vertical-align: middle;
    padding-top: 7px;
    padding-bottom: 7px;
}
/* Two label/value columns per row in the project-overview meta. Auto-sized
   `<th>` cells (no fixed 110px width) + `white-space: nowrap` keeps the
   labels on a single line — fixes "Estimated print time" wrapping into two
   lines on narrow viewports. The td values stretch to fill remaining width. */
.hm-summary-meta th { width: 1%; padding-right: 6px; }
.hm-summary-meta td { width: 49%; padding-right: 12px; }
.hm-summary-meta td:last-child { padding-right: 0; }

/* Plates table — Colours column lists each filament on its own row
   (swatch + brand/name) so multi-filament plates stay readable. */
.hm-summary-plate-colours {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.hm-summary-plate-colour-row {
    display: flex;
    align-items: center;
    gap: 6px;
}

.hm-summary-swatch {
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: 3px;
    border: 1px solid #555;
    vertical-align: middle;
}

/* Print-area wrapper — relative positioning so the floating "Include plates"
   panel can absolute-position to its left edge, mirroring the 2D Cell Painter
   layer panel. Without --with-panel, behaves as a plain pass-through. */
.hm-print-area-wrap {
    position: relative;
    /* Fill available vertical space from .hm-canvas parent. */
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

/* ── Assembly guide floating layer card ────────────────
   Mirrors the Mount workspace's .hm-construct-summary-card:
   absolute-positioned, glass-blur background, collapsible to
   an icon. Sits top-left of the print area. d-print-none
   hides it from printouts. */
.hm-guide-layers-card {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 5;
    width: 230px;
    padding: 10px 12px 12px;
    background: color-mix(in srgb, var(--bs-body-bg) 90%, transparent);
    border: 1px solid var(--bs-border-color);
    border-radius: 10px;
    backdrop-filter: blur(6px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    opacity: 0.9;
    transition: opacity 150ms ease;
    pointer-events: auto;
}
.hm-guide-layers-card:hover { opacity: 1; }
.hm-guide-layers-card.collapsed {
    width: auto;
    padding: 4px 6px;
}
.hm-guide-layers-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-guide-layers-card.collapsed .hm-guide-layers-header {
    margin-bottom: 0;
    padding-bottom: 0;
    border-bottom: 0;
}
.hm-guide-layers-title {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
    flex: 1 1 auto;
}
.hm-guide-layers-toggle {
    background: transparent;
    border: 0;
    color: var(--bs-body-color);
    font-size: 1rem;
    padding: 4px 8px;
    line-height: 1;
    cursor: pointer;
    border-radius: 4px;
    flex: 0 0 auto;
}
.hm-guide-layers-toggle:hover { background: rgba(128,128,128,0.18); }
.hm-guide-layers-card.collapsed .hm-guide-layers-toggle {
    padding: 6px 8px;
    font-size: 1.05rem;
    color: var(--bs-primary);
}
.hm-guide-layers-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.hm-guide-layer-row {
    display: grid;
    grid-template-columns: 18px 1fr 26px;
    align-items: center;
    gap: 6px;
    padding: 3px 4px;
    border-radius: 4px;
}
.hm-guide-layer-row:hover { background: var(--bs-tertiary-bg); }
.hm-guide-layer-icon {
    font-size: 0.85rem;
    color: var(--bs-secondary-color);
}
.hm-guide-layer-name {
    font-size: 0.8rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hm-guide-layer-eye {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    color: var(--bs-body-color);
    cursor: pointer;
    font-size: 0.85rem;
}
.hm-guide-layer-eye:hover {
    background: var(--bs-secondary-bg);
    border-color: var(--bs-border-color);
}
/* Sub-row for sliders — label + range input. Reduced left indent
   to prevent label truncation in narrow floating cards. */
.hm-guide-layer-sub {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 2px 4px 2px 4px;
}
.hm-guide-layer-sub .hm-guide-layer-name {
    flex: 0 0 auto;
    white-space: nowrap;
}
.hm-guide-layer-slider {
    flex: 1 1 auto;
    min-width: 0;
}
/* Mobile: collapse layer card to bottom of print area */
@media (max-width: 768px) {
    .hm-guide-layers-card {
        position: static;
        width: auto;
        margin-bottom: 12px;
        opacity: 1;
    }
}

/* ── Assembly guide zoom bar + pannable canvas ──────────── */
.hm-guide-zoom-bar {
    display: flex;
    align-items: center;
    gap: 6px;
    justify-content: center;
    padding: 6px 0 2px;
}
.hm-guide-zoom-level {
    font-size: 0.78rem;
    font-weight: 600;
    min-width: 44px;
    text-align: center;
    color: var(--bs-body-color);
    user-select: none;
}
.hm-guide-canvas {
    overflow: hidden;
    position: relative;
    border-radius: 6px;
    /* Fill all remaining vertical space in the print-area column. */
    flex: 1 1 0;
    min-height: 300px;
    /* Prevent text selection while dragging. */
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
}
/* In print mode, hide the interactive canvas (print pages render separately). */
@media print {
    .hm-guide-canvas {
        display: none !important;
    }
}

/* ── Exploded 3D plate view (Three.js) ────────────────── */
/* Floating layer palette for the exploded view — same glass-blur card
   pattern as .hm-guide-layers-card but on the left of the 3D canvas. */
.hm-explode-layers-card {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 5;
    width: 220px;
    padding: 10px 12px 12px;
    background: color-mix(in srgb, var(--bs-body-bg) 90%, transparent);
    border: 1px solid var(--bs-border-color);
    border-radius: 10px;
    backdrop-filter: blur(6px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    opacity: 0.92;
    transition: opacity 150ms ease;
    pointer-events: auto;
    max-height: calc(100% - 60px);
    overflow-y: auto;
}
.hm-explode-layers-card:hover { opacity: 1; }
.hm-explode-layers-card.collapsed {
    width: auto;
    padding: 6px;
}
.hm-explode-layers-card.collapsed .hm-guide-layers-list,
.hm-explode-layers-card.collapsed .hm-guide-layers-title,
.hm-explode-layers-card.collapsed .hm-guide-layer-eye {
    display: none;
}
.hm-explode-layer-swatches {
    display: flex;
    gap: 2px;
    align-items: center;
    min-width: 18px;
}
.hm-explode-view-actions {
    display: flex;
    justify-content: center;
    padding: 4px 0;
    gap: 8px;
}
.hm-explode-canvas {
    width: 100%;
    /* Fill all remaining vertical space — not a fixed height. */
    flex: 1 1 0;
    min-height: 400px;
    border-radius: 6px;
    overflow: hidden;
    background: #1a1d21;
}
@media (max-width: 768px) {
    .hm-explode-layers-card {
        position: static;
        width: auto;
        margin-bottom: 8px;
        opacity: 1;
    }
}
/* Print-only pages — hidden on screen, shown by printIsolated */
@media screen {
    .hm-guide-print-pages { display: none; }
}
@media print {
    .hm-guide-print-pages { display: block; }
}
.hm-print-area-wrap--with-panel .hm-print-area {
    /* Push print pages right so the floating panel doesn't overlap them. The
       panel is 220px wide + 12px gap. */
    padding-left: 244px;
}
@media (max-width: 900px) {
    /* On narrow viewports, stack the panel above the print area instead of
       floating left — frees up the limited screen width for the actual pages. */
    .hm-print-area-wrap--with-panel .hm-print-area { padding-left: 0; }
    .hm-platesheet-layers {
        position: static !important;
        width: auto !important;
        bottom: auto !important;
        margin-bottom: 12px;
    }
}

.hm-platesheet-layers {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 10;
    width: 220px;
    background: color-mix(in srgb, var(--bs-body-bg) 75%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.hm-platesheet-layers-header {
    flex: 0 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--bs-secondary-color);
    padding: 8px 10px;
    border-bottom: 1px solid var(--bs-border-color);
}
/* Title stays on one line — uppercase letterspacing makes "Include plates"
   tight; without nowrap a narrow panel would split it across two rows
   pushing the eye toggle down. */
.hm-platesheet-layers-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1 1 auto;
    min-width: 0;
}
.hm-platesheet-layers-toggleall-btn {
    flex: 0 0 auto;
}
.hm-platesheet-layers-list {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 6px 8px 8px 8px;
    display: flex;
    flex-direction: column;
    gap: 3px;
}
/* Per-plate row: swatches (1-4 small dots showing the plate's filaments) +
   "Plate N" label + cell count + eye toggle. Reuses .hm-layer-row from the
   cell-painter so the visual is consistent. */
.hm-platesheet-layers-swatches {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 2px;
}
.hm-platesheet-layers-swatches .hm-summary-swatch { border-width: 1px; }

/* Plate-sheet mode — 4-up grid (2×2) of compact plate cards per page. Each
   card has its own border so they read as discrete sections at a glance.
   Compact font sizes (7-8pt) keep the stats on a single line per row, the
   plate preview gets ~60% of the card height. */
.hm-platesheet-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 4mm;
    flex: 1 1 auto;
    min-height: 0;
    padding: 2mm;
    color: #111;
}
.hm-platesheet-card {
    border: 1px solid #999;
    border-radius: 1.5mm;
    padding: 2.5mm 3mm 3mm 3mm;
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    color: #111;
    background: #fff;
}
.hm-platesheet-card-header {
    display: flex;
    align-items: baseline;
    /* space-between pins the plate label to the left edge and the at-a-glance
       icon summary to the right edge. Gap acts as a minimum separator if the
       label gets long. */
    justify-content: space-between;
    gap: 4mm;
    border-bottom: 1px solid #ccc;
    padding-bottom: 1mm;
    margin-bottom: 1.5mm;
    flex: 0 0 auto;
}
.hm-platesheet-card-header strong {
    font-size: 10pt;
    color: #111;
    flex: 0 0 auto;
}
.hm-platesheet-card-sub {
    font-size: 7.5pt;
    color: #555;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 0 1 auto;
    text-align: right;
}
/* Stats: icon + value pairs in a single non-wrapping row. Icons mirror the
   chip vocabulary used on-screen (aspect-ratio, pie-chart, puzzle, clock). */
.hm-platesheet-card-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 2mm 3mm;
    font-size: 7.5pt;
    color: #111;
    margin-bottom: 1.5mm;
    flex: 0 0 auto;
}
.hm-platesheet-card-stats span {
    display: inline-flex;
    align-items: center;
    gap: 0.8mm;
    flex: 0 0 auto;
    white-space: nowrap;
}
.hm-platesheet-card-stats i { color: #555; font-size: 8pt; }
.hm-platesheet-card-header .hm-platesheet-card-sub i {
    color: #888;
    font-size: 7.5pt;
    margin-right: 0.4mm;
}
.hm-platesheet-card-fil-count i {
    font-size: 6.5pt;
    color: #888;
    margin-right: 0.3mm;
}
.hm-platesheet-card-fils {
    font-size: 7pt;
    color: #111;
    margin-bottom: 1.5mm;
    flex: 0 0 auto;
    /* Cap to a couple of rows so a wild palette doesn't blow up the card. */
    max-height: 12mm;
    overflow: hidden;
}
.hm-platesheet-card-fil {
    display: flex;
    align-items: center;
    gap: 1.5mm;
    line-height: 1.4;
}
.hm-platesheet-card-fil .hm-summary-swatch {
    width: 8px;
    height: 8px;
    border-radius: 1.5px;
    flex: 0 0 auto;
}
.hm-platesheet-card-fil-name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hm-platesheet-card-fil-count { color: #555; flex: 0 0 auto; }
.hm-platesheet-card-svg {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    overflow: hidden;
}
.hm-platesheet-card-svg svg {
    max-width: 100%;
    max-height: 100%;
    width: auto !important;
    height: auto;
    display: block;
}

/* Print rules: ensure summary / plate-sheet pages also flow as separate pages. */
@media print {
    .hm-summary-body,
    .hm-platesheet-body { color: #111 !important; }
    .hm-summary-meta th,
    .hm-summary-filaments th,
    .hm-summary-plates th { color: #444 !important; }

    /* Tighter typography for the printed Project summary — the on-screen
       sizes (8.5pt headers / 9pt body) leave too much whitespace once the
       page is on physical paper. Knock everything down a notch so a fuller
       summary fits cleanly within an A4 portrait page. */
    .hm-summary-body h2 { font-size: 14pt !important; margin-bottom: 8pt !important; }
    .hm-summary-section-title { font-size: 10pt !important; margin-bottom: 4pt !important; }
    .hm-summary-meta th,
    .hm-summary-filaments th,
    .hm-summary-plates th {
        font-size: 7.5pt !important;
        padding-top: 4px !important;
        padding-bottom: 4px !important;
    }
    .hm-summary-meta td,
    .hm-summary-filaments td,
    .hm-summary-plates td {
        font-size: 8pt !important;
        padding-top: 4px !important;
        padding-bottom: 4px !important;
    }
    .hm-summary-section { padding: 6pt 10pt !important; margin-bottom: 8pt !important; }
}

/* ================= Interactive crop tool ================= */
/* Container positions the overlay directly on top of the image. Image uses
   `display: block` so there's no inline whitespace offset, and the SVG uses
   absolute positioning matching the image's exact bounds. */
.hm-crop-container {
    position: relative;
    display: inline-block;
    max-width: 100%;
    border-radius: 8px;
    overflow: hidden;
    background: #000;
    line-height: 0;  /* kill baseline gap between inline image and svg */
}
.hm-crop-img {
    display: block;
    max-width: 100%;
    height: auto;
    user-select: none;
    -webkit-user-drag: none;
}
.hm-crop-overlay {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    /* Prevent the browser from scrolling / zooming while the user is drawing a crop. */
    touch-action: none;
    cursor: crosshair;
}
/* ================= End crop tool ================= */

/* ================= Depth painter ================= */
.hm-depth-panel {
    max-width: 100%;
}
.hm-depth-tools {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-depth-canvas-wrap {
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    overflow: hidden;
    background: #222;
    position: relative;
}
.hm-depth-canvas {
    width: 100%;
    height: 100%;
    display: block;
    cursor: crosshair;
    /* touch-action is set inline via JS to keep scroll behaviour right on mobile */
}
/* ================= End depth painter ================= */

/* ================= Clickable plate cards ================= */
.hm-plate-card-clickable,
.hm-plate-preview-clickable {
    cursor: pointer;
    transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
}
.hm-plate-card-clickable:hover {
    transform: translateY(-1px);
    border-color: var(--hm-accent);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
}
.hm-plate-card-clickable:focus-visible,
.hm-plate-preview-clickable:focus-visible {
    outline: 2px solid var(--hm-accent);
    outline-offset: 2px;
}
.hm-plate-preview-clickable:hover {
    /* In the export panel's horizontal layout we don't lift the preview — just hint
       with a subtle border/outline to avoid nudging the neighbouring controls. */
    box-shadow: 0 0 0 2px var(--hm-accent);
}

/* Plate-details modal preview area */
.hm-plate-details-preview {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 300px;
    padding: 12px;
    background: var(--bs-tertiary-bg);
    border-radius: 8px;
    border: 1px solid var(--bs-border-color);
}
/* ================= End clickable plate cards ================= */

/* ================= Two-line chip variant ================= */
/* Used for filament chips that show name + material stacked. Overrides the single-line
   ellipsis truncation so the second line can appear beneath. Maintains consistent icon
   alignment by using flex align-items: flex-start for the icon and letting the text
   stack expand vertically. */
.hm-plate-chip.hm-chip-two-line {
    align-items: flex-start;
    line-height: 1.25;
    padding: 5px 8px;
}
.hm-plate-chip.hm-chip-two-line .hm-chip-icon {
    margin-top: 2px;  /* Nudge the colour swatch to align with the first line of text */
}
.hm-plate-chip .hm-chip-text-stack {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
    gap: 1px;
}
.hm-plate-chip .hm-chip-text-stack .hm-chip-text {
    /* Stacked chips allow wrapping — longer filament names read as a second line within
       the name area itself; material sits below. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hm-plate-chip .hm-chip-subtext {
    font-size: 0.68rem;
    color: var(--bs-secondary-color);
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* ================= End two-line chip ================= */

/* ================= FilamentPicker ================= */
.hm-filament-picker {
    position: relative;
    display: inline-block;
    width: 100%;
    max-width: 320px;
}
/* Fluid variant — drops the max-width so the picker fills its container.
   Wrap the FilamentPicker in <div class="hm-fp-fluid"> when it sits in a
   half-row column or other context where the 320px cap leaves dead space.
   Used by the Auxiliary filament section in PaletteAmsModal. */
.hm-fp-fluid > .hm-filament-picker {
    max-width: none;
}
.hm-filament-picker-trigger {
    display: flex;
    align-items: center;
    gap: 8px;
    text-align: left;
    width: 100%;
    cursor: pointer;
    /* Keep the form-select look but override height so the stacked-text label fits. */
    padding: 4px 28px 4px 8px;
    min-height: 38px;
    line-height: 1.25;
}
.hm-filament-picker-swatch {
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    border-radius: 4px;
    border: 1px solid rgba(0, 0, 0, 0.2);
    /* Subtle inner shadow helps distinguish pale colours against pale backgrounds. */
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
    position: relative;
    overflow: hidden;
}
/* Translucent overlay handled by the shared .hm-swatch-translucent class
   (defined in the misc section higher up). The picker swatch is smaller,
   but the universal stripe pattern reads fine across sizes. */
.hm-filament-picker-swatch-empty {
    background: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 3px,
        rgba(128, 128, 128, 0.35) 3px,
        rgba(128, 128, 128, 0.35) 6px
    );
}
.hm-filament-picker-text {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    line-height: 1.2;
}
.hm-filament-picker-name {
    font-size: 0.85rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hm-filament-picker-material {
    font-size: 0.68rem;
    color: var(--bs-secondary-color);
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* SKU chip rendered after the filament name in the picker's selected
   display + library rows. Code-styled, muted — reads as a technical
   identifier rather than competing with the name. May 2026 — Bambu
   SKUs are useful when re-ordering / loading the right reel. */
.hm-filament-picker-sku {
    font-size: 0.7em;
    color: var(--bs-secondary-color);
    margin-left: 4px;
    opacity: 0.85;
}
/* Backdrop (legacy) — no longer used since JS handles outside-click dismissal globally.
   Kept as a class in case older code paths refer to it; new popovers don't render it. */
.hm-filament-picker-backdrop {
    display: none;
}
/* The pop-out menu — `position: fixed` means it's anchored to the viewport, not any
   scrolling/overflow:hidden parent. JS (interop.js::anchorPopover) sets top/left/width
   on open and keeps them in sync on scroll/resize. This is essential because the picker
   is used inside cards that clip overflow, and a regular `absolute` positioning would
   cut the menu off at the card edge (which is what you saw before). */
.hm-filament-picker-menu {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1050;
    max-height: 360px;
    overflow-y: auto;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
    padding: 4px;
}
.hm-filament-picker-option {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 6px 8px;
    background: transparent;
    border: none;
    border-radius: 4px;
    text-align: left;
    cursor: pointer;
    color: var(--bs-body-color);
}
.hm-filament-picker-option:hover {
    background: var(--bs-tertiary-bg);
}
.hm-filament-picker-option.selected {
    background: color-mix(in srgb, var(--hm-accent) 18%, transparent);
    border-left: 2px solid var(--hm-accent);
    padding-left: 6px;
}

/* Generic "action dropdown" — used by the Match-colours dropdown in the
   Palette modal (May 2026). Trigger is a regular Bootstrap button; the menu
   below pops down with absolute positioning, anchored to the trigger's
   right edge so it doesn't run off the modal's right side. Each item is a
   row with an icon on the left and a label + description stack on the
   right — denser than a separate row of buttons but more readable than a
   plain <select>.

   No JS framework — open/closed state lives in component state, the menu
   is rendered conditionally on a boolean. Items close the menu before
   running their action so the user sees their click acknowledged. */
.hm-action-dropdown {
    position: relative;
    display: inline-block;
}
/* Full-width block variant — used when the dropdown should sit on its own
   row inside a form (e.g. the heights-strategy picker in CellShapeModal).
   Inline-block + Bootstrap's w-100 caused width-resolution flakiness in
   testing; the explicit `display: block` here is more predictable. */
.hm-action-dropdown-block {
    display: block;
    width: 100%;
}
.hm-action-dropdown-trigger {
    display: inline-flex;
    align-items: center;
}
/* Form-control-like styling for the strategy trigger in CellShapeModal —
   matches the visual weight of nearby form-control / form-select inputs but
   without their built-in chevron (we render our own next to the icon + label
   content for a richer composed trigger). */
.hm-strategy-trigger {
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    padding: 6px 10px;
    color: var(--bs-body-color);
    cursor: pointer;
    text-align: left;
    font-size: 0.875rem;
    line-height: 1.5;
}
.hm-strategy-trigger:hover {
    border-color: color-mix(in srgb, var(--hm-accent) 60%, var(--bs-border-color));
}
.hm-action-dropdown.open .hm-strategy-trigger {
    border-color: var(--hm-accent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--hm-accent) 25%, transparent);
}
.hm-strategy-trigger-label {
    font-weight: 500;
}
.hm-action-dropdown-menu {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    z-index: 1050;
    min-width: 320px;
    max-width: 400px;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
    padding: 4px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
/* Filter row inside an action dropdown — sits at the top, divider below
   separates it from the action items. Used by the Match-colours dropdown
   in PalettePanel for the "Filament type" select. */
.hm-action-dropdown-filter {
    padding: 8px 10px 10px;
    border-bottom: 1px solid var(--bs-border-color);
    margin-bottom: 4px;
}
.hm-action-dropdown-filter .form-label {
    color: var(--bs-secondary-color);
}
.hm-action-dropdown-item {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: 100%;
    padding: 8px 10px;
    background: transparent;
    border: none;
    border-radius: 4px;
    text-align: left;
    cursor: pointer;
    color: var(--bs-body-color);
}
.hm-action-dropdown-item > i.bi {
    flex: 0 0 auto;
    font-size: 1.1rem;
    color: var(--hm-accent);
    margin-top: 2px;
}
.hm-action-dropdown-item:hover {
    background: var(--bs-tertiary-bg);
}
.hm-action-dropdown-item-label {
    font-weight: 600;
    font-size: 0.875rem;
    line-height: 1.3;
}
.hm-action-dropdown-item-desc {
    font-size: 0.75rem;
    line-height: 1.35;
    color: var(--bs-secondary-color);
    margin-top: 2px;
}
/* Custom material-filter dropdown — replaces the native <select> in
   the Match Colours dropdown's filter row so catalogue-only items can
   carry a stylised pill badge (native <option> doesn't accept HTML).
   Trigger looks like a Bootstrap form-select; menu drops down below
   the trigger when open. Same hover / outside-click hygiene as
   `.hm-action-dropdown` but locally scoped to avoid disturbing the
   parent dropdown's state. May 2026 — option D phase 2. */
.hm-mat-filter {
    position: relative;
}
.hm-mat-filter-trigger {
    width: 100%;
    cursor: pointer;
}
.hm-mat-filter-trigger:focus { outline: none; box-shadow: 0 0 0 0.2rem rgba(13,110,253,0.15); }
.hm-mat-filter-menu {
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    z-index: 1080;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    max-height: 280px;
    overflow-y: auto;
}
.hm-mat-filter-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 6px 10px;
    border: 0;
    background: transparent;
    font-size: 0.875rem;
    text-align: left;
    color: var(--bs-emphasis-color);
}
.hm-mat-filter-item:hover {
    background: var(--bs-tertiary-bg);
}
.hm-mat-filter-pill {
    display: inline-block;
    padding: 1px 7px;
    margin-left: 8px;
    border: 1px solid var(--bs-border-color);
    border-radius: 999px;
    background: var(--bs-tertiary-bg);
    color: var(--bs-secondary-color);
    font-size: 0.62rem;
    font-weight: 500;
    line-height: 1.3;
    vertical-align: middle;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    /* `<sup>` element provides the raised baseline; reset its default
       baseline shift since we're emulating it visually with a pill. */
    top: 0;
}
/* Green "from library" variant of the pill — visually distinguishes
   materials the user already owns vs catalogue-only suggestions when
   both toggles are active. Bootstrap success palette via theme tokens
   so dark-mode contrast stays correct. */
.hm-mat-filter-pill-library {
    background: var(--bs-success-bg-subtle, #d1e7dd);
    color: var(--bs-success-text-emphasis, #0a3622);
    border-color: var(--bs-success-border-subtle, #a3cfbb);
}
/* Toggle row at the top of the dropdown — selects which sources
   contribute materials to the list. Sticky so it stays visible while
   scrolling through long lists. Subtle bg differentiates it from
   the items below. */
.hm-mat-filter-toggles {
    display: flex;
    gap: 14px;
    padding: 8px 10px;
    background: var(--bs-tertiary-bg);
    position: sticky;
    top: 0;
    z-index: 1;
}
.hm-mat-filter-toggles .form-check {
    cursor: pointer;
    margin-right: 0;
}
.hm-mat-filter-toggles .form-check-label {
    cursor: pointer;
    user-select: none;
}
.hm-mat-filter-divider {
    height: 1px;
    background: var(--bs-border-color);
}

/* Section header + divider for the action dropdown — used to group the
   "From Bambu catalogue" actions distinctly from the inventory matchers
   in the Match Colours dropdown (May 2026 — option D phase 2). */
.hm-action-dropdown-divider {
    height: 1px;
    background: var(--bs-border-color);
    margin: 6px 0;
}
.hm-action-dropdown-section {
    padding: 4px 12px 2px;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
}
/* Wider variant of the action dropdown — used by the heights-strategy picker
   in CellShapeModal where 8 options with descriptions need more room than the
   3-item Match-colours menu. Sets a larger min/max width so the description
   text doesn't wrap aggressively. */
.hm-action-dropdown-menu-wide {
    min-width: 100%;
    max-width: 460px;
    max-height: 360px;
    overflow-y: auto;
}
/* Disabled item styling — used by the heights dropdown when a strategy isn't
   compatible with the current project mode (e.g. DepthPainter in Random Cells).
   Greyed out + non-clickable; tooltip explains why. */
.hm-action-dropdown-item.disabled,
.hm-action-dropdown-item:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    pointer-events: none;
}
/* Selected-row highlight in the action dropdown — same accent stripe as the
   filament picker so the two controls feel consistent. */
.hm-action-dropdown-item.selected {
    background: color-mix(in srgb, var(--hm-accent) 14%, transparent);
    border-left: 2px solid var(--hm-accent);
    padding-left: 8px;
}

/* ================= Construct workspace (exploded build diagram) ================= */
/* Two-column layout: SVG iso scene + zoom controls on the left, layers
   panel + per-material controls + build summary on the right. Same
   proportions as RoomPreview for muscle-memory consistency. The SVG is
   pure vector — no GPU, no JS interop, no ResizeObserver. See
   MountPreview.razor for the iso math + scope notes (the file kept its
   original name for stability; the workspace label is "Construct"). */
.hm-construct-preview {
    flex: 1 1 auto;
    /* Column stack: toolbar at the top, then the canvas (which carries
       the floating cards). All other controls (Layers/View, Settings)
       float over the canvas as collapsible cards — there's no separate
       side-panel column. */
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}

/* Toolbar — header strip above the Mount canvas, mirroring Preview2D's
   `.hm-viewer-toolbar`. Left: brief interaction hint. Right: persistent
   action buttons (reset / defaults / print) so they're reachable even
   when the left HUD card is collapsed. */
.hm-construct-toolbar {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .4rem .75rem;
    background: var(--bs-secondary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 8px 8px 0 0;
    border-bottom: 0;
    flex: 0 0 auto;
}
.hm-construct-toolbar-info {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 0.78rem;
    color: var(--bs-secondary-color);
}
.hm-construct-toolbar-info > i.bi {
    opacity: 0.85;
}
.hm-construct-toolbar-actions {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: 0 0 auto;
}
/* Toolbar buttons follow the icon-then-label pattern. The CSS adds the
   margin between icon and label so the markup stays compact. At narrow
   viewports the label is hidden (see media query below) and the icon
   stands alone — title attribute on the button covers accessibility. */
.hm-construct-toolbar-actions .btn > i.bi + .hm-construct-btn-label {
    margin-left: 0.4rem;
}
/* Icon-only toolbar buttons (e.g. the shadows toggle) — square-ish
   footprint so they read as "icon button" rather than a stretched
   labelled button with no label. */
.hm-construct-toolbar-actions .hm-construct-icon-btn {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
    min-width: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
/* Toolbar joins the canvas seamlessly — when both are present, the
   canvas's top corners flatten so they read as a single panel. */
.hm-construct-preview > .hm-construct-toolbar + .hm-construct-canvas {
    border-radius: 0 0 8px 8px;
    border-top: 0;
}
.hm-construct-canvas {
    /* flex: 1 1 auto so the canvas fills the preview flex container.
       Previously this was inside a grid cell which sized it implicitly;
       the grid was removed when the settings panel became a floating card. */
    flex: 1 1 auto;
    min-width: 0;
    position: relative;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Three.js mount target — fills the canvas. mount3d.js's ResizeObserver
   keeps the renderer in sync with this element's size. */
.hm-construct-canvas-3d {
    flex: 1 1 auto;
    width: 100%;
    height: 100%;
    min-height: 0;
    cursor: grab;
}
.hm-construct-canvas-3d:active {
    cursor: grabbing;
}
.hm-construct-canvas-3d canvas {
    display: block;
}
/* Legacy SVG class — kept as a no-op so any cached references don't break. */
.hm-construct-svg {
    width: 100%;
    height: 100%;
    max-height: 100%;
    display: block;
}
.hm-construct-empty {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    text-align: center;
}
.hm-construct-empty > i.bi {
    font-size: 2rem;
    opacity: 0.5;
}
.hm-construct-controls {
    overflow-y: auto;
    padding: 14px;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.hm-construct-section {
    padding-bottom: 14px;
    padding-top: 14px;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-construct-section:first-child {
    padding-top: 0;
}
.hm-construct-section:last-child {
    border-bottom: 0;
    padding-bottom: 0;
}
.hm-construct-section-title {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
    margin-bottom: 10px;
    display: flex;
    align-items: center;
}
.hm-construct-section-title > i.bi {
    color: var(--bs-secondary-color);
    font-size: 0.95rem;
}

/* Zoom controls — overlayed on top-right of the canvas. Floats in via
   absolute positioning so the SVG continues to use the full canvas area. */
.hm-construct-zoom-controls {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    background: color-mix(in srgb, var(--bs-body-bg) 86%, transparent);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    backdrop-filter: blur(6px);
}
.hm-construct-zoom-range {
    width: 100px;
    margin: 0;
}

/* Layers panel — show/hide eye toggle, name, opacity slider per layer.
   Mirrors the Cell Painter layers panel pattern (.hm-layer-row) but
   adapted for the Construct view's needs (per-row opacity slider rather
   than a "active layer" concept). */
.hm-construct-layers {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.hm-construct-layer-row {
    display: grid;
    /* icon · name · opacity-slider · eye  (May 2026 reorder) */
    grid-template-columns: 18px 1fr auto 26px;
    align-items: center;
    gap: 8px;
    padding: 4px 6px;
    border-radius: 4px;
    transition: background 120ms ease, opacity 120ms ease;
}
.hm-construct-layer-row.hidden {
    opacity: 0.55;
}
.hm-construct-layer-row:hover {
    background: var(--bs-tertiary-bg);
}
.hm-construct-layer-eye {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    color: var(--bs-body-color);
    cursor: pointer;
}
.hm-construct-layer-eye:hover {
    background: var(--bs-secondary-bg);
    border-color: var(--bs-border-color);
}
.hm-construct-layer-icon {
    font-size: 0.95rem;
}
.hm-construct-layer-name {
    font-size: 0.85rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hm-construct-layer-opacity {
    width: 90px;
    margin: 0;
}

/* Build summary list — DEPRECATED. The build summary moved (May 2026) to
   a floating overlay card on the canvas (.hm-construct-summary-card). The
   class is left in place as a no-op fallback for any legacy markup that
   might still reference it. */
.hm-construct-summary {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 0.78rem;
}

/* ===== Floating Build Summary card (overlay on canvas, bottom-right) =====
   Pinned to the canvas's bottom-right corner with frosted-glass styling and
   slightly lowered opacity so a mosaic design extending into this region
   stays partly visible behind it. Hover bumps to full opacity for legibility.
   Each row is a 3-column grid: icon · label · value, so wood-cut quantities
   line up vertically — reads like a build-pick-list rather than a single
   run-on string. */
.hm-construct-summary-card {
    position: absolute;
    bottom: 12px;
    left: 12px;
    z-index: 5;
    /* Fixed width (not min/max) so the Layers card and the Build summary
       card always render the same width regardless of content. Their
       content-driven widths previously diverged — Layers content pushed
       to the max-width, Build summary content sat closer to the min. */
    width: 320px;
    padding: 14px 16px 16px;
    background: color-mix(in srgb, var(--bs-body-bg) 88%, transparent);
    border: 1px solid var(--bs-border-color);
    border-radius: 10px;
    backdrop-filter: blur(6px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    opacity: 0.88;
    transition: opacity 150ms ease;
    pointer-events: auto;
}
.hm-construct-summary-card:hover {
    opacity: 1;
}
.hm-construct-summary-card-title {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--bs-secondary-color);
    margin-bottom: 10px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--bs-border-color);
}
/* Roomier vertical rhythm in the summary list — each row gets a noticeable
   gap so lines don't feel cramped. row-gap controls vertical spacing between
   rows; the column-gap stays tight so the icon/label/value stay aligned. */
.hm-construct-summary-grid {
    display: grid;
    grid-template-columns: 16px auto 1fr;
    gap: 13px 8px;
    font-size: 0.78rem;
    line-height: 1.4;
}
/* Print/Save button — matches the summary card's text size (0.78rem) so
   it doesn't visually shout louder than the data above it; relies on the
   solid btn-primary fill + faint shadow for emphasis instead. */
.hm-construct-summary-print {
    margin-top: 14px !important;
    padding-top: 0.45rem;
    padding-bottom: 0.45rem;
    font-size: 0.78rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}

/* View + Layers HUD card — top-left mirror of the bottom-left build
   summary card. Inherits .hm-construct-summary-card's fixed width so
   the two cards are visually identical in size; just override position. */
.hm-construct-view-card {
    top: 12px;
    left: 12px;
    bottom: auto;
    max-height: calc(100% - 24px);
    overflow-y: auto;
}

/* Settings card — top-right floating panel for the build-spec inputs
   (battens, acrylic, clear zone). Same fixed width + frosted-glass
   styling as the other floating cards; just pinned top-right. Internal
   scroll if the content exceeds the viewport height. */
.hm-construct-settings-card {
    top: 12px;
    right: 12px;
    bottom: auto;
    left: auto;
    max-height: calc(100% - 24px);
    overflow-y: auto;
}
/* When the settings card is collapsed (chevron pointing right), the
   chevron rotates so the icon makes intuitive sense — clicking the
   chevron-bar-right when expanded "pushes" the card off to the right;
   clicking the bi-sliders icon when collapsed "pulls" it back in. */
.hm-construct-settings-card.hm-construct-card-collapsed .hm-construct-card-toggle {
    color: var(--bs-primary);
}

/* ============= Mobile / narrow viewport ============= */
/* On narrow screens the floating cards crowd the canvas. Switch to a
   vertical stack: canvas first with a fixed sensible height, then the
   three control cards in document flow underneath in the order
   Layers → Settings → Build summary. The cards live inside
   .hm-construct-canvas (a flex container), so we just need to make
   that container column-direction on mobile and let the cards' `order`
   put them in the desired sequence. */
@media (max-width: 768px) {
    .hm-construct-preview {
        flex-direction: column;
        overflow-y: auto;
    }
    /* Toolbar on mobile — hide the interaction-hint text + shrink the
       three action buttons (smaller padding + font) so they fit on a
       single row. */
    .hm-construct-toolbar-info { display: none; }
    .hm-construct-toolbar-actions { flex: 1 1 auto; justify-content: flex-end; gap: 4px; }
    .hm-construct-toolbar-actions .btn {
        padding: 0.2rem 0.45rem;
        font-size: 0.72rem;
        white-space: nowrap;
    }
}
/* Narrow phones — collapse toolbar buttons to icon-only (title
   attribute carries the meaning for hover / a11y). Sits OUTSIDE the
   `<= 768` block so the rule chain reads as a single cascade. */
@media (max-width: 540px) {
    .hm-construct-toolbar-actions .btn .hm-construct-btn-label {
        display: none;
    }
    .hm-construct-toolbar-actions .btn > i.bi + .hm-construct-btn-label {
        margin-left: 0;
    }
    .hm-construct-canvas {
        flex-direction: column;
        align-items: stretch;
        justify-content: flex-start;
        height: auto;
        overflow: visible;
        padding: 8px;
    }
    .hm-construct-canvas-3d {
        order: 0;
        flex: 0 0 auto;
        height: 55vh;
        min-height: 320px;
        border-radius: 6px;
    }
    /* Order: Layers → Settings (Build summary now lives inside the
       Settings card as its bottom section, so only two cards stack). */
    .hm-construct-view-card     { order: 1; }
    .hm-construct-settings-card { order: 2; }
    /* All floating cards drop their absolute positioning and become
       full-width inline cards. Frosted-glass effect off so they read
       cleanly against the page background. */
    .hm-construct-summary-card,
    .hm-construct-view-card,
    .hm-construct-settings-card {
        position: static;
        width: 100%;
        max-width: none;
        max-height: none;
        margin-top: 10px;
        opacity: 1;
        backdrop-filter: none;
    }
    /* Collapsed state still works on mobile — but expanded width matches
       the row instead of shrinking to a fixed icon. The toggle button
       still lets the user fold a section away to save scroll. */
    .hm-construct-summary-card.hm-construct-card-collapsed {
        width: auto;
        align-self: flex-start;
    }
}
/* Sub-section header inside the View card — separates Layers (above)
   from View controls (below). */
.hm-construct-view-card-divider {
    margin-top: 14px !important;
}

/* ============= Collapsible canvas cards ============= */
/* Header row at the top of each floating card — title left, collapse
   chevron right. When the card is collapsed, the title is hidden and
   the toggle becomes the only visible thing (just an icon button).
   The border-bottom replaces the one that used to live on
   .hm-construct-summary-card-title (which now sits inline in the header
   with the divider classes overridden). */
.hm-construct-card-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-construct-summary-card.hm-construct-card-collapsed .hm-construct-card-header {
    margin-bottom: 0;
    padding-bottom: 0;
    border-bottom: 0;
}
.hm-construct-card-toggle {
    background: transparent;
    border: 0;
    color: var(--bs-body-color);
    font-size: 1rem;
    padding: 4px 8px;
    line-height: 1;
    cursor: pointer;
    border-radius: 4px;
    flex: 0 0 auto;
}
.hm-construct-card-toggle:hover {
    background: rgba(128, 128, 128, 0.18);
}
/* Collapsed state — card shrinks to a small floating icon button. The
   user can still see WHERE the panel sits (top-left or bottom-left) but
   the canvas is unobstructed for clean screenshots / print previews. */
.hm-construct-summary-card.hm-construct-card-collapsed {
    min-width: 0;
    max-width: none;
    width: auto;
    padding: 4px 6px;
    overflow: visible;
}
.hm-construct-summary-card.hm-construct-card-collapsed .hm-construct-card-toggle {
    padding: 6px 8px;
    font-size: 1.05rem;
    color: var(--bs-primary);
}
.hm-construct-summary-row {
    display: contents; /* lets the row's children participate in the parent grid */
}
.hm-construct-summary-row > i.bi {
    color: var(--bs-secondary-color);
    align-self: center;
}
.hm-construct-summary-label {
    color: var(--bs-secondary-color);
    /* Don't wrap — labels like "Horizontal segments" were splitting onto
       two lines inside the 320px-wide card grid. With both label and
       value as nowrap and the column gap at 8px, the longest pair
       ("Horizontal segments" + "2 × ~XXX mm") fits comfortably inside
       the card's content width. */
    white-space: nowrap;
}
.hm-construct-summary-value {
    font-weight: 600;
    color: var(--bs-body-color);
    text-align: right;
    white-space: nowrap;
}
/* Meta row (small, dim) for supplementary info like cross-section + mitre type. */
.hm-construct-summary-row-meta > .hm-construct-summary-label,
.hm-construct-summary-row-meta > .hm-construct-summary-value {
    font-size: 0.72rem;
    font-weight: 400;
    color: var(--bs-secondary-color);
}

/* ================= Canvas drawer — actual mosaic size sub-card ================= */
/* Sub-card inside the Canvas drawer that displays the cells' real bounding-box
   extent (vs the user-declared canvas size in the W/H inputs above). Card
   styling so it reads as a distinct "this is the source of truth" panel
   rather than another dim helper line. Subtle accent-tinted background +
   border to draw the eye without screaming. May 2026 UX pass — see
   `MosaicResultExtensions.ComputeActualExtentMm` for the geometry rationale
   and the FUTURE_IDEAS.md Edge Fit section for the long-term fix. */
.hm-actual-size-card {
    padding: 8px 10px;
    border: 1px solid color-mix(in srgb, var(--bs-success) 35%, var(--bs-border-color));
    border-radius: 6px;
    background: color-mix(in srgb, var(--bs-success) 7%, var(--bs-body-bg));
}
.hm-actual-size-value {
    font-weight: 600;
    font-size: 1rem;
    line-height: 1.25;
    color: var(--bs-body-color);
}

/* ================= Cell-shape modal accordions ================= */
/* Generic accordion used by CellShapeModal to chunk the controls column into
   collapsible sections (Choose shape / Size / Heights / Cell Assembly / Flat
   edge frame). Visual style matches .hm-experimental-shapes for consistency
   but uses a longer-form summary row with a "meta" slot on the right (e.g.
   "Random per cell" next to the Heights summary) so the user can read the
   current setting without expanding. */
.hm-cells-accordion {
    margin-bottom: 10px;
}
.hm-cells-accordion > .hm-cells-accordion-summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    user-select: none;
    font-size: 0.9rem;
    font-weight: 600;
}
.hm-cells-accordion > .hm-cells-accordion-summary::-webkit-details-marker { display: none; }
.hm-cells-accordion > .hm-cells-accordion-summary::marker { content: none; }
.hm-cells-accordion > .hm-cells-accordion-summary:hover {
    background: var(--bs-secondary-bg);
}
.hm-cells-accordion-title {
    flex: 1 1 auto;
}
.hm-cells-accordion-meta {
    flex: 0 0 auto;
    font-weight: 400;
    font-size: 0.78rem;
    color: var(--bs-secondary-color);
}
.hm-cells-accordion-chevron {
    flex: 0 0 auto;
    color: var(--bs-secondary-color);
    transition: transform 150ms ease;
    font-size: 0.9em;
}
.hm-cells-accordion[open] > .hm-cells-accordion-summary .hm-cells-accordion-chevron {
    transform: rotate(90deg);
}
.hm-cells-accordion[open] > .hm-cells-accordion-summary {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
.hm-cells-accordion-body {
    padding: 12px;
    border: 1px solid var(--bs-border-color);
    border-top: 0;
    border-bottom-left-radius: 6px;
    border-bottom-right-radius: 6px;
    background: var(--bs-body-bg);
}

/* ================= Mini "3 cells" preview (CellShapeModal) ================= */
/* Tiny SVG illustration of 3 cells side-by-side with the current cell width,
   gap, and a representative height spread. Sits under the main shape-icon
   preview on the left side of CellShapeModal so the user can see how their
   geometry settings combine without committing Apply. The SVG is rendered
   inline by RenderMiniCellsSvg() — uses currentColor for fill, which we set
   via the wrapper's color rule below. */
.hm-mini-cells {
    width: 100%;
    max-width: 260px;
    margin: 0 auto;
    color: var(--hm-accent);
    opacity: 0.85;
}
.hm-mini-cells.clumped {
    /* Subtle visual cue that the bases are connected — slight lift via shadow. */
    filter: drop-shadow(0 1px 0 color-mix(in srgb, var(--hm-accent) 30%, transparent));
}
.hm-mini-cells svg {
    display: block;
}
/* Bullet-summary card under the mini-visual. Wraps the list in a bordered
   panel so it reads as a separate "details" surface, visually distinct from
   the shape-icon preview above. Slightly smaller text (~0.78rem) than the
   default `.small` (0.875rem) so the card sits as supporting metadata
   rather than competing with the primary preview. */
.hm-mini-cells-card {
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    padding: 10px 12px;
}
.hm-mini-cells-summary {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 8px; /* breathing room between rows — each fact gets its own line */
    font-size: 0.78rem;
    line-height: 1.35;
}
.hm-mini-cells-summary > li {
    display: flex;
    align-items: flex-start;
    gap: 8px;
}
.hm-mini-cells-summary > li > i.bi {
    flex: 0 0 16px;
    color: var(--hm-accent);
    font-size: 0.95em;
    margin-top: 1px;
}
.hm-mini-cells-summary > li > span {
    flex: 1 1 auto;
}
/* Sticky search + sort toolbar at the top of the popover. position:sticky keeps
   it pinned while the option list scrolls underneath, so the search field is
   always reachable in long libraries. */
.hm-filament-picker-toolbar {
    position: sticky;
    top: -4px; /* cancel the menu's 4px padding so it hugs the menu edge */
    z-index: 1;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 4px 4px 6px;
    margin: -4px -4px 4px;
    background: var(--bs-body-bg);
    border-bottom: 1px solid var(--bs-border-color);
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
}
.hm-filament-picker-search {
    flex: 1 1 100%; /* full width on its own row — search box benefits from horizontal space */
    min-width: 0;
}
.hm-filament-picker-material-filter,
.hm-filament-picker-sort {
    flex: 1 1 0;
    min-width: 0;
    width: auto;
}
.hm-filament-picker-empty {
    border-top: 1px dashed var(--bs-border-color);
    margin-top: 4px;
}
/* ================= End FilamentPicker ================= */

/* ================= Drag-drop image overlay ================= */
/* Full-viewport overlay shown while a file is being dragged over the window. The
   overlay itself uses pointer-events:none so dragover/drop events still reach the
   window listeners — without that, drop would land on the overlay div and the
   handler would never see the file. */
.hm-drop-overlay {
    position: fixed;
    inset: 0;
    z-index: 9999;
    background: color-mix(in srgb, var(--bs-primary) 18%, transparent);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    display: none;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.hm-drop-overlay.show { display: flex; }
.hm-drop-overlay-inner {
    background: var(--bs-body-bg);
    border: 3px dashed var(--bs-primary);
    border-radius: 16px;
    padding: 36px 56px;
    text-align: center;
    color: var(--bs-body-color);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}
.hm-drop-overlay-inner i {
    font-size: 3rem;
    color: var(--bs-primary);
    display: block;
    line-height: 1;
    margin-bottom: 12px;
}
.hm-drop-overlay-text {
    font-size: 1.25rem;
    font-weight: 600;
}
.hm-drop-overlay-sub {
    font-size: 0.85rem;
    color: var(--bs-secondary-color);
    margin-top: 4px;
}
/* ================= End drag-drop overlay ================= */

/* ================= Cell hover-info tooltip ================= */
/* Floating tooltip used by the 2D preview's hover-info layer (FUTURE_IDEAS #6+8).
   pointer-events:none so it never steals events from the SVG below. position:fixed
   with JS-set top/left so it tracks the cursor. */
.hm-cell-tip {
    position: fixed;
    z-index: 1100;
    pointer-events: none;
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
    padding: 8px 10px;
    font-size: 0.78rem;
    line-height: 1.35;
    /* width:max-content + auto-sizing grid columns means the tooltip grows to fit
       its widest stat row instead of clipping or wrapping mid-value. */
    width: max-content;
    max-width: 360px;
}
.hm-cell-tip-row { display: flex; align-items: center; gap: 6px; }
.hm-cell-tip-id {
    font-weight: 600;
    margin-bottom: 2px;
    color: var(--bs-secondary-color);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.hm-cell-tip-fil { margin-bottom: 4px; }
.hm-cell-tip-swatch {
    display: inline-block;
    width: 12px;
    height: 12px;
    border-radius: 2px;
    border: 1px solid var(--bs-border-color);
    flex: 0 0 auto;
}
.hm-cell-tip-name {
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hm-cell-tip-stats {
    display: grid;
    /* Auto-sized columns sized to the widest cell in each column — keeps each stat
       on a single line ("Volume 4,160.7 mm³" never breaks across rows). */
    grid-template-columns: auto auto;
    column-gap: 16px;
    row-gap: 2px;
    font-size: 0.74rem;
    color: var(--bs-secondary-color);
    border-top: 1px dashed var(--bs-border-color);
    padding-top: 4px;
    margin-top: 4px;
}
.hm-cell-tip-stats > span {
    white-space: nowrap;
}
.hm-cell-tip-stats b {
    color: var(--bs-body-color);
    font-weight: 600;
    margin-left: 2px;
}
/* ================= End cell hover-info tooltip ================= */

/* ================= Room preview ================= */
/* Layered composite: wall (bottom) → mosaic art on the wall → foreground PNG
   (table + plants) on top. The art's on-screen size is driven by a CSS custom
   property (--hm-art-scale) so the slider can re-size without re-rendering
   the SVG. The foreground PNG is hard-anchored to the bottom centre because
   its perspective expects to sit there. */
.hm-room-preview {
    position: relative;
    width: 100%;
    height: 100%;
    flex: 1 1 auto;
    min-height: 360px;
    overflow: hidden;
    border-radius: 8px;
    border: 1px solid var(--bs-border-color);
    isolation: isolate; /* contains the absolutely-positioned children */
    background: #000; /* shows through any wall colour with alpha */
    /* Establish a size-query container so descendants can use cqw units —
       used by the picture frame to express its border thickness in real
       mm relative to the assumed 4000mm room width: 1mm = (1/4000)*100cqw
       = 0.025cqw. Lets the frame scale naturally with the panel size. */
    container-type: inline-size;
}
.hm-room-wall {
    position: absolute;
    inset: 0;
    z-index: 1;
}
.hm-room-art {
    position: absolute;
    /* Centre horizontally AND vertically around (50%, 38%) of the panel.
       The art, clear zone, and frame all share this anchor so that as the
       clear-zone padding grows, the matte expands concentrically around the
       art (rather than hanging below it). 38% sits the centre slightly
       above mid-panel so the framed piece reads as a hung artwork — high
       enough to clear the foreground PNG (table + plants) which occupies
       the lower third. width + aspect-ratio come from RoomPreview's inline
       ArtStyle(). max-height clamps extreme portrait aspects from running
       off the top of the panel into the foreground PNG. */
    top: 38%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 2;
    /* No frame / box-shadow / background here — the Three.js canvas is
       transparent and the directional light's projected shadow handles the
       "hung on a wall" cue. */
    max-height: 70%;
    pointer-events: none;
}
.hm-room-art-canvas {
    width: 100%;
    height: 100%;
    /* Three.js sizes its renderer to the host element via ResizeObserver, so
       we just provide a stable box. Block layout on the canvas child stops
       the inline-by-default whitespace gap. */
}
.hm-room-art-canvas canvas { display: block; }

/* Canvas clear zone — coloured rectangle behind the art that acts as a
   matte board, sized larger than the art by the user's clear-zone padding.
   Sibling of .hm-room-art and .hm-room-frame; positioning rules are
   duplicated so all three boxes share the same anchor (top + horizontal
   centre). Width + aspect-ratio come from inline ClearZoneStyle(); the
   matte fill colour comes from inline MatteStyle().

   Z-index 2 (same as the art) but earlier in DOM order, so the art's
   transparent canvas renders ON TOP — letting the matte show through cell
   gaps while keeping the cells visually centred over it. The frame
   (also z-index 2, later in DOM) renders on top of both, wrapping the
   clear zone with its border. */
.hm-room-clear-zone {
    position: absolute;
    /* Concentric with .hm-room-art and .hm-room-frame — all three share the
       same centre at (50%, 38%) so the clear zone expands around the art. */
    top: 38%;
    left: 50%;
    transform: translate(-50%, -50%);
    /* width + aspect-ratio come from inline ClearZoneStyle(); background
       comes from inline MatteStyle(). */
    max-height: 70%;
    z-index: 2;
    pointer-events: none;
}

/* Picture frame overlay — sibling of .hm-room-art (NOT a child) so it can
   share the same positioning rules without nesting. .hm-room-art creates a
   stacking context via transform: translateX(-50%), so keeping the frame
   inside would also lock its z-index inside that context.

   Z-index 2 (BELOW foreground at 3) — plants correctly occlude the frame's
   bottom edge, just like real plants in front of a hung picture. (An earlier
   draft had this at 4 above the foreground; user feedback said the natural
   look is plants in front.)

   Positioning is duplicated from .hm-room-art so the two boxes align
   exactly (same top, left, transform, height, aspect-ratio, max-height).
   The Razor side passes the same ArtStyle() inline string to both so
   width + aspect-ratio match in lockstep.

   Border-style + colour + thickness are set inline via FrameStyle(). The
   shadows give the depth cues:
     - inset shadow on the inner edge — bevel/matte under the frame.
     - outer drop shadow — frame casting onto the wall. */
.hm-room-frame {
    position: absolute;
    /* Concentric with .hm-room-art and .hm-room-clear-zone — same centre
       at (50%, 38%) so the frame wraps the matte symmetrically. */
    top: 38%;
    left: 50%;
    transform: translate(-50%, -50%);
    /* width + aspect-ratio come from inline ArtStyle(). */
    max-height: 70%;
    box-sizing: border-box;
    border-style: solid;
    pointer-events: none;
    z-index: 2;
    box-shadow:
        /* inner edge shadow — frame casting onto the print */
        inset 0 0 8px rgba(0, 0, 0, 0.45),
        /* outer drop shadow — frame casting onto the wall */
        0 4px 10px rgba(0, 0, 0, 0.28),
        0 14px 28px rgba(0, 0, 0, 0.18);
}
.hm-room-foreground {
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-height: 100%;
    object-fit: contain;
    object-position: bottom center;
    z-index: 3;
    pointer-events: none;
    /* user-select prevents the image being draggable as an OS-level drag. */
    user-select: none;
    -webkit-user-drag: none;
}
.hm-room-controls {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 4;
    /* Slightly translucent so the wall colour shows through subtly —
       reads more like an overlay than an opaque widget. color-mix is
       theme-aware (works for both light + dark via --bs-body-bg).
       backdrop-filter blurs whatever's behind for a frosted-glass look;
       gracefully degrades to a solid-ish panel on browsers without
       support (older Firefox). */
    background-color: color-mix(in srgb, var(--bs-body-bg) 82%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    padding: 10px 12px;
    width: 280px;
    max-height: calc(100% - 24px);
    overflow-y: auto;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
    /* Smooth the expand/collapse animation. width + padding tween so the
       panel doesn't snap when toggled. */
    transition: width 0.18s ease, padding 0.18s ease;
}
/* Collapsed state — shrinks to a small floating button so the user can
   take a clean screenshot of the framed mosaic without UI overlay. The
   toggle button inside (.hm-room-controls-toggle) becomes the only
   visible element. */
.hm-room-controls.hm-room-controls-collapsed {
    width: auto;
    padding: 4px 6px;
    overflow: visible;
}
/* Header row at the top of the controls panel — title on the left,
   collapse toggle on the right. Flex so the title takes the available
   space and the toggle stays pinned right. Margin-bottom separates it
   from the first form-label below. */
.hm-room-controls-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}
.hm-room-controls-title {
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--bs-body-color);
    flex: 1 1 auto;
    /* Subtle uppercase + tracking gives it a "panel-title" feel without
       shouting — matches the visual weight of the surrounding form
       labels rather than competing with them. */
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.85;
}
.hm-room-controls-toggle {
    background: transparent;
    border: none;
    color: var(--bs-body-color);
    font-size: 1rem;
    padding: 4px 8px;
    cursor: pointer;
    border-radius: 4px;
    line-height: 1;
    flex: 0 0 auto;
}
.hm-room-controls-toggle:hover {
    background: rgba(128, 128, 128, 0.18);
}
/* Collapsed: header collapses to just the toggle button, no margin so
   the panel hugs the icon tightly. */
.hm-room-controls.hm-room-controls-collapsed .hm-room-controls-header {
    margin-bottom: 0;
}
.hm-room-controls.hm-room-controls-collapsed .hm-room-controls-toggle {
    padding: 6px 8px;
}
.hm-room-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    gap: 8px;
    font-size: 0.95rem;
}
.hm-room-empty i { font-size: 3rem; }

/* Draggable light-bulb handle. Sits above the canvas + frame + foreground
   so it's always visible. pointer-events:none so the parent's pointerdown
   handler still fires when the user clicks ON the bulb itself — JS routes
   the drag via the parent .hm-room-preview's pointer capture. */
.hm-room-light-bulb {
    position: absolute;
    z-index: 5;
    transform: translate(-50%, -50%);
    color: #ffd86b;
    font-size: 1.6rem;
    pointer-events: none;
    /* Multi-direction text-shadow for the lightbulb glyph so it pops
       against ANY wall colour — beige walls used to swallow the warm
       yellow icon. Layered: a tight black halo (0–2px) for edge
       contrast, then a softer drop shadow (1–3px) for depth. */
    text-shadow:
        0 0 2px rgba(0, 0, 0, 0.85),
        0 0 4px rgba(0, 0, 0, 0.55),
        0 1px 3px rgba(0, 0, 0, 0.5);
    /* Smooth bulb travel as the slider/pointer moves it — short duration so
       it feels responsive but not laggy. Disabled while actively dragging
       (the JS handler updates many times per second; transitions would lag
       behind the cursor). */
    transition: left 80ms ease, top 80ms ease;
}
.hm-room-light-bulb.dragging {
    transition: none;
}
/* Dashed affordance ring around the bulb. The bulb glyph alone is small
   (1.6rem) and easy to miss / fiddly to grab on a busy wall image; a
   ring at 60px diameter signals "this is a draggable handle" and gives
   the cursor a much larger visual target without making the bulb itself
   bigger (which would dominate the preview).

   Explicit width / height (NOT `inset:-Npx`) so the ring is a true circle
   regardless of the parent .hm-room-light-bulb's content box — the icon
   glyph is taller than wide, which made `inset` produce an oval. Centred
   over the parent via the same translate(-50%,-50%) trick the parent
   itself uses. Brighter + solid + larger while dragging so the user gets
   clear feedback that they've engaged the handle.

   Two-tone for visibility on any wall: an outer dark ring (box-shadow)
   for contrast against light walls + an inner cream-white dash for
   contrast against dark walls. Without the dark companion, the warm
   amber dashes vanished against beige / sand wall colours. */
.hm-room-light-bulb::before {
    content: "";
    position: absolute;
    width: 60px;
    height: 60px;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border: 2px dashed rgba(255, 248, 220, 0.95);
    border-radius: 50%;
    box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.35),
        0 0 6px rgba(0, 0, 0, 0.25);
    pointer-events: none;
    transition: width 140ms ease, height 140ms ease, border-color 140ms ease, border-style 0ms;
}
.hm-room-light-bulb.dragging::before {
    width: 76px;
    height: 76px;
    border-color: #ffd86b;
    border-style: solid;
}
/* Soft halo around the bulb — sells it as a glowing light source. Larger
   when actively dragging so the bulb feels "energised". */
.hm-room-light-bulb-glow {
    position: absolute;
    inset: -28px;
    border-radius: 50%;
    background: radial-gradient(closest-side,
        rgba(255, 220, 120, 0.55),
        rgba(255, 220, 120, 0.18) 45%,
        transparent 70%);
    pointer-events: none;
    transition: inset 120ms ease;
}
.hm-room-light-bulb.dragging .hm-room-light-bulb-glow {
    inset: -40px;
}
/* Dimension annotations (toggle: RoomSettings.ShowDimensions). A SINGLE
   wrapper sized to the frame's outer extent (= ClearZoneStyle()), with
   four children that hang off each edge. Mosaic dim lines (top + left)
   are scaled to the cells' actual width/height via --hm-mw-ratio /
   --hm-mh-ratio CSS vars set inline on the wrapper; outer-frame dim
   lines (bottom + right) span the full wrapper. This keeps all four
   dim labels OUTSIDE the frame regardless of which extent they
   measure — engineering-drawing convention.

   z-index 4 (above frame at 2, above foreground PNG at 3) so plants
   don't occlude the labels. */
.hm-room-dim-wrap {
    position: absolute;
    top: 38%;
    left: 50%;
    transform: translate(-50%, -50%);
    max-height: 70%;
    z-index: 4;
    pointer-events: none;
}
.hm-room-dim {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--bs-emphasis-color);
    font-size: 0.7rem;
    font-weight: 500;
    line-height: 1;
}
/* Top + bottom dims hang off the wrapper's top / bottom edges. The
   pill sits AWAY from the frame (above for top, below for bottom);
   the line sits adjacent to the frame. 18px gap between frame edge
   and dimension line — far enough that the dimension reads as
   separate annotation rather than encroaching on the artwork. */
.hm-room-dim-top,
.hm-room-dim-bottom {
    left: 0;
    right: 0;
    flex-direction: column;
    gap: 6px;
}
.hm-room-dim-top    { bottom: 100%; padding-bottom: 18px; flex-direction: column-reverse; }
.hm-room-dim-bottom { top: 100%; padding-top: 18px; }
.hm-room-dim-left,
.hm-room-dim-right {
    top: 0;
    bottom: 0;
    flex-direction: row;
    gap: 6px;
}
.hm-room-dim-left  { right: 100%; padding-right: 18px; flex-direction: row-reverse; }
.hm-room-dim-right { left: 100%; padding-left: 18px; }
/* Line + arrowheads. Mosaic-class dim lines shrink via CSS vars set
   inline on the wrapper; outer-frame dims span 100%. Lines centre
   inside their flex parent automatically (align-items: center on
   .hm-room-dim), so a shrunk line still hangs centred over the
   cells / matte. */
.hm-room-dim-line {
    position: relative;
    background: var(--bs-emphasis-color);
}
.hm-room-dim-top .hm-room-dim-line,
.hm-room-dim-bottom .hm-room-dim-line {
    width: 100%;
    height: 1.5px;
}
.hm-room-dim-left .hm-room-dim-line,
.hm-room-dim-right .hm-room-dim-line {
    width: 1.5px;
    height: 100%;
}
/* Mosaic dim lines scale to the cells' extent — shorter than the
   wrapper so the line reads as "this measures the cells" while the
   pill still sits clear of the frame. */
.hm-room-dim-mosaic.hm-room-dim-top .hm-room-dim-line,
.hm-room-dim-mosaic.hm-room-dim-bottom .hm-room-dim-line {
    width: var(--hm-mw-ratio, 100%);
}
.hm-room-dim-mosaic.hm-room-dim-left .hm-room-dim-line,
.hm-room-dim-mosaic.hm-room-dim-right .hm-room-dim-line {
    height: var(--hm-mh-ratio, 100%);
}
/* Arrowheads at line ends — CSS triangles via the border-trick. Apex
   sits AT the measurement endpoint (= the frame / mosaic edge), with
   the body extending INWARD into the line for 7px. This way the
   arrow's tip exactly marks the frame corner; nothing extends past
   the measured extent. The line under the arrow body is covered by
   the solid triangle, so visually you see ◁━━━━━━━━━━▷ with apexes
   on the corners. Sized 7×4 px — reads clearly at panel scale
   without dominating. */
.hm-room-dim-line::before,
.hm-room-dim-line::after {
    content: "";
    position: absolute;
    width: 0;
    height: 0;
    background: none;
}
/* Horizontal lines: left-end arrow apex at line's left edge (x=0), right-end at line's right edge. */
.hm-room-dim-top .hm-room-dim-line::before,
.hm-room-dim-bottom .hm-room-dim-line::before {
    left: 0;
    top: 50%;
    transform: translate(0, -50%);
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    border-right: 7px solid var(--bs-emphasis-color);
}
.hm-room-dim-top .hm-room-dim-line::after,
.hm-room-dim-bottom .hm-room-dim-line::after {
    right: 0;
    top: 50%;
    transform: translate(0, -50%);
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    border-left: 7px solid var(--bs-emphasis-color);
}
/* Vertical lines: same logic rotated. */
.hm-room-dim-left .hm-room-dim-line::before,
.hm-room-dim-right .hm-room-dim-line::before {
    top: 0;
    left: 50%;
    transform: translate(-50%, 0);
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-bottom: 7px solid var(--bs-emphasis-color);
}
.hm-room-dim-left .hm-room-dim-line::after,
.hm-room-dim-right .hm-room-dim-line::after {
    bottom: 0;
    left: 50%;
    transform: translate(-50%, 0);
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 7px solid var(--bs-emphasis-color);
}
/* Endpoint ticks — short perpendicular bars at each line end, going
   from the dim line INTO the gap between line and frame. CAD-style
   anchor that visually pins the dimension to the corners of the
   measured extent. Drawn on .hm-room-dim's pseudo-elements (the
   line's are taken by arrowheads). Position at the line endpoints —
   for mosaic dims that's offset inward from the wrapper edges by
   (1 - mw/mh-ratio) / 2; for outer-frame dims it's at the wrapper
   edges. */
.hm-room-dim::before,
.hm-room-dim::after {
    content: "";
    position: absolute;
    background: var(--bs-emphasis-color);
    pointer-events: none;
}
/* Horizontal dims: 1px × 14px vertical ticks. */
.hm-room-dim-top::before,
.hm-room-dim-top::after,
.hm-room-dim-bottom::before,
.hm-room-dim-bottom::after {
    width: 1px;
    height: 14px;
}
.hm-room-dim-top::before, .hm-room-dim-top::after { bottom: 4px; }
.hm-room-dim-bottom::before, .hm-room-dim-bottom::after { top: 4px; }
.hm-room-dim-mosaic.hm-room-dim-top::before,
.hm-room-dim-mosaic.hm-room-dim-bottom::before {
    left: calc((100% - var(--hm-mw-ratio, 100%)) / 2);
}
.hm-room-dim-mosaic.hm-room-dim-top::after,
.hm-room-dim-mosaic.hm-room-dim-bottom::after {
    right: calc((100% - var(--hm-mw-ratio, 100%)) / 2);
}
.hm-room-dim-frame.hm-room-dim-top::before,
.hm-room-dim-frame.hm-room-dim-bottom::before { left: 0; }
.hm-room-dim-frame.hm-room-dim-top::after,
.hm-room-dim-frame.hm-room-dim-bottom::after { right: 0; }
/* Vertical dims: 14px × 1px horizontal ticks. */
.hm-room-dim-left::before,
.hm-room-dim-left::after,
.hm-room-dim-right::before,
.hm-room-dim-right::after {
    width: 14px;
    height: 1px;
}
.hm-room-dim-left::before, .hm-room-dim-left::after { right: 4px; }
.hm-room-dim-right::before, .hm-room-dim-right::after { left: 4px; }
.hm-room-dim-mosaic.hm-room-dim-left::before,
.hm-room-dim-mosaic.hm-room-dim-right::before {
    top: calc((100% - var(--hm-mh-ratio, 100%)) / 2);
}
.hm-room-dim-mosaic.hm-room-dim-left::after,
.hm-room-dim-mosaic.hm-room-dim-right::after {
    bottom: calc((100% - var(--hm-mh-ratio, 100%)) / 2);
}
.hm-room-dim-frame.hm-room-dim-left::before,
.hm-room-dim-frame.hm-room-dim-right::before { top: 0; }
.hm-room-dim-frame.hm-room-dim-left::after,
.hm-room-dim-frame.hm-room-dim-right::after { bottom: 0; }
/* Dashed extension lines — connect each mosaic dim-line tick to the
   cell's corresponding corner so the dimension visually anchors to the
   measurement points. Four lines: vertical pair (top dim → cell top
   edge) at left + right cell-x positions, horizontal pair (left dim →
   cell left edge) at top + bottom cell-y positions. Positioned at
   wrapper level (NOT inside dim children) so % offsets resolve against
   the wrapper height/width directly. Length composes the 18px gap
   between dim line and frame outer edge with the cell-inset
   percentage of wrapper extent — calc() handles the px+% mix.

   Pattern via background-image linear-gradient (NOT border-style:
   dashed) — gives a precise 2px-dash / 2px-gap rhythm at 1px width,
   much finer than the browsers' default dashed-border cadence which
   is ~3px / 3px and reads heavy at this scale. May 2026 follow-up. */
.hm-room-mosaic-ext {
    position: absolute;
    pointer-events: none;
}
/* Vertical extensions: from above-wrapper (at top dim's tick height)
   DOWN to the cell top edge inside the wrapper. */
.hm-room-mosaic-ext-top-left,
.hm-room-mosaic-ext-top-right {
    width: 1px;
    top: -18px;
    height: calc(18px + var(--hm-my-inset, 0%));
    background-image: linear-gradient(to bottom, var(--bs-emphasis-color) 50%, transparent 50%);
    background-size: 1px 4px;
    background-repeat: repeat-y;
}
.hm-room-mosaic-ext-top-left  { left:  var(--hm-mx-inset, 0%); }
.hm-room-mosaic-ext-top-right { right: var(--hm-mx-inset, 0%); }
/* Horizontal extensions: from left-of-wrapper (at left dim's tick X)
   RIGHT to the cell left edge inside the wrapper. */
.hm-room-mosaic-ext-left-top,
.hm-room-mosaic-ext-left-bottom {
    height: 1px;
    left: -18px;
    width: calc(18px + var(--hm-mx-inset, 0%));
    background-image: linear-gradient(to right, var(--bs-emphasis-color) 50%, transparent 50%);
    background-size: 4px 1px;
    background-repeat: repeat-x;
}
.hm-room-mosaic-ext-left-top    { top:    var(--hm-my-inset, 0%); }
.hm-room-mosaic-ext-left-bottom { bottom: var(--hm-my-inset, 0%); }

/* Pill label — themed via Bootstrap CSS vars so it auto-adapts to dark
   mode without overrides. Reads on any wall colour. */
.hm-room-dim-pill {
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-emphasis-color);
    color: var(--bs-emphasis-color);
    border-radius: 999px;
    padding: 2px 8px;
    white-space: nowrap;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.hm-room-dim-pill strong { font-weight: 600; margin-right: 4px; }
/* Vertical pills — writing-mode rotation so the text reads along the
   dim line direction. */
.hm-room-dim-left .hm-room-dim-pill,
.hm-room-dim-right .hm-room-dim-pill {
    writing-mode: vertical-rl;
    text-orientation: mixed;
    padding: 8px 2px;
}
.hm-room-dim-left .hm-room-dim-pill { transform: rotate(180deg); }
/* ================= End room preview ================= */

/* About modal — hero hexagon glyph with depth + Built-with stack columns.
   Logo: stacked drop-shadow filter creates the "extruded hexagon" look —
   first shadow is a solid darker green offset down (the side wall), second
   is a soft ambient shadow (grounds it). Mimics the cell-preview 3D look
   while keeping the top-down hex outline. Accent-hover token gives a
   theme-consistent darker-green for the side wall. */
.hm-about-logo {
    font-size: 3.25rem;
    line-height: 1;
    display: inline-block;
    filter:
        drop-shadow(0 3px 0 var(--hm-accent-hover))
        drop-shadow(0 6px 8px rgba(0, 0, 0, 0.22));
}
/* Built-with stack — CSS multi-column layout so items flow top-to-bottom
   in column 1 then column 2. Lets us group related entries (e.g. all
   SixLabors libraries) together by putting them sequentially in the
   markup. break-inside: avoid stops the browser splitting a single
   entry across columns mid-line. */
.hm-about-stack {
    column-count: 2;
    column-gap: 2rem;
}
/* Mobile — collapse to a single column so the right-hand entries
   (which `white-space: nowrap` would otherwise clip off-screen) sit
   on top of each other instead. Drop `nowrap` too so any single
   long entry can wrap onto a second line if it really needs to. */
@media (max-width: 540px) {
    .hm-about-stack {
        column-count: 1;
    }
    .hm-about-stack > div {
        white-space: normal !important;
    }
}
.hm-about-stack > div {
    break-inside: avoid;
    page-break-inside: avoid;
    line-height: 1.7;
    white-space: nowrap;
}
/* ================= End About modal ================= */

/* ================= Empty-state action buttons ================= */
/* Tighten the two CTAs in EmptyState so they read as a deliberate pair:
   primary action filled (Choose an image), secondary action outlined (Start
   with a blank canvas). In light mode the outlined button switches to a
   strong neutral grey — the accent cyan looks washed-out against white,
   while a dark-grey outline + text pops cleanly. Dark mode keeps the accent. */
.hm-empty-state .btn-primary {
    min-width: 200px;
}
/* The previous .hm-empty-state .btn-outline-primary special-case (light mode
   grey, dark mode cyan accent) was removed — home-screen cards now use
   filled .btn-primary, and the global .btn-outline-primary rule below
   handles every other call site uniformly with the theme accent. */

/* ================= Empty-state "or" divider ================= */
/* Used by EmptyState.razor between the "Upload an image" prompt and the
   "Start with a blank canvas" alternative. A horizontal rule with the word
   "or" set into the middle — communicates "two equal options, pick one". */
.hm-empty-divider {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 240px;
    color: var(--bs-secondary-color);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.hm-empty-divider::before,
.hm-empty-divider::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--bs-border-color);
}
/* ================= End empty-state divider ================= */

/* ================= Empty-state v2 — three-card home screen ================= */
/* New home screen lays out three project-creation paths side-by-side:
     [Load image]   [Blank canvas]   [Random cells]
   Each card is a clickable surface (button or label) with an icon, title,
   short description, and CTA. Cards stack vertically on narrow viewports.
   The wrapper centres horizontally and gives the whole stack room to breathe. */
.hm-empty-state-v2 {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 32px;
    min-height: 60vh;
    padding: 32px 16px;
}
.hm-empty-hero {
    max-width: 640px;
}
.hm-empty-cards {
    display: grid;
    /* 4 cards in a 2×2 layout on desktop — the four entry points (Load
       image / Generate AI / Blank canvas / Random cells) read as two
       balanced rows rather than one wide row, which kept individual
       cards too narrow for their copy. Mobile collapses to a single
       column via the breakpoint below. */
    grid-template-columns: repeat(2, minmax(260px, 1fr));
    gap: 16px;
    width: 100%;
    max-width: 720px;
    /* Stretch every card to the height of the tallest one in the row so
       all CTAs sit on the same baseline within each row. */
    align-items: stretch;
}
.hm-empty-caption-row {
    /* Mirrors the cards grid so the caption in column 1 lines up under
       card 1 on desktop. With the 2×2 layout the caption still sits
       below the cards, in the left column under "Load image". */
    display: grid;
    grid-template-columns: repeat(2, minmax(260px, 1fr));
    gap: 16px;
    width: 100%;
    max-width: 720px;
}

/* Returning-user "Open a project" card sits below the four create-new
   options, separated by a subtle divider so the visual grouping
   reads as "create new (above) / open existing (below)". The card
   itself uses lighter weight than the four primaries (no solid
   primary CTA — outline-secondary instead) so it reads as a
   companion option rather than a fifth headline.

   Width matches the cards-row (max-width: 720px) so it visually
   anchors under the 2×2 grid above. Single column always; the
   caption row already covers desktop alignment of the upload
   hint. */
.hm-empty-projects-divider {
    width: 100%;
    max-width: 720px;
    border-top: 1px solid var(--bs-border-color);
    margin-top: 8px;
    padding-top: 8px;
}
.hm-empty-projects-card {
    width: 100%;
    max-width: 720px;
    /* Slightly muted vs the four primaries — lower-key visual weight,
       since "open existing" is supportive, not the headline action. */
    background: var(--bs-secondary-bg);
}
/* Landing screen — 2 or 3 cards (New / [Load Previous] / Import) shown
   on first session load. auto-fill with minmax so 2 cards centre
   naturally when "Load previous" is hidden (new user). */
.hm-landing-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
    width: 100%;
    max-width: 720px;
    align-items: stretch;
}
@media (max-width: 767.98px) {
    .hm-landing-cards {
        grid-template-columns: 1fr;
        max-width: 420px;
    }
}
/* Tablet/mobile breakpoint — Bootstrap md/sm boundary. Below 768px the
   four cards collapse from the 2×2 desktop layout to a single column
   (1×n: one card per row, four rows), and the caption row follows suit
   so the hint sits naturally below the first card in the linear flow. */
@media (max-width: 767.98px) {
    .hm-empty-cards,
    .hm-empty-caption-row {
        grid-template-columns: 1fr;
        max-width: 420px;
    }
}
.hm-empty-card {
    /* Reset native button + label styles so the card surface is uniform.
       Card background uses --bs-tertiary-bg which sits one notch above
       --bs-body-bg in both light and dark themes — gives the card a
       subtle "raised" tint without hard-coding a colour. Default elevation
       shadow is light but visible enough to read as a card surface. */
    appearance: none;
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
    border-radius: 14px;
    padding: 24px 20px 20px;
    cursor: pointer;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04),
                0 2px 6px rgba(0, 0, 0, 0.05);
    transition: border-color 0.18s ease,
                transform 0.12s ease,
                box-shadow 0.18s ease,
                background-color 0.18s ease;
    /* Ensure label-style cards (with hidden InputFile inside) aren't affected
       by their child <input>'s default block layout. */
    margin: 0;
}
.hm-empty-card:hover {
    /* Stronger lift + accent-tinted border + halo shadow signals "this is
       interactive". Background brightens slightly via Bootstrap's
       --bs-secondary-bg (one notch lighter than tertiary in light mode,
       one notch lighter than body in dark mode). */
    background: var(--bs-secondary-bg);
    border-color: var(--hm-accent, #3B82F6);
    transform: translateY(-3px);
    box-shadow: 0 6px 14px rgba(0, 0, 0, 0.08),
                0 12px 28px rgba(0, 0, 0, 0.10);
}
.hm-empty-card:active {
    /* Pressed-down feedback — collapses the lift + tightens the shadow so
       a click feels physical. Quick transition catches the press moment. */
    transform: translateY(-1px);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08),
                0 2px 6px rgba(0, 0, 0, 0.06);
    transition-duration: 0.05s;
}
.hm-empty-card:focus-visible {
    outline: 2px solid var(--hm-accent, #3B82F6);
    outline-offset: 2px;
}
/* Dark mode: shadows on a dark bg are mostly invisible. A faint accent
   glow on hover gives the same "lift" feedback that shadows give in light
   mode without faking a black-on-black shadow. */
[data-bs-theme="dark"] .hm-empty-card {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3),
                0 2px 6px rgba(0, 0, 0, 0.25);
}
[data-bs-theme="dark"] .hm-empty-card:hover {
    box-shadow: 0 6px 14px rgba(0, 0, 0, 0.4),
                0 12px 28px rgba(0, 0, 0, 0.35),
                0 0 0 1px rgba(59, 130, 246, 0.15);
}
.hm-empty-card-icon {
    font-size: 2.4rem;
    line-height: 1;
    color: var(--hm-accent, #3B82F6);
    margin-bottom: 4px;
}
.hm-empty-card-title {
    font-weight: 600;
    font-size: 1.05rem;
}
.hm-empty-card-body {
    font-size: 0.85rem;
    color: var(--bs-secondary-color);
    margin: 0;
    flex-grow: 1;
}
.hm-empty-card-cta {
    /* margin-top:auto pushes the CTA to the bottom of the card regardless
       of how tall or short the body text is — so all three CTAs sit on
       the same baseline across the row. */
    margin-top: auto;
    padding-top: 12px;
}
.hm-empty-card-caption {
    /* Span the full caption-row grid (`1 / -1`) so the hint centres
       under the entire card grid — not just column 1. With the 2×2
       card layout the caption now sits balanced beneath the bottom
       row (Blank canvas + Random cells), reading as a global hint
       rather than a card-1-specific one. */
    grid-column: 1 / -1;
    text-align: center;
    padding: 0 4px;
}

/* First-run filament onboarding card — sits between the hero and the
   four entry-point cards on EmptyState. Subtle accent treatment so it
   reads as guidance, not a competing primary action. Hidden once
   inventory is populated OR the user dismisses it (option D, May 2026). */
.hm-empty-onboarding {
    display: flex;
    align-items: center;
    gap: 16px;
    width: 100%;
    max-width: 720px;
    padding: 14px 18px;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-left: 3px solid var(--hm-accent);
    border-radius: 8px;
    margin-top: 4px;
}
.hm-empty-onboarding-icon {
    font-size: 1.6rem;
    color: var(--hm-accent);
    flex-shrink: 0;
    line-height: 1;
}
.hm-empty-onboarding-body {
    flex-grow: 1;
    min-width: 0;
}
.hm-empty-onboarding-title {
    font-weight: 600;
    color: var(--bs-emphasis-color);
}
.hm-empty-onboarding-desc {
    margin-top: 2px;
}
.hm-empty-onboarding-actions {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
}
@media (max-width: 600px) {
    .hm-empty-onboarding {
        flex-direction: column;
        text-align: center;
    }
    .hm-empty-onboarding-actions {
        flex-wrap: wrap;
        justify-content: center;
    }
}
/* ================= End empty-state v2 ================= */

/* ================= AiImageModal — preview area =================
   Shown inside the modal between the prompt + canvas controls and the
   footer buttons. Three states: empty (before generation), loading
   (during the network call), and image (after a successful response).
   Errors render via Bootstrap's .alert-danger so we don't need a custom
   error skin. */
.hm-ai-preview-empty,
.hm-ai-preview-loading {
    border: 1px dashed var(--bs-border-color);
    border-radius: 6px;
    padding: 28px 16px;
    text-align: center;
    background: var(--bs-tertiary-bg);
}
.hm-ai-preview-image {
    text-align: center;
}
.hm-ai-preview-image img {
    max-width: 100%;
    /* Cap the preview height so a tall portrait result doesn't push the
       footer off the modal viewport. The full-resolution bytes still get
       handed to the build pipeline; this is just the in-modal thumbnail. */
    max-height: 320px;
    border-radius: 6px;
    border: 1px solid var(--bs-border-color);
    object-fit: contain;
    background: var(--bs-tertiary-bg);
}
/* ================= End AiImageModal ================= */

/* ================= RandomCellsModal + SettingsPanel batch summary ================= */
/* Small colour swatch shown next to each batch row in SettingsPanel's
   random-cells summary. Matches the FilamentPicker swatch shape so the
   visual language is consistent across the app. */
.hm-rc-batch-swatch {
    display: inline-block;
    width: 18px;
    height: 18px;
    border-radius: 3px;
    border: 1px solid var(--bs-border-color);
    flex-shrink: 0;
}
/* Batch table inside RandomCellsModal — kept compact so a 5-batch project
   fits on screen without scrolling. Cells use Bootstrap's table-sm
   defaults; we tighten vertical padding a little more for density. */
.hm-rc-batch-table th,
.hm-rc-batch-table td {
    padding: 0.35rem 0.5rem;
    vertical-align: middle;
}
.hm-rc-batches {
    /* Allow the table to overflow horizontally on narrow viewports rather
       than mangling the layout. The modal itself caps at 780px wide. */
    overflow-x: auto;
}

/* Suppress the up/down spinner buttons on number inputs inside the Random
   Cells modal. Native spinners eat ~20 px of trigger width on Chromium and
   the cells/min/max columns are narrow enough that they squeeze the actual
   digits out of view. !important is needed because Bootstrap's form-control
   has its own appearance rules (notably `appearance: none` for some inputs
   that re-establishes the default spinner styling on number type). The user
   types the number — +/- buttons are dead weight here.
   We attach the rule via the modal's host class (`hm-rc-modal`) which we
   apply on the ModalShell body wrapper, so it's robust to subtree changes
   without scoping per-table. */
.hm-rc-modal input[type="number"]::-webkit-outer-spin-button,
.hm-rc-modal input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none !important;
    appearance: none !important;
    margin: 0 !important;
}
.hm-rc-modal input[type="number"] {
    -moz-appearance: textfield !important;
    appearance: textfield !important;
}

/* Compact CellShapePicker — used in the Random Cells batch table's Shape
   column. Keeps the shape thumbnail (so the user can identify the selection
   at a glance) but tightens the chrome so the trigger fits in a narrow
   tabular cell: smaller padding, smaller text, no excess whitespace between
   the thumb and the label. */
.hm-shape-picker.hm-shape-picker-compact .hm-shape-picker-trigger {
    padding: 0.15rem 1.4rem 0.15rem 0.4rem; /* leave room for the chevron */
    font-size: 0.72rem;  /* slightly smaller than the default 0.78rem so long
                            shape names ("Cylinder — round top") don't truncate
                            in the narrow batch-table column */
    gap: 0.3rem;
    line-height: 1.2;
}
.hm-shape-picker.hm-shape-picker-compact .hm-shape-picker-thumb {
    flex: 0 0 18px;
    width: 18px;
    height: 18px;
}
.hm-shape-picker.hm-shape-picker-compact .hm-shape-picker-name {
    /* Truncate with ellipsis rather than wrap — keeps row height tight even
       on long shape names ("Random tilted quad", "Cylinder — round top"). */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ----- Batches summary-card in SettingsPanel ----- */
/* The card itself extends .hm-summary-card. Override its default flex layout
   (icon + details + chevron in a single row) so the body fills the full
   width with a stack of rich batch rows. */
.hm-rc-batches-card {
    align-items: stretch;
    text-align: left;
    padding: 8px 12px;
    width: 100%;
}
.hm-rc-batches-card .hm-summary-chevron {
    align-self: center;
}
.hm-rc-batches-list {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}
.hm-rc-batch-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 4px 0;
    border-bottom: 1px solid var(--bs-border-color);
    min-width: 0;
}
.hm-rc-batch-row:last-child {
    border-bottom: none;
}
.hm-rc-batch-num {
    color: var(--bs-secondary-color);
    font-size: 0.75rem;
    width: 1.2rem;
    text-align: center;
    flex-shrink: 0;
}
.hm-rc-batch-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
}
.hm-rc-batch-name-line {
    font-size: 0.85rem;
    font-weight: 500;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
}
.hm-rc-batch-material {
    font-size: 0.7rem;
    color: var(--bs-secondary-color);
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
}
.hm-rc-batch-ams {
    color: var(--hm-accent, #3B82F6);
    margin-left: 4px;
    font-size: 0.75rem;
}
.hm-rc-batch-stats {
    flex-shrink: 0;
    text-align: right;
    font-size: 0.78rem;
    line-height: 1.2;
    color: var(--bs-body-color);
}
.hm-rc-batch-stats-sub {
    color: var(--bs-secondary-color);
    font-size: 0.7rem;
}
.hm-rc-batch-shape {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--bs-secondary-color);
}
.hm-rc-batch-shape svg {
    display: block;
}
/* Total-cells caption under the batches card. Sits like a footnote — small
   and right-aligned so it reads as a quiet summary of the rows above. */
.hm-rc-total {
    text-align: right;
    margin-top: -8px;
    padding-right: 4px;
}
/* ================= End random-cells styles ================= */

/* ================= Slicer Config — printer preset row ================= */
/* The Apply button sits next to a form-select-sm in a flex row with
   align-items:stretch. Without these rules the button's intrinsic height
   ends up larger than the select (Bootstrap btn-sm has slightly different
   vertical padding from form-select-sm). flex centring of the content
   keeps the icon + label aligned in the stretched-height button. */
.hm-preset-apply {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    flex-shrink: 0;
    /* Explicit min-height keyed to form-select-sm's computed height
       (~31px at default font size) so even when the row collapses to
       single column on narrow viewports, the button stays compact. */
    min-height: 31px;
}
/* ================= End preset row ================= */

/* ================= Fast tooltips ================= */
/* Custom tooltip rendered by enableFastTooltips() in interop.js. Replaces
   the browser's native tooltip (which has a slow, non-tunable show delay).
   Position is set inline via JS; the rest is style. */
.hm-fast-tooltip {
    position: fixed;
    z-index: 9999;
    background: rgba(20, 22, 26, 0.94);
    color: #f5f5f5;
    padding: 5px 9px;
    border-radius: 5px;
    font-size: 0.78rem;
    line-height: 1.35;
    pointer-events: none;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.32);
    max-width: 320px;
    white-space: normal;
}

/* ================= Cell Painter layer panel ================= */
/* Vertical panel pinned to the left edge of the canvas while Cell Painter
   mode is active. Each row is a layer (Default + each used shape + any
   user-added shapes): click main area to activate, click eye to hide/show.
   Hidden layers' cells render as outlines (no fill) in the SVG — actually
   easier to scan than the previous low-opacity approach because the cells
   stay structurally visible. */
.hm-cellpainter-layers {
    position: absolute;
    top: 12px;
    bottom: 12px;       /* span the full canvas height */
    left: 12px;
    z-index: 10;
    width: 240px;
    /* Translucent background so the canvas behind shows through faintly —
       gives the user a sense of where the cells are even when the panel
       overlaps them. backdrop-filter blur softens whatever's underneath
       so the layer text stays legible. color-mix on the body bg with 75%
       opacity hits the "barely visible" target. */
    background: color-mix(in srgb, var(--bs-body-bg) 75%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
    display: flex;
    flex-direction: column;
    overflow: hidden;   /* outer container clips; inner list handles scroll */
}
/* Inner scroll region holds the layer rows. Header + actions sit outside so
   they stay anchored top + bottom while the rows scroll. */
.hm-cellpainter-layers-list {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 0 8px 8px 8px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.hm-cellpainter-layers-header {
    flex: 0 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--bs-secondary-color);
    padding: 8px 10px;
    border-bottom: 1px solid var(--bs-border-color);
}
/* All on / All off mini-actions on the right of the header. Plain text
   buttons (btn-link) so they read as utility commands rather than
   primary actions. */
.hm-cellpainter-layers-toggleall .btn-link {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    text-decoration: none;
}
.hm-cellpainter-layers-toggleall .btn-link:hover {
    text-decoration: underline;
}

/* Mobile-collapsed layer panel — only the header bar shows, with the
   chevron toggle visible. The list + actions sections are hidden
   while the panel is collapsed so the canvas underneath isn't
   obscured. Transitions kept subtle (height animation would fight
   the absolute-positioning + flex content).
   IMPORTANT: gated behind the mobile breakpoint so resizing the window
   back up to desktop auto-restores the expanded view, regardless of
   the component-local `_layersPanelCollapsed` flag. The toggle button
   itself is `d-md-none` so on desktop the user can't get into a
   collapsed state via UI; this rule guarantees they aren't trapped
   in one if they collapsed on mobile and then resized. */
@media (max-width: 768px) {
    .hm-cellpainter-layers--collapsed .hm-cellpainter-layers-list,
    .hm-cellpainter-layers--collapsed .hm-cellpainter-layers-actions {
        display: none;
    }
    .hm-cellpainter-layers--collapsed {
        bottom: auto;       /* let the panel auto-shrink to header height */
        height: auto;
    }
}
@media (max-width: 768px) {
    /* Narrower panel on phones so it doesn't dominate the canvas. */
    .hm-cellpainter-layers { width: 200px; }
}
@media (max-width: 540px) {
    .hm-cellpainter-layers {
        width: calc(100% - 24px);
        right: 12px;
    }
}
.hm-cellpainter-layers-actions {
    flex: 0 0 auto;
    padding: 8px;
    border-top: 1px solid var(--bs-border-color);
}
.hm-layer-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 6px;
    border: 1px solid transparent;
    border-radius: 6px;
    cursor: pointer;
    user-select: none;
    transition: background 80ms, border-color 80ms;
}
.hm-layer-row:hover {
    background: color-mix(in srgb, var(--hm-accent) 8%, transparent);
}
.hm-layer-row.active {
    background: color-mix(in srgb, var(--hm-accent) 18%, transparent);
    border-color: var(--hm-accent);
}
.hm-layer-row.hidden {
    opacity: 0.55;
}
.hm-layer-row-icon {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* No background or border — let the SVG icon sit cleanly against the
       panel so the colour and shape are the visual anchor. */
    /* Inherited `color` for the SVG (uses currentColor) so the layered
       shape icon picks up the theme accent. */
    color: var(--hm-accent);
}
.hm-layer-row-icon svg { width: 100%; height: 100%; }
.hm-layer-row-name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.82rem;
}
.hm-layer-row-count {
    flex: 0 0 auto;
    color: var(--bs-secondary-color);
    font-variant-numeric: tabular-nums;
    font-size: 0.74rem;
}
.hm-layer-row-eye {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: none;
    background: transparent;
    border-radius: 50%;
    color: var(--bs-secondary-color);
    cursor: pointer;
}
.hm-layer-row-eye:hover {
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
}
/* Per-plate "Printed" tick in the print-guide side panel. Same shape
   as the eye toggle so the row keeps a tidy fixed-width control
   column, but colour comes from inline style (state-driven —
   accent / amber / secondary). May 2026 plate-printed tracker. */
.hm-layer-row-printed {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: none;
    background: transparent;
    border-radius: 50%;
    cursor: pointer;
    transition: transform 80ms ease;
}
.hm-layer-row-printed:hover {
    background: var(--bs-body-bg);
    transform: scale(1.05);
}

/* "+ Add layer" popover — small floating list of grid-compatible shapes
   that aren't already a layer. Lives anchored to the Add-layer button. */
.hm-add-layer-popover {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: 50;
    min-width: 220px;
    max-height: 320px;
    overflow-y: auto;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22);
    padding: 4px;
}
.hm-add-layer-popover-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 8px;
    background: transparent;
    border: none;
    border-radius: 4px;
    width: 100%;
    text-align: left;
    color: var(--bs-body-color);
    cursor: pointer;
}
.hm-add-layer-popover-row:hover {
    background: var(--bs-tertiary-bg);
}
/* ================= End Cell Painter layer strip ================= */

/* ================= Neighbourhood 3D mini preview ================= */
/* Floating card in the top-right of the 2D canvas showing a small 3x3
   patch of cells around the hovered cell while the rotation tool is
   active. Persistent Three.js context. */
.hm-neighbourhood-preview {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 10;
    width: 296px;
    background: color-mix(in srgb, var(--bs-body-bg) 70%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
    overflow: hidden;
    pointer-events: none;  /* don't intercept canvas pointer events */
}
.hm-neighbourhood-header {
    padding: 4px 8px;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-neighbourhood-preview > div:last-child {
    /* The Three.js container */
    border-radius: 0 0 8px 8px;
}
@media (max-width: 768px) {
    /* Hide on mobile — too small to be useful and covers the canvas. */
    .hm-neighbourhood-preview { display: none; }
}
/* ================= End Neighbourhood 3D mini preview ================= */

/* ================= Filament library CSV import modal ================= */
/* Section blocks for the Add / Update / Remove groupings in
   FilamentImportModal. Each section gets a header row with a master checkbox,
   colour-coded badge, and count summary. */
.hm-import-section {
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    padding: 8px 10px;
    margin-bottom: 12px;
    background: var(--bs-tertiary-bg);
}
.hm-import-section-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
    font-weight: 500;
}
/* ================= End CSV import modal ================= */

/* ================= Filament library table ================= */
/* Tab strip above the library card — switches between the "My filaments"
   inventory view and the "Catalogue" browser. Underline-style active
   state matches the rest of the app's flat-tab idiom. May 2026 — option D
   refactor adds the Catalogue tab as the primary path to populate
   inventory after the auto-seed was removed. */
.hm-fil-tabs {
    display: flex;
    gap: 4px;
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-fil-tab {
    background: transparent;
    border: none;
    padding: 8px 14px;
    font-size: 0.9rem;
    color: var(--bs-secondary-color);
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.hm-fil-tab:hover {
    color: var(--bs-emphasis-color);
}
.hm-fil-tab.active {
    color: var(--bs-emphasis-color);
    border-bottom-color: var(--hm-accent);
    font-weight: 600;
}

/* Mobile (≤ 768 px) — the four-tab strip overflows on narrow viewports
   and hides the rightmost tab (Materials). Hide the strip behind a
   hamburger trigger button rendered above it; clicking the trigger
   flips `.hm-fil-tabs--mobile-open` on the strip and the tabs become
   a stacked-vertical menu beneath the trigger. The trigger itself is
   `display: none` ≥ 769 px so desktop is unchanged. */
.hm-fil-tabs-trigger {
    /* Bootstrap d-md-none hides this on ≥ 768 px viewports. */
}
@media (max-width: 767.98px) {
    .hm-fil-tabs {
        display: none;
        flex-direction: column;
        gap: 2px;
        border-bottom: none;
        border: 1px solid var(--bs-border-color);
        border-radius: 8px;
        padding: 4px;
        background: var(--bs-tertiary-bg);
    }
    .hm-fil-tabs.hm-fil-tabs--mobile-open {
        display: flex;
    }
    .hm-fil-tab {
        padding: 10px 14px;
        border-radius: 6px;
        border-bottom: none;
        margin-bottom: 0;
        justify-content: flex-start;
        width: 100%;
    }
    .hm-fil-tab.active {
        background: var(--bs-secondary-bg);
        border-bottom: none;
    }
}

/* Wishlist row distinction — muted text-secondary so the wishlist Filament
   reads as "buy-prompt, not stock you own". Bookmark badge in the name
   column carries the explicit label; this row tint reinforces it. The
   row's editable behaviour is unchanged. */
.hm-fil-table tr.hm-fil-row-wishlist td {
    color: var(--bs-secondary-color);
    font-style: italic;
}
.hm-fil-table tr.hm-fil-row-wishlist .hm-fil-row-name {
    color: var(--bs-emphasis-color);
    font-style: normal;
}

/* ================= Landing page (public "/") ================= */

.hm-landing-layout {
    overflow-y: auto;
    height: 100vh;
    background: var(--bs-body-bg);
    scroll-behavior: smooth;
}
/* Offset anchor targets below the sticky nav (56px + 16px breathing room) */
.hm-landing-layout [id] {
    scroll-margin-top: 72px;
}

/* ── Nav ── */
.hm-landing-nav {
    position: sticky;
    top: 0;
    z-index: 1030;
    background: rgba(var(--bs-body-bg-rgb, 255, 255, 255), 0.85);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--bs-border-color);
}
[data-bs-theme="dark"] .hm-landing-nav {
    background: rgba(18, 20, 22, 0.85);
}
.hm-landing-nav-inner {
    max-width: 1120px;
    margin: 0 auto;
    padding: 0 24px;
    height: 56px;
    display: flex;
    align-items: center;
    gap: 24px;
}
.hm-landing-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 600;
    font-size: 1.05rem;
    color: var(--bs-body-color);
    text-decoration: none;
    white-space: nowrap;
}
.hm-landing-brand:hover { color: var(--bs-body-color); text-decoration: none; }
.hm-landing-nav-links {
    display: flex;
    gap: 20px;
    margin-left: auto;
}
.hm-landing-nav-links a {
    font-size: 0.88rem;
    color: var(--bs-secondary-color);
    text-decoration: none;
    transition: color 0.15s;
}
.hm-landing-nav-links a:hover { color: var(--bs-body-color); }
.hm-landing-nav-cta {
    margin-left: auto;
    display: flex;
    align-items: center;
    white-space: nowrap;
}
/* When links are visible, CTA doesn't need its own margin-left: auto */
.hm-landing-nav-links + .hm-landing-nav-cta { margin-left: 0; }

/* ── Container ── */
.hm-landing-container {
    max-width: 1120px;
    margin: 0 auto;
    padding: 0 24px;
}

/* ── Hero ── */
.hm-landing-hero {
    padding: 80px 0 64px;
    text-align: center;
}
.hm-landing-hero-title {
    font-size: clamp(2rem, 5vw, 3.2rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.15;
    margin-bottom: 20px;
    max-width: 600px;
    margin-left: auto;
    margin-right: auto;
    outline: none;
}
.hm-landing-hero-sub {
    font-size: 1.1rem;
    color: var(--bs-secondary-color);
    max-width: 600px;
    margin: 0 auto 32px;
    line-height: 1.6;
}
.hm-landing-hero-actions {
    display: flex;
    gap: 12px;
    justify-content: center;
    flex-wrap: wrap;
}

/* ── Sections ── */
.hm-landing-section {
    padding: 64px 0;
}
.hm-landing-section--alt {
    background: var(--bs-tertiary-bg);
}
.hm-landing-section-title {
    font-size: 1.75rem;
    font-weight: 700;
    text-align: center;
    margin-bottom: 8px;
}
.hm-landing-section-sub {
    text-align: center;
    color: var(--bs-secondary-color);
    max-width: 520px;
    margin: 0 auto 40px;
}

/* ── Feature grid ── */
.hm-landing-features {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 32px;
}
.hm-landing-feature {
    text-align: center;
}
.hm-landing-feature-icon {
    width: 48px;
    height: 48px;
    border-radius: 12px;
    background: var(--bs-tertiary-bg);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.25rem;
    color: var(--hm-accent);
    margin-bottom: 12px;
}
.hm-landing-section--alt .hm-landing-feature-icon {
    background: var(--bs-secondary-bg);
}
.hm-landing-feature h3 {
    font-size: 1rem;
    font-weight: 600;
    margin-bottom: 6px;
}
.hm-landing-feature p {
    font-size: 0.88rem;
    color: var(--bs-secondary-color);
    line-height: 1.55;
    margin: 0;
}

/* ── Steps ── */
.hm-landing-steps {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 32px;
    text-align: center;
}
.hm-landing-step-num {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--hm-accent);
    color: #fff;
    font-weight: 700;
    font-size: 1rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 12px;
}
.hm-landing-step h3 {
    font-size: 1rem;
    font-weight: 600;
    margin-bottom: 6px;
}
.hm-landing-step p {
    font-size: 0.88rem;
    color: var(--bs-secondary-color);
    line-height: 1.55;
    margin: 0;
}

/* ── Hero image ── */
.hm-landing-hero-img {
    margin-top: 48px;
    max-width: 900px;
    margin-left: auto;
    margin-right: auto;
}
.hm-landing-hero-img img {
    width: 100%;
    border-radius: 12px;
    border: 1px solid var(--bs-border-color);
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
[data-bs-theme="dark"] .hm-landing-hero-img img {
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}
/* Real screenshots replace placeholders — same frame treatment */
.hm-landing-showcase-img img {
    width: 100%;
    border-radius: 12px;
    border: 1px solid var(--bs-border-color);
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
[data-bs-theme="dark"] .hm-landing-showcase-img img {
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}

/* ── Showcase: alternating image + text rows ── */
.hm-landing-showcase {
    padding: 64px 0;
}
.hm-landing-showcase--alt {
    background: var(--bs-tertiary-bg);
}
.hm-landing-showcase-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 48px;
    align-items: center;
}
.hm-landing-showcase-row--reverse .hm-landing-showcase-img {
    order: 2;
}
.hm-landing-showcase-row--reverse .hm-landing-showcase-text {
    order: 1;
}
.hm-landing-showcase-badge {
    display: inline-block;
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--hm-accent);
    margin-bottom: 8px;
}
.hm-landing-showcase-text h2 {
    font-size: 1.5rem;
    font-weight: 700;
    margin-bottom: 12px;
    line-height: 1.25;
}
.hm-landing-showcase-text p {
    font-size: 0.95rem;
    color: var(--bs-secondary-color);
    line-height: 1.65;
    margin: 0;
}

/* ── CTA block ── */
.hm-landing-cta-block {
    margin-top: 32px;
}

/* ── Footer ── */
.hm-landing-footer {
    border-top: 1px solid var(--bs-border-color);
    padding: 24px 0;
}
.hm-landing-footer-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
}
.hm-landing-footer-brand {
    display: flex;
    align-items: center;
    gap: 6px;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--bs-body-color);
}
.hm-landing-footer-copy {
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
}

/* ── Responsive ── */
@media (max-width: 768px) {
    .hm-landing-hero { padding: 48px 0 40px; }
    .hm-landing-hero-img { margin-top: 32px; }
    .hm-landing-features { grid-template-columns: repeat(2, 1fr); gap: 24px; }
    .hm-landing-steps { grid-template-columns: repeat(2, 1fr); gap: 24px; }
    .hm-landing-section { padding: 48px 0; }
    .hm-landing-showcase { padding: 48px 0; }
    .hm-landing-showcase-row {
        grid-template-columns: 1fr;
        gap: 24px;
    }
    /* On mobile, always image on top, text below — regardless of reverse */
    .hm-landing-showcase-row--reverse .hm-landing-showcase-img { order: 0; }
    .hm-landing-showcase-row--reverse .hm-landing-showcase-text { order: 0; }
}
@media (max-width: 540px) {
    .hm-landing-features { grid-template-columns: 1fr; }
    .hm-landing-steps { grid-template-columns: 1fr; }
    .hm-landing-nav-inner { padding: 0 16px; gap: 12px; }
    .hm-landing-container { padding: 0 16px; }
    .hm-landing-footer-inner { justify-content: center; text-align: center; }
}

/* ================= Auth pages (login / register) ================= */
.hm-auth-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: var(--bs-tertiary-bg);
}
.hm-auth-card {
    width: 100%;
    max-width: 420px;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 12px;
    padding: 32px 28px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
}
.hm-auth-header {
    text-align: center;
    margin-bottom: 24px;
}
.hm-auth-form { margin-bottom: 16px; }
.hm-auth-footer {
    text-align: center;
    font-size: 0.9rem;
    padding-top: 16px;
    border-top: 1px solid var(--bs-border-color);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
}

/* ================= Mobile responsive helpers =================

   Three reusable patterns used across the app for narrow viewports:

   1. `.hm-stack-mobile` on a <table> — the table renders normally on
      desktop but collapses to a card-stacked layout at ≤ 540 px. Each
      <tr> becomes a padded card; each <td> becomes a flex row showing
      `[ data-label : value ]` so the column header inlines beside the
      value. Cells without a data-label collapse to value-only.
      Add a data-label attribute to every <td> you want labelled.

   2. `.hm-btn-label` — wrap the text portion of a button-with-icon in
      this span. The text is hidden ≤ 540 px so the button collapses to
      icon-only. `<button><i class="bi bi-x"></i><span class="hm-btn-label"> Save</span></button>`.

   3. `.hm-statusbar` already overflowed badly at narrow widths. Now
      switches to horizontal scroll on phones with `nowrap` segments and
      a subtle inertia-feel via `-webkit-overflow-scrolling: touch`.
*/
@media (max-width: 540px) {
    /* Pattern 1 — table-to-cards collapse. */
    .hm-stack-mobile thead { display: none; }
    .hm-stack-mobile,
    .hm-stack-mobile tbody,
    .hm-stack-mobile tr,
    .hm-stack-mobile td { display: block; width: 100%; }
    /* Per-row card on mobile — each filament gets its own bordered
       card with a subtle background tint. Theme-aware via
       `--bs-body-bg` (lifts the row off the outer card it sits in
       without going dark-mode-black or light-mode-white). Borders
       trace each row distinctly so the user reads them as separate
       items rather than a continuous list. */
    .hm-stack-mobile tbody tr {
        margin-bottom: 10px;
        padding: 10px 12px;
        border: 1px solid var(--bs-border-color);
        border-radius: 8px;
        background: var(--bs-body-bg);
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    }
    /* Totals row — emphasised by accent border + slightly stronger
       background tint. Still a card, not a stripe. */
    .hm-stack-mobile tbody tr.hm-totals-row {
        margin-top: 14px;
        background: var(--bs-tertiary-bg);
        border-color: var(--bs-border-color);
        border-width: 1px;
    }
    .hm-stack-mobile tbody td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        padding: 4px 0;
        text-align: right !important; /* override .text-end being redundant + force value-right */
        border: none;
    }
    /* Render the column label inline before the value. Empty
       data-label means "value only, no label" (e.g. swatch cell). */
    .hm-stack-mobile tbody td[data-label]:not([data-label=""])::before {
        content: attr(data-label);
        font-weight: 500;
        color: var(--bs-secondary-color);
        font-size: 0.85em;
        text-align: left;
    }
    /* The Filament cell carries multi-line content (display name,
       material, "merged" sub-line) — let it break naturally rather
       than fight the flex-row. */
    .hm-stack-mobile tbody td[data-label="Filament"] {
        flex-direction: column;
        align-items: flex-start;
        text-align: left !important;
    }
    .hm-stack-mobile tbody td[data-label="Filament"]::before {
        margin-bottom: 2px;
    }

    /* Pattern 2 — icon-only buttons. */
    .hm-btn-label { display: none; }

    /* Pattern 3 — status-bar horizontal scroll. */
    .hm-statusbar {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        flex-wrap: nowrap;
        scrollbar-width: thin;
    }
    .hm-statusbar-section,
    .hm-statusbar-divider,
    .hm-statusbar-spacer {
        flex-shrink: 0;
        white-space: nowrap;
    }
    /* The right-aligned weight/cost section was reached via .ms-auto;
       on horizontal scroll it just sits at the end of the scrolling
       row without forcing a flex grow. */
    .hm-statusbar-spacer { flex: 0 0 auto; min-width: 8px; }

    /* Top-bar — keep wordmark visible (smaller font, no-wrap) and
       collapse the action cluster's labels to icons-only via the
       `.hm-btn-label` rule above. The BETA pill also drops a few
       px in font-size + padding so it doesn't crowd the wordmark. */
    .hm-topbar { gap: 8px; padding: 6px 8px; }
    .hm-topbar-brand { font-size: 0.85rem; }
    .hm-topbar-wordmark { font-size: 0.85rem; }
    .hm-topbar-actions { gap: 4px; }
    .hm-beta-pill {
        font-size: 0.6rem;
        padding: 0.14rem 0.36rem;
        margin-left: 0.3rem;
        letter-spacing: 0.04em;
    }
}
/* Wordmark never wraps — applies at every viewport so "Mosaic 3D"
   doesn't break onto a separate line from "Print". */
.hm-topbar-wordmark {
    white-space: nowrap;
}
/* ================= End mobile responsive helpers ================= */

/* MaterialsView — Library workspace's "Materials" tab. Card-per-family
   layout: header (always visible — name + tagline + key pills) and
   body (visible only when expanded — full description + temp ranges +
   strengths/drawbacks/variants). Pure-CSS chevron rotation isn't used;
   the icon class flips between bi-chevron-up / bi-chevron-down in
   markup so screen readers see a meaningful icon name either way. */
.hm-mat-view {
    /* Container — wraps the toolbar bar + cards list. */
}
/* Cards list inset slightly from the container edges so the family
   cards read as content sitting inside the workspace, not full-width
   chrome. Horizontal padding on the wrapper keeps the responsive
   behaviour simple — cards still fill whatever width is left. */
.hm-mat-cards {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0 16px;
}
@media (max-width: 540px) {
    /* Tighten the inset on phones so we don't waste space when the
       container is already narrow. */
    .hm-mat-cards {
        padding: 0 6px;
    }
}
.hm-mat-card {
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
    background: var(--bs-body-bg);
    overflow: hidden;
}
.hm-mat-card-header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    width: 100%;
    background: var(--bs-tertiary-bg);
    border: none;
    text-align: left;
    cursor: pointer;
}
.hm-mat-card-header:hover {
    background: var(--bs-secondary-bg);
}
.hm-mat-card-titles {
    flex: 1 1 auto;
    min-width: 0;
}
.hm-mat-card-title {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px;
    font-size: 1rem;
}
.hm-mat-card-tagline {
    margin-top: 2px;
}
.hm-mat-card-pills {
    flex-shrink: 0;
}
.hm-mat-card-chevron {
    flex-shrink: 0;
    color: var(--bs-secondary-color);
}
.hm-mat-card-body {
    padding: 16px 12px 12px 12px;
    border-top: 1px solid var(--bs-border-color);
}
/* Narrow / mobile layout — at desktop widths the header is one row
   (titles | pills | chevron). On phones the pill cluster gets squeezed
   between the titles and the chevron, forcing the title block into a
   single-character-per-line wrap. Stack the rows: titles + chevron on
   row 1, pills wrap to row 2 below. Order overrides put pills last
   regardless of DOM order. */
@media (max-width: 540px) {
    .hm-mat-card-header {
        flex-wrap: wrap;
        align-items: flex-start;
    }
    .hm-mat-card-titles {
        flex: 1 1 auto;
        order: 1;
    }
    .hm-mat-card-chevron {
        order: 2;
    }
    .hm-mat-card-pills {
        flex-basis: 100%;
        order: 3;
        margin-top: 6px;
    }
}

/* Per-attribute pills in the card header. Same colour vocabulary as
   the StatusBar three-bucket badges (success / warning / danger) but
   rendered as filled pills instead of inline icons. */
.hm-mat-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 0.18rem 0.5rem;
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1;
    border-radius: 999px;
    white-space: nowrap;
}
.hm-mat-pill-success { background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); }
.hm-mat-pill-warning { background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); }
.hm-mat-pill-danger { background: var(--bs-danger-bg-subtle); color: var(--bs-danger-text-emphasis); }
.hm-mat-pill-neutral { background: var(--bs-secondary-bg); color: var(--bs-secondary-color); border: 1px solid var(--bs-border-color); }

/* Definition list inside the expanded card body — two-column layout,
   labels in muted colour. Used for "Print profile" + "Material flags"
   sections. */
.hm-mat-dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 4px 12px;
    margin: 0;
}
.hm-mat-dl dt {
    color: var(--bs-secondary-color);
    font-weight: 500;
    font-size: 0.85rem;
}
.hm-mat-dl dd {
    margin: 0;
    font-size: 0.9rem;
}

/* Use-case tags — small inline chips in "Best for" section. */
.hm-mat-tag {
    display: inline-block;
    padding: 0.18rem 0.5rem;
    font-size: 0.75rem;
    border-radius: 999px;
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
}

/* Strengths / drawbacks lists — tight, no left padding bullet. */
.hm-mat-list {
    padding-left: 18px;
    font-size: 0.9rem;
}
.hm-mat-list li {
    margin-bottom: 2px;
}

/* Variants grid — stack vertically on narrow viewports. */
.hm-mat-variants {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 8px;
}
.hm-mat-variant {
    padding: 8px 10px;
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
}
.hm-mat-variant-name {
    font-weight: 600;
    font-size: 0.9rem;
    margin-bottom: 2px;
}

/* Toolbar bar for the Materials tab — search input + difficulty
   filter pills + expand/collapse helpers. Rendered as a tinted
   panel-header strip so the controls read as workspace chrome
   distinct from the cards content below. Border-radius matches
   the cards inset; the divider beneath separates it from the
   cards list. */
.hm-mat-toolbar {
    padding: 12px 16px;
    margin-bottom: 12px;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 8px;
}

/* SharedProjectImportModal — list of filaments auto-imported into the
   user's wishlist on cross-user project load. Each row is a flex
   strip: swatch · name+meta · per-row actions. Mirrors the visual
   weight of the .hm-fil-table rows but without the table semantics —
   a review surface, not an editor. Schema v3.0 (May 2026). */
.hm-share-import-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.hm-share-import-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 10px;
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
}
.hm-share-import-row:hover {
    background: var(--bs-secondary-bg);
}

/* Filament status flags — small, consistent chips that render next to a
   filament name in the picker, palette mapping, etc. Three states:
     ★  ams  — owned + loaded in the AMS
     ✚  added — manually added via touch-up paint (palette-entry only)
     bookmark wish — on the user's wishlist; they don't own it yet
   Defined as a shared class so picker, palette table, and the legend
   below the mapping all share visuals. May 2026 — option D wishlist pass. */
.hm-fil-flag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    line-height: 1;
    font-weight: 600;
}
.hm-fil-flag-ams { color: var(--hm-accent, #f5a623); }
.hm-fil-flag-wish { color: var(--hm-info-text, #0a6e9e); font-style: normal; }
.hm-fil-flag-added { color: var(--hm-accent, #f5a623); }

/* Legend strip below the palette mapping table — chips of each flag
   above paired with a short label, separated by middots. Replaces the
   earlier prose paragraph that buried the icon meanings in running text
   and duplicated the height-strategy explanation already in the
   strategy section. */
.hm-fil-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 8px;
    padding: 6px 10px;
    margin-top: 6px;
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
}
.hm-fil-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.hm-fil-legend-sep {
    opacity: 0.5;
    user-select: none;
}

/* Palette banner rows — wider per-row banners (missing filament, catalogue
   suggestion) sit in their own <tr> spanning Filament+Height+Cells via
   colspan=3 so the banner copy fits on one line and the underlying picker
   row stays compact. CSS removes the row's bottom border + the next row's
   top border, visually fusing them into one logical entry. May 2026 —
   option D phase-2 banner layout. */
.hm-palette-banner-row > td {
    padding-top: 6px;
    padding-bottom: 0;
    border-bottom: none;
}
tr.hm-palette-row-after-banner > td {
    padding-top: 6px;
    border-top: none;
}

/* Card owns its own scroll so the sticky header sticks to the top of the
   card body, NOT the canvas. Otherwise the scrolled rows appeared above the
   sticky header in the canvas's padding area (canvas had overflow:auto, so
   it was the scroll container, and the sticky header lived inside it but
   below the padding). */
.hm-fil-card {
    display: flex;
    flex-direction: column;
    /* flex: 1 instead of height:100% so the card grows to fill the column-flex
       canvas without overflowing it (parent has min-height:0 implicitly via
       overflow:auto). */
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;  /* internal scroll happens in .card-body */
}
.hm-fil-card .card-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 0;
}
/* Sticky header strip with count + import/export/add buttons. Sticks to the
   top of the card body's own scroll container — fully opaque background so
   scrolled rows can't bleed through. */
.hm-fil-card .hm-fil-card-header {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    padding: 10px 14px;
    background: var(--bs-body-bg);
    border-bottom: 1px solid var(--bs-border-color);
}
.hm-fil-card .table-responsive { padding: 0 14px 14px; }
/* Read-only row layout — Filament cell shows "Brand Name" as the prominent
   line with Material as small secondary text. Mirrors the Estimate panel's
   visual hierarchy. The table is read-only here; editing happens in the
   FilamentEditModal opened by the pencil button per row. */
.hm-fil-table .hm-fil-row-name {
    font-weight: 500;
    line-height: 1.25;
}
/* Whole-row click target for the edit modal. Bootstrap's table-hover handles
   the row background via --bs-table-hover-bg on each <td>. We override that
   variable on the rows we want clickable (skipping if a future row needs to
   stay non-interactive) and add a pointer cursor. Accent-tinted so the hover
   reads as "this is interactive" rather than a passive table-row stripe. */
.hm-fil-table .hm-fil-row-clickable {
    cursor: pointer;
    --bs-table-hover-bg: color-mix(in srgb, var(--hm-accent) 14%, transparent);
    --bs-table-hover-color: var(--bs-body-color);
}
/* Larger swatch used in the edit modal header for at-a-glance preview. */
.hm-swatch-lg {
    width: 36px;
    height: 36px;
    border-radius: 6px;
    border: 1px solid var(--bs-border-color);
}
/* ================= End filament library table ================= */

/* ================= CellShapePicker ================= */
/* Custom shape-with-thumbnail dropdown. Same anchoring + popover machinery as
   FilamentPicker (anchorPopoverForDotNet) so it inherits the outside-click /
   Escape / scroll-reposition behaviour. */
.hm-shape-picker {
    position: relative;
}
.hm-shape-picker-trigger {
    display: flex;
    align-items: center;
    gap: 8px;
    text-align: left;
    width: 100%;
    height: auto;
    padding-top: 4px;
    padding-bottom: 4px;
}
.hm-shape-picker-name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hm-shape-picker-thumb {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    border-radius: 4px;
    background: var(--bs-tertiary-bg);
    overflow: hidden;
    /* Inherited `color` for the embedded SVG (ShapeIconSvg + HeightStrategyIconSvg
       use `currentColor`). Picks up the theme accent via :root --hm-accent. */
    color: var(--hm-accent);
}
.hm-shape-picker-thumb-lg {
    width: 48px;
    height: 48px;
}
.hm-shape-picker-thumb svg {
    width: 100%;
    height: 100%;
}
.hm-shape-picker-menu {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1050;
    max-height: 360px;
    overflow-y: auto;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
    padding: 4px;
    min-width: 240px;
}
.hm-shape-picker-option {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 6px 8px;
    background: transparent;
    border: none;
    border-radius: 4px;
    text-align: left;
    cursor: pointer;
    color: var(--bs-body-color);
}
.hm-shape-picker-option:hover {
    background: var(--bs-tertiary-bg);
}
.hm-shape-picker-option.selected {
    background: color-mix(in srgb, var(--hm-accent) 18%, transparent);
    border-left: 2px solid var(--hm-accent);
    padding-left: 6px;
}
/* ================= End CellShapePicker ================= */

/* ================= Depth-painter hint (shape modal preview column) ================= */
/* Compact accent-coloured info note. Rendered at the bottom of the shape modal's
   preview column so the empty space under the shape thumbnail gets used for useful
   guidance instead of being left blank, and the modal's Apply button doesn't move when
   the user picks DepthPainter.

   The element is a flex-column child inside .hm-shape-modal-preview. `margin-top: auto`
   consumes any free space above, pushing this hint to the bottom of the column. If the
   column has no free space (user has made the modal short), the hint sits naturally
   below the summary text. */
.hm-depth-hint {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    width: 100%;
    padding: 10px 12px;
    border-radius: 6px;
    background: color-mix(in srgb, var(--hm-accent) 14%, transparent);
    border-left: 3px solid var(--hm-accent);
    line-height: 1.35;
    margin-top: auto;
}
.hm-depth-hint > i {
    flex: 0 0 auto;
    margin-top: 2px;
    color: var(--hm-accent);
}
/* ================= End depth-painter hint ================= */

/* ================= Plate swatch strip ================= */
/* Prominent horizontal row of filament swatches used in the Export plate cards. Each
   swatch uses the same dual-ring keyline technique as .hm-chip-icon so dark colours
   stay visible on dark backgrounds and vice versa, but sized larger (24px) to be
   genuinely at-a-glance. */
.hm-plate-swatch-strip {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
}
.hm-plate-swatch {
    display: inline-block;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 1px solid rgba(0, 0, 0, 0.55);
    box-shadow:
        inset 0 0 0 1px rgba(255, 255, 255, 0.4),  /* inner light ring for dark swatches */
        0 1px 2px rgba(0, 0, 0, 0.2);              /* subtle drop so it lifts off the card */
    cursor: help;
    flex: 0 0 auto;
}
/* Compact variant for the narrower Estimate tab plate cards (220px wide grid). 20px
   swatch keeps multi-colour plates readable even with 6+ filaments in the strip. */
.hm-plate-swatch-strip-compact .hm-plate-swatch {
    width: 20px;
    height: 20px;
}
/* ================= End plate swatch strip ================= */

/* ================= Filament assignments accordion ================= */
/* Native <details> wrapping the filament-assignment table in the Palette & AMS modal.
   Hides the default disclosure triangle since we render our own chevron that rotates. */
.hm-filament-assignments {
    margin-top: 12px;
}
.hm-filament-assignments > summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 8px 10px;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    user-select: none;
}
.hm-filament-assignments > summary::-webkit-details-marker {
    display: none;  /* hide default Safari triangle */
}
.hm-filament-assignments > summary .hm-chevron {
    transition: transform 150ms ease;
    color: var(--bs-secondary-color);
}
.hm-filament-assignments[open] > summary .hm-chevron {
    transform: rotate(90deg);
}
.hm-filament-assignments[open] > summary {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    border-bottom: 1px dashed var(--bs-border-color);
}
.hm-filament-assignments-summary:hover {
    background: var(--bs-secondary-bg);
}
/* ================= End filament assignments accordion ================= */

/* ================= Build overlay ================= */
/* Shown while a mosaic rebuild is in flight so the user sees "something's happening"
   rather than staring at the stale preview. Semi-transparent scrim + spinner + text.
   Pinned to the tab body (not the viewport) so the sidebar, tabs, and header stay
   interactive. */
.hm-build-overlay {
    position: absolute;
    inset: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Backdrop uses a colour-mix of the body background so it adapts to light/dark
       mode automatically — no separate theme override needed. */
    background: color-mix(in srgb, var(--bs-body-bg) 70%, transparent);
    backdrop-filter: blur(2px);
    pointer-events: all;  /* block clicks on the stale preview underneath */
}
.hm-build-overlay-inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 20px 28px;
    border-radius: 10px;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.hm-build-overlay-text {
    font-weight: 600;
    color: var(--bs-body-color);
}
/* ================= End build overlay ================= */

/* ================= Slicer assumptions accordion ================= */
/* Collapsible settings card at the top of the Estimate panel. Similar styling to the
   filament-assignments accordion but sits inside the main content area rather than a
   modal. */
.hm-slicer-assumptions {
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    padding: 10px 12px;
}
.hm-slicer-assumptions > summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    user-select: none;
}
.hm-slicer-assumptions > summary::-webkit-details-marker { display: none; }
.hm-slicer-assumptions > summary .hm-chevron {
    transition: transform 150ms ease;
    color: var(--bs-secondary-color);
}
.hm-slicer-assumptions[open] > summary .hm-chevron {
    transform: rotate(90deg);
}
/* ================= End slicer assumptions accordion ================= */

/* ================= Large chip variant (for the plate details modal) ================= */
/* The default .hm-plate-chip is sized for thumbnail cards (220-300px wide). In the
   plate details modal the viewport is much larger (~900px) and the existing 0.72rem
   font + 18px swatch feel lost. This variant bumps everything to a readable scale
   while keeping the exact same layout rules — only padding / font / icon size change. */
.hm-plate-chip-lg {
    padding: 8px 12px;
    font-size: 0.95rem;
    line-height: 1.35;
    gap: 10px;
}
.hm-plate-chip-lg .hm-chip-icon {
    /* Bumped from 18px → 26px swatch. Keeps the dual-ring keyline so it still reads
       on any background, just larger. */
    flex: 0 0 26px;
    width: 26px;
    height: 26px;
}
.hm-plate-chip-lg .hm-chip-text {
    font-size: 0.95rem;
}
.hm-plate-chip-lg .hm-chip-subtext {
    font-size: 0.82rem;  /* bumped from 0.68rem — material readable in modal context */
}
.hm-plate-chip-lg i.bi {
    /* Bootstrap icons scale with surrounding font-size, but we add a little extra
       weight here so they match the larger text visually. */
    font-size: 1.1rem;
}

/* Compact variant for the info chip grid in the plate details modal. The filament
   chip at the top of the modal keeps .hm-plate-chip-lg (two-line name + material
   needs vertical breathing room), but the Size / Cells / Colours / Clumps chips
   below it are single-line and were looking too tall. This variant removes the
   top/bottom padding entirely — vertical size is governed by line-height alone.
   Only used alongside .hm-plate-chip, not instead of it. */
.hm-plate-chip-compact {
    padding: 0 12px;
    font-size: 0.95rem;
    line-height: 1.35;
    gap: 10px;
    /* min-height keeps chips visually matched to a reasonable hit target despite
       having no padding — rows still look balanced if some wrap. */
    min-height: 32px;
}
.hm-plate-chip-compact .hm-chip-icon {
    flex: 0 0 26px;
    width: 26px;
    height: 26px;
}
.hm-plate-chip-compact .hm-chip-text {
    font-size: 0.95rem;
}
.hm-plate-chip-compact i.bi {
    font-size: 1.1rem;
}
/* Clickable chip variant — used by the per-plate "Printed" toggle
   in PlateDetailsModal so the chip itself is the click target.
   Resets native button styling (no border, no native bg) and adds a
   subtle hover halo so the affordance is visible without breaking
   the chip's visual mass. May 2026 plate-printed tracker. */
.hm-plate-chip-button {
    border: 0;
    background: var(--bs-tertiary-bg, transparent);
    text-align: left;
    transition: background 80ms ease, transform 80ms ease;
}
.hm-plate-chip-button:hover {
    background: var(--bs-secondary-bg);
    transform: translateY(-1px);
}
.hm-plate-chip-button:focus-visible {
    outline: 2px solid var(--hm-accent);
    outline-offset: 2px;
}
/* ================= End large chip variant ================= */

/* ================= Plate details chip grid ================= */
/* 2-column grid of info chips in the plate details modal. Tighter row/column gap than
   Bootstrap's wrapped .row/.col pattern, so Size / Cells / Colours / Clumps sit close
   together without the implicit margin-top Bootstrap adds to wrapped rows. Falls back
   to single column below 560px where two chips side-by-side start to truncate. */
.hm-plate-details-chip-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 6px 8px;  /* row-gap 6px, col-gap 8px */
}
@media (max-width: 560px) {
    .hm-plate-details-chip-grid { grid-template-columns: 1fr; }
}
/* ================= End plate details chip grid ================= */

/* ================= Experimental shapes accordion ================= */
/* The shape picker keeps the 8 core shapes always-visible in a grid at the top;
   the 7 newer shapes sit behind this <details> accordion to de-clutter the picker.
   Matches the visual language of .hm-filament-assignments (summary pill + rotating
   chevron) so the two accordions feel consistent across the app. */
.hm-experimental-shapes > summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 12px;
    border: 1px solid var(--bs-border-color);
    border-radius: 6px;
    background: var(--bs-tertiary-bg);
    user-select: none;
    font-size: 0.85rem;
    font-weight: 500;
}
.hm-experimental-shapes > summary::-webkit-details-marker {
    display: none;  /* hide default disclosure triangle */
}
.hm-experimental-shapes > summary::marker {
    content: none;  /* hide default disclosure triangle (Firefox) */
}
.hm-experimental-chevron {
    color: var(--bs-secondary-color);
    transition: transform 150ms ease;
    font-size: 0.9em;
    margin-left: auto;
}
.hm-experimental-shapes[open] > summary .hm-experimental-chevron {
    transform: rotate(90deg);
}
.hm-experimental-shapes > summary:hover {
    background: var(--bs-secondary-bg);
}
.hm-experimental-label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: 1;
}
.hm-experimental-label i.bi {
    color: var(--hm-accent);
}
.hm-experimental-count {
    /* Small badge showing "7" — gives a hint of how much content is hidden so users
       know it is worth expanding. Muted so it doesnt dominate the summary row. */
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 999px;
    padding: 1px 8px;
    font-weight: 500;
    min-width: 22px;
    text-align: center;
}
/* ================= End experimental shapes accordion ================= */

/* ================= Right-side drawer ================= */
/* Used by SlicerAssumptionsDrawer (and any future right-side drawer panels).
   Backdrop dims the page below; the panel slides in via translateX. Sits below
   the modal-shell stack (z 1050) so an opened modal will always appear on top
   of an open drawer. */
.hm-drawer-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 1040;
}
.hm-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 480px;
    max-width: 92vw;
    background: var(--bs-body-bg);
    border-left: 1px solid var(--bs-border-color);
    box-shadow: -4px 0 12px rgba(0, 0, 0, 0.18);
    transform: translateX(100%);
    transition: transform 200ms ease;
    z-index: 1045;
    display: flex;
    flex-direction: column;
}
.hm-drawer.hm-drawer-open {
    transform: translateX(0);
}
.hm-drawer-header {
    flex: 0 0 auto;
    padding: 12px 16px;
    border-bottom: 1px solid var(--bs-border-color);
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: var(--bs-secondary-bg);
}
.hm-drawer-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 16px;
}
.hm-drawer-footer {
    flex: 0 0 auto;
    padding: 12px 16px;
    border-top: 1px solid var(--bs-border-color);
    background: var(--bs-secondary-bg);
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}
.hm-drawer-close {
    background: transparent;
    border: none;
    color: var(--bs-body-color);
    font-size: 1.1rem;
    line-height: 1;
    padding: 4px 8px;
    cursor: pointer;
    border-radius: 4px;
}
.hm-drawer-close:hover {
    background: var(--bs-tertiary-bg);
}
/* ================= End drawer ================= */


/* ================= 2D-preview touch-up paint tool ================= */
.hm-paint-toolbar {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    background: var(--bs-tertiary-bg);
    border-bottom: 1px solid var(--bs-border-color);
    flex-wrap: wrap;
}
/* Guidance banner that sits between the canvas header (zoom + cell info)
   and the paint toolbar. Soft accent tint so it visually separates from
   both the header above and the click+drag toolbar below — the user
   doesn't conflate it with the inline "Click + drag" instruction. Slight
   green left border picks up the brand accent and signals "actionable
   guidance" rather than "informational noise". */
.hm-paint-toolbar-hint {
    padding: 6px 12px;
    background: color-mix(in srgb, var(--hm-accent) 10%, var(--bs-body-bg));
    border-bottom: 1px solid var(--bs-border-color);
    border-left: 3px solid var(--hm-accent);
    color: var(--bs-body-color);
    font-size: 0.78rem;
    line-height: 1.35;
}
.hm-paint-toolbar-hint .bi {
    margin-right: 6px;
    color: var(--hm-accent);
}
.hm-paint-swatch {
    width: 26px;
    height: 26px;
    border-radius: 4px;
    border: 2px solid var(--bs-border-color);
    cursor: pointer;
    padding: 0;
    transition: transform 80ms ease;
}
.hm-paint-swatch:hover {
    transform: scale(1.1);
}
.hm-paint-swatch.selected {
    border-color: var(--hm-accent);
    box-shadow: 0 0 0 2px var(--hm-accent), inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}
.hm-viewer-canvas.hm-paint-mode {
    cursor: crosshair;
}

/* Cell hover-highlight — visible across every interaction mode (info,
   touch-up paint, cell painter, depth painter). Yellow stroke + slight
   brighten so the user can pinpoint exactly which cell the cursor is
   over without obscuring the underlying colour. SVG hover pseudo-class
   works even while the canvas captures pointer events for the painters
   (hover is geometry-only, not event-driven).
   The stroke-width nudge has to be `!important` because the inline
   stroke-width attribute on the polygon would otherwise win.
   Each render path uses a slightly different DOM shape:
     - Preview2D wraps each cell in <g data-cellindex>; we target the
       polygon / circle / path child on hover.
     - DepthPainterPanel emits <polygon data-col data-row> directly;
       hover hits the polygon itself. */
.hm-viewer-canvas svg g[data-cellindex]:hover > polygon,
.hm-viewer-canvas svg g[data-cellindex]:hover > circle,
.hm-viewer-canvas svg g[data-cellindex]:hover > path {
    stroke: #ffd166 !important;
    stroke-width: 0.65 !important;
    filter: brightness(1.12);
}
.hm-depth-painter-svg polygon[data-col]:hover,
svg polygon[data-col][data-row]:hover {
    stroke: #ffd166 !important;
    stroke-width: 0.5 !important;
    filter: brightness(1.12);
}
/* ================= End touch-up paint tool ================= */
