:root {
    /* ============================================================
       Cute pastel theme — with a hard split between DECORATIVE colour
       and TEXT colour (WCAG 2.2 AA, quality bar row E3).

       The candy pinks below (--accent-color / --accent-hover) are fills,
       borders and shadows. None of them clears 4.5:1 on a near-white card,
       so none of them may ever be a `color:`. Text that has to be read uses
       --accent-text / --teal-text / --ok-text / --warn-text / --fail-text,
       every one of which is measured by tests/a11y-static.mjs.

       The two text surfaces are OPAQUE on purpose. They used to be
       rgba-over-a-gradient, which makes the contrast of everything on top
       of them unknowable — not just unmeasured, genuinely undefined, since
       the body's three radial gradients change the backdrop with the
       viewport size. The values below are what the translucent versions
       composited to at the sidebar, so the design is unchanged to look at
       and every ratio on it is now a number.
       ============================================================ */
    --bg-1: #fff5fb;
    --bg-2: #f1ecff;
    --bg-3: #eafbf4;
    --sidebar-bg: #fff8fc;
    --card-bg: #fffcfe;
    --border-color: rgba(255, 143, 184, 0.28);
    --border-soft: rgba(160, 130, 220, 0.18);
    /* The edge of a section card. Opaque, and deliberately NOT white.
       ---------------------------------------------------------------
       This used to be rgba(255,255,255,0.9) — white, at 90%, drawn on a
       #fff8fc sidebar behind a #fffcfe card. Three near-identical whites, so
       the "border" was invisible and the only thing separating one section
       from the next was a soft shadow. The stack read as one long scroll
       rather than eight titled cards.

       #e9cee0 sits about 1.4:1 against the sidebar — a clearly visible line
       that still belongs to a pastel design. Going to the 3:1 that WCAG 1.4.11
       asks of a CONTROL boundary would be right for a button and much too
       heavy for a container; these are panels, and the controls inside them
       keep their own borders. */
    --border-card: #e9cee0;
    --border-card-open: #dcb8d2;
    --text-primary: #5d4a66;
    --text-secondary: #736180;
    --accent-color: #ff7aa8;     /* candy pink — decoration only */
    --accent-hover: #ff5c93;     /* decoration only */
    --accent-2: #b39bff;          /* lavender — decoration only */
    --accent-3: #74dcb4;          /* mint — decoration only */
    --accent-text: #b3275d;       /* the readable pink */
    --teal-text: #0a6b78;
    --ok-text: #1c6b4a;
    --warn-text: #8a4b00;
    --fail-text: #a3243c;
    --shadow-pink: rgba(255, 122, 168, 0.28);
    --shadow-soft: rgba(150, 120, 200, 0.18);
    --sidebar-width: clamp(500px, 32vw, 560px);
    --focus-ring: #b3275d;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* The hidden attribute must always win. Author display rules (e.g.
   .slider-container { display: flex }) silently override the UA's
   [hidden] default — this bit organicFootRow, and previously
   #imagePerimeterPresetControls and .organic-sketch-panel. */
[hidden] {
    display: none !important;
}

/* ============================================================
   FOCUS, SKIP LINKS AND SCREEN-READER TEXT (quality bar rows E2, E5).

   One global :focus-visible rule, so a new control is visible to the
   keyboard the moment it exists rather than when somebody remembers to
   style it. The ring is a dark rose against a white halo: 6.1:1 on the
   card surface, and the halo keeps it visible on the pink buttons too,
   which satisfies 2.4.11 Focus Appearance on every surface in the app.

   Nothing in this file may set `outline: none` without replacing it —
   tests/a11y-static.mjs fails the build if it does.
   ============================================================ */
:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
    border-radius: 6px;
}
.sidebar :focus-visible,
.ctx-menu :focus-visible,
.guide-card :focus-visible {
    box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.95), 0 0 0 9px rgba(179, 39, 93, 0.22);
}
/* Range thumbs draw their own ring: the outline sits on the whole track,
   which says "this slider is focused" but not where the thumb is. */
input[type="range"]:focus-visible::-webkit-slider-thumb {
    box-shadow: 0 0 0 3px #fff, 0 0 0 6px var(--focus-ring);
}
input[type="range"]:focus-visible::-moz-range-thumb {
    box-shadow: 0 0 0 3px #fff, 0 0 0 6px var(--focus-ring);
}

.skip-link {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 3000;
    transform: translateY(-140%);
    padding: 0.7rem 1.2rem;
    background: #fff;
    color: var(--accent-text);
    border: 3px solid var(--focus-ring);
    border-radius: 0 0 14px 0;
    font: 700 0.95rem 'Fredoka', 'Quicksand', sans-serif;
    text-decoration: none;
    transition: transform 0.15s ease;
}
.skip-link:nth-of-type(2) { left: 15rem; }
.skip-link:focus { transform: translateY(0); }

/* Visible to a screen reader, invisible on screen. Used where the visual
   label is carried by an adjacent swatch or a unit suffix. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Every animation in the app is decorative. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }
}

body {
    font-family: 'Quicksand', 'Outfit', sans-serif;
    background-color: var(--bg-1);
    background-image:
        radial-gradient(at 0% 0%, rgba(255, 175, 209, 0.45) 0px, transparent 55%),
        radial-gradient(at 100% 0%, rgba(179, 155, 255, 0.40) 0px, transparent 55%),
        radial-gradient(at 50% 100%, rgba(116, 220, 180, 0.35) 0px, transparent 55%);
    color: var(--text-primary);
    min-height: 100vh;
    overflow: hidden;
}

#app {
    display: flex;
    height: 100vh;
    width: 100vw;
}

/* Sidebar — soft frosted card */
.sidebar {
    width: var(--sidebar-width);
    flex: 0 0 var(--sidebar-width);
    background: var(--sidebar-bg);
    backdrop-filter: blur(18px) saturate(1.4);
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    border-right: 1px solid rgba(255, 255, 255, 0.6);
    padding: 1.75rem;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    overflow-y: auto;
    overflow-x: hidden;
    z-index: 10;
    box-shadow: 6px 0 30px var(--shadow-soft);
}

/* Cute pink scrollbar */
.sidebar::-webkit-scrollbar { width: 10px; }
.sidebar::-webkit-scrollbar-track { background: transparent; }
.sidebar::-webkit-scrollbar-thumb {
    background: rgba(255, 143, 184, 0.45);
    border-radius: 10px;
    border: 2px solid transparent;
    background-clip: content-box;
}
.sidebar::-webkit-scrollbar-thumb:hover { background: rgba(255, 122, 168, 0.7); background-clip: content-box; }

.logo { flex: none; }

.logo h1 {
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-size: 2.1rem;
    font-weight: 700;
    /* Darker stops than the decorative pinks: this is text, and every stop
       has to clear 3:1 (large text) on the sidebar. Pink #ff7aa8 was 2.3:1. */
    background: linear-gradient(115deg, #e0447a 0%, #7b5ce0 55%, #2a9c78 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    margin-bottom: 0.15rem;
    letter-spacing: -0.5px;
}

.logo p {
    color: var(--text-secondary);
    font-size: 0.95rem;
    font-weight: 600;
}

.logo .brand-line {
    font-size: 0.82rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent-text);
    margin-bottom: 0.15rem;
}

/* ============================================================
   THE CONTROL STACK — grouped, collapsible, scannable (row E6).

   The stack was 7,434 px of permanently-open cards: a wall a first-time
   visitor scrolls past rather than reads. Each card now lives in a native
   <details> section whose <summary> carries a number, a name and a live
   one-line summary of that section's current settings, so the closed
   stack is a table of contents rather than a list of headings.
   ============================================================ */
.stack-nav {
    position: sticky;
    top: -1.75rem;
    z-index: 12;
    flex: none;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin: -0.35rem -0.35rem 0;
    padding: 0.6rem 0.35rem 0.7rem;
    background: var(--sidebar-bg);
    border-bottom: 1.5px dashed var(--border-color);
}
.stack-nav-chips {
    display: contents;
}
.stack-nav-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.28rem;
    min-height: 30px;
    padding: 0.28rem 0.62rem;
    border: 1.5px solid var(--border-soft);
    border-radius: 999px;
    background: #fff;
    color: var(--text-primary);
    font: 700 0.72rem 'Quicksand', sans-serif;
    cursor: pointer;
}
.stack-nav-chip:hover { border-color: var(--accent-color); background: #fff3f8; }
.stack-nav-chip.is-open {
    border-color: var(--accent-color);
    background: #ffeaf3;
    color: var(--accent-text);
}
.stack-nav-chip .chip-index {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.15rem;
    height: 1.15rem;
    border-radius: 999px;
    background: rgba(179, 39, 93, 0.12);
    color: var(--accent-text);
    font-size: 0.62rem;
}
.stack-nav-actions { display: flex; gap: 0.35rem; margin-left: auto; }
.stack-nav-all {
    min-height: 30px;
    padding: 0.25rem 0.62rem;
    border: 1px solid var(--border-soft);
    border-radius: 9px;
    background: #fff;
    color: var(--text-secondary);
    font: 700 0.68rem 'Quicksand', sans-serif;
    cursor: pointer;
}
.stack-nav-all:hover { color: var(--accent-text); border-color: var(--accent-color); }

/* `flex: none` matters: the sidebar is a flex column and `overflow: hidden`
   below sets these items' automatic minimum size to zero, so without it the
   browser shrinks every section to its 2 px border to fit the viewport. */
.stack-section {
    flex: none;
    border: 1.5px solid var(--border-card);
    border-radius: 22px;
    background: var(--card-bg);
    /* Two shadows doing different jobs. The wide soft one lifts the card off
       the sidebar; the 1px inset highlight along the top edge is what makes
       the border read as a raised edge rather than a drawn line, and it is the
       reason this can stay a pale pink instead of going grey. */
    box-shadow: 0 6px 18px var(--shadow-soft),
                inset 0 1px 0 rgba(255, 255, 255, 0.9);
    overflow: hidden;
}
/* An open section is the one being worked in, so it gets the stronger edge and
   lifts further. Closed sections recede — which is the whole point of a
   collapsible stack and was not previously visible at all. */
.stack-section[open] {
    border-color: var(--border-card-open);
    box-shadow: 0 10px 26px var(--shadow-soft),
                inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
/* The summary holds ONE child, an <h2>, because that is all the HTML spec
   allows next to heading content inside <summary> — three siblings around a
   heading is invalid markup, and invalid markup is where assistive
   technology starts guessing. The flex row therefore lives on the heading. */
.stack-section > summary {
    display: block;
    list-style: none;
    cursor: pointer;
    min-height: 48px;
    padding: 0.7rem 1rem;
    user-select: none;
    /* A faint tinted band, so the title belongs to a header rather than
       floating above the first control. #fdf4fa is light enough that the
       --text-primary title on it stays well clear of 4.5:1 — the whole point
       of the note at the top of this file is that a tint like this must not
       quietly become a contrast failure. */
    background: #fdf4fa;
}
.stack-section > summary::-webkit-details-marker { display: none; }
.stack-section > summary:hover { background: #fbe8f4; }
/* Only when open: a closed section is a single band and a line across it
   would look like an empty card rather than a collapsed one. */
.stack-section[open] > summary {
    border-bottom: 1px solid var(--border-card);
}
.stack-section .section-title {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 34px;
}
/* `content: 'x' / ''` gives the generated glyph an EMPTY alternative text, so
   it stays out of the accessibility tree. Without the `/ ''`, NVDA reads the
   chevron aloud on every section — "1 Start here, Flat button, ▾, heading,
   level 2, button, expanded" — which is the state the button already
   announces, said twice, once as a symbol. Found in the NVDA pass. */
.stack-section .section-title::after {
    content: '▸' / '';
    margin-left: auto;
    color: var(--accent-text);
    font-size: 0.9rem;
    flex: none;
}
.stack-section[open] .section-title::after { content: '▾' / ''; }
.stack-section .section-index {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: 999px;
    background: rgba(179, 39, 93, 0.12);
    color: var(--accent-text);
    font: 700 0.78rem 'Quicksand', sans-serif;
}
/* An <h2> so the stack has a real outline for heading navigation: h1 product
   name → h2 section → h3 card. */
.stack-section .section-title {
    margin: 0;
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-size: 1.02rem;
    font-weight: 600;
    color: var(--text-primary);
}
.stack-section .section-name { flex: none; }
.stack-section .section-meta {
    min-width: 0;
    flex: 1;
    color: var(--text-secondary);
    font-family: 'Quicksand', sans-serif;
    font-size: 0.74rem;
    font-weight: 600;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stack-section .section-meta:empty { display: none; }
.stack-section > .section-body {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    padding: 0 0.6rem 0.6rem;
}
/* Cards inside a section drop their own frame and most of their padding —
   the section is the card now, and the double frame was pure height. */
.stack-section .control-group {
    gap: 0.7rem;
    padding: 0.85rem;
    border-radius: 14px;
    box-shadow: none;
    border-color: var(--border-soft);
    background: #fff;
}
.stack-section .control-group h3 {
    font-size: 0.95rem;
    padding-bottom: 0.5rem;
    margin-bottom: 0.1rem;
}
/* …except when the viewport selection points at one, which is exactly when
   it has to stand out from its neighbours. */
.stack-section .control-group.viewport-selected {
    background: #fff;
}
.stack-section.section-empty { display: none; }

/* ============================================================
   SAVE & SHARE — the export dock, a strip along the bottom of the 3D view.

   It used to be sticky to the bottom of the sidebar, which cost the control
   stack ~200 px of its height permanently and grew every time a printability
   warning got longer. It is an overlay on the viewport now, and it folds to a
   pill.

   WHY A WIDE STRIP RATHER THAN A CORNER PANEL. As a 400 px column down the
   right it was ~460 px tall, and it ate the part of the view a model is
   actually in. Laid out along the bottom the same content is ~150 px tall,
   because the reading half and the button half sit SIDE BY SIDE instead of
   stacking their heights — so the model keeps the full width of the canvas
   and all but the last sixth of its height.

   Every size here is relative to the canvas, so the strip can never be
   taller or wider than the thing it sits on at any viewport.

   z-index 9 is deliberately BELOW .loading-overlay (20): a full-screen build
   overlay covers this, and the compact rebuild chip is top-right, so the two
   never collide. src/export-dock.js owns the folding.
   ============================================================ */
.export-float {
    position: absolute;
    left: 14px;
    right: 14px;
    bottom: 14px;
    z-index: 9;
    display: flex;
    flex-direction: column;
    /* The head is a pill, not a bar: right-aligned above the strip, and the
       only thing left when the strip is folded away. */
    align-items: flex-end;
    gap: 0.45rem;
    max-height: calc(100% - 28px);
}
/* Folded, it is only as wide as its own pill — so it releases the left edge
   it claims above. */
.export-float[data-collapsed="true"] {
    left: auto;
    max-width: calc(100% - 28px);
}
.export-float-head {
    flex: none;
    max-width: 100%;
    display: flex;
    align-items: center;
    gap: 0.4rem;
}
.export-float-tab {
    flex: none;
    min-width: 0;
    min-height: 34px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    padding: 0.4rem 0.8rem;
    border: 1.5px solid var(--border-color);
    border-radius: 999px;
    background: #fff;
    color: var(--accent-text);
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-size: 0.82rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 0 6px 20px var(--shadow-soft);
    transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.export-float-tab:hover {
    border-color: var(--accent-color);
    transform: translateY(-1px);
}
/* Inline-block, or the rotation below is silently a no-op. */
.export-float-chevron {
    display: inline-block;
    font-size: 0.72rem;
    transition: transform 0.2s ease;
}
.export-float[data-collapsed="true"] .export-float-chevron { transform: rotate(180deg); }

/* The folded verdict. Shown ONLY while the panel is folded, so that exactly
   one of this and #printCheck is ever a live region a screen reader can
   announce — a display:none live region is silent, which is what makes the
   pair safe. Empty until the first build says something. */
.export-float-verdict {
    flex: 0 1 auto;
    min-width: 0;
    padding: 0.36rem 0.65rem;
    border: 1.5px solid var(--border-color);
    border-radius: 999px;
    background: #fff;
    font-family: 'Quicksand', sans-serif;
    font-size: 0.74rem;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-shadow: 0 6px 20px var(--shadow-soft);
}
.export-float-verdict:empty { display: none; }
.export-float-verdict.ok { color: var(--ok-text); }
.export-float-verdict.warn { color: var(--warn-text); }
.export-float[data-collapsed="false"] .export-float-verdict { display: none; }

/* The strip itself. `flex-wrap` and the two flex-basis values below are the
   whole responsive story: while the canvas is wide enough for both columns
   they sit side by side, and when it is not they wrap to one column each —
   no breakpoint, so it behaves the same in a narrow window, at 400 % zoom
   and on a phone. */
.export-dock {
    width: 100%;
    flex: 0 1 auto;
    min-height: 0;
    display: flex;
    flex-flow: row wrap;
    align-items: flex-start;
    gap: 0.5rem 0.9rem;
    padding: 0.75rem 0.85rem;
    border: 1.5px solid var(--border-color);
    border-radius: 18px;
    background: var(--sidebar-bg);
    box-shadow: 0 10px 30px var(--shadow-soft);
}
.export-float[data-collapsed="true"] .export-dock { display: none; }

/* Left: what there is to read. It is the half that grows without limit — a
   printability verdict can run to a paragraph — so it is the half that gets
   the scrollbar, and the buttons beside it never move. */
.export-dock-read {
    flex: 1 1 260px;
    min-width: 0;
    max-height: 7rem;
    overflow-y: auto;
}
.export-dock .print-check { margin-top: 0; }
/* Right: what there is to press.

   The basis is 300, not the ~380 the three secondary labels want, because
   tests/a11y-reflow.mjs forbids any flex basis over 320 px — a basis wider
   than a 320 px viewport is how a "responsive" column ends up forcing
   horizontal scroll at 400 % zoom, and `flex-shrink` is not something that
   gate is willing to take on trust. It costs nothing here: the two columns
   share the slack equally, so on any canvas over ~700 px this one is wider
   than 380 anyway, and below that the row drops to two buttons a line. */
.export-dock-actions {
    flex: 1 1 300px;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.export-dock .primary-btn { margin-top: 0; }
/* auto-fit, not `repeat(3, …)`: the strip is as wide as the canvas, which is
   not a number this file knows. The row gives up columns by itself rather
   than squeezing three labels onto two lines each.

   The 109 px is measured, not chosen: at the type size below the widest label
   ("Save photo 📸") needs 95 px of content box, plus 9.6 px of padding and the
   2 px border. That is what makes three fit in the narrowest side-by-side
   layout the strip produces (a ~700 px canvas). Falling to two columns is
   untidy, not broken, so nothing here is load-bearing. */
.export-dock-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(109px, 1fr));
    gap: 0.45rem;
}
.export-dock-row .secondary-btn {
    margin-top: 0.55rem;
    padding: 0.55rem 0.3rem;
    font-size: 0.84rem;
}
.export-notes {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-top: 0.5rem;
    color: var(--text-secondary);
    font-size: 0.72rem;
    line-height: 1.4;
}

/* Colour palette dropdown + live swatch preview */
.palette-pick {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.palette-pick select { flex: 1; }
.palette-pick .swatches {
    display: flex;
    gap: 3px;
    min-width: 52px;
}
.palette-pick .swatches i {
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 1.5px solid rgba(255, 255, 255, 0.9);
    box-shadow: 0 1px 3px var(--shadow-soft);
}

/* One-tap colour palettes (legacy chips) */
.palette-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0.4rem;
}
.palette-chip {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
    padding: 0.45rem 0.2rem;
    font-family: 'Quicksand', sans-serif;
    font-size: 0.62rem;
    font-weight: 700;
    color: var(--text-primary);
    background: #fff;
    border: 2px solid var(--border-soft);
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.18s ease;
}
.palette-chip .swatches {
    display: flex;
    gap: 2px;
}
.palette-chip .swatches i {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 1.5px solid rgba(255, 255, 255, 0.9);
    box-shadow: 0 1px 3px var(--shadow-soft);
}
.palette-chip:hover {
    border-color: var(--accent-color);
    background: #fff;
    transform: translateY(-2px) scale(1.04);
    box-shadow: 0 6px 14px var(--shadow-pink);
}

.control-group {
    background: var(--card-bg);
    border: 1.5px solid rgba(255, 255, 255, 0.7);
    border-radius: 22px;
    padding: 1.35rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    box-shadow: 0 8px 22px var(--shadow-soft);
}

.control-group h3 {
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--text-primary);
    border-bottom: 2px dashed var(--border-color);
    padding-bottom: 0.7rem;
    margin-bottom: 0.35rem;
}

/* Upload Button */
.upload-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0.85rem 1rem;
    background: #fff;
    border: 2.5px dashed var(--accent-2);
    border-radius: 16px;
    cursor: pointer;
    transition: all 0.2s ease;
    text-align: center;
    font-weight: 600;
    color: var(--accent-text);
}

.upload-btn:hover {
    background: #fff;
    border-color: var(--accent-color);
    transform: translateY(-2px);
    box-shadow: 0 8px 18px var(--shadow-pink);
}
/* A file input is the real control; the label is its visible surface, so the
   focus ring has to land on the label, not on the hidden input. */
.upload-btn:focus-within {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
}

.upload-btn input[type="file"] {
    display: none;
}


.font-upload-btn {
    padding: 0.6rem 0.75rem;
    border-style: solid;
    border-width: 2px;
    font-size: 0.9rem;
    margin-top: 0.15rem;
}

.preview-container {
    width: 100%;
    border-radius: 16px;
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #fff;
    border: 1.5px solid var(--border-soft);
}

.preview-container img {
    max-width: 100%;
    max-height: 150px;
    object-fit: contain;
    padding: 0.5rem;
}

/* Sliders */
.slider-container {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.slider-container label {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-primary);
    display: flex;
    justify-content: space-between;
}

.slider-container small {
    font-size: 0.76rem;
    font-weight: 500;
    color: var(--text-secondary);
}

/* The track used to BE the element, so the pointer target for every slider
   in the app was 8 px tall. The track is now painted by ::-*-track and the
   element itself is 26 px, which clears 2.5.8 Target Size without changing
   how the slider looks. */
input[type="range"] {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 26px;
    background: none;
    cursor: pointer;
}

input[type="range"]::-webkit-slider-runnable-track {
    height: 8px;
    border-radius: 10px;
    background: linear-gradient(90deg, #ffd4e6, #e3d8ff);
}

input[type="range"]::-moz-range-track {
    height: 8px;
    border-radius: 10px;
    background: linear-gradient(90deg, #ffd4e6, #e3d8ff);
}

input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 22px;
    height: 22px;
    margin-top: -7px;
    border-radius: 50%;
    background: var(--accent-color);
    border: 3px solid #fff;
    box-shadow: 0 3px 8px var(--shadow-pink);
    cursor: pointer;
    transition: transform 0.15s ease, background 0.2s ease;
}

input[type="range"]::-webkit-slider-thumb:hover {
    background: var(--accent-hover);
    transform: scale(1.18);
}

input[type="range"]::-moz-range-thumb {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--accent-color);
    border: 3px solid #fff;
    box-shadow: 0 3px 8px var(--shadow-pink);
    cursor: pointer;
}

input[type="range"]:disabled { cursor: not-allowed; }
input[type="range"]:disabled::-webkit-slider-thumb { background: #b6a7c4; }
input[type="range"]:disabled::-moz-range-thumb { background: #b6a7c4; }

/* A checkbox and its label are one control: the whole row is the target, and
   the row is 44 px so it clears the AA size bar with room to spare. */
.check-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 44px;
}
.check-row input[type="checkbox"] {
    flex: none;
    width: 24px;
    height: 24px;
}
.check-row label {
    margin-bottom: 0;
    flex: 1;
    align-self: stretch;
    display: flex;
    align-items: center;
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-primary);
    cursor: pointer;
}

/* Preset chips */
.preset-row {
    display: flex;
    gap: 0.5rem;
}
.preset-btn {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    min-height: 44px;
    padding: 0.7rem 0.4rem;
    font-family: 'Quicksand', sans-serif;
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--text-primary);
    background: #fff;
    border: 2px solid var(--border-soft);
    border-radius: 14px;
    cursor: pointer;
    transition: all 0.18s ease;
}
.preset-btn span { display: block; }
.preset-btn:first-letter { font-size: 1.3rem; }
.preset-btn:hover {
    border-color: var(--accent-color);
    background: #fff;
    transform: translateY(-2px);
    box-shadow: 0 6px 14px var(--shadow-pink);
}

/* v2.0 mode tabs (Flat / Organic) */
.mode-btn.active {
    border-color: var(--accent-color);
    background: rgba(255, 122, 168, 0.14);
    box-shadow: 0 4px 12px var(--shadow-pink);
}

/* Starter icon library */
.icon-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 0.4rem;
}
.icon-chip {
    aspect-ratio: 1 / 1;
    min-height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 5px;
    background: #fff;
    border: 2px solid var(--border-soft);
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.18s ease;
}
.icon-chip img { width: 100%; height: 100%; object-fit: contain; }
.icon-chip:hover {
    border-color: var(--accent-color);
    background: #fff;
    transform: translateY(-2px) scale(1.04);
    box-shadow: 0 6px 14px var(--shadow-pink);
}

/* Shape picker */
.shape-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0.4rem;
}
.shape-chip {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    min-height: 44px;
    padding: 0.5rem 0.2rem;
    font-family: 'Quicksand', sans-serif;
    font-size: 0.62rem;
    font-weight: 700;
    line-height: 1.05;
    text-align: center;
    color: var(--text-primary);
    background: #fff;
    border: 2px solid var(--border-soft);
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.18s ease;
}
.shape-chip .shape-emoji { font-size: 1.25rem; line-height: 1; }
.shape-chip:hover {
    border-color: var(--accent-color);
    background: #fff;
    transform: translateY(-2px) scale(1.04);
    box-shadow: 0 6px 14px var(--shadow-pink);
}
.shape-chip.active {
    border-color: var(--accent-color);
    background: rgba(255, 122, 168, 0.14);
    box-shadow: 0 4px 12px var(--shadow-pink);
}

/* Printability check banner */
.print-check {
    font-family: 'Quicksand', sans-serif;
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.3;
    padding: 0.6rem 0.75rem;
    border-radius: 12px;
    margin-top: 0.25rem;
}
.print-check.ok {
    color: var(--ok-text);
    background: #e8f7ef;
    border: 1.5px solid rgba(80, 200, 140, 0.55);
}
.print-check.warn {
    color: var(--warn-text);
    background: #fff2dd;
    border: 1.5px solid rgba(220, 145, 35, 0.55);
}
.print-check a,
.print-check button.print-check-fix {
    color: var(--accent-text);
    font: inherit;
    font-weight: 800;
    background: none;
    border: 0;
    padding: 0;
    text-decoration: underline;
    cursor: pointer;
}

/* Drag-and-drop highlight */
.drag-over {
    border-color: var(--accent-color) !important;
    background: rgba(255, 122, 168, 0.12) !important;
}

/* Text input */
.text-input {
    width: 100%;
    min-height: 40px;
    padding: 0.7rem 0.9rem;
    font-family: 'Quicksand', sans-serif;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-primary);
    background: #fff;
    border: 2px solid var(--border-soft);
    border-radius: 14px;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.text-input::placeholder { color: var(--text-secondary); font-weight: 500; }
.text-input:focus {
    border-color: var(--accent-color);
    box-shadow: 0 0 0 4px rgba(255, 122, 168, 0.18);
}

/* Colour pickers */
.color-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.color-row label {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-primary);
    flex: none;
}
/* The per-control `style="flex:1"` these selects used to carry is now here,
   so the layout survives anyone editing the markup. */
.color-row .text-input { flex: 1; min-width: 0; }

input[type="color"] {
    -webkit-appearance: none;
    appearance: none;
    width: 56px;
    height: 36px;
    min-height: 36px;
    border: 2px solid #fff;
    border-radius: 12px;
    box-shadow: 0 3px 8px var(--shadow-soft);
    cursor: pointer;
    padding: 0;
    background: none;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: 10px; }
input[type="color"]::-moz-color-swatch { border: none; border-radius: 10px; }

/* Checkboxes get the candy-pink tint */
input[type="checkbox"] {
    accent-color: #c8306a;
    cursor: pointer;
}

/* Primary Button — bouncy candy gradient.
   The stops are deeper than --accent-color / --accent-2 because this is the
   one place white sits ON the gradient: at #ff7aa8 that was 2.4:1, and this
   is the button the whole product funnels into. 5.1:1 at the pink end. */
.primary-btn {
    min-height: 48px;
    padding: 1rem;
    background: linear-gradient(115deg, #c8306a, #6d4fd4);
    color: #fff;
    border: none;
    border-radius: 18px;
    font-size: 1.1rem;
    font-weight: 700;
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    cursor: pointer;
    transition: all 0.2s ease;
    margin-top: 0.5rem;
    box-shadow: 0 8px 20px var(--shadow-pink);
}

.primary-btn:hover:not(:disabled) {
    transform: translateY(-3px) scale(1.02);
    box-shadow: 0 12px 26px var(--shadow-pink);
}

.primary-btn:active:not(:disabled) {
    transform: translateY(0) scale(0.98);
}

.primary-btn:disabled {
    background: #e6dfee;
    color: var(--text-primary);
    cursor: not-allowed;
    box-shadow: none;
}

/* Quieter sibling of .primary-btn for secondary actions (STL / share). */
.secondary-btn {
    display: block;
    width: 100%;
    min-height: 44px;
    padding: 0.7rem 1rem;
    background: #fff;
    color: var(--text-primary);
    border: 2px solid rgba(255, 122, 168, 0.45);
    border-radius: 18px;
    font-size: 0.98rem;
    font-weight: 600;
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    cursor: pointer;
    transition: all 0.2s ease;
    margin-top: 0.7rem;
}

.secondary-btn:hover:not(:disabled) {
    transform: translateY(-2px);
    background: #fff5f9;
    box-shadow: 0 8px 18px var(--shadow-pink);
}

.secondary-btn:active:not(:disabled) {
    transform: translateY(0) scale(0.98);
}

/* `opacity` on a disabled control drags its text under the contrast floor
   and is unreadable rather than merely quiet. The border and the cursor
   carry the "not yet" signal instead. */
.secondary-btn:disabled {
    background: #f2edf6;
    border-color: rgba(160, 130, 220, 0.3);
    color: var(--text-secondary);
    cursor: not-allowed;
}

/* Canvas Area */
.canvas-container {
    flex: 1;
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
}

#canvas-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    /* The viewport interprets touch itself — one finger draws in the sketch
       editor, two navigate. Without this the browser treats a drag as a page
       scroll and fires pointercancel partway through every gesture. */
    touch-action: none;
}

/* Overlays sit on the 3D canvas, whose colour is whatever the user just
   designed. Their backgrounds are opaque so the text on them has a contrast
   ratio at all — a translucent panel over an arbitrary render does not. */
.instructions {
    position: relative;
    z-index: 5;
    text-align: center;
    pointer-events: none;
    background: var(--card-bg);
    padding: 2.25rem 3rem;
    border-radius: 26px;
    border: 2px solid rgba(255, 255, 255, 0.8);
    backdrop-filter: blur(10px);
    box-shadow: 0 14px 36px var(--shadow-soft);
}

.instructions h2 {
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-size: 1.55rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 0.5rem;
}

.instructions p {
    color: var(--text-secondary);
    font-weight: 500;
}

/* Loading Overlay */
.loading-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--bg-1);
    backdrop-filter: blur(6px);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 20;
    color: var(--text-primary);
    font-weight: 600;
}

/* Once a model is already visible, rebuilds stay non-blocking: the previous
   geometry remains inspectable while this compact status chip is shown. */
.loading-overlay.compact {
    top: 3.25rem;
    right: 1rem;
    left: auto;
    width: min(340px, calc(100% - 2rem));
    height: auto;
    padding: 0.78rem 0.9rem;
    border: 1px solid rgba(255, 122, 168, 0.28);
    border-radius: 16px;
    background: #fff;
    backdrop-filter: blur(10px);
    box-shadow: 0 8px 24px var(--shadow-soft);
    align-items: stretch;
    flex-direction: column;
    gap: 0.45rem;
    pointer-events: none;
}
.loading-overlay.compact .spinner { display: none; }
.loading-overlay.compact p {
    margin: 0;
    text-align: left;
    font-size: 0.82rem;
}
.loading-overlay.compact .viewer-progress,
.loading-overlay.compact .viewer-progress-meta { width: 100%; }

.spinner {
    width: 52px;
    height: 52px;
    border: 5px solid rgba(255, 143, 184, 0.25);
    border-left-color: var(--accent-color);
    border-radius: 50%;
    animation: spin 0.9s linear infinite;
    margin-bottom: 1rem;
}

/* Uploaded-artwork configuration mirrors the viewport right-click menu. */
.artwork-perimeter-card {
    display: grid;
    gap: 0.65rem;
    margin-top: 0.7rem;
    padding: 0.75rem;
    border: 1px solid var(--border-soft);
    border-radius: 14px;
    background: #fff;
}
#imagePerimeterPresetControls { display: grid; gap: 0.6rem; }
.image-tuning { display: flex; flex-direction: column; gap: 0.7rem; }

.artwork-config {
    margin-top: 0.85rem;
    padding: 0.8rem;
    border: 1px solid rgba(179, 155, 255, 0.35);
    border-radius: 16px;
    background: #fbf9ff;
}
.artwork-config-head,
.artwork-piece-head,
.artwork-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.55rem;
}
.artwork-config-summary {
    margin: 0.45rem 0 0.65rem;
    color: var(--text-secondary);
    font-size: 0.8rem;
    line-height: 1.35;
}
.trace-status {
    padding: 0.2rem 0.5rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 700;
    white-space: nowrap;
}
.trace-status.ok { color: var(--ok-text); background: #dff5ea; }
.trace-status.warn { color: var(--warn-text); background: #fdeed6; }
.trace-status.neutral { color: var(--text-secondary); background: #f1edf4; }
.artwork-actions { margin-bottom: 0.7rem; }
.mini-action {
    flex: 1;
    min-height: 36px;
    padding: 0.38rem 0.52rem;
    border: 1px solid rgba(255, 122, 168, 0.3);
    border-radius: 10px;
    background: #fff;
    color: var(--text-primary);
    font: 600 0.72rem 'Quicksand', sans-serif;
    cursor: pointer;
}
.mini-action:disabled {
    background: #f2edf6;
    color: var(--text-secondary);
    cursor: default;
}
.artwork-piece-list { display: grid; gap: 0.6rem; }
.artwork-piece-card {
    padding: 0.65rem;
    border: 1px solid var(--border-soft);
    border-radius: 13px;
    background: #fff;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.artwork-piece-card.selected {
    border-color: var(--accent-color);
    box-shadow: 0 0 0 3px rgba(255, 122, 168, 0.12);
}
.artwork-piece-title {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 0.82rem;
    font-weight: 700;
}
.artwork-swatch {
    width: 17px;
    height: 17px;
    border-radius: 5px;
    border: 1px solid rgba(0, 0, 0, 0.16);
    flex: none;
}
.artwork-piece-badges { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.artwork-badge {
    padding: 0.15rem 0.38rem;
    border-radius: 999px;
    background: #f0ecfa;
    color: var(--text-secondary);
    font-size: 0.65rem;
    font-weight: 700;
}
.artwork-badge.switch { color: var(--ok-text); background: #dff5ea; }
.artwork-piece-controls {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
    margin-top: 0.55rem;
}
.artwork-piece-controls .wide { grid-column: 1 / -1; }
.artwork-piece-controls label {
    display: grid;
    gap: 0.2rem;
    color: var(--text-secondary);
    font-size: 0.68rem;
    font-weight: 700;
}
.artwork-piece-controls select,
.artwork-piece-controls input[type="number"] {
    width: 100%;
    min-width: 0;
    min-height: 32px;
    padding: 0.42rem 0.48rem;
    border: 1px solid var(--border-soft);
    border-radius: 9px;
    background: #fff;
    color: var(--text-primary);
    font: 600 0.72rem 'Quicksand', sans-serif;
}
.artwork-switch-toggle {
    display: flex !important;
    grid-template-columns: none !important;
    flex-direction: row;
    align-items: center;
    gap: 0.4rem !important;
    color: var(--text-primary) !important;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* ============================================================
   REFLOW (quality bar row E4 — 1.4.10 Reflow, and row D1).

   400 % zoom on a 1280 px screen is a 320 px CSS viewport, so the same
   breakpoint that serves a phone has to serve a zoomed desktop: the layout
   goes to one column and nothing may scroll sideways. The sidebar's
   `clamp(500px, …)` floor is what used to force horizontal scrolling, so
   it is capped by the viewport at every width, not only under 768 px.
   ============================================================ */
.sidebar { max-width: 100vw; }

@media (max-width: 768px) {
    body { overflow: auto; }
    #app { flex-direction: column; height: auto; min-height: 100vh; }
    .sidebar {
        width: 100%;
        flex: none;
        max-height: none;
        overflow-y: visible;
        border-right: none;
        border-bottom: 1px solid rgba(255, 255, 255, 0.6);
        padding: 1.25rem;
        gap: 1rem;
        order: 2;
    }
    .canvas-container {
        order: 1;
        height: 46vh;
        min-height: 320px;
        flex: none;
    }
    .logo h1 { font-size: 1.8rem; }
    .icon-grid { grid-template-columns: repeat(6, 1fr); }
    /* The sidebar is the page here, so its sticky children would pin to the
       document and cover the controls instead of following the stack. */
    .stack-nav { position: static; margin-left: 0; margin-right: 0; }
    /* The viewport is only 46vh here, so the dock is folded by default (see
       src/export-dock.js) and its pill is tucked in tighter. The strip needs
       no width rule — it already spans the canvas, and its two columns wrap
       to one on their own flex-basis. */
    .export-float { left: 10px; right: 10px; bottom: 10px; }
    .export-float[data-collapsed="true"] { max-width: calc(100% - 20px); }
    /* NO `top` HERE, and that is not a style choice. The hiding mechanism is
       transform: translateY(-140%), which only clears the viewport from
       top: 0 — at top t and height h the bottom edge lands at t − 0.4h. At
       48 px and 45 px that left 30 px of a LIVE link permanently over the
       top-left of the 3D model, so a tap near the viewer corner navigated
       away from it. Both links can share top: 0; only one is ever focused. */
    .skip-link:nth-of-type(2) { left: 0; }
}

/* Very narrow (a 320 px viewport is 1280 px at 400 % zoom): every grid that
   assumes three or four columns has to give up columns rather than overflow. */
@media (max-width: 420px) {
    .sidebar { padding: 0.85rem; }
    .shape-grid { grid-template-columns: repeat(3, 1fr); }
    .icon-grid { grid-template-columns: repeat(4, 1fr); }
    .sketch-button-row, .sketch-tool-grid { grid-template-columns: repeat(2, 1fr); }
    .edge-presets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .edge-metrics { grid-template-columns: 1fr; }
    .artwork-piece-controls { grid-template-columns: 1fr; }
    .organic-sketch-actions { grid-template-columns: 1fr; }
    .export-dock-row { grid-template-columns: 1fr; }
    .color-row { flex-wrap: wrap; gap: 0.4rem; }
    /* The flex row is the <h2>, not the <summary> — see the summary markup
       note above. Wrapping the wrong element here left the settings summary
       squeezed to an ellipsis at 320 px instead of dropping to its own line. */
    .stack-section .section-title { flex-wrap: wrap; }
    .stack-section .section-meta { flex-basis: 100%; white-space: normal; }
    .stack-section .section-title::after { order: -1; margin-left: auto; }
    .logo h1 { font-size: 1.5rem; }
}

/* A short viewport (a laptop at 200 % zoom, or a landscape phone) cannot
   spare a permanently-parked strip at the top of the stack either. The export
   dock needs nothing here: it is capped at the canvas height and scrolls
   inside itself. */
@media (max-height: 620px) {
    .stack-nav { position: static; }
}

/* ============================================================
   Phase F0 — direct-manipulation UI (interact.js)
   ============================================================ */

/* Live value bubble that follows the cursor while dragging an accessory. */
.viewport-readout {
    position: absolute;
    display: none;
    pointer-events: none;
    z-index: 30;
    background: #fff;
    border: 2px solid #ffb3c6;
    border-radius: 999px;
    padding: 0.3rem 0.85rem;
    font-family: 'Quicksand', sans-serif;
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--accent-text);
    box-shadow: 0 4px 14px rgba(210, 61, 109, 0.18);
    white-space: nowrap;
}

/* Viewport component menu. Opened by right-click, by the Menu key, by
   Shift+F10, or by Enter on the focused viewport — one menu, one item list,
   four ways in (see the keyboard section of src/interact.js). */
.ctx-menu {
    position: fixed;
    z-index: 1000;
    min-width: 210px;
    max-width: min(340px, calc(100vw - 16px));
    background: #fff;
    border: 2px solid #ffd1e8;
    border-radius: 16px;
    padding: 0.35rem;
    box-shadow: 0 10px 30px rgba(180, 120, 170, 0.25);
    font-family: 'Quicksand', sans-serif;
    max-height: calc(100vh - 16px);
    overflow-y: auto;
}
.ctx-menu-header {
    padding: 0.35rem 0.75rem 0.25rem;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.ctx-menu button {
    display: block;
    width: 100%;
    min-height: 32px;
    text-align: left;
    border: none;
    background: none;
    border-radius: 10px;
    padding: 0.45rem 0.75rem;
    font-family: inherit;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--text-primary);
    cursor: pointer;
}
.ctx-menu button:hover,
.ctx-menu button:focus-visible {
    background: #ffe4f0;
    color: var(--accent-text);
}
/* Empty alt text: aria-checked already makes NVDA say "checked", so without
   the `/ ''` the tick is announced a second time as a glyph. */
.ctx-menu button[aria-checked="true"]::after {
    content: ' ✓' / '';
    color: var(--ok-text);
    font-weight: 800;
}
/* The keyboard hint at the foot of every menu. */
.ctx-menu-hint {
    padding: 0.4rem 0.75rem 0.25rem;
    margin-top: 0.2rem;
    border-top: 1px dashed var(--border-color);
    color: var(--text-secondary);
    font-size: 0.68rem;
    font-weight: 600;
    line-height: 1.35;
}

/* Phase F1 - sliders scrub quickly; paired numbers provide exact input. */
.number-control {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.35rem;
    margin-top: 0.45rem;
}
.exact-number {
    width: 6.4rem;
    min-height: 34px;
    border: 2px solid #efd8eb;
    border-radius: 10px;
    background: #fff;
    color: var(--text-primary);
    padding: 0.35rem 0.5rem;
    font: 700 0.85rem 'Quicksand', sans-serif;
    text-align: right;
}
.exact-number:focus {
    border-color: #ff8fb8;
    box-shadow: 0 0 0 3px rgba(255, 143, 184, 0.18);
}
.number-unit {
    min-width: 1.8rem;
    color: var(--text-secondary);
    font-size: 0.78rem;
    font-weight: 700;
}

/* Viewport selection and its matching sidebar control are one surface. */
.control-group.viewport-linked {
    transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}
.control-group.viewport-selected {
    border-color: #ff8fb8;
    box-shadow: 0 0 0 3px rgba(255, 143, 184, 0.18), 0 8px 22px rgba(180, 120, 170, 0.16);
    transform: translateX(2px);
}
#canvas-wrapper:focus-visible {
    outline: 3px solid rgba(255, 143, 184, 0.65);
    outline-offset: -3px;
}

/* Phase F4 - clicked edge: mini slider-and-number popover. */
.edge-pop {
    min-width: 220px;
}
.edge-pop-row {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.35rem 0.75rem 0.55rem;
}
.edge-pop-row input[type='range'] {
    flex: 1;
    min-width: 100px;
    margin: 0;
}
.edge-pop-row input[type='number'] {
    width: 3.6rem;
    border: 2px solid #efd8eb;
    border-radius: 10px;
    background: #fff;
    color: var(--text-primary);
    padding: 0.25rem 0.4rem;
    font: 700 0.85rem 'Quicksand', sans-serif;
    text-align: right;
}
.edge-pop-row input[type='number']:focus {
    border-color: #ff8fb8;
    box-shadow: 0 0 0 3px rgba(255, 143, 184, 0.18);
}


/* Visible per-edge geometry validation panel */
.edge-summary { padding: 0.7rem 0.8rem; border-radius: 12px; font-size: 0.78rem; font-weight: 650; line-height: 1.35; background: #f2eefb; border: 1px solid var(--border-soft); }
.edge-summary.ok { background: #e6f7ef; border-color: rgba(70,180,135,.45); }
.edge-summary.warn { background: #fdf1de; border-color: rgba(220,145,35,.5); }
.edge-summary.fail { background: #fdeaee; border-color: rgba(220,65,95,.5); }
.edge-card { padding: 0.85rem; border-radius: 15px; background: #fff; border: 1px solid var(--border-soft); display: flex; flex-direction: column; gap: 0.55rem; }
.edge-card-head { display:flex; align-items:center; justify-content:space-between; gap:.5rem; }
.edge-status { padding:.2rem .48rem; border-radius:999px; font-size:.68rem; font-weight:800; white-space:nowrap; background:#f1edf4; color:var(--text-secondary); }
.edge-status.ok { background:#dff5ea; color:var(--ok-text); }
.edge-status.warn { background:#fdeed6; color:var(--warn-text); }
.edge-status.fail { background:#fde3e8; color:var(--fail-text); }
.edge-status.checking { background:#ece7fa; color:#544495; }
.edge-field { display:grid; grid-template-columns: 1fr 1.25fr; gap:.55rem; align-items:center; font-size:.78rem; font-weight:700; }
.edge-number { margin-top:-.25rem; }
.edge-presets { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:.32rem; }
.edge-preset { min-height:30px; border:1px solid var(--border-soft); background:#fff; color:var(--text-primary); border-radius:9px; padding:.34rem .15rem; font-size:.67rem; font-weight:750; cursor:pointer; }
.edge-preset:hover { border-color:var(--accent-color); background:#fff5f9; }
.edge-preset.selected { background:#ffe7f0; border-color:var(--accent-color); }
.edge-preset.above-safe { border-style:dashed; color:var(--warn-text); background:#fdf3e2; }
.edge-preset.failed { border-color:#d44b67; color:var(--fail-text); background:#fdeaee; }
.edge-metrics { display:grid; grid-template-columns:repeat(3,1fr); gap:.35rem; font-size:.65rem; color:var(--text-secondary); }
.edge-metrics span { display:flex; flex-direction:column; gap:.12rem; padding:.35rem; border-radius:8px; background:#f6f2fc; }
.edge-metrics b { color:var(--text-primary); font-size:.7rem; }
/* `opacity` here would have dimmed the labels below the contrast floor; the
   sharp mode is signalled by the disabled controls themselves. */
.edge-card.sharp-mode input[type="range"]::-webkit-slider-runnable-track,
.edge-card.sharp-mode input[type="range"]::-moz-range-track { background:#ece7f2; }
.edge-card.sharp-mode .edge-presets .edge-preset { border-style:dotted; }

/* Organic orthographic sketch editor */
.organic-sketch-panel {
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
    padding: 0.85rem;
    border-radius: 16px;
    background: #f7f4ff;
    border: 1.5px solid rgba(179, 155, 255, 0.34);
}

/* v2.7 UX: numbered workflow strip + switch-position row */
.organic-step-strip {
    display: flex;
    gap: 0.4rem;
    margin-bottom: 0.5rem;
    list-style: none;
}
.org-step {
    flex: 1;
    text-align: center;
    padding: 0.3rem 0.2rem;
    border-radius: 10px;
    font: 700 0.68rem 'Quicksand', sans-serif;
    color: var(--text-secondary);
    background: #fff;
    border: 1.5px solid rgba(179, 155, 255, 0.22);
}
.org-step.done {
    color: var(--ok-text);
    background: #e2f6ec;
    border-color: rgba(102, 214, 158, 0.6);
}
.org-step.active {
    color: #5b2ba3;
    background: #ede7fb;
    border-color: rgba(179, 155, 255, 0.7);
}
.organic-anchor-row {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding: 0.6rem;
    border-radius: 12px;
    background: #f7f4ff;
    border: 1.5px solid rgba(179, 155, 255, 0.28);
}
.control-group, .slider-container, .organic-sketch-panel,
.organic-anchor-row, .organic-recess-strategy { min-width: 0; }
.control-group small, .organic-sketch-panel small { overflow-wrap: anywhere; }

.organic-anchor-row .anchor-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
}
.organic-anchor-row .anchor-controls label {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    font: 600 0.7rem 'Quicksand', sans-serif;
}
.organic-anchor-row .anchor-controls input[type="number"] {
    width: 3.9rem;
    min-height: 32px;
    border-radius: 8px;
    border: 1px solid rgba(179, 155, 255, 0.4);
    background: #fff;
    color: var(--text-primary);
    padding: 0.15rem 0.3rem;
}
.organic-fast-mode {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding: 0.55rem 0.65rem;
    border-radius: 11px;
    background: #e6f7ef;
    border: 1.5px solid rgba(66, 170, 128, 0.4);
}
.organic-fast-mode strong { font-size: 0.78rem; color: var(--ok-text); }
.organic-fast-mode small { line-height: 1.35; }

.organic-recess-strategy {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
    padding: 0.65rem;
    border-radius: 12px;
    background: #fff;
    border: 1.5px solid rgba(179, 155, 255, 0.28);
}
.organic-recess-strategy .sketch-button-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* The view-relative nudge pad. The arrows keep .mini-action's 36 px target (and
   mobile.css's 44) — this rule only lays them out and stops them stretching to
   the full row width the way a flex:1 action button does. */
.organic-anchor-row .anchor-nudge {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
}
.organic-anchor-row .anchor-nudge .anchor-nudge-label {
    font: 600 0.7rem 'Quicksand', sans-serif;
}
.organic-anchor-row .anchor-nudge .mini-action {
    flex: 0 1 2.75rem;
    font-size: 0.95rem;
    line-height: 1;
}
.organic-anchor-row .mini-action.active {
    background: #ece2fb;
    border-color: rgba(122, 63, 209, 0.65);
    color: #5b2ba3;
}
.organic-sketch-section {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
}
.organic-sketch-section > strong {
    font-size: 0.82rem;
    color: var(--text-primary);
}
.sketch-button-row {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.35rem;
}
.sketch-tool-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.35rem;
}
/* Auto-edge (Bambu-style) row: magnetic snap toggle + one-click trace. */
.auto-edge-row {
    grid-template-columns: repeat(2, 1fr) !important;
    margin-top: 0.15rem;
}
.auto-edge-row .sketch-btn span { margin-left: 0.3rem; }
#organicMagnetToggle.active {
    background: #dff5f7;
    border-color: #2bb6c9;
    color: var(--teal-text);
    box-shadow: 0 3px 10px rgba(43, 182, 201, 0.28);
}
#organicTraceOutline:not(:disabled) {
    border-style: dashed;
    border-color: #2bb6c9;
    color: var(--teal-text);
}
.sketch-btn {
    min-height: 34px;
    border: 1.5px solid var(--border-soft);
    border-radius: 10px;
    padding: 0.48rem 0.35rem;
    background: #fff;
    color: var(--text-primary);
    font: 700 0.72rem 'Quicksand', sans-serif;
    cursor: pointer;
    transition: all 0.15s ease;
}
.sketch-btn:hover:not(:disabled) {
    border-color: var(--accent-color);
    transform: translateY(-1px);
}
.sketch-btn.active {
    background: #ffeaf3;
    border-color: var(--accent-color);
    color: var(--accent-text);
    box-shadow: 0 3px 10px var(--shadow-pink);
}
.sketch-btn:disabled {
    background: #f2edf6;
    color: var(--text-secondary);
    border-style: dotted;
    cursor: not-allowed;
}
/* Selection regions: the add/subtract mode pair plus one chip per region on
   the active plane. The chip's three parts each do one thing — the sign
   toggles add/subtract, the label selects the region for handle editing, and
   the cross deletes it. */
#organicDrawOp {
    grid-template-columns: repeat(2, 1fr);
}
#organicDrawOp .sketch-btn span { margin-left: 0.3rem; }
#organicDrawOp [data-draw-op="subtract"].active {
    background: #fdeed6;
    border-color: #e07a24;
    color: var(--warn-text);
    box-shadow: 0 3px 10px rgba(224, 122, 36, 0.26);
}
.region-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
}
.region-chips:empty { display: none; }
.region-chip {
    display: inline-flex;
    align-items: stretch;
    border: 1.5px solid var(--border-soft);
    border-radius: 999px;
    background: #fff;
    overflow: hidden;
}
.region-chip.active {
    border-color: var(--accent-color);
    box-shadow: 0 2px 8px var(--shadow-pink);
}
.region-chip.subtract { border-color: #e9a765; }
.region-chip.invalid { border-color: var(--danger-color, #ff5b69); }
.region-chip button {
    border: 0;
    background: none;
    padding: 0.3rem 0.45rem;
    font: 700 0.72rem 'Quicksand', sans-serif;
    color: var(--text-primary);
    cursor: pointer;
    min-height: 30px;
}
.region-chip button:hover:not(:disabled) { background: #ffeaf3; }
.region-chip button:disabled { color: var(--text-secondary); cursor: not-allowed; }
.region-chip .region-op { color: var(--ok-text); }
.region-chip.subtract .region-op { color: var(--warn-text); }
.region-chip.active .region-pick { color: var(--accent-text); }
.region-chip .region-del { color: var(--fail-text); padding-left: 0.3rem; }
.organic-sketch-actions {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0.4rem;
}
.primary-mini {
    background: linear-gradient(135deg, #c8306a, #6d4fd4) !important;
    color: #fff !important;
    border-color: transparent !important;
}
#seamStatus.valid { color: var(--ok-text); font-weight: 700; }
#seamStatus.invalid { color: var(--fail-text); font-weight: 700; }
.organic-snap-label {
    position: absolute;
    z-index: 12;
    pointer-events: none;
    padding: 0.2rem 0.42rem;
    border-radius: 7px;
    background: #3d3050;
    color: white;
    font-size: 0.68rem;
    font-weight: 700;
    box-shadow: 0 4px 12px rgba(50, 30, 70, 0.25);
}
canvas.organic-sketch-active { cursor: crosshair !important; }

/* v2.6 · imported-asset colour + intersected three-plane sketches */
.organic-import-info {
    margin-top: 0.7rem;
    padding: 0.65rem;
    border: 1px solid rgba(160, 125, 190, 0.22);
    border-radius: 12px;
    background: #fff;
}
/* The "as imported / as printed" toggle. Full width under the swatches,
   because it is the answer to a question people ask while looking at exactly
   those swatches: "why are there only four?" */
.source-view-btn {
    display: block;
    width: 100%;
    margin: 0.5rem 0 0.25rem;
    min-height: 44px;
}

.source-view-btn[aria-pressed="true"] {
    background: #6d2fc9;
    border-color: #6d2fc9;
    color: #fff;
}

.organic-palette-swatches {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0.45rem 0 0.25rem;
}
.organic-palette-swatches span {
    width: 24px;
    height: 24px;
    border-radius: 7px;
    border: 2px solid rgba(255,255,255,0.85);
    box-shadow: 0 0 0 1px rgba(65,45,80,0.18);
}
/* `opacity: 0.62` here dropped this label to 2.7:1. A small caption that
   already sits under a heading does not need to be faded as well. */
.plane-state {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.6rem;
    line-height: 1;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.plane-state.ready { color: var(--ok-text); }
.plane-state.invalid { color: var(--fail-text); }
.sketch-btn.active .plane-state { color: var(--accent-text); }
.viewer-progress {
    width: min(310px, 72vw);
    height: 10px;
    overflow: hidden;
    border-radius: 999px;
    background: #fff;
    box-shadow: inset 0 0 0 1px rgba(80,60,100,0.28);
}
.viewer-progress-fill {
    width: 0%;
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--accent-color), var(--accent-2));
    transition: width 160ms ease;
}
.viewer-progress-meta {
    width: min(310px, 72vw);
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    font-size: 0.76rem;
}
.viewer-progress-meta span { text-align: right; color: var(--text-secondary); }
.loading-overlay.complete .spinner { display: none; }
.loading-overlay.complete #loadingStage::before { content: '✓ ' / ''; color: var(--ok-text); }
/* `--text-color` was never defined, so this chip has been inheriting the body
   colour by accident since it was written. */
.viewer-build-status {
    position: absolute;
    right: 14px;
    top: 14px;
    z-index: 8;
    max-width: min(360px, 70vw);
    padding: 0.42rem 0.65rem;
    border-radius: 999px;
    background: #fff;
    color: var(--text-primary);
    font-size: 0.72rem;
    font-weight: 700;
    box-shadow: 0 5px 18px rgba(50,30,70,0.13);
    pointer-events: none;
}
.viewer-build-status[data-state="working"] { color: var(--warn-text); }
.viewer-build-status[data-state="ready"] { color: var(--ok-text); }
.viewer-build-status[data-state="error"] { color: var(--fail-text); }

/* ============================================================
   STARTUP GUIDE — first-run walkthrough (src/guide.js).
   The backdrop is four strips around the highlight, never a sheet over it,
   so the control being explained stays lit and usable.
   ============================================================ */
.guide-open-btn {
    margin-top: 0.6rem;
    min-height: 40px;
    padding: 0.4rem 0.9rem;
    border: 2px solid var(--accent-color);
    border-radius: 999px;
    background: #fff;
    color: var(--accent-text);
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-size: 0.85rem;
    font-weight: 700;
    cursor: pointer;
    transition: all 0.2s ease;
}
.guide-open-btn:hover {
    background: #ffe4f0;
    transform: translateY(-1px);
    box-shadow: 0 6px 16px var(--shadow-pink);
}

.guide-root {
    position: fixed;
    inset: 0;
    z-index: 2000;
    font-family: 'Quicksand', sans-serif;
    /* The root spans the screen only so its children can be positioned
       against it — it must never intercept a click, or the hole in the
       backdrop would not be a hole. The strips and the card opt back in. */
    pointer-events: none;
}
.guide-mask {
    position: fixed;
    pointer-events: auto;
    background: rgba(60, 35, 75, 0.42);
    backdrop-filter: blur(1.5px);
    -webkit-backdrop-filter: blur(1.5px);
}
.guide-ring {
    position: fixed;
    border-radius: 20px;
    border: 3px solid var(--accent-color);
    box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.7), 0 10px 30px var(--shadow-pink);
    pointer-events: none;
    animation: guide-pulse 1.8s ease-in-out infinite;
}
@keyframes guide-pulse {
    0%, 100% { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.7), 0 10px 30px var(--shadow-pink); }
    50%      { box-shadow: 0 0 0 7px rgba(255, 214, 233, 0.55), 0 10px 30px var(--shadow-pink); }
}

.guide-card {
    position: fixed;
    pointer-events: auto;
    padding: 1.15rem 1.25rem 1rem;
    background: #fff;
    border: 2px solid #ffd1e8;
    border-radius: 22px;
    box-shadow: 0 18px 50px rgba(140, 90, 140, 0.32);
    color: var(--text-primary);
}
.guide-progress {
    height: 5px;
    border-radius: 999px;
    background: rgba(255, 143, 184, 0.2);
    overflow: hidden;
    margin-bottom: 0.7rem;
}
.guide-progress-fill {
    height: 100%;
    background: linear-gradient(90deg, #ff7aa8, #b39bff);
    border-radius: 999px;
    transition: width 0.25s ease;
}
.guide-count {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.guide-title {
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--accent-text);
    margin: 0.15rem 0 0.45rem;
}
.guide-body p {
    font-size: 0.92rem;
    font-weight: 600;
    line-height: 1.45;
    margin-bottom: 0.5rem;
}
.guide-body strong { color: var(--accent-text); }
.guide-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.9rem;
}
.guide-spacer { flex: 1; }
.guide-actions button {
    min-height: 44px;
    border-radius: 999px;
    font-family: 'Fredoka', 'Quicksand', sans-serif;
    font-weight: 700;
    font-size: 0.9rem;
    cursor: pointer;
    padding: 0.5rem 1.1rem;
    transition: all 0.18s ease;
}
.guide-skip, .guide-back {
    border: 2px solid var(--accent-color);
    background: #fff;
    color: var(--text-primary);
}
.guide-skip:hover, .guide-back:hover { background: #ffe4f0; color: var(--accent-text); }
.guide-next {
    border: none;
    background: linear-gradient(115deg, #c8306a, #6d4fd4);
    color: #fff;
    box-shadow: 0 8px 18px var(--shadow-pink);
}
.guide-next:hover { transform: translateY(-2px); }

@media (max-width: 900px) {
    .guide-card { width: calc(100vw - 32px) !important; }
}
