app-canvas {
    flex: 1;
    position: relative;
    display: block;
    background: radial-gradient(ellipse at center, #0d0d25 0%, #060612 100%);
    overflow: hidden;
    border: 1px solid var(--border);
}
#viewport-canvas {
    width: 100%;
    height: 100%;
    display: block;
    cursor: grab;
    /* REQUIRED, not an optimisation. The canvas implements two-finger
       pan+pinch and one-finger orbit via pointer events, and without this the
       browser claims the gesture first - a two-finger drag becomes a page
       zoom and the handler never sees it. `none` also stops the double-tap-to-
       zoom that would otherwise fight the double-tap-resets-view handler.
       Set here rather than in main.css so it cannot be overridden by a later
       broad rule; a canvas that scrolls the page is worse than one that does
       not respond at all. */
    touch-action: none;
}
#viewport-canvas:active { cursor: grabbing; }

.canvas-overlay {
    position: absolute;
    top: 10px;
    left: 10px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    pointer-events: none;
    z-index: 5;
}
.canvas-overlay .info-tag {
    background: rgba(18, 18, 42, 0.85);
    border: 1px solid var(--border);
    padding: 4px 10px;
    border-radius: 4px;
    font-size: 10px;
    color: var(--text2);
    letter-spacing: 0.3px;
    pointer-events: auto;
}
.canvas-overlay .info-tag span { color: var(--accent); font-family: var(--mono); }

/* Result-set picker row (canvas overlay, top-left) */
.canvas-overlay .result-picker {
    display: flex;
    align-items: center;
    gap: 6px;
    width: fit-content;
    padding: 3px 8px;
}
.canvas-overlay .info-tag .rp-label {
    color: var(--text2);
    font-family: inherit;
    font-size: 10px;
    letter-spacing: 0.3px;
    white-space: nowrap;
}
.result-picker select {
    font-size: 11px;
    font-family: var(--mono);
    color: var(--accent);
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 2px 4px;
    min-width: 230px;
    max-width: min(420px, calc(100vw - 320px));
    cursor: pointer;
}
.result-picker select:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}
.result-picker select option,
.result-picker select optgroup {
    /* These WERE hard-coded (#cfd6ff on #14142e), which is why the Result
       dropdown stayed dark in the sepia and light themes while every control
       around it re-themed. A native <select> paints its OPEN list from the
       option/optgroup colours on most platforms, so pinning the closed control
       is not enough -- both halves must be tokenised. `color: var(--text)` and
       `background: var(--panel2)` reproduce the dark default (#e0e0f0 on
       #1a1a3a) closely enough to read as unchanged, and now follow the theme
       in the other two. */
    color: var(--text);
    background: var(--panel2);
}

/* Top row of the overlay: the Result-set picker and the view-mode toggle.
   Both are in normal flow, so a narrow frame wraps the toggle onto its own
   line instead of stacking it on top of the picker. The picker is allowed to
   shrink first (flex:1 1 auto on it, min-width:0 so the select can actually
   give ground); the toggle does not, because a control that reflows under
   pressure is a control the user has to hunt for. */
.canvas-toprow {
    display: flex;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 5px;
    pointer-events: none;
}
.canvas-toprow > * { pointer-events: auto; }
.canvas-toprow .result-picker { flex: 1 1 320px; min-width: 0; }

/* View-mode toggle. Lives on the canvas frame rather than the header toolbar
   because it redraws the viewport and nothing else — from the bar it read as
   if it changed the whole model. Styled from the same theme variables as the
   other HUD chips so it follows Night / Light / Sepia with no per-theme
   override; .vbtn.active signals state with the accent rather than a fixed
   fill, so it stays legible on all three backgrounds. */
.view-toggle { flex: 0 0 auto; }
.vbtn {
    display: inline-grid;          /* both labels stack in ONE cell */
    padding: 4px 10px;
    font-size: 10px;
    letter-spacing: 0.3px;
    white-space: nowrap;
    color: var(--text2);
    background: var(--panel2);
    border: 1px solid var(--border);
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.2s;
}
.vbtn:hover { border-color: var(--accent); color: var(--accent); }
.vbtn.active { border-color: var(--accent); color: var(--accent); }

/* The 3D / Schematic labels. Both occupy the same grid cell; `visibility`
   rather than `display` is load-bearing — it removes the hidden label from the
   accessibility tree AND keeps its width reserved, which is the whole reason
   this is done in CSS instead of by swapping textContent in JS. A magic
   min-width would have been the other way to stop the button jumping, but it
   would have to be re-guessed per font, per theme and per label edit. */
.vlabel { grid-area: 1 / 1; text-align: center; }
.vlabel-sch { visibility: hidden; }
.vbtn.schematic .vlabel-3d { visibility: hidden; }
.vbtn.schematic .vlabel-sch { visibility: visible; }

.legend {
    position: absolute;
    bottom: 10px;
    left: 10px;
    display: flex;
    gap: 12px;
    pointer-events: none;
    z-index: 5;
    flex-wrap: wrap;
}
.legend-item {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 10px;
    color: var(--text2);
    background: rgba(18, 18, 42, 0.8);
    padding: 3px 8px;
    border-radius: 4px;
    border: 1px solid var(--border);
}
.legend-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    display: inline-block;
}

.viz-grid {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 5px;
    margin-top: 6px;
    pointer-events: auto;
    max-width: 940px;
    width: 100%;
}
.viz-grid .vc {
    background: rgba(18, 18, 42, 0.85);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 3px 5px;
}
.viz-grid .chk {
    display: flex;
    align-items: center;
    gap: 3px;
    font-size: 9px;
    color: var(--text2);
    cursor: pointer;
    margin-bottom: 3px;
    white-space: nowrap;
}
.viz-grid .chk input { accent-color: var(--accent); margin:0; }
.viz-grid .chk.on { color:#fff; }
.viz-grid .sz {
    display: flex;
    align-items: center;
    gap: 3px;
}
.viz-grid .sz input[type="range"] {
    flex: 1 1 auto;
    min-width: 24px;
    margin: 0;
}
.viz-grid .sz b {
    font-family: var(--mono);
    font-size: 9px;
    min-width: 26px;
    text-align: right;
    color: var(--accent2);
}

/* ---- Touch: stop the controls sitting on top of the model -----------------
   MEASURED on a 390px phone: 80% of the canvas was covered by overlay
   controls, hit-tested element by element - the tally was `div.vc` x8,
   `label.chk` x8, `div.viz-grid` x6. That is the app's own controls burying
   the thing they exist to help you look at, and it made the area you would
   naturally touch to orbit the model mostly dead.

   Enlarging the viz controls for touch (3 columns instead of 6, 24px
   checkboxes) is what pushed the grid from a short strip to a tall one, so
   this is a cost of the touch work being paid back here. On a phone the
   controls move OUT of the picture and become a strip below it: the canvas
   takes the remaining height, the overlay keeps its natural flow height, and
   the whole viewport is draggable.

   `min-height: 0` on the canvas is load-bearing inside a flex column - without
   it a flex item's automatic minimum size is its content size, the canvas
   refuses to shrink, and the strip is pushed out of the container entirely. */
@media (pointer: coarse) {
    app-canvas {
        display: flex;
        flex-direction: column;
    }
    #viewport-canvas {
        flex: 1 1 auto;
        height: auto;
        min-height: 120px;
    }
    .canvas-overlay {
        position: static;
        flex: 0 0 auto;
        width: 100%;
        padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
        gap: 8px;
        /* The container's `pointer-events: none` is what let the old overlay
           pass touches through to the canvas between controls; now that it is
           a real strip it no longer needs to sit over anything, but the
           children keep `auto` so they remain individually tappable. */
        overflow: hidden;
    }
    .canvas-overlay .result-picker { flex-wrap: wrap; }
}

/* ---- Touch: the visualisation controls ----------------------------------
   MEASURED at 390px and 320px: 49 tap targets were under 44px, and the worst
   were the six viz checkboxes at 13x13 with 13.3px labels - an 8mm target,
   well under even the WCAG 2.5.8 AA floor of 24x24.

   The 6-column grid is what makes them small, so the columns are reduced
   rather than the boxes grown into each other: at 3 columns a chip is about
   100px wide instead of 50, and the checkbox and its slider both get room to
   be hit. `pointer: coarse` keeps all of this off a mouse-driven desktop. */
@media (pointer: coarse) {
    .viz-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 8px;
    }
    .viz-grid .vc { padding: 8px 8px 10px; }
    .viz-grid .chk {
        font-size: 13px;
        min-height: 34px;
    }
    /* WCAG 2.5.8 AA: 24x24. The label sits next to it, so the whole chip row
       is the comfortable target and the box itself only has to be visible.
       `flex: 0 0 24px` is load-bearing and not a restatement of the width:
       the input is a flex item inside `.chk`, so a `width` alone was measured
       rendering at 21px - flex-shrink squeezed it back when the chip was
       tight. That is a defect a `width` assertion would have missed. */
    .viz-grid .chk input {
        flex: 0 0 24px;
        width: 24px;
        height: 24px;
        margin: -4px 0 -4px -4px;
    }
    /* Sliders need a taller track and a real thumb; a 4px-tall range input is
       the other half of the sub-44px population. */
    .viz-grid .sz input[type="range"] {
        height: 32px;
        min-width: 48px;
    }
    .viz-grid .sz b { font-size: 13px; min-width: 34px; }
}