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

    /* HIG-referenced icon system (js/icon-sprite.js + js/icon-text.js) —
       every icon is a <use> ref into a hidden sprite sheet, sized to the
       surrounding text and tinted via currentColor so it matches whatever
       button/label color it sits in, in both themes and every state
       (hover / active / disabled) with zero extra CSS per icon. */
    .icon {
      width: 1em; height: 1em; display: inline-block; flex-shrink: 0;
      vertical-align: -0.15em;
      fill: none; stroke: currentColor; stroke-width: 1.75;
      stroke-linecap: round; stroke-linejoin: round;
    }
    .icon + span, .icon + text { margin-left: 0.35em; }

    /* L4: every font-size below 12px (9/10/11/11.5px, ~92 rules here and
       more across VanVRAi.html/rbac-dashboard.html's inline <style>
       blocks) was raised to a 12px practical floor for sustained reading —
       below the review's own words, "unrelated to the viewport constraint
       that motivated it". Rules at 12px and above were left untouched. */

    /* Design tokens (colors, --vv-text-scale, light theme) and the shared
       :focus baseline moved to css/tokens.css on 2026-09-28 (UI-001, D1) so
       rbac-dashboard.html uses the same values. VanVRApp.html loads
       tokens.css before this file. */

    .btn-primary:focus, .draw-fab:focus, .tab-btn.active:focus {
      outline: 2px solid #fff;
      outline-offset: 2px;
      box-shadow: 0 0 0 4px var(--accent);
    }
    /* Model-detail segment bar (Info/Annotations/Draw) is styled like .tab-btn — same
       treatment so "this is a tab" reads identically at both nesting levels. */
    .seg-btn.active:focus {
      outline: 2px solid #fff;
      outline-offset: 2px;
      box-shadow: 0 0 0 4px var(--accent);
    }
    /* Region cue for the two chrome areas the viewer screen is made of, so
       "my keyboard is somewhere in here" is readable without hunting for the
       2px ring. Deliberately quiet — a tinted edge, not a second highlight. */
    header:focus-within { border-bottom-color: var(--accent); }
    .sidebar:focus-within { border-left-color: var(--accent); }

    /* L5: no prefers-reduced-motion support at all previously. Vestibular-
       disorder users who've asked the OS for reduced motion still got
       every CSS transition/animation at full strength. This doesn't
       require touching every individual rule — near-zero durations make
       transitions and animations resolve instantly instead of easing. */
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
      }
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: var(--bg);
      color: var(--text);
      /* 2026-10-06: iPad/iPhone Safari measures 100vh as the viewport with the
         browser toolbars HIDDEN, so with the tab bar + address bar showing the
         page was taller than the screen and the bottom of the canvas, library
         list and info panel were cut off (overflow: hidden, so unreachable).
         100dvh tracks the visible area; 100vh stays as the fallback for older
         browsers. Every other vh in the CSS got the same dvh override. */
      height: 100vh; height: 100dvh;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    /* `body.auth-check { display: none }` lived here to blank the page while
       the old blanket auth gate redirected to login. That gate was removed on
       2026-08-28 (see js/vanvrapp-auth-gate.js for the full reasoning) and was
       this rule's only writer, so the rule went with it. */

    /* ══════════════════════════════════════
       HEADER
    ══════════════════════════════════════ */
    header {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 0 14px;
      height: 48px;
      background: var(--surface);
      border-bottom: 1px solid var(--border);
      flex-shrink: 0;
      z-index: 20;
    }

    #lib-toggle-btn {
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 5px 11px;
      border-radius: 7px;
      font-size: calc(12px * var(--vv-text-scale));
      font-weight: 600;
      cursor: pointer;
      border: 1px solid var(--border);
      background: transparent;
      color: var(--muted);
      transition: all 0.15s;
      white-space: nowrap;
      flex-shrink: 0;
    }
    #lib-toggle-btn:hover { background: var(--surface2); color: var(--text); }
    #lib-toggle-btn.active {
      background: var(--accent-dim);
      color: var(--accent-l);
      border-color: rgba(124,58,237,0.4);
    }
    #lib-toggle-btn .lib-count {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: var(--accent-dim);
      color: var(--accent-l);
      border-radius: 8px;
      font-size: calc(12px * var(--vv-text-scale));
      font-weight: 800;
      min-width: 16px;
      height: 16px;
      padding: 0 4px;
      display: none;
    }
    #lib-toggle-btn .lib-count.show { display: inline-flex; }

    .header-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }

    .logo {
      display: flex;
      align-items: center;
      gap: 7px;
      flex-shrink: 0;
      text-decoration: none;
    }
    .logo-icon {
      /* CI v2 symbol (img), no tile */
      width: 28px; height: 28px; object-fit: contain; flex-shrink: 0;
    }
    .logo-text {
      font-size: calc(15px * var(--vv-text-scale)); font-weight: 800; letter-spacing: -0.5px;
      background: linear-gradient(135deg, var(--accent-l), var(--accent2));
      -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
    }

    .model-name {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); flex: 1;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
    }
    .model-name.loaded { color: var(--text); }

    .btn {
      display: inline-flex; align-items: center; gap: 5px;
      padding: 5px 12px; border-radius: 7px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 500;
      cursor: pointer; border: none; transition: all 0.15s;
      white-space: nowrap; flex-shrink: 0;
    }
    .btn-primary {
      background: linear-gradient(135deg, var(--accent-l), var(--accent2));
      color: #fff; box-shadow: 0 2px 8px rgba(124,58,237,0.25);
    }
    .btn-primary:hover { opacity: 0.85; }
    .btn-ghost { background: transparent; color: var(--muted); border: 1px solid var(--border); }
    .btn-ghost:hover { background: var(--surface2); color: var(--text); }
    .btn-ghost:disabled { opacity: 0.28; cursor: not-allowed; pointer-events: none; }
    /* UI-202: baked-in background asset missing on the server. Stays clickable
       (so the real error toast + Dark fallback still fire) but visually flagged. */
    .btn-ghost.bg-asset-missing { opacity: 0.75; border-style: dashed; }
    .btn-danger { background: rgba(248,113,113,0.1); color: var(--error); border: 1px solid rgba(248,113,113,0.3); }
    .btn-danger:hover { background: rgba(248,113,113,0.2); border-color: var(--error); }
    .btn-annot { background: transparent; color: var(--muted); border: 1px solid var(--border); }
    .btn-annot:hover { background: var(--annot-dim); color: var(--annot); border-color: var(--annot); }
    .btn-annot.active { background: var(--annot-dim); color: var(--annot); border-color: var(--annot); }
    .btn-close { background: transparent; color: var(--muted); border: 1px solid var(--border); }
    .btn-close:hover { background: var(--surface2); color: var(--text); }
    .btn-wide { width: 100%; justify-content: center; }
    /* UI-204: Display > Text Size controls — three "A" presets sized to
       visually preview small/medium/large before clicking (more intuitive
       than a "Normal/Large/X-Large" text label), plus a slider for
       fine-grained control. Reuses .btn-annot's accent "active" state.
       NOTE: the .ts-icon sizes below are fixed reference sizes for the
       preview glyphs themselves, not part of the --vv-text-scale system —
       they always look like small/medium/large regardless of the user's
       chosen scale. */
    .text-size-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
    .ts-btn { padding: 4px 12px; min-width: 38px; justify-content: center; }
    .ts-icon { font-weight: 700; line-height: 1; }
    .ts-icon-sm { font-size: 11px; }
    .ts-icon-md { font-size: 15px; }
    .ts-icon-lg { font-size: 19px; }
    .text-size-custom {
      display: flex; align-items: center; gap: 8px; margin-top: 8px;
    }
    .text-size-slider { flex: 1; min-width: 0; }
    .text-size-val {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted);
      min-width: 34px; text-align: right; font-variant-numeric: tabular-nums;
    }

    .theme-toggle {
      height: 28px; padding: 0 9px; border-radius: 7px;
      background: var(--surface2); border: 1px solid var(--border);
      color: var(--text); font-size: calc(.82rem * var(--vv-text-scale)); cursor: pointer;
      display: flex; align-items: center; gap: 5px;
      transition: all .2s; white-space: nowrap; flex-shrink: 0;
    }
    .theme-toggle:hover { border-color: var(--accent-l); background: var(--accent-dim); }

    /* ══════════════════════════════════════
       BODY LAYOUT
    ══════════════════════════════════════ */
    .app-body { display: flex; flex: 1; overflow: hidden; }

    /* ══════════════════════════════════════
       LIBRARY PANEL
    ══════════════════════════════════════ */
    #library-panel {
      width: var(--lib-width); min-width: var(--lib-width);
      background: var(--surface); border-right: 1px solid var(--border);
      display: flex; flex-direction: column; overflow: hidden;
      transition: width 0.25s cubic-bezier(.4,0,.2,1),
                  min-width 0.25s cubic-bezier(.4,0,.2,1), opacity 0.2s;
      flex-shrink: 0;
    }
    /* P0-4 follow-up (2026-08-11): width/opacity alone left the search input,
       sort <select>, and every .lib-item inside still in the Tab order even
       though the panel is invisible and zero-width — a keyboard user could
       Tab straight into content they can't see or interact with. `visibility`
       removes the whole subtree from the tab order; it isn't transition-able
       so it just switches with the rest of the collapse, no separate delay
       needed since opacity is already carrying the visual fade. */
    #library-panel.collapsed { width: 0; min-width: 0; opacity: 0; pointer-events: none; visibility: hidden; }

    .lib-search { padding: 10px 10px 8px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
    .lib-search-wrap { position: relative; }
    .lib-search-wrap input {
      width: 100%; padding: 7px 10px 7px 30px;
      background: var(--surface2); border: 1px solid var(--border);
      border-radius: 7px; color: var(--text); font-size: calc(12px * var(--vv-text-scale));
      outline: none; transition: border-color 0.2s; font-family: inherit;
    }
    .lib-search-wrap input:focus { border-color: var(--accent-l); }
    .lib-search-wrap input::placeholder { color: var(--muted); }
    .lib-search-icon {
      position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
      color: var(--muted); font-size: calc(12px * var(--vv-text-scale)); pointer-events: none;
    }

    .lib-header {
      display: flex; align-items: center; justify-content: space-between;
      padding: 7px 12px 5px; flex-shrink: 0;
    }
    .lib-header-title {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; letter-spacing: 1.2px;
      text-transform: uppercase; color: var(--muted);
    }
    .lib-sort-btn {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); background: none; border: none;
      cursor: pointer; padding: 2px 5px; border-radius: 4px; transition: all 0.15s;
    }
    .lib-sort-btn:hover { background: var(--surface2); color: var(--text); }

    .lib-list { flex: 1; overflow-y: auto; }
    .lib-list::-webkit-scrollbar { width: 4px; }
    .lib-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

    .lib-empty {
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      height: 100%; gap: 10px; padding: 20px; color: var(--muted);
    }
    .lib-empty-icon { font-size: calc(36px * var(--vv-text-scale)); opacity: 0.2; }
    .lib-empty-text { font-size: calc(12px * var(--vv-text-scale)); text-align: center; line-height: 1.7; }
    .lib-empty-sub  { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); opacity: 0.7; text-align: center; }

    /* CSP: extracted from a JS-generated style="..." attribute (see
       groupHead() in viewer-model.js) — the color itself is still applied
       per-group via CSSOM since it's data-driven, this just holds the
       static layout that used to live inline alongside it. */
    .lib-group-head {
      display: flex; align-items: center; gap: 7px; padding: 11px 4px 5px;
      font-size: calc(9px * var(--vv-text-scale)); font-weight: 800; letter-spacing: 1px; text-transform: uppercase;
      color: var(--muted);
    }
    .lib-group-dot {
      width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex-shrink: 0;
    }
    /* Accordion (2026-10-14): the head is a button that folds its group. The
       count already sits at the right (csp-s-ff7831cb); the chevron follows it. */
    .lib-group-head[data-group-toggle] {
      cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent;
      padding: 11px 10px 5px 4px;
    }
    .lib-group-head[data-group-toggle]:hover { filter: brightness(1.25); }
    .lib-group-head[data-group-toggle]:focus { outline: 2px solid var(--accent-l); outline-offset: -2px; border-radius: 4px; }
    .lib-group-head[data-group-toggle]::after {
      content: ''; flex: none; width: 5px; height: 5px; margin-left: 2px;
      border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
      transform: rotate(45deg) translateY(-1px); transition: transform .15s;
    }
    .lib-group.collapsed > .lib-group-head[data-group-toggle]::after { transform: rotate(-45deg); }
    .lib-group.collapsed > .lib-group-body { display: none; }

    .lib-item {
      display: flex; align-items: center; gap: 9px; padding: 9px 11px;
      cursor: pointer; border-bottom: 1px solid rgba(39,44,63,0.5);
      transition: background 0.1s; position: relative;
    }
    .lib-item:hover { background: var(--surface2); }
    .lib-item.active {
      background: var(--surface3); border-left: 3px solid var(--accent-l); padding-left: 8px;
    }
    .lib-item.active .lib-item-name { color: var(--accent-l); }

    .lib-item-icon {
      width: 36px; height: 36px; border-radius: 8px;
      background: linear-gradient(135deg, var(--accent-dim), var(--accent2-dim));
      border: 1px solid rgba(124,58,237,0.2);
      display: flex; align-items: center; justify-content: center;
      font-size: calc(16px * var(--vv-text-scale)); flex-shrink: 0; transition: all 0.15s;
    }
    .lib-item.active .lib-item-icon {
      background: linear-gradient(135deg, rgba(124,58,237,0.25), rgba(96,165,250,0.2));
      border-color: rgba(124,58,237,0.4);
    }
    .lib-item-body { flex: 1; min-width: 0; }
    .lib-item-name {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 600; color: var(--text);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.3; margin-bottom: 2px;
    }
    .lib-item-meta {
      display: flex; align-items: center; gap: 5px; font-size: calc(12px * var(--vv-text-scale)); color: var(--muted);
    }
    .lib-item-dot { opacity: 0.7; }

    .lib-item-badges { display: flex; align-items: center; gap: 4px; margin-top: 3px; }
    .lib-badge {
      display: inline-flex; align-items: center; gap: 2px;
      padding: 1px 5px; border-radius: 4px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; white-space: nowrap;
    }
    .lib-badge-annot {
      background: var(--annot-dim); color: var(--annot); border: 1px solid rgba(255,200,50,0.3);
    }

    .lib-match-snippet {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-top: 3px; line-height: 1.4;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      display: flex; align-items: center; gap: 4px;
    }
    .lib-match-snippet mark { background: rgba(255,200,50,0.25); color: var(--annot); border-radius: 2px; padding: 0 2px; font-style: normal; }
    .lib-match-source {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; padding: 1px 4px; border-radius: 3px; white-space: nowrap; flex-shrink: 0;
    }
    .lib-match-source.name   { background: var(--accent2-dim); color: var(--accent2); }
    .lib-match-source.annot  { background: var(--annot-dim);   color: var(--annot); }
    .lib-item-name mark { background: var(--accent2-dim); color: var(--accent2); border-radius: 2px; padding: 0 1px; font-style: normal; }
    .lib-share-btn { cursor: pointer; color: var(--muted); transition: color 0.15s; font-size: calc(12px * var(--vv-text-scale)); }
    .lib-share-btn:hover { color: var(--text); }
    /* Course-mode sidebar: course + week info panel */
    .lib-course-info-btn {
      flex-shrink: 0; cursor: pointer; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
      color: var(--accent-l); background: rgba(124,58,237,0.12);
      border: 1px solid rgba(124,58,237,0.35); border-radius: 6px;
      padding: 3px 8px; white-space: nowrap; transition: all 0.12s;
    }
    .lib-course-info-btn:hover, .lib-course-info-btn.active { background: rgba(124,58,237,0.28); }
    .lib-info-panel { margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(124,58,237,0.25); }
    .lib-info-row { display: flex; gap: 8px; font-size: calc(12px * var(--vv-text-scale)); line-height: 1.7; }
    .lib-info-row span { color: var(--muted); min-width: 74px; flex-shrink: 0; }
    .lib-info-row b { color: var(--text); font-weight: 700; word-break: break-word; }
    .lib-info-week { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--border); }
    .lib-info-week-title { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l); margin-bottom: 4px; }
    .lib-info-week-desc { font-size: calc(12px * var(--vv-text-scale)); color: var(--text); line-height: 1.6; }
    .lib-info-week-desc p { margin: 3px 0; }
    .lib-info-week-desc h2, .lib-info-week-desc h3 { margin: 5px 0 3px; }
    .lib-info-week-desc ul, .lib-info-week-desc ol { padding-left: 16px; margin: 3px 0; }
    .lib-info-week-desc img { max-width: 100%; border-radius: 6px; margin: 4px 0; display: block; }
    .lib-info-week-desc .rte-video-wrap, .lib-info-week-desc .rte-audio-wrap { margin: 5px 0; border-radius: 6px; overflow: hidden; background: #000; }
    .lib-info-week-desc .rte-video-wrap iframe, .lib-info-week-desc .rte-video-wrap video { width: 100%; aspect-ratio: 16/9; display: block; border: none; }
    .lib-info-week-desc .rte-audio-wrap audio { width: 100%; display: block; }
    .lib-info-attach { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
    .lib-info-attach-item {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--accent-l); text-decoration: none;
      padding: 4px 7px; background: var(--surface2); border: 1px solid var(--border);
      border-radius: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .lib-info-attach-item:hover { border-color: var(--accent); }

    /* ── Course banner ── */
    /* Sean (2026-08-11): this showed up empty even in plain/public Library
       mode. Root cause: it defaulted to `display:flex` here with nothing
       ever setting it back to `none` on a normal page load — `flex` only
       ever got explicitly APPLIED (never removed) by JS, so on any load
       that never happened to touch it, it just sat there visible and blank.
       Defaulting to `none` and letting js/site-config.js's vvUpdateCourse
       Banner() be the single place that flips it to `flex` (only once a
       real course context exists) means a code path that forgets to call
       it now fails safe — hidden — instead of showing an empty bar. */
    #course-banner {
      display: none; align-items: center; gap: 8px;
      padding: 6px 16px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 600;
      background: linear-gradient(90deg, rgba(124,58,237,0.18), rgba(96,165,250,0.10));
      border-bottom: 1px solid rgba(124,58,237,0.3);
      flex-shrink: 0;
    }
    .course-banner-sep { color: var(--border); }
    #sdb-instructor-toggle.active { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }
    /* T4 (2026-08-17): #admin-btn ("Manage") is now role-aware in course mode —
       owner/school_admin/instructor keep it (body.can-manage, set in
       vvRefreshAuthBtns()/viewer-core.js); student/viewer/anonymous still get
       it hidden. #share-btn stays unconditionally hidden in course mode —
       separate, intentional decision, not part of this fix.

       2026-08-21 (Sean: "학생으로 들어갔을 때 모델을 열고 헤더에 2번째 스크린샷
       처럼 메니지 버튼 넣어줘"): signed-in students/viewers now get the button
       too (body.can-study), but it opens #student-panel, NOT #admin-panel —
       see openStudentPanel() in js/student-panel.js. Only anonymous visitors
       stay hidden, and they have nothing to open anyway. can-manage and
       can-study are mutually exclusive by construction in
       vvRefreshAuthBtns(); the :not() pair is belt-and-braces. */
    body.course-mode:not(.can-manage):not(.can-study) #admin-btn { display: none !important; }
    body.course-mode #share-btn { display: none !important; }

    /* ══════════════════════════════════════
       LIVE VIEW SYNC (Phase 2.5)
       docs/LIVE_VIEW_SYNC_PLAN.md §3.2 / §5.5
    ══════════════════════════════════════ */
    @keyframes vvLivePulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

    #vvLiveBtn .vv-live-dot {
      display: inline-block; width: 7px; height: 7px; border-radius: 50%;
      background: var(--muted); margin-right: 5px;
    }
    #vvLiveBtn.vv-live-on { background: rgba(248,113,113,0.12); color: var(--error); border-color: rgba(248,113,113,0.4); }
    #vvLiveBtn.vv-live-on .vv-live-dot { background: var(--error); animation: vvLivePulse 1.4s ease-in-out infinite; }

    /* Instructor panel */
    .vv-live-panel {
      position: fixed; top: 48px; right: 16px; z-index: 220;
      width: 300px; max-width: calc(100vw - 32px);
      background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
      box-shadow: 0 12px 40px rgba(0,0,0,0.5); padding: 14px; font-size: calc(12px * var(--vv-text-scale));
      transition: box-shadow 0.15s;
    }
    .vv-live-panel.vv-live-panel-positioned { right: auto; }
    .vv-live-panel.vv-live-panel-dragging {
      box-shadow: 0 12px 40px rgba(0,0,0,0.8), 0 0 20px rgba(124,58,237,0.3);
    }
    .vlp-header { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: calc(13px * var(--vv-text-scale)); margin-bottom: 10px; color: var(--text); user-select: none; }
    /* Minimize/expand toggle (Sean, 2026-08-11) — lives inside .vlp-header,
       which live-panel-drag.js treats as the drag handle; its own
       "skip drag on button clicks" check already exempts this. */
    .vlp-collapse-btn {
      flex-shrink: 0; margin-left: auto; width: 22px; height: 22px; border-radius: 6px;
      border: 1px solid transparent; background: transparent; color: var(--muted);
      cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0;
      transition: all .15s;
    }
    .vlp-collapse-btn .icon { width: 14px; height: 14px; }
    .vlp-collapse-btn:hover { background: var(--surface2); color: var(--text); border-color: var(--border); }
    /* Collapsed: header row only, no bottom margin so the panel shrinks to
       a slim bar instead of leaving empty space where the body used to be. */
    .vv-live-panel.vlp-collapsed .vlp-header { margin-bottom: 0; }
    .vlp-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex-shrink: 0; }
    .vlp-dot.on { background: var(--error); animation: vvLivePulse 1.4s ease-in-out infinite; }
    .vlp-sub { color: var(--muted); font-weight: 500; margin-left: auto; font-size: calc(11px * var(--vv-text-scale)); }
    .vlp-modes { display: flex; gap: 6px; margin-bottom: 10px; }
    .vlp-mode-btn {
      flex: 1; padding: 6px 0; border-radius: 7px; border: 1px solid var(--border);
      background: transparent; color: var(--muted); font-size: calc(11px * var(--vv-text-scale)); cursor: pointer; transition: all .15s;
    }
    .vlp-mode-btn:hover { background: var(--surface2); color: var(--text); }
    .vlp-mode-btn.active { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }
    .vlp-sync-btn {
      width: 100%; padding: 10px; border-radius: 9px; border: none; cursor: pointer;
      background: linear-gradient(135deg, var(--accent-l), var(--accent2)); color: #fff;
      font-weight: 700; font-size: calc(13px * var(--vv-text-scale)); margin-bottom: 10px; transition: opacity .15s;
    }
    .vlp-sync-btn:hover { opacity: .85; }
    .vlp-sync-btn.cancelling { background: rgba(248,113,113,0.15); color: var(--error); }
    .vlp-roster { max-height: 160px; overflow-y: auto; border-top: 1px solid var(--border); padding-top: 8px; margin-bottom: 8px; }
    .vlp-roster-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; color: var(--text); }
    .vlp-roster-row .vru { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .vlp-roster-row .vrs { font-size: calc(10px * var(--vv-text-scale)); color: var(--muted); flex-shrink: 0; }
    .vlp-roster-row.ready .vrs { color: var(--success); }
    .vlp-roster-empty { color: var(--muted); text-align: center; padding: 6px 0; }
    .vlp-start-btn, .vlp-end-btn, .vlp-takeover-btn {
      width: 100%; padding: 9px; border-radius: 8px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 600;
      cursor: pointer; border: 1px solid var(--border); background: transparent; color: var(--text);
    }
    .vlp-start-btn, .vlp-takeover-btn { background: linear-gradient(135deg, var(--accent-l), var(--accent2)); color: #fff; border: none; }
    .vlp-end-btn { color: var(--error); border-color: rgba(248,113,113,0.3); }
    .vlp-hint { color: var(--muted); font-size: calc(11px * var(--vv-text-scale)); line-height: 1.5; margin-bottom: 10px; }

    /* LV-802: instructor's student-pointer/pin visibility toggle + reset,
       inside the Live panel. .vlp-mode-btn is reused for the two toggle
       buttons (same active/hover states as the Snap/Follow/Lock selector
       above), just laid out with a label instead of stretching edge-to-edge. */
    .vlp-share-row {
      display: flex; align-items: center; gap: 6px; margin-bottom: 8px;
      padding-top: 8px; border-top: 1px solid var(--border);
    }
    .vlp-share-label {
      font-size: calc(11px * var(--vv-text-scale)); color: var(--muted); flex: 1;
    }
    .vlp-share-row .vlp-mode-btn { flex: 0 0 auto; padding: 5px 10px; }
    .vlp-reset-pins-btn {
      width: 100%; padding: 7px; border-radius: 8px; font-size: calc(11px * var(--vv-text-scale));
      font-weight: 600; cursor: pointer; border: 1px solid var(--border);
      background: transparent; color: var(--muted); margin-bottom: 10px; transition: all .15s;
    }
    .vlp-reset-pins-btn:hover { background: var(--surface2); color: var(--text); }

    /* LV-802: student-side "Drop Pin" button, in the Drawing sidebar section
       (student-draw-section). Your pointer itself has no button — see the
       comment on #vvl-point-pin-row in VanVRAi.html. */
    .vvl-point-pin-row { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
    #vvl-pin-btn.active {
      background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4);
    }
    /* Instructor roster row's per-student "watch this pointer" toggle. */
    .vlp-watch-btn {
      flex-shrink: 0; width: 22px; height: 22px; border-radius: 6px; border: 1px solid var(--border);
      background: transparent; color: var(--muted); cursor: pointer; display: flex;
      align-items: center; justify-content: center; padding: 0; transition: all .15s;
    }
    .vlp-watch-btn .icon { width: 13px; height: 13px; }
    .vlp-watch-btn:hover { background: var(--surface2); color: var(--text); }
    .vlp-watch-btn.active { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }

    /* Student countdown overlay */
    .vv-live-countdown {
      position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
      z-index: 90; pointer-events: none;
    }
    .vv-live-countdown[hidden] { display: none; }
    .vlc-card {
      background: rgba(20,23,32,0.92); border: 1px solid var(--border); border-radius: 14px;
      padding: 22px 32px; text-align: center; box-shadow: 0 12px 40px rgba(0,0,0,0.6);
      backdrop-filter: blur(6px); animation: vlcIn .2s ease-out;
    }
    @keyframes vlcIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
    .vlc-title { font-size: calc(13px * var(--vv-text-scale)); color: var(--muted); margin-bottom: 6px; }
    .vlc-digit { font-size: calc(72px * var(--vv-text-scale)); font-weight: 800; font-variant-numeric: tabular-nums; color: var(--accent-l); line-height: 1; }
    .vlc-progress { margin-top: 10px; }
    .vlc-progress-label { font-size: calc(11px * var(--vv-text-scale)); color: var(--muted); margin-bottom: 4px; }
    .vlc-progress-track { width: 180px; height: 4px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
    .vlc-progress-fill { height: 100%; width: 0%; background: var(--accent2); transition: width .15s; }
    @media (prefers-reduced-motion: reduce) {
      .vlc-card { animation: none; }
    }

    /* Student persistent banner */
    .vv-live-banner {
      position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 80;
      display: flex; align-items: center; gap: 10px; background: rgba(20,23,32,0.92);
      border: 1px solid var(--border); border-radius: 999px; padding: 7px 14px; font-size: calc(12px * var(--vv-text-scale));
      color: var(--text); box-shadow: 0 6px 20px rgba(0,0,0,0.4); backdrop-filter: blur(6px);
    }
    .vv-live-banner[hidden] { display: none; }
    /* Focus mode (Sean, 2026-08-10 — renamed from "Lock", full instructor
       control): needs to read as important at a glance, not blend in with
       the neutral loading/paused/following states the same pill also
       carries. Colors are fixed here rather than pulled from --error —
       the light-theme override for that token is one of the still-open
       accessibility gaps (UX-908, pre-deploy backlog), so a safety-relevant
       banner shouldn't depend on it resolving correctly in both themes.
       White text on a solid, moderately dark red keeps contrast well above
       4.5:1 in both dark and light mode regardless of that gap. */
    .vv-live-banner.vlb-focus {
      background: #b91c1c; border-color: #7f1d1d; color: #fff;
      box-shadow: 0 6px 20px rgba(185,28,28,0.45);
    }
    .vlb-action { background: none; border: none; color: var(--accent-l); font-weight: 700; cursor: pointer; font-size: calc(12px * var(--vv-text-scale)); padding: 0; }
    .vv-live-banner.vlb-focus .vlb-action { color: #fff; text-decoration: underline; }
    .vlb-action[hidden] { display: none; }
    /* Student view guard (2026-10-19) — see vvGuardPost() in js/viewer-core.js.
       Top-centre so it neither covers the bottom follow/focus pill nor the
       top-left share indicator. Keyboard focus is always visible (:focus). */
    .vv-model-off-banner {
      position: absolute; top: 12px; left: 50%; transform: translateX(-50%); z-index: 80;
      display: flex; align-items: center; gap: 10px; max-width: min(92%, 560px);
      background: rgba(20,23,32,0.92); border: 1px solid var(--border); border-radius: 999px;
      padding: 7px 14px; font-size: calc(12px * var(--vv-text-scale)); color: var(--text);
      box-shadow: 0 6px 20px rgba(0,0,0,0.4); backdrop-filter: blur(6px);
    }
    /* Edge cue (2026-10-19, js/edge-cue.js). Opacity is written per edge by JS
       (0..~0.3), so every edge starts invisible. Above the canvas, below the
       banners/pills (z 80), never intercepts input. */
    .vv-edge-cue { position: absolute; inset: 0; z-index: 70; pointer-events: none; overflow: hidden; }
    .vv-edge-cue .vec { position: absolute; opacity: 0; }
    .vv-edge-cue .vec-l, .vv-edge-cue .vec-r { top: 0; bottom: 0; width: 16%; }
    .vv-edge-cue .vec-t, .vv-edge-cue .vec-b { left: 0; right: 0; height: 22%; }
    .vv-edge-cue .vec-l { left: 0;   background: linear-gradient(to right,  var(--accent-l, #8ab4ff), transparent); }
    .vv-edge-cue .vec-r { right: 0;  background: linear-gradient(to left,   var(--accent-l, #8ab4ff), transparent); }
    .vv-edge-cue .vec-t { top: 0;    background: linear-gradient(to bottom, var(--accent-l, #8ab4ff), transparent); }
    .vv-edge-cue .vec-b { bottom: 0; background: linear-gradient(to top,    var(--accent-l, #8ab4ff), transparent); }
    .vv-model-off-banner[hidden] { display: none; }
    .vv-model-off-banner .vlb-action { white-space: nowrap; }
    .vv-model-off-banner .vlb-action:focus { outline: 2px solid var(--accent-l); outline-offset: 2px; border-radius: 4px; }

    /* P0-1 (UX-903, pre-deploy backlog 2026-08-10): persistent "you're
       visible to your instructor" pill. Deliberately quieter/smaller than
       .vv-live-banner (top-left, not competing with the bottom-center
       follow/focus banner) since it's on screen for the entire session,
       not just during a special mode — see _updateShareIndicator() in
       js/live-sync.js. */
    .vv-live-share-indicator {
      position: absolute; top: 12px; left: 12px; z-index: 80;
      display: flex; align-items: center; gap: 6px; background: rgba(20,23,32,0.85);
      border: 1px solid var(--border); border-radius: 999px; padding: 5px 12px;
      font-size: calc(11px * var(--vv-text-scale)); color: var(--muted);
      box-shadow: 0 4px 14px rgba(0,0,0,0.35); backdrop-filter: blur(6px);
      pointer-events: none;
    }
    .vv-live-share-indicator[hidden] { display: none; }
    .vv-live-share-indicator .icon { width: 13px; height: 13px; flex-shrink: 0; }

    .lib-footer { padding: 8px 12px; border-top: 1px solid var(--border); flex-shrink: 0; }
    .lib-footer-stat { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); display: flex; justify-content: space-between; }

    /* ══════════════════════════════════════
       MAIN (canvas + right sidebar)
    ══════════════════════════════════════ */
    .main { display: flex; flex: 1; overflow: hidden; }

    #canvas-wrap {
      flex: 1; position: relative;
      background: radial-gradient(ellipse at 50% 35%, #151c3a 0%, var(--bg) 70%);
      overflow: hidden;
    }
    #three-canvas {
      width: 100% !important; height: 100% !important; display: block; position: relative; z-index: 2;
      /* Stop mobile Safari from treating orbit-drag / pencil strokes as a page
         gesture (overscroll bounce, pull-to-refresh, double-tap zoom) — all
         panning/zooming here is handled by OrbitControls / the draw tool. */
      touch-action: none;
    }
    /* P0-4 fix (UX-907): always-visible focus indicator while the canvas
       owns keyboard input (added tabindex="0" in VanVRAi.html) — not just
       :focus-visible, since the point is "this region is now receiving your
       key presses" regardless of whether focus arrived by click or Tab, same
       spirit as the border Claude shows while it's driving a screen. Drawn
       INSET (box-shadow, not outline) because #canvas-wrap is
       `overflow: hidden` and the canvas is flush against its edges — an
       outline would get clipped on at least one side.
       transition: none. Deliberately, so the ring appears/disappears in sync
       with the actual keyboard-ownership change, not lagging behind it.

       BUG (found 2026-08-16 while answering Sean's "이 영역으로 들어왔을 때도
       해줘"): as written, this ring NEVER appeared. An inset box-shadow paints
       between an element's background and its CONTENT — and this element's
       content is an opaque WebGL bitmap covering every pixel, so the ring was
       drawn and then immediately painted over. Verified live: focus the canvas
       and nothing changes on screen. The `outline: none` below is still right
       for the reason given above (#canvas-wrap clips it), so the ring moved to
       an ::after overlay on the WRAPPER, which stacks above the canvas instead
       of underneath its bitmap. pointer-events: none so it never eats a
       click or a stroke.

       2026-09-23 (Sean): a ring that stays lit for the whole time the canvas
       is focused reads as a permanent distraction from the model, not a
       one-time notice. Switched from `:has(#three-canvas:focus)` (always on
       while focused) to a JS-driven `.focus-ring-active` class (js/viewer-core.js)
       that's added on focus and auto-removed 3s later, or immediately on
       blur. A plain class instead of :has() also means this no longer needs
       :has() browser support. */
    #three-canvas:focus { outline: none; }
    #canvas-wrap.focus-ring-active::after {
      content: ''; position: absolute; inset: 0; z-index: 8; pointer-events: none;
      box-shadow: inset 0 0 0 3px var(--accent-l), inset 0 0 24px rgba(124,58,237,0.35);
    }
    #canvas-wrap.annot-mode { cursor: crosshair; }
    #canvas-wrap.move-mode #three-canvas { cursor: move; }
    #move-crosshair {
      display: none; position: fixed; z-index: 6; transform: translate(-50%, -50%);
      font-size: calc(30px * var(--vv-text-scale)); line-height: 1; color: var(--accent-l);
      text-shadow: 0 0 6px rgba(0,0,0,0.8), 0 0 2px rgba(124,58,237,0.9);
      pointer-events: none; user-select: none;
    }
    #move-mode-btn.active, #move-mode-btn-adm.active { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }

    /* Floating labels that mark which model is Primary / Secondary in compare mode */
    .compare-label {
      display: none; position: fixed; z-index: 6; transform: translate(-50%, -100%);
      padding: 3px 9px; border-radius: 999px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
      white-space: nowrap; pointer-events: none; user-select: none;
      border: 1px solid rgba(255,255,255,0.18);
      box-shadow: 0 2px 8px rgba(0,0,0,0.45); text-shadow: 0 1px 2px rgba(0,0,0,0.55);
      opacity: 0.85; transition: opacity 0.15s;
    }
    .compare-label.primary   { background: rgba(124,58,237,0.9); color: #fff; }
    .compare-label.secondary { background: rgba(56,189,248,0.92); color: #062536; }
    .compare-label.active    { opacity: 1; outline: 2px solid #fff; outline-offset: 1px; }
    #compare-mode-btn.active  { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }

    /* Orientation gizmo — Blender-style clickable view-axis widget */
    #orient-gizmo {
      position: absolute; top: 16px; right: 16px; width: 104px; height: 104px;
      z-index: 7; pointer-events: auto; user-select: none; display: none;
      cursor: grab; touch-action: none;
    }
    #orient-gizmo.dragging { cursor: grabbing; }
    #orient-gizmo svg { overflow: visible; }
    #orient-gizmo .gz-bg { fill: rgba(16,20,33,0.30); transition: fill .15s; }
    #orient-gizmo:hover .gz-bg { fill: rgba(16,20,33,0.5); }
    #orient-gizmo .gz-ball { cursor: pointer; }
    #orient-gizmo .gz-ball:hover circle { filter: brightness(1.3); }
    #orient-gizmo .gz-label { font: 700 13px ui-sans-serif, system-ui, sans-serif; fill: #fff; pointer-events: none; }

    /* Fullscreen toggle — bottom-left of the viewer */
    /* ══ Header Share menu (2026-08-20) ══
     Replaced the separate Share and Screenshot buttons. Wrapper is
     position:relative so the popover can hang off it without the header
     needing to become a positioning context itself. */
  .share-wrap { position: relative; display: inline-flex; }
  .share-caret {
    width: 0; height: 0; margin-left: 1px;
    border-left: 3.5px solid transparent; border-right: 3.5px solid transparent;
    border-top: 4px solid currentColor; opacity: .7;
  }
  .share-menu {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: 300;
    min-width: 168px; padding: 5px;
    display: flex; flex-direction: column; gap: 2px;
    background: rgba(20,23,32,0.97); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 11px;
    box-shadow: 0 10px 34px rgba(0,0,0,0.5);
  }
  .share-menu.csp-hide { display: none; }
  .share-item {
    display: flex; align-items: center; gap: 8px;
    padding: 7px 9px; border: 0; border-radius: 7px;
    background: transparent; color: var(--text);
    font: 600 11px inherit; text-align: left; cursor: pointer;
    transition: background .12s;
  }
  .share-item .icon { width: 15px; height: 15px; flex-shrink: 0; color: var(--muted); }
  .share-item:hover { background: var(--surface3); }
  /* Project rule: always :focus, never :focus-visible. */
  .share-item:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: -2px; }
  /* Copy link before a model is loaded. Greyed in place rather than removed —
     a menu whose rows move between openings is harder to aim at than one with
     a disabled row that says why. */
  .share-item:disabled { opacity: .4; cursor: not-allowed; }
  .share-item:disabled:hover { background: transparent; }

  /* ══ ViewCube actions (2026-08-20) ══
     Home (default camera) and Reset model, tucked directly under the
     orientation ball at top:16 right:16 / 104x104 — hence top:124 = 16 + 104 + 4.
     Shown and hidden with the ball by updateOrientGizmo() in js/viewer-core.js,
     which is why `display` is set there and only the flex direction here.

     These are ACTIONS, not modes: they live beside the thing they act on
     rather than on the mode rail, which holds only sticky states. Sized and
     surfaced to match #fullscreen-btn and .scene-btn so all the small round
     viewport chrome reads as one family.

     Right-aligned to the ball's right edge rather than centred under it: the
     ball's own glyphs drift as the camera turns, so a centred pair would look
     mis-aligned half the time. */
  .view-actions {
    position: absolute; top: 124px; right: 16px; z-index: 7;
    display: none; flex-direction: row; gap: 6px;
  }
  .vc-btn {
    width: 32px; height: 32px; padding: 0; border-radius: 9px;
    border: 1px solid var(--border); background: rgba(16,20,33,0.55);
    backdrop-filter: blur(4px); color: var(--muted); cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: background .12s, color .12s;
  }
  .vc-btn .icon { width: 16px; height: 16px; }
  .vc-btn:hover { background: var(--surface3); color: var(--text); }
  /* Project rule: always :focus, never :focus-visible. */
  .vc-btn:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 2px; }
  /* On a phone the ball is the only thing worth the corner; drop the two
     actions rather than stack more chrome over a small viewport. The hidden
     header buttons they delegate to are still reachable from the admin panel. */
  @media (max-width: 768px) {
    .view-actions { display: none !important; }
  }

  #fullscreen-btn {
      position: absolute; bottom: 16px; right: 16px; z-index: 7;
      width: 40px; height: 40px; border-radius: 10px;
      display: flex; align-items: center; justify-content: center;
      background: rgba(16,20,33,0.55); color: var(--text);
      border: 1px solid var(--border); cursor: pointer; font-size: calc(18px * var(--vv-text-scale)); line-height: 1;
      backdrop-filter: blur(4px); transition: background .15s, transform .1s;
    }
    #fullscreen-btn:hover { background: rgba(16,20,33,0.85); }
    #fullscreen-btn:active { transform: scale(0.94); }

    #drop-overlay {
      position: absolute; inset: 0;
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 16px; pointer-events: none; transition: opacity 0.3s;
    }
    #drop-overlay.hidden { opacity: 0; pointer-events: none; }
    .drop-icon {
      width: 76px; height: 76px; border-radius: 50%;
      background: var(--accent-dim); border: 2px dashed rgba(124,58,237,0.5);
      display: flex; align-items: center; justify-content: center; font-size: calc(30px * var(--vv-text-scale));
    }
    #drop-overlay h2 { font-size: calc(18px * var(--vv-text-scale)); font-weight: 700; color: var(--text); }
    #drop-overlay p  { font-size: calc(13px * var(--vv-text-scale)); color: var(--muted); text-align: center; line-height: 1.7; }
    #drop-overlay .upload-btn-wrap { pointer-events: all; }

    /* ── Model loading card (2026-08-21) ──
       Replaces the old 3px #loading-bar-wrap hairline at the canvas's bottom
       edge. Centred on the canvas, where the model is about to appear, and
       big enough to actually read on a multi-second download. */
    #vv-load-card {
      position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
      /* Above the canvas furniture — .mode-rail is 41 and .draw-toolbar 40.
         They are corner-anchored and this is centred, so they rarely collide,
         but on a narrow viewport they would, and while a model is loading
         this card is the most important thing on the canvas. */
      z-index: 45;
      width: min(340px, calc(100% - 48px));
      display: flex; flex-direction: column; gap: 9px;
      padding: 16px 18px 15px;
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 14px; box-shadow: 0 18px 48px rgba(0,0,0,0.55);
      /* The canvas behind stays draggable — this reports, it does not block.
         Re-enabled on the one thing inside that is clickable. */
      pointer-events: none;
    }
    #vv-load-card.csp-hide { display: none; }
    .vvl-card-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
    .vvl-card-icon { color: var(--accent-l); display: inline-flex; flex-shrink: 0; }
    .vvl-card-name {
      font-size: calc(13px * var(--vv-text-scale)); font-weight: 700; color: var(--text);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .vvl-track {
      height: 6px; border-radius: 3px; background: var(--surface2);
      overflow: hidden; position: relative;
    }
    .vvl-fill {
      height: 100%; width: 0%; border-radius: 3px;
      background: linear-gradient(90deg, var(--accent-l), var(--accent2));
      /* DELIBERATELY no `transition: width`. A streamed download fires the
         progress callback once per chunk — hundreds of times a second — and a
         0.2s transition restarted that often left the bar rendering at 0px
         while style.width said 32%. Caught only by sampling the PAINTED width;
         style.width had looked perfect the whole time. VVLoad coalesces its
         writes to one per animation frame instead, which is already smooth. */
    }
    /* Used for the two phases where NO byte count exists: the GLTF parse (it
       reports nothing at all), and a response with no Content-Length. A bar
       frozen at 90% reads as a hang; a moving stripe reads as work.
       NOTE: VVLoad must clear the INLINE width for this to have any effect —
       an inline `width: 0%` beats this rule and animates a zero-width stripe,
       i.e. nothing at all. */
    .vvl-track.indeterminate .vvl-fill {
      width: 35%;
      animation: vvlSlide 1.15s ease-in-out infinite;
    }
    @keyframes vvlSlide {
      0%   { transform: translateX(-100%); }
      100% { transform: translateX(340%); }
    }
    @media (prefers-reduced-motion: reduce) {
      /* No sliding stripe for anyone who asked not to have one — a steady
         two-thirds bar still reads as "working, length unknown". */
      .vvl-track.indeterminate .vvl-fill { animation: none; width: 66%; opacity: 0.65; }
    }
    .vvl-card-foot {
      display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
      font-size: calc(11px * var(--vv-text-scale)); color: var(--muted);
    }
    .vvl-bytes { font-variant-numeric: tabular-nums; flex-shrink: 0; }
    .vvl-cancel {
      pointer-events: all; align-self: stretch; justify-content: center;
      padding: 7px 0; font-size: calc(12px * var(--vv-text-scale));
    }
    .vvl-cancel.csp-hide { display: none; }

    /* ── Upload progress card (2026-09-21) ──
       Sits under the Upload button in Manage → Models. Same visual language
       as #vv-load-card; same "no width transition" rule, for the same reason
       (progress events arrive far faster than a transition can settle —
       VVUpload coalesces its writes to one per animation frame instead). */
    .upl-card {
      display: flex; flex-direction: column; gap: 8px;
      margin-top: 10px; padding: 12px 14px;
      background: var(--surface2); border: 1px solid var(--border); border-radius: 10px;
    }
    .upl-card.csp-hide { display: none; }
    .upl-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
    .upl-icon { color: var(--accent-l); display: inline-flex; flex-shrink: 0; }
    .upl-name {
      flex: 1; min-width: 0;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; color: var(--text);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .upl-pct {
      flex-shrink: 0; font-variant-numeric: tabular-nums;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; color: var(--accent-l);
    }
    .upl-track {
      height: 6px; border-radius: 3px; background: var(--surface);
      overflow: hidden; position: relative;
    }
    .upl-fill {
      height: 100%; width: 0%; border-radius: 3px;
      background: linear-gradient(90deg, var(--accent-l), var(--accent2));
    }
    /* The server-side phase (write to disk, validate) reports nothing, so it
       gets the moving stripe rather than a bar frozen at 100%. VVUpload clears
       the inline width first, or the inline 100% would beat this rule. */
    .upl-track.indeterminate .upl-fill {
      width: 35%;
      animation: vvlSlide 1.15s ease-in-out infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      .upl-track.indeterminate .upl-fill { animation: none; width: 66%; opacity: 0.65; }
    }
    .upl-card.upl-done  .upl-fill { background: var(--success); }
    .upl-card.upl-error .upl-fill { background: var(--error); width: 100%; }
    .upl-card.upl-error .upl-phase { color: var(--error); }
    .upl-foot {
      display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
      font-size: calc(11px * var(--vv-text-scale)); color: var(--muted);
    }
    .upl-phase { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .upl-bytes { font-variant-numeric: tabular-nums; flex-shrink: 0; }
    .upl-cancel {
      align-self: stretch; justify-content: center;
      padding: 6px 0; font-size: calc(12px * var(--vv-text-scale));
    }
    .upl-cancel.csp-hide { display: none; }

    /* ── Right Sidebar ── */
    .sidebar {
      position: relative;
      width: var(--info-w, 224px); background: var(--surface); border-left: 1px solid var(--border);
      display: flex; flex-direction: column; padding: 12px; gap: 14px;
      overflow-y: auto; flex-shrink: 0;
    }
    .sidebar::-webkit-scrollbar { width: 3px; }
    .sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
    /* Drag handle to resize the info sidebar width (desktop) */
    .info-resizer {
      position: absolute; left: 0; top: 0; bottom: 0; width: 12px;
      cursor: ew-resize; z-index: 15;
    }
    .info-resizer::after {
      content: ''; position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
      width: 5px; height: 60px; border-radius: 4px;
      background: var(--accent-l); opacity: 0.85;
      box-shadow: 0 0 8px rgba(124,58,237,0.5); transition: opacity 0.15s;
    }
    .info-resizer:hover::after, body.info-resizing .info-resizer::after { opacity: 1; box-shadow: 0 0 12px rgba(124,58,237,0.8); }
    body.info-resizing { cursor: ew-resize; user-select: none; }
    /* Vertical drag handle for the info sidebar bottom-sheet (mobile) */
    .info-resizer-v {
      display: none; position: absolute; left: 50%; transform: translateX(-50%);
      top: 0; width: 160px; height: 22px; cursor: ns-resize; z-index: 210; touch-action: none;
    }
    .info-resizer-v::after {
      content: ''; position: absolute; left: 50%; top: 7px; transform: translateX(-50%);
      width: 56px; height: 6px; border-radius: 4px;
      background: var(--accent-l); opacity: 0.9; box-shadow: 0 0 8px rgba(124,58,237,0.5);
    }
    body.info-resizing-v { user-select: none; }

    .sidebar-section h3 {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; letter-spacing: 1.3px;
      text-transform: uppercase; color: var(--muted); margin-bottom: 9px;
    }
    /* 2026-08-26: Lighting-panel note for unlit (MeshBasicMaterial) models —
       see the HTML comment above #lighting-unlit-note and vvSyncLightingControls()
       in js/scene-panel.js. */
    /* Per-model lighting defaults (2026-08-28) — sits under the two sliders in
       the Scene panel's Lighting section; owner-only, see
       vvRefreshLightingDefaultBtns() in js/viewer-model.js. */
    .lighting-default-row { display: flex; gap: 6px; margin-top: 8px; }
    .lighting-default-row.csp-hide { display: none; }
    .lighting-default-row .btn { flex: 1; font-size: calc(11px * var(--vv-text-scale)); padding: 5px 6px; }
    .lighting-default-row .btn.csp-hide { display: none; }
    /* Upload dialog's lighting sliders. Three columns so the label, the track
       and the number stay aligned across both rows. */
    .um-slider-row {
      display: grid; grid-template-columns: 74px 1fr 40px;
      align-items: center; gap: 8px; margin-bottom: 6px;
    }
    .um-slider-label { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); }
    .um-slider-val {
      font-size: calc(11px * var(--vv-text-scale)); color: var(--text);
      font-variant-numeric: tabular-nums; text-align: right;
    }
    .lighting-unlit-note {
      font-size: calc(11px * var(--vv-text-scale)); line-height: 1.4; color: var(--muted);
      margin: 6px 0 0; padding: 7px 9px; background: var(--surface2);
      border: 1px solid var(--border); border-radius: 8px;
    }
    #sl-ambient:disabled, #sl-dir:disabled { opacity: 0.4; cursor: not-allowed; }
    /* Collapsible (accordion) control sections — Position/Display/Lighting/Background/Export */
    .sidebar-section.acc > h3 {
      cursor: pointer; user-select: none; display: flex; align-items: center;
      justify-content: space-between; margin-bottom: 9px;
    }
    .sidebar-section.acc > h3:hover { color: var(--text); }
    .sidebar-section.acc.acc-collapsed > h3 { margin-bottom: 0; }
    .sb-acc-caret { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); transition: transform .25s ease; margin-left: 8px; }
    .sb-acc-body  { overflow: hidden; transition: max-height .34s ease, opacity .22s ease; }
    /* 2026-08-27: .mi-stats (Manage → Info's File/Size/Meshes/Polygons/
       Dimensions rows) reuses this exact .row/.val markup but was never
       added to this selector when Details moved into the Manage panel
       (2026-08-19 sidebar rework) — so those rows have been rendering with
       ZERO styling (no flex layout, default 16px browser font, no theme
       color) ever since, not just for the Dimensions row added this week.
       Folded in here rather than duplicated, so both surfaces always match. */
    .sidebar-section .row, .mi-stats .row {
      display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;
    }
    .sidebar-section .row label, .mi-stats .row label { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); }
    .sidebar-section .row .val, .mi-stats .row .val  { font-size: calc(12px * var(--vv-text-scale)); color: var(--text); font-variant-numeric: tabular-nums; }

    .toggle-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
    .toggle-row label { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); }
    .toggle { position: relative; width: 32px; height: 17px; }
    .toggle input { opacity: 0; width: 0; height: 0; }
    .toggle-slider {
      position: absolute; inset: 0;
      background: var(--surface2); border: 1px solid var(--border);
      border-radius: 17px; cursor: pointer; transition: 0.2s;
    }
    .toggle-slider::before {
      content: ''; position: absolute;
      width: 11px; height: 11px; left: 2px; top: 2px;
      background: var(--muted); border-radius: 50%; transition: 0.2s;
    }
    .toggle input:checked + .toggle-slider { background: var(--accent-dim); border-color: var(--accent-l); }
    .toggle input:checked + .toggle-slider::before { transform: translateX(15px); background: var(--accent-l); }

    input[type=range] { width: 100%; accent-color: var(--accent-l); cursor: pointer; }
    input[type=range].x { accent-color: var(--axis-x); }
    input[type=range].y { accent-color: var(--axis-y); }
    input[type=range].z { accent-color: var(--axis-z); }

    .divider { height: 1px; background: var(--border); }

    .axis-row {
      display: grid; grid-template-columns: 17px 1fr 36px;
      align-items: center; gap: 6px; margin-bottom: 9px;
    }
    .axis-label {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; text-align: center;
      border-radius: 3px; padding: 2px 0;
    }
    .axis-label.x { color: var(--axis-x); background: rgba(247,110,110,0.12); }
    .axis-label.y { color: var(--axis-y); background: rgba(110,247,138,0.12); }
    .axis-label.z { color: var(--axis-z); background: rgba(110,174,247,0.12); }
    .axis-val { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }

    .kbd-hint {
      display: inline-block; background: var(--surface2);
      border: 1px solid var(--border); border-radius: 3px;
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); padding: 1px 4px;
      margin-right: 1px; font-family: monospace;
    }
    .kbd-note { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); line-height: 1.7; margin-top: 3px; }

    /* Canvas overlays */
    #controls-hint {
      position: absolute; bottom: 14px; left: 14px;
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted);
      pointer-events: none; opacity: 0; transition: opacity 0.4s;
      line-height: 1.9; background: rgba(0,0,0,0.35);
      padding: 6px 10px; border-radius: 7px; backdrop-filter: blur(4px);
    }
    #controls-hint.show { opacity: 1; }

    #move-indicator {
      position: absolute; top: 14px; right: 14px;
      background: var(--accent-dim); border: 1px solid rgba(124,58,237,0.4);
      border-radius: 6px; padding: 5px 11px; font-size: calc(12px * var(--vv-text-scale)); color: var(--accent-l);
      pointer-events: none; opacity: 0; transition: opacity 0.3s;
    }
    #move-indicator.show { opacity: 1; }

    #annot-indicator {
      position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
      background: var(--annot-dim); border: 1px solid var(--annot);
      border-radius: 6px; padding: 5px 14px; font-size: calc(12px * var(--vv-text-scale)); color: var(--annot);
      pointer-events: none; opacity: 0; transition: opacity 0.3s; white-space: nowrap;
      display: flex; align-items: center; gap: 10px;
    }
    #annot-indicator.show { opacity: 1; pointer-events: all; }
    #annot-cancel-btn {
      background: rgba(255,200,50,0.15); border: 1px solid rgba(255,200,50,0.5);
      color: var(--annot); border-radius: 4px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
      padding: 2px 8px; cursor: pointer; transition: all 0.15s; white-space: nowrap;
    }
    #annot-cancel-btn:hover { background: rgba(255,200,50,0.3); }

    #toast {
      position: absolute; bottom: 50px; left: 50%;
      transform: translateX(-50%) translateY(20px);
      background: var(--surface2); border: 1px solid var(--border);
      border-radius: 8px; padding: 8px 16px; font-size: calc(12px * var(--vv-text-scale)); color: var(--text);
      transition: transform 0.3s ease, opacity 0.3s; white-space: nowrap;
      z-index: 100; opacity: 0; pointer-events: none; backdrop-filter: blur(6px);
    }
    #toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }

    /* ══════════════════════════════════════
       ANNOTATION DISPLAY
    ══════════════════════════════════════ */
    .annot-marker {
      position: absolute; width: 26px; height: 26px;
      border-radius: 50% 50% 50% 0; transform: translate(-50%, -100%) rotate(-45deg);
      background: var(--pin-color, var(--annot)); border: 2px solid #fff;
      box-shadow: 0 2px 10px rgba(0,0,0,0.5); cursor: pointer; z-index: 50;
      transition: transform 0.15s, background 0.15s, opacity 0.25s, filter 0.25s;
      pointer-events: all; display: flex; align-items: center; justify-content: center;
    }
    .annot-marker-num {
      transform: rotate(45deg); font-size: calc(12px * var(--vv-text-scale)); font-weight: 800;
      color: var(--pin-text, #1a1200); line-height: 1; pointer-events: none;
    }
    /* Hover/active keep the category color (previously forced yellow / orange). */
    .annot-marker:hover { transform: translate(-50%, -100%) rotate(-45deg) scale(1.15); filter: brightness(1.15); }
    .annot-marker.active { transform: translate(-50%, -100%) rotate(-45deg) scale(1.1); box-shadow: 0 0 0 3px rgba(255,255,255,0.9), 0 2px 10px rgba(0,0,0,0.5); }
    .annot-marker.behind { opacity: 0.38; filter: grayscale(100%) brightness(0.85); }
    .annot-marker.repositioning { background: #60a5fa; box-shadow: 0 0 0 3px #fff, 0 0 12px #60a5fa; animation: pulse-reposition 1s ease-in-out infinite; }
    @keyframes pulse-reposition { 0%,100%{box-shadow:0 0 0 3px #fff,0 0 12px #60a5fa} 50%{box-shadow:0 0 0 3px #fff,0 0 22px #60a5fa} }

    .annot-popup {
      position: absolute; background: var(--surface); border: 1px solid var(--annot);
      border-radius: 10px; padding: 12px 14px 10px;
      min-width: 200px; max-width: 260px;
      z-index: 60; box-shadow: 0 6px 24px rgba(0,0,0,0.5); pointer-events: all;
      cursor: move;   /* drag to move off the pin */
    }
    .annot-popup button, .annot-popup a, .annot-popup .annot-popup-desc { cursor: auto; }
    .annot-popup .annot-popup-actions button { cursor: pointer; }
    .annot-popup-close { cursor: pointer; }
    .annot-popup-title { font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; color: var(--annot); margin-bottom: 4px; padding-right: 22px; }
    .annot-popup-desc  { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); line-height: 1.55; margin-bottom: 8px; }
    .annot-popup-close {
      position: absolute; top: 8px; right: 10px;
      font-size: calc(16px * var(--vv-text-scale)); cursor: pointer; color: var(--muted);
      background: none; border: none; padding: 0; line-height: 1;
    }
    .annot-popup-close:hover { color: var(--text); }
    .annot-popup-actions { display: flex; gap: 5px; flex-wrap: wrap; }
    .annot-popup-actions button {
      background: none; border: 1px solid var(--border);
      border-radius: 5px; color: var(--muted);
      font-size: calc(12px * var(--vv-text-scale)); padding: 3px 7px; cursor: pointer; transition: all 0.15s;
    }
    .annot-popup-actions button:hover { background: var(--surface2); color: var(--text); }

    .annot-popup-cat {
      display: inline-flex; align-items: center; gap: 3px;
      padding: 2px 8px; border-radius: 20px;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; margin-bottom: 5px;
      border: 1px solid transparent;
    }

    /* Annotation sidebar list */
    #annot-list .annot-list-item {
      display: flex; align-items: center; gap: 6px;
      padding: 4px 3px; border-bottom: 1px solid var(--border);
      border-radius: 5px; margin-bottom: 1px;
    }
    #annot-list .annot-list-item:last-child { border-bottom: none; }
    .annot-list-focus-area {
      display: flex; align-items: center; gap: 6px;
      flex: 1; min-width: 0; cursor: pointer;
      border-radius: 4px; padding: 2px 4px; margin: -2px -4px;
      transition: background 0.13s;
    }
    .annot-list-focus-area:hover { background: var(--annot-dim); }
    .annot-list-focus-area:hover .annot-badge { background: #ffe066; }
    .annot-list-focus-area:hover .annot-list-title { color: var(--annot); }
    .annot-badge {
      width: 18px; height: 18px; border-radius: 50%;
      background: var(--annot); color: #1a1200;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 800;
      display: flex; align-items: center; justify-content: center;
      flex-shrink: 0; transition: background 0.13s;
    }
    .annot-list-title {
      flex: 1; min-width: 0; font-size: calc(12px * var(--vv-text-scale)); color: var(--text);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: color 0.13s;
    }
    .annot-list-cat { font-size: calc(12px * var(--vv-text-scale)); flex-shrink: 0; opacity: 0.85; }
    #annot-list .empty-note {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); text-align: center; padding: 8px 0; line-height: 1.8;
    }

    .annot-section-header {
      display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px;
    }
    .annot-section-header h3 { margin-bottom: 0; }
    .annot-count-badge {
      display: none; align-items: center; justify-content: center;
      background: var(--annot-dim); color: var(--annot);
      border: 1px solid rgba(255,200,50,.35); border-radius: 9px;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 800;
      min-width: 16px; height: 16px; padding: 0 4px; margin-left: 5px;
    }
    .annot-count-badge.show { display: inline-flex; }
    .btn-annot-toggle {
      background: none; border: 1px solid var(--border); border-radius: 4px;
      color: var(--muted); cursor: pointer; font-size: calc(12px * var(--vv-text-scale));
      padding: 2px 6px; transition: all .15s; line-height: 1.5; white-space: nowrap;
    }
    .btn-annot-toggle:hover { background: var(--annot-dim); color: var(--annot); border-color: var(--annot); }
    .btn-annot-toggle.annots-hidden { color: var(--text); border-style: dashed; }   /* was opacity:.55 — 3.6:1; icon switches to eye-off */

    /* Model metadata display (sidebar) */
    #model-meta-block { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
    #model-meta-display-name { font-size: calc(13px * var(--vv-text-scale)); font-weight: 700; color: var(--text); line-height: 1.3; margin-bottom: 5px; }
    /* Readable hyperlinks inside rendered rich content (esp. dark mode) */
    #model-meta-desc a, .annot-popup-desc a, .rte-editor a, .wd-content a, .info-page-body a {
      color: var(--link); text-decoration: underline; text-underline-offset: 2px;
      text-decoration-color: color-mix(in srgb, var(--link) 55%, transparent);
    }
    #model-meta-desc a:hover, .annot-popup-desc a:hover, .rte-editor a:hover,
    .wd-content a:hover, .info-page-body a:hover { color: var(--link-hover); text-decoration-color: var(--link-hover); }
    #model-meta-desc { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); line-height: 1.6; margin-top: 5px; }
    #model-meta-desc img { max-width: 100%; border-radius: 6px; margin: 5px 0; display: block; }
    #model-meta-desc h2, #model-meta-desc h3 { color: var(--text); margin: 6px 0 3px; }
    #model-meta-desc p { margin: 3px 0; }
    #model-meta-desc ul, #model-meta-desc ol { padding-left: 16px; margin: 3px 0; }
    #model-meta-desc .rte-video-wrap, #model-meta-desc .rte-audio-wrap { margin: 6px 0; border-radius: 8px; overflow: hidden; background: #000; }
    #model-meta-desc .rte-video-wrap iframe, #model-meta-desc .rte-video-wrap video { width: 100%; aspect-ratio: 16/9; display: block; border: none; }
    #model-meta-desc .rte-audio-wrap audio { width: 100%; display: block; }
    .anat-chip {
      display: inline-flex; align-items: center; gap: 4px;
      padding: 2px 8px; border-radius: 20px;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; border: 1px solid transparent; white-space: nowrap;
    }

    /* ══════════════════════════════════════
       ADMIN OVERLAY (inline sidebar)
    ══════════════════════════════════════ */
    #vvAdminOverlay {
      position: fixed; top: 0; right: 0; bottom: 0;
      width: var(--admin-w, 380px); max-width: 100vw; z-index: 500;
      background: var(--bg);
      border-left: 1px solid #272c3f;
      box-shadow: -8px 0 32px rgba(0,0,0,0.55);
      display: none;
      flex-direction: column;
      overflow: hidden;
    }
    #vvAdminOverlay.show { display: flex; }
    /* Match canvas width to the panel and hide the info sidebar (no black gap) */
    body.admin-open #canvas-wrap { margin-right: var(--admin-w, 380px); }
    body.admin-open .sidebar { display: none !important; }
    /* Drag handle on the panel's left edge */
    .admin-resizer {
      position: absolute; left: 0; top: 0; bottom: 0; width: 12px;
      cursor: ew-resize; z-index: 40; background: transparent;
    }
    .admin-resizer::after {
      content: ''; position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
      width: 5px; height: 60px; border-radius: 4px;
      background: var(--accent-l); opacity: 0.9;
      box-shadow: 0 0 8px rgba(124,58,237,0.5); transition: background 0.15s, opacity 0.15s;
    }
    .admin-resizer:hover::after, body.admin-resizing .admin-resizer::after { opacity: 1; box-shadow: 0 0 12px rgba(124,58,237,0.8); }
    body.admin-resizing { cursor: ew-resize; user-select: none; }
    /* Vertical (mobile bottom-sheet) drag handle — hidden on desktop */
    .admin-resizer-v {
      display: none; position: absolute; left: 50%; transform: translateX(-50%);
      top: 0; width: 160px; height: 24px;
      cursor: ns-resize; z-index: 40; touch-action: none;
    }
    .admin-resizer-v::after {
      content: ''; position: absolute; left: 50%; top: 8px; transform: translateX(-50%);
      width: 60px; height: 6px; border-radius: 4px;
      background: var(--accent-l); opacity: 0.9;
      box-shadow: 0 0 8px rgba(124,58,237,0.5); transition: opacity 0.15s;
    }
    .admin-resizer-v:active::after, body.admin-resizing-v .admin-resizer-v::after { opacity: 1; box-shadow: 0 0 12px rgba(124,58,237,0.8); }
    .admin-resizer-v:active::after, body.admin-resizing-v .admin-resizer-v::after { background: var(--accent-l); }
    body.admin-resizing-v { cursor: ns-resize; user-select: none; }
    #adm-mobile-size { display: none; }

    /* ══════════════════════════════════════
       ADMIN PANEL CSS (from admin.html)
    ══════════════════════════════════════ */
    #auth-gate {
      position: absolute; inset: 0;
      background: rgba(5,7,15,0.97); backdrop-filter: blur(12px);
      display: flex; align-items: center; justify-content: center; z-index: 100;
    }
    #auth-gate.hidden { display: none; }

    .login-card {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 20px; padding: 38px 38px 30px; width: 300px;
      box-shadow: 0 28px 72px rgba(0,0,0,0.85);
      display: flex; flex-direction: column; align-items: center;
    }
    .login-logo {
      width: 58px; height: 58px; object-fit: contain; margin-bottom: 18px;
    }
    .login-card h2 { font-size: calc(18px * var(--vv-text-scale)); font-weight: 800; margin-bottom: 5px; text-align: center; }
    .login-card > p { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); text-align: center; margin-bottom: 24px; line-height: 1.6; }
    .login-card input[type=password] {
      width: 100%; background: var(--surface2); border: 1px solid var(--border);
      border-radius: 9px; color: var(--text); font-size: calc(14px * var(--vv-text-scale)); padding: 11px 14px;
      outline: none; font-family: inherit; margin-bottom: 8px; transition: border-color 0.2s;
    }
    .login-card input[type=password]:focus { border-color: var(--accent-l); }
    .login-card input.pw-error { border-color: var(--error) !important; animation: shake 0.35s; }
    @keyframes shake {
      0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-7px)} 40%,80%{transform:translateX(7px)}
    }
    #login-error { font-size: calc(12px * var(--vv-text-scale)); color: var(--error); margin-bottom: 10px; display: none; width: 100%; text-align: center; }
    #login-error.show { display: block; }
    .login-btns { display: flex; gap: 8px; width: 100%; }
    /* .btn is inline-flex with no justify-content, so once these buttons are
       stretched to flex:1 the label+icon sit flush-left instead of centered.
       Scoped here (not on .btn globally) since only this stretched-row case
       needs it — Sean reported Cancel/Go to Login text off-center. */
    .login-btns button { flex: 1; padding: 10px 0; font-size: calc(13px * var(--vv-text-scale)); justify-content: center; }

    #admin-panel { display: flex; flex-direction: column; height: 100%; }
    #admin-panel.hidden { display: none; }

    /* ── Student panel (2026-08-21) ──
       Same box as #admin-panel, and it deliberately reuses .admin-header /
       .tab-bar / .tab-content / .sec-title rather than growing a parallel set
       of look-alike classes: a student switching between this and the model
       grid should not feel like they left the app. Only what is genuinely new
       gets a class here. */
    #student-panel { display: flex; flex-direction: column; height: 100%; }
    #student-panel.hidden { display: none; }
    /* .tab-content is a column flexbox with gap:16px, which would otherwise
       apply between a heading and the block it labels exactly as it does
       between sections. .stu-sec welds each pair together so the 16px falls
       only where a section actually ends. */
    .stu-sec { display: flex; flex-direction: column; }
    .stu-block { display: flex; flex-direction: column; gap: 6px; }
    .stu-model-name { font-size: calc(14px * var(--vv-text-scale)); font-weight: 700; }
    .stu-model-cat  { font-size: calc(12px * var(--vv-text-scale)); color: var(--accent-l); }
    .stu-model-desc { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); line-height: 1.7; }
    .stu-note { font-size: calc(11px * var(--vv-text-scale)); color: var(--muted); line-height: 1.6; }
    /* A <button>, not a <div> — the row moves the camera, so it has to be
       reachable and operable from the keyboard like any other control. */
    .stu-annot-row {
      display: flex; align-items: center; gap: 9px; width: 100%;
      padding: 7px 9px; border-radius: 9px; text-align: left;
      background: var(--surface2); border: 1px solid var(--border);
      color: var(--text); font: inherit; cursor: pointer;
      transition: border-color 0.15s, background 0.15s;
    }
    .stu-annot-row:hover { background: var(--surface); border-color: var(--accent-l); }
    /* Project rule: always :focus, never :focus-visible — this list is
       keyboard-walked and the ring has to show for pointer users too. */
    .stu-annot-row:focus { outline: 2px solid var(--accent-l); outline-offset: 2px; }
    .stu-annot-title {
      flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      font-size: calc(12px * var(--vv-text-scale));
    }
    .stu-annot-cat { flex-shrink: 0; color: var(--muted); display: inline-flex; }
    .stu-kv { display: flex; justify-content: space-between; gap: 12px; font-size: calc(12px * var(--vv-text-scale)); }
    .stu-kv-k { color: var(--muted); flex-shrink: 0; }
    .stu-kv-v { color: var(--text); font-weight: 600; text-align: right; word-break: break-word; }

    .admin-header {
      display: flex; align-items: center; gap: 10px; padding: 0 16px;
      height: 52px; background: var(--surface); border-bottom: 1px solid var(--border);
      flex-shrink: 0;
    }
    .admin-logo-icon {
      width: 30px; height: 30px; object-fit: contain; flex-shrink: 0;
    }
    .admin-logo-text {
      font-size: calc(15px * var(--vv-text-scale)); font-weight: 800; letter-spacing: -0.5px;
      background: linear-gradient(135deg, var(--accent-l), var(--accent2));
      -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
      flex-shrink: 0;
    }
    .admin-badge {
      padding: 2px 9px; border-radius: 20px; flex-shrink: 0; white-space: nowrap;
      background: linear-gradient(135deg,rgba(124,58,237,0.18),rgba(96,165,250,0.14));
      border: 1px solid rgba(124,58,237,0.45);
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l);
      letter-spacing: 0.6px; text-transform: uppercase;
    }
    .h-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
    /* Left group shrinks/clips as the panel narrows; action group stays pinned so
       the close button is always visible regardless of panel width. */
    .admin-header-left { display: flex; align-items: center; gap: 10px; flex: 1 1 auto; min-width: 0; overflow: hidden; }
    .admin-header-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
    .admin-model-label { flex: 1; font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
    .admin-model-label.loaded { color: var(--text); }

    .tab-bar {
      display: flex; border-bottom: 1px solid var(--border);
      background: var(--surface); flex-shrink: 0;
    }
    /* Viewer (read-only): hide editing affordances. Server also blocks writes.
       Annotations/Draw used to be top-level .tab-btn elements; they're now segments
       nested inside a selected model (Models tab), so the gate moved to .seg-btn. */
    body.role-viewer #upload-btn,
    body.role-viewer #annot-mode-btn,
    body.role-viewer #clear-library-btn,
    body.role-viewer .model-item-btns,
    body.role-viewer .seg-btn[data-seg="annotations"],
    body.role-viewer .seg-btn[data-seg="draw"] { display: none !important; }
    /* No 'library' permission (school_admin / instructor / student / viewer):
       the owner's global model library is read-only for them — the server
       returns 403 on upload/rename/delete/thumbnail/registry writes, so don't
       offer the controls. 2026-08-17; set by applyRolePermissions() in
       viewer-model.js. Course assignment (the graduation-cap button) is NOT
       hidden — that writes courses.json, which instructors legitimately own.
       Upload/Delete/Clear-Library stay hidden everywhere for these roles. */
    body.no-library #upload-btn,
    body.no-library #clear-library-btn,
    body.no-library .model-item-btns .lib-only { display: none !important; }
    /* The Info segment used to be hidden outright for no-library roles
       (saving it PUT the whole registry, which they can't write). 2026-08-18:
       course-scoped description/tag overrides reuse this same Info segment,
       so it reappears INSIDE a course — see mi-lib-only below for how the
       owner-only fields (thumbnail/name/category/Access/Sensitive) inside it
       stay hidden even there. */
    body.no-library:not(.course-mode) .seg-btn[data-seg="info"],
    body.no-library:not(.course-mode) .model-item-btns .mi-edit-btn { display: none !important; }
    /* Owner-only Info fields, hidden only for the course-scoped edit form —
       library edits (outside course mode) still show everything, gated by
       the rule above instead. Toggled via the mi-lib-only class added to the
       relevant rows in populateModelInfoForm()/VanVRAi.html. */
    body.no-library.course-mode .mi-lib-only { display: none !important; }
    .course-override-badge, .gl-card-course-badge {
      display: inline-flex; align-items: center; font-size: calc(10px * var(--vv-text-scale));
      font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
      padding: 2px 7px; border-radius: 20px; margin-left: 6px;
      color: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent);
      border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
    }
    .gl-card-course-badge { position: absolute; top: 8px; right: 8px; margin-left: 0; }
    .tab-btn {
      flex: 1; padding: 10px 0; font-size: calc(12px * var(--vv-text-scale)); font-weight: 600;
      background: none; border: none; border-bottom: 2px solid transparent;
      color: var(--muted); cursor: pointer; transition: all 0.15s;
    }
    .tab-btn:hover { color: var(--text); }
    .tab-btn.active { color: var(--accent-l); border-bottom-color: var(--accent-l); }

    .tab-content { display: none; flex: 1; overflow-y: auto; padding: 16px; flex-direction: column; gap: 16px; }
    .tab-content.active { display: flex; }
    .tab-content::-webkit-scrollbar { width: 4px; }
    .tab-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

    /* ── Model detail (nested inside the Models tab: list ↔ one model's Info/Annotations/Draw) ── */
    .lib-pane { display: flex; flex-direction: column; gap: 16px; }
    /* Explicit, so display:flex above never wins a specificity tie against .csp-hide
       (same convention as .dt-canvas-opt.csp-hide etc. — see the toolbar rules below). */
    .lib-pane.csp-hide { display: none; }

    .model-detail-header { display: flex; align-items: center; gap: 10px; }
    .model-detail-name {
      font-size: calc(14px * var(--vv-text-scale)); font-weight: 700;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
    }

    .seg-bar { display: flex; gap: 4px; border-bottom: 1px solid var(--border); }
    .seg-btn {
      padding: 8px 14px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 600;
      background: none; border: none; border-bottom: 2px solid transparent;
      color: var(--muted); cursor: pointer; transition: all 0.15s;
    }
    .seg-btn:hover { color: var(--text); }
    .seg-btn.active { color: var(--accent-l); border-bottom-color: var(--accent-l); }

    .seg-pane { display: flex; flex-direction: column; gap: 16px; }
    .seg-pane.csp-hide { display: none; }

    .sec-title {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; letter-spacing: 1.3px;
      text-transform: uppercase; color: var(--muted); margin-bottom: 8px;
    }

    .model-list { display: flex; flex-direction: column; gap: 6px; }
    .model-item {
      display: flex; align-items: center; gap: 10px;
      padding: 10px 12px; border-radius: 10px;
      border: 1px solid var(--border); background: var(--surface2);
      cursor: pointer; transition: all 0.15s;
    }
    .model-item:hover { border-color: rgba(124,58,237,0.4); background: var(--surface3); }
    .model-item.active { border-color: var(--accent-l); background: var(--accent-dim); }
    .model-item-icon { font-size: calc(20px * var(--vv-text-scale)); flex-shrink: 0; }
    .model-item-body { flex: 1; min-width: 0; }
    .model-item-name { font-size: calc(12px * var(--vv-text-scale)); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .model-item.active .model-item-name { color: var(--accent-l); }
    .model-item-meta { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-top: 2px; }
    .model-item-btns { display: flex; gap: 4px; flex-shrink: 0; }

    .icon-btn {
      width: 26px; height: 26px; border-radius: 6px;
      background: transparent; border: 1px solid var(--border);
      color: var(--muted); cursor: pointer; font-size: calc(12px * var(--vv-text-scale));
      display: flex; align-items: center; justify-content: center; transition: all 0.15s;
    }
    .icon-btn:hover { background: var(--surface3); color: var(--text); }
    .icon-btn.del:hover { background: rgba(248,113,113,0.12); border-color: rgba(248,113,113,0.3); color: var(--error); }
    .icon-btn.has-view { color: var(--annot); border-color: var(--annot); background: var(--annot-dim); }

    .info-card {
      background: var(--surface2); border: 1px solid var(--border);
      border-radius: 12px; padding: 16px;
    }
    .form-field { margin-bottom: 12px; }
    .form-label { font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; letter-spacing: 1.1px; text-transform: uppercase; color: var(--muted); display: block; margin-bottom: 5px; }
    .form-input {
      width: 100%; background: var(--surface); border: 1px solid var(--border);
      border-radius: 7px; color: var(--text); font-size: calc(12px * var(--vv-text-scale)); padding: 8px 11px;
      outline: none; font-family: inherit; transition: border-color 0.15s; resize: vertical;
    }
    .form-input:focus { border-color: var(--accent-l); }
    .form-input::placeholder { color: var(--muted); }

    .cat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; margin-bottom: 12px; }
    .cat-item {
      padding: 8px 10px; border-radius: 8px; border: 1.5px solid var(--border);
      cursor: pointer; background: var(--surface); transition: all 0.15s;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 600; color: var(--muted); text-align: center; user-select: none;
    }
    /* 2026-09-18: hover no longer paints the accent border — on the Info
       form a hovered tile looked exactly like a selected one. */
    .cat-item:hover { border-color: var(--muted); color: var(--text); background: var(--surface3); }
    .cat-item.selected { border-width: 2px; }

    /* ── Access / Sensitive fields + backfill banner (2026-08-18) ── */
    .mi-check-label {
      display: flex; align-items: center; gap: 8px; cursor: pointer;
      font-size: calc(12px * var(--vv-text-scale)); color: var(--text); user-select: none;
    }
    .mi-check-label input[type="checkbox"] { accent-color: var(--accent-l); width: 16px; height: 16px; flex-shrink: 0; }
    .form-hint {
      font-size: calc(11px * var(--vv-text-scale)); color: var(--muted); line-height: 1.5; margin-top: 5px;
    }
    .lib-health-banner {
      background: rgba(250,204,21,0.08); border: 1px solid rgba(250,204,21,0.3);
      border-radius: 10px; padding: 10px 12px; margin-bottom: 4px;
    }
    .lib-health-head {
      display: flex; align-items: center; gap: 6px;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; color: var(--warn); margin-bottom: 6px;
    }
    .lib-health-list { display: flex; flex-direction: column; gap: 4px; }
    .lib-health-row {
      display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
      width: 100%; text-align: left; background: transparent; border: none;
      border-radius: 6px; padding: 4px 6px; cursor: pointer; font-family: inherit;
      color: var(--text); transition: background 0.12s;
    }
    .lib-health-row:hover { background: var(--surface3); }
    .lib-health-name { font-size: calc(12px * var(--vv-text-scale)); font-weight: 600; }
    .lib-health-missing { font-size: calc(11px * var(--vv-text-scale)); color: var(--muted); }

    /* ── Tag chip input (2026-08-18) ── */
    .tag-input-wrap {
      position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
      background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
      padding: 6px 8px; min-height: 36px;
    }
    .tag-input-wrap:focus-within { border-color: var(--accent-l); }
    .tag-chip-list { display: contents; }
    .tag-chip {
      display: inline-flex; align-items: center; gap: 5px;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 600; color: var(--accent-l);
      background: var(--accent-dim); border: 1px solid rgba(124,58,237,0.3);
      border-radius: 20px; padding: 2px 4px 2px 9px;
    }
    .tag-chip-remove {
      display: inline-flex; align-items: center; justify-content: center;
      width: 16px; height: 16px; border-radius: 50%; border: none; cursor: pointer;
      background: transparent; color: var(--accent-l); opacity: 0.7; font-size: 13px; line-height: 1;
    }
    .tag-chip-remove:hover { opacity: 1; background: rgba(124,58,237,0.2); }
    .tag-input {
      flex: 1; min-width: 90px; background: transparent; border: none; outline: none;
      color: var(--text); font-size: calc(12px * var(--vv-text-scale)); font-family: inherit; padding: 3px 2px;
    }
    .tag-input::placeholder { color: var(--muted); }
    .tag-suggest-list {
      position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 20;
      background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
      box-shadow: 0 10px 28px rgba(0,0,0,0.4); max-height: 160px; overflow-y: auto;
    }
    .tag-suggest-item {
      display: block; width: 100%; text-align: left; background: transparent; border: none;
      color: var(--text); font-family: inherit; font-size: calc(12px * var(--vv-text-scale));
      padding: 7px 10px; cursor: pointer;
    }
    .tag-suggest-item:hover { background: var(--surface3); }

    .annot-list { display: flex; flex-direction: column; gap: 5px; }
    .annot-item {
      display: flex; align-items: center; gap: 8px;
      padding: 8px 10px; border-radius: 8px;
      border: 1px solid var(--border); background: var(--surface2);
    }
    .annot-num {
      width: 20px; height: 20px; border-radius: 50%;
      background: var(--annot); color: #1a1200;
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 800;
      display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    }
    .annot-item-title { flex: 1; font-size: calc(12px * var(--vv-text-scale)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .annot-item-cat { font-size: calc(13px * var(--vv-text-scale)); flex-shrink: 0; }
    .annot-item-btns { display: flex; gap: 3px; flex-shrink: 0; }

    .annot-cat-row { display: flex; gap: 4px; flex-wrap: wrap; }
    .annot-cat-chip {
      padding: 3px 9px; border-radius: 20px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
      cursor: pointer; border: 1.5px solid transparent; transition: all 0.15s;
      opacity: 0.7; user-select: none;
    }
    /* Selection is marked by a border too, not by opacity alone, now that
       unselected chips are no longer faded to 45%. */
    .annot-cat-chip.selected { opacity: 1; border-color: currentColor; }
    .annot-cat-chip:hover { opacity: 0.9; }

    .undo-row { display: flex; gap: 6px; }
    .undo-row .btn { flex: 1; justify-content: center; }

    .settings-stat { display: flex; justify-content: space-between; font-size: calc(12px * var(--vv-text-scale)); padding: 4px 0; }
    .settings-stat span:first-child { color: var(--muted); }

    /* ── Academic tab ── */
    .ac-subnav { display: flex; gap: 4px; margin-bottom: 4px; }
    .ac-subbtn {
      flex: 1; padding: 5px 4px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; border-radius: 7px;
      background: transparent; border: 1px solid var(--border); color: var(--muted);
      cursor: pointer; transition: all 0.15s; font-family: inherit;
    }
    .ac-subbtn:hover { background: var(--surface2); color: var(--text); }
    .ac-subbtn.active { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }
    .ac-sub { display: none; flex-direction: column; gap: 10px; }
    .ac-sub.active { display: flex; }
    .ac-list { display: flex; flex-direction: column; gap: 6px; }
    .ac-item {
      background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
      padding: 8px 10px; display: flex; align-items: flex-start; gap: 8px;
    }
    .ac-item-body { flex: 1; min-width: 0; }
    .ac-item-name { font-size: calc(13px * var(--vv-text-scale)); font-weight: 700; color: var(--text); }
    .ac-item-meta { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-top: 2px; }
    .ac-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
    .ac-week-list { display: flex; flex-direction: column; gap: 6px; }
    .ac-week-row {
      background: var(--surface3); border: 1px solid var(--border); border-radius: 7px;
      padding: 8px 10px; display: flex; flex-direction: column; gap: 6px;
    }
    .ac-week-row-header { display: flex; align-items: center; gap: 6px; }
    .ac-week-num { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l);
      background: var(--accent-dim); border-radius: 4px; padding: 1px 6px; flex-shrink: 0; }
    .ac-week-title-input { flex: 1; background: var(--surface2); border: 1px solid var(--border);
      border-radius: 6px; color: var(--text); font-size: calc(12px * var(--vv-text-scale)); padding: 4px 8px;
      outline: none; font-family: inherit; }
    .ac-week-title-input:focus { border-color: var(--accent-l); }
    .ac-week-models { display: flex; flex-wrap: wrap; gap: 4px; }
    .ac-week-model-chip {
      font-size: calc(12px * var(--vv-text-scale)); padding: 2px 7px; border-radius: 20px; cursor: pointer;
      background: var(--surface); border: 1px solid var(--border); color: var(--muted);
      transition: all 0.12s;
    }
    .ac-week-model-chip.selected { background: var(--accent-dim); border-color: rgba(124,58,237,0.5); color: var(--accent-l); font-weight: 700; }
    .ac-share-url {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--accent2); background: var(--accent2-dim);
      border: 1px solid rgba(96,165,250,0.2); border-radius: 5px; padding: 3px 7px;
      cursor: pointer; word-break: break-all; transition: opacity 0.15s;
    }
    .ac-share-url:hover { opacity: 0.75; }

    /* ── Course assign on model card ── */
    /* ── Thumbnail editor ── */
    /* ══════════════════════════════════════
       SHARED RICH TEXT EDITOR
    ══════════════════════════════════════ */
    .rte-toolbar {
      display: flex; flex-wrap: wrap; gap: 3px; padding: 6px 8px;
      background: var(--surface3); border: 1px solid var(--border);
      border-bottom: none; border-radius: 8px 8px 0 0;
    }
    .rte-btn {
      padding: 3px 8px; border-radius: 5px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
      background: transparent; border: 1px solid transparent; color: var(--text);
      cursor: pointer; font-family: inherit; transition: all 0.12s; line-height: 1.4;
    }
    .rte-btn:hover { background: var(--surface2); border-color: var(--border); }
    .rte-btn.active { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }
    .rte-sep { width: 1px; background: var(--border); margin: 2px 2px; align-self: stretch; }
    .rte-editor {
      min-height: 100px; max-height: 320px; overflow-y: auto;
      padding: 10px 12px; background: var(--surface2);
      border: 1px solid var(--border); border-radius: 0 0 8px 8px;
      color: var(--text); font-size: calc(13px * var(--vv-text-scale)); line-height: 1.7;
      outline: none; word-break: break-word;
    }
    .rte-editor:focus { border-color: var(--accent-l); }
    .rte-editor h2  { font-size: calc(15px * var(--vv-text-scale)); font-weight: 800; margin: 10px 0 4px; color: var(--accent-l); }
    .rte-editor h3  { font-size: calc(13px * var(--vv-text-scale)); font-weight: 700; margin: 8px 0 3px; }
    .rte-editor p   { margin: 4px 0; }
    .rte-editor ul, .rte-editor ol { padding-left: 18px; margin: 4px 0; }
    .rte-editor img { max-width: 100%; border-radius: 6px; margin: 6px 0; display: block; }
    /* Embedded media */
    .rte-video-wrap, .rte-audio-wrap {
      margin: 8px 0; border-radius: 8px; overflow: hidden;
      background: #000; position: relative;
    }
    .rte-video-wrap iframe, .rte-video-wrap video {
      width: 100%; aspect-ratio: 16/9; display: block; border: none;
    }
    .rte-audio-wrap audio { width: 100%; display: block; }
    /* Placeholder for empty contenteditable */
    .rte-editor:empty::before, .rte-editor:has(> p:only-child:empty)::before {
      content: attr(placeholder); color: var(--muted); pointer-events: none;
    }
    /* Popup rich content rendering */
    .annot-popup-desc h2  { font-size: calc(13px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l); margin: 6px 0 3px; }
    .annot-popup-desc h3  { font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; margin: 5px 0 2px; }
    .annot-popup-desc p   { margin: 3px 0; }
    .annot-popup-desc ul, .annot-popup-desc ol { padding-left: 14px; margin: 3px 0; }
    .annot-popup-desc img { max-width: 100%; border-radius: 4px; margin: 4px 0; }
    .annot-popup-desc .rte-video-wrap { margin: 6px 0; }
    .annot-popup-desc .rte-audio-wrap { margin: 6px 0; }
    /* Attachments */
    .wd-attach-row {
      display: flex; align-items: center; gap: 8px;
      padding: 6px 8px; background: var(--surface2);
      border: 1px solid var(--border); border-radius: 7px; margin-top: 5px;
    }
    .wd-attach-name { flex: 1; font-size: calc(12px * var(--vv-text-scale)); color: var(--accent2); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .wd-attach-name:hover { text-decoration: underline; }
    .wd-attach-size { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); flex-shrink: 0; }

    /* ── Background fx sliders ── */
    .bg-slider-row { display: flex; flex-direction: column; gap: 3px; }
    .bg-slider-label { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); display: flex; justify-content: space-between; }
    .bg-slider-row input[type=range] {
      width: 100%; accent-color: var(--accent-l); height: 4px; cursor: pointer;
    }

    .mi-thumb-wrap { display: flex; flex-direction: column; gap: 8px; }
    .mi-thumb-preview {
      width: 100%; height: 120px; border-radius: 10px;
      background: var(--surface3); border: 1px solid var(--border);
      display: flex; align-items: center; justify-content: center;
      overflow: hidden; position: relative;
    }
    /* wrap: the admin panel is resizable down to 300px and this row now holds
       three buttons (Choose Image / Capture View / Remove). */
    .mi-thumb-actions { display: flex; gap: 6px; flex-wrap: wrap; }
    /* Capture View is disabled whenever the Info form is open for a model that
       isn't the one currently in the 3D viewer — see miUpdateCaptureBtn(). */
    #mi-capture-btn:disabled {
      opacity: 0.45; cursor: not-allowed; pointer-events: none;
    }
    .mi-capture-hint {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); line-height: 1.5;
    }
    /* Manage → Info: per-model Lighting + Initial Placement (2026-09-18).
       The slider rows reuse the upload dialog's .um-slider-row grid. */
    .mi-inline-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
    /* Copy-with-fewer-structures picker (2026-10-16) */
    .mi-subset-list {
      display: flex; flex-direction: column; gap: 6px; margin-top: 8px; padding: 8px 10px;
      max-height: 240px; overflow-y: auto;
      border: 1px solid var(--border, rgba(255,255,255,0.12)); border-radius: 8px;
    }
    .mi-subset-row { justify-content: space-between; }
    .mi-subset-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
    .mi-subset-row .mi-subset-tris { color: var(--muted); font-size: calc(11px * var(--vv-text-scale)); white-space: nowrap; }
    /* Viewer preview (2026-10-27): ticked rows read as chosen at a glance, and
       the row whose structure is shown alone in the viewer is marked. */
    .mi-subset-row { border-radius: 6px; padding: 2px 6px; margin: 0 -6px; }
    .mi-subset-row:has(.mi-subset-cb:checked) { background: color-mix(in srgb, var(--accent) 16%, transparent); }
    .mi-subset-row.is-isolated { outline: 2px solid var(--accent); outline-offset: -2px; }
    .mi-subset-row .mi-subset-cb:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
    .mi-subset-preview-note:empty { display: none; }
    /* Edit structures of a copy (2026-10-28): a structure that the update would
       add or remove says so in words, not only by colour. */
    .mi-subset-origin { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border, rgba(255,255,255,0.12)); }
    .mi-subset-unmatched:empty { display: none; }
    .mi-subset-unmatched { color: var(--warn); }
    .mi-subset-row.is-added .mi-subset-left::after,
    .mi-subset-row.is-removed .mi-subset-left::after {
      font-size: calc(10px * var(--vv-text-scale)); font-weight: 600; padding: 0 6px; border-radius: 999px; white-space: nowrap;
    }
    .mi-subset-row.is-added .mi-subset-left::after { content: 'add'; color: var(--success); border: 1px solid var(--success); }
    .mi-subset-row.is-removed .mi-subset-left::after { content: 'remove'; color: var(--warn); border: 1px solid var(--warn); }
    .mi-subset-row.is-removed .mi-subset-left > span { text-decoration: line-through; }
    .mi-xf-grid {
      display: grid; grid-template-columns: 74px repeat(3, minmax(0, 1fr));
      align-items: center; gap: 6px;
    }
    .mi-xf-axis {
      font-size: calc(11px * var(--vv-text-scale)); color: var(--muted);
      text-align: center; font-weight: 600;
    }
    .mi-xf-row-label { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); }
    .mi-xf-grid .mi-xf-input {
      min-width: 0; padding: 6px 7px; font-variant-numeric: tabular-nums; text-align: right;
    }
    .mi-xf-scale-note {
      grid-column: span 2; font-size: calc(11px * var(--vv-text-scale)); color: var(--muted);
    }
    #mi-xf-use-current:disabled, #mi-xf-preview:disabled { opacity: 0.45; cursor: not-allowed; }
    .mi-xf-cam {
      grid-column: span 2; font-size: calc(11.5px * var(--vv-text-scale)); color: var(--muted);
      font-variant-numeric: tabular-nums; min-width: 0;
    }
    .mi-xf-cam.is-custom { color: var(--accent-l); }
    #mi-xf-cam-clear { justify-self: end; }
    #mi-xf-cam-clear.csp-hide { display: none; }
    #mi-ambient:disabled, #mi-dir:disabled, #mi-light-reset:disabled { opacity: 0.4; cursor: not-allowed; }
    .lighting-unlit-note#mi-lighting-unlit-note { margin: 0 0 8px; }

    /* ── Manage → Info UX pass (2026-09-18) ────────────────────────────
       Collapsible field groups, a single "Load in viewer" banner, and a
       sticky Save/Discard footer with an unsaved-changes count. */
    .mi-file-line {
      font-size: calc(11px * var(--vv-text-scale)); color: var(--muted);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: -6px 0 0;
    }
    .mi-file-line #mi-file-name { color: var(--text); }
    .info-card.mi-form { padding: 4px 16px; }
    .mi-group { border-bottom: 1px solid var(--border); }
    .mi-group:last-child { border-bottom: none; }
    .mi-group-head {
      list-style: none; cursor: pointer; user-select: none;
      display: flex; align-items: center; gap: 8px; padding: 12px 0;
    }
    .mi-group-head::-webkit-details-marker { display: none; }
    .mi-group-head::before {
      content: ''; width: 6px; height: 6px; flex-shrink: 0;
      border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
      transform: rotate(-45deg); transition: transform .15s;
    }
    .mi-group[open] > .mi-group-head::before { transform: rotate(45deg); }
    .mi-group-title {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; letter-spacing: 1.1px;
      text-transform: uppercase; color: var(--text);
    }
    .mi-group-sum {
      margin-left: auto; font-size: calc(11px * var(--vv-text-scale)); color: var(--muted);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
    }
    .mi-group-sum.is-custom { color: var(--accent-l); }
    .mi-group-body { padding: 2px 0 8px; }
    .mi-group-head:focus { outline-offset: 0; border-radius: 6px; }
    .mi-load-banner {
      display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
      padding: 10px 12px; border-radius: 10px;
      background: var(--accent2-dim); border: 1px solid rgba(96,165,250,0.35);
    }
    .mi-load-banner.csp-hide { display: none; }
    .mi-load-text { flex: 1 1 180px; font-size: calc(12px * var(--vv-text-scale)); color: var(--text); line-height: 1.45; }
    .mi-footer {
      position: sticky; bottom: -16px; z-index: 3;
      margin: -8px -16px -16px; padding: 12px 16px;
      display: flex; align-items: center; gap: 8px;
      background: var(--surface); border-top: 1px solid var(--border);
      box-shadow: 0 -8px 16px rgba(0,0,0,0.25);
    }
    .mi-dirty-note {
      flex: 1; min-width: 0; font-size: calc(11.5px * var(--vv-text-scale)); color: var(--muted);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .mi-footer.is-dirty .mi-dirty-note { color: var(--warn); }
    .mi-footer.is-dirty .mi-dirty-note::before { content: '● '; }
    .mi-footer .btn:disabled { opacity: 0.45; cursor: not-allowed; }
    /* Selected category gets a check so it no longer reads the same as a
       hovered one (both used to be "a coloured border"). */
    .cat-item.selected::before { content: '✓ '; }

    .model-course-tags { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }
    .model-course-tag {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; padding: 1px 6px; border-radius: 20px;
      background: var(--accent2-dim); color: var(--accent2);
      border: 1px solid rgba(96,165,250,0.25);
    }

    /* ── Course Assign Dialog ── */
    #course-assign-dialog {
      display: none; position: fixed; inset: 0; z-index: 500;
      align-items: center; justify-content: center;
      background: rgba(5,7,15,0.85); backdrop-filter: blur(8px);
    }
    #course-assign-dialog.show { display: flex; }
    .ca-card {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 16px; padding: 24px; width: 360px; max-height: 80vh; max-height: 80dvh;
      display: flex; flex-direction: column; gap: 14px;
      box-shadow: 0 24px 64px rgba(0,0,0,0.8);
    }
    .ca-title { font-size: calc(15px * var(--vv-text-scale)); font-weight: 800; }
    .ca-model-name { font-size: calc(12px * var(--vv-text-scale)); color: var(--accent-l); font-weight: 700; }
    .ca-list { display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; }
    .ca-course-block { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; }
    .ca-course-name { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; margin-bottom: 6px; }
    .ca-weeks { display: flex; flex-direction: column; gap: 4px; }
    .ca-week-row { display: flex; align-items: center; gap: 8px; }
    .ca-week-check {
      width: 16px; height: 16px; border-radius: 4px; border: 2px solid var(--border);
      cursor: pointer; accent-color: var(--accent-l); flex-shrink: 0;
    }
    .ca-week-label { font-size: calc(12px * var(--vv-text-scale)); flex: 1; cursor: pointer; }
    .ca-empty { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); text-align: center; padding: 20px 0; }
    .ca-footer { display: flex; gap: 8px; }
    .ca-no-courses { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); text-align: center; padding: 16px 0; line-height: 1.6; }

    /* Course-scoped model-info edit (2026-08-18): banner shown at the top of
       the Info segment when a school_admin/instructor is editing this
       model's description+tags for the current course, plus the picker
       dialog for "Copy from another course". Reuses the .ca-card/.ca-list
       dialog chrome above so it looks like the rest of the Manage panel. */
    .mi-course-banner {
      background: color-mix(in srgb, var(--accent) 10%, var(--surface2));
      border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
      border-radius: 10px; padding: 12px 14px; margin-bottom: 14px;
    }
    .mi-course-banner-title { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l); }
    .mi-course-banner-sub   { font-size: calc(11.5px * var(--vv-text-scale)); color: var(--muted); margin-top: 4px; line-height: 1.5; }
    .mi-course-banner-meta  { font-size: calc(11px * var(--vv-text-scale)); color: var(--muted); margin-top: 6px; font-style: italic; }
    .mi-course-banner-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
    .btn-sm { padding: 5px 10px; font-size: calc(11.5px * var(--vv-text-scale)); }

    #mi-copy-source-dialog {
      display: none; position: fixed; inset: 0; z-index: 500;
      align-items: center; justify-content: center;
      background: rgba(5,7,15,0.85); backdrop-filter: blur(8px);
    }
    #mi-copy-source-dialog.show { display: flex; }
    #mi-copy-source-dialog .ca-card { width: 420px; }
    .mi-copy-source-row {
      background: var(--surface2); border: 1px solid var(--border); border-radius: 10px;
      padding: 10px 12px; cursor: pointer; transition: border-color 0.15s;
    }
    .mi-copy-source-row:hover, .mi-copy-source-row:focus-visible { border-color: var(--accent-l); }
    .mi-copy-source-course { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; margin-bottom: 4px; }
    .mi-copy-source-meta   { font-size: calc(10.5px * var(--vv-text-scale)); color: var(--muted); margin-bottom: 6px; }
    .mi-copy-source-preview {
      font-size: calc(11.5px * var(--vv-text-scale)); color: var(--text); line-height: 1.4;
      max-height: 3.2em; overflow: hidden;
    }

    .empty-state {
      text-align: center; color: var(--muted); font-size: calc(12px * var(--vv-text-scale));
      padding: 28px 16px; line-height: 1.9;
    }
    .empty-icon { font-size: calc(28px * var(--vv-text-scale)); display: block; margin-bottom: 8px; opacity: 0.25; }

    .overlay-dialog {
      position: fixed; inset: 0; background: rgba(0,0,0,0.75);
      z-index: 600; display: none; align-items: center; justify-content: center;
    }
    .overlay-dialog.show { display: flex; }

    .dialog-card {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 14px; padding: 24px 26px; width: 300px;
      box-shadow: 0 16px 48px rgba(0,0,0,0.7);
    }
    .dialog-card h3 { font-size: calc(14px * var(--vv-text-scale)); font-weight: 700; margin-bottom: 8px; }
    .dialog-card p { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-bottom: 18px; line-height: 1.6; }
    .dialog-btns { display: flex; gap: 8px; justify-content: flex-end; }

    .annot-dialog-card {
      background: var(--surface); border: 1px solid var(--annot);
      border-radius: 14px; padding: 20px; width: 300px;
      box-shadow: 0 16px 48px rgba(0,0,0,0.7);
    }
    .annot-dialog-card h4 {
      font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; color: var(--annot);
      margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.8px;
    }

    .btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }

    /* ── Upload metadata dialog (2026-08-18, Phase 3) ── */
    .um-card {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 16px; padding: 22px 24px; width: 380px; max-height: 86vh; max-height: 86dvh;
      display: flex; flex-direction: column; gap: 4px;
      box-shadow: 0 24px 64px rgba(0,0,0,0.8);
    }
    .um-title {
      display: flex; align-items: center; gap: 8px;
      font-size: calc(15px * var(--vv-text-scale)); font-weight: 800;
    }
    .um-filename {
      font-size: calc(12px * var(--vv-text-scale)); color: var(--muted);
      margin-bottom: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .um-body { overflow-y: auto; padding-right: 2px; }
    .um-warn {
      display: flex; align-items: flex-start; gap: 8px;
      background: rgba(250,204,21,0.08); border: 1px solid rgba(250,204,21,0.3);
      border-radius: 8px; padding: 8px 10px; font-size: calc(11px * var(--vv-text-scale));
      color: var(--warn); line-height: 1.5; margin-top: 4px;
    }
    .um-warn .icon { flex-shrink: 0; margin-top: 1px; }

    /* ── Sensitive-content consent dialog (2026-08-18, Phase 5) ── */
    .sc-card { width: 340px; }
    .sc-card h3 {
      display: flex; align-items: center; gap: 8px; color: var(--warn);
      font-size: calc(14px * var(--vv-text-scale)); font-weight: 800; margin-bottom: 10px;
    }
    .sc-consent { font-size: calc(13px * var(--vv-text-scale)); margin-bottom: 4px; }

    #pw-msg { font-size: calc(12px * var(--vv-text-scale)); margin-bottom: 8px; display: none; }
    #pw-msg.ok  { color: var(--success); display: block; }
    #pw-msg.err { color: var(--error);   display: block; }

    /* ══════════════════════════════════════
       MOBILE
    ══════════════════════════════════════ */
    @media (max-width: 768px) {
      /* Bottom-sheet panel, resizable up/down via the top handle */
      #vvAdminOverlay {
        width: 100%; left: 0; top: auto; bottom: 0;
        height: var(--admin-h, 72vh); height: var(--admin-h, 72dvh);
        border-left: none; border-top: 1px solid var(--border);
        border-radius: 16px 16px 0 0;
        box-shadow: 0 -10px 32px rgba(0,0,0,0.55);
      }
      #vvAdminOverlay.admin-full { height: 100vh; height: 100dvh; top: 0; border-radius: 0; }
      body.admin-open #canvas-wrap { margin-right: 0; }
      .admin-resizer   { display: none; }          /* no horizontal edge-drag on mobile */
      .admin-resizer-v { display: block; }          /* vertical (up/down) drag handle */
      .info-resizer    { display: none; }          /* info sidebar is a bottom sheet on mobile */
      #adm-mobile-size { display: inline-flex; }     /* full / sheet toggle */

      header {
        gap: 6px; flex-wrap: nowrap; height: 48px; padding: 0 10px;
        overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch;
      }
      header::-webkit-scrollbar { height: 0; display: none; }
      header > * { flex-shrink: 0; }
      .model-name { flex: 0 1 auto; min-width: 0; }
      .logo-text { display: none; }
      .btn { font-size: calc(12px * var(--vv-text-scale)); padding: 4px 9px; }
      /* Declutter: secondary actions move into the ☰ sidebar (Quick Controls) */
      #reset-view-btn, #reset-transform-btn, #share-btn, #themeToggle, .header-sep { display: none !important; }
      #mobile-quick-controls { display: block !important; }
      /* 2026-10-14: was display:none, so the Library button did nothing visible
         on phones. Now a drawer over the canvas, like tablet portrait below. */
      .app-body { position: relative; }
      #library-panel {
        position: absolute; top: 0; left: 0; bottom: 0; z-index: 150;
        width: min(86vw, 340px); min-width: 0;
        box-shadow: 8px 0 32px rgba(0,0,0,0.55);
      }
      #library-panel.collapsed { width: 0; }
      .sidebar {
        position: fixed; bottom: 0; left: 0; right: 0;
        width: 100%!important; height: var(--info-h, 55vh); height: var(--info-h, 55dvh); max-height: 95vh; max-height: 95dvh;
        border-left: none; border-top: 1px solid var(--border);
        border-radius: 14px 14px 0 0; z-index: 200;
        transform: translateY(110%);
        transition: transform 0.3s cubic-bezier(.4,0,.2,1);
        box-shadow: 0 -8px 32px rgba(0,0,0,0.6);
        padding: 22px 14px 24px; overflow-y: auto; overflow-x: hidden;
        -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
        /* 2026-10-06 (Sean): sections stack and the sheet scrolls up/down.
           It used to lay sections side by side and scroll sideways, which
           cut off whatever was past the right edge (e.g. Transform). */
        display: flex!important; flex-direction: column!important;
        flex-wrap: nowrap; gap: 14px;
      }
      .sidebar.mob-open { transform: translateY(0); }
      .info-resizer-v { display: block; }   /* vertical drag handle for the info sheet */
      .sidebar-section { min-width: 0; flex-shrink: 0; }   /* don't squash in the column */
      .divider { flex-shrink: 0; }
      .main { flex-direction: column; }
      #canvas-wrap { flex: 1; }
      #mob-sidebar-toggle {
        display: inline-flex!important; align-items: center; gap: 5px;
        padding: 4px 9px; border-radius: 6px; background: transparent;
        color: var(--muted); border: 1px solid var(--border);
        font-size: calc(12px * var(--vv-text-scale)); font-weight: 500; cursor: pointer; transition: all 0.15s;
      }
      #mob-sidebar-toggle.open { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }
    }

    /* ══════════════════════════════════════
       TABLET PORTRAIT (iPad portrait, ~769–1024px wide)
       2026-10-06: iPad portrait is wider than the 768px phone breakpoint, so
       it got the desktop three-column layout and the canvas was squeezed to
       roughly 330px between the library and the info panel. Here the library
       floats over the canvas as a drawer instead of pushing it aside, so the
       canvas keeps its full width whether the library is open or closed.
       js/viewer-model.js (vvLibIsOverlay) uses the same media query to close
       the drawer after a pick and to skip auto-opening it on a direct link.
    ══════════════════════════════════════ */
    @media (min-width: 769px) and (max-width: 1024px) and (orientation: portrait) {
      .app-body { position: relative; }
      #library-panel {
        position: absolute; top: 0; left: 0; bottom: 0; z-index: 150;
        box-shadow: 8px 0 32px rgba(0,0,0,0.55);
      }
    }

    /* Keyboard-shortcut hints (Q/E, arrows, +/-) mean nothing on a touch-only
       device such as an iPad without a keyboard/trackpad. A trackpad or mouse
       reports hover, so the hints come back when one is attached. */
    @media (any-hover: none) and (any-pointer: coarse) {
      .kbd-note { display: none; }
    }

    /* ══════════════════════════════════════
       NARROW PHONE (iPhone SE / mini and similar, ~375–430px)
       The 768px breakpoint above targets tablet-portrait widths; these rules
       tighten things further so the header, bottom-sheet sidebar, and draw
       toolbar don't feel cramped on the smallest common phone screens.
    ══════════════════════════════════════ */
    @media (max-width: 430px) {
      #orient-gizmo { width: 72px; height: 72px; top: 10px; right: 10px; }
      #orient-gizmo .gz-label { font-size: calc(12px * var(--vv-text-scale)); }
      .sidebar-section { min-width: 0; }
      .sidebar { gap: 10px; padding: 18px 10px 20px; }
      .draw-toolbar {
        right: 10px; bottom: 64px; gap: 5px; padding: 6px 8px; max-width: calc(100% - 16px);
      }
      .dt-tool, .dt-act { width: 30px; height: 30px; font-size: calc(14px * var(--vv-text-scale)); }
    }

  /* ══════════════════════════════════════
     LANDING SCREEN
  ══════════════════════════════════════ */
  /* 2026-10-05: the nav and footer used to be position:absolute over a
     centred, non-scrolling body, so on a phone (or any short window) the
     title slid under the nav and the cards under the footer. They are now
     in normal flow: nav on top, content centred in the leftover space,
     footer at the bottom, and the whole screen scrolls when it doesn't fit. */
  #landing-screen {
    position: fixed; inset: 0; z-index: 200;
    display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start;
    overflow-x: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch;
    background: var(--bg);
  }
  .landing-bg {
    position: fixed; inset: 0; pointer-events: none;
    background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(124,58,237,0.12) 0%, transparent 70%),
                radial-gradient(ellipse 50% 40% at 80% 100%, rgba(96,165,250,0.08) 0%, transparent 70%);
  }
  .landing-inner {
    position: relative; text-align: center;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px;
    flex: 1 0 auto; margin: 0 auto;
    padding: 24px 24px 40px; max-width: 720px; width: 100%; box-sizing: border-box;
  }
  .landing-logo { display: flex; align-items: center; gap: 10px; }
  .landing-logo-icon {
    /* CI v2: origami-brain symbol (assets/brand), no tile behind it */
    width: 44px; height: 44px; object-fit: contain; flex-shrink: 0;
  }
  .landing-logo-text {
    font-size: calc(22px * var(--vv-text-scale)); font-weight: 900; letter-spacing: -0.5px;
    background: linear-gradient(135deg, var(--accent-l), var(--accent2));
    -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
  }
  .landing-logo-badge {
    padding: 2px 9px; border-radius: 20px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 800;
    text-transform: uppercase; letter-spacing: 0.8px; color: var(--accent-l);
    background: var(--accent-dim); border: 1px solid rgba(124,58,237,0.3);
    -webkit-text-fill-color: var(--accent-l);
  }
  .landing-title { font-size: calc(28px * var(--vv-text-scale)); font-weight: 900; margin: 0; letter-spacing: -0.5px; }
  .landing-sub { font-size: calc(14px * var(--vv-text-scale)); color: var(--muted); margin: -12px 0 0; }
  .landing-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; width: 100%; }
  .landing-card {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 20px; padding: 32px 28px; cursor: pointer;
    display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
    text-align: left; font-family: inherit; color: var(--text);
    transition: all 0.2s; position: relative; overflow: hidden;
  }
  .landing-card::before {
    content: ''; position: absolute; inset: 0; opacity: 0; transition: opacity 0.2s;
    background: linear-gradient(135deg, var(--accent-dim), transparent);
  }
  .landing-card:hover { border-color: rgba(124,58,237,0.5); transform: translateY(-2px);
    box-shadow: 0 12px 40px rgba(0,0,0,0.3); }
  .landing-card:hover::before { opacity: 1; }
  .landing-card-icon { font-size: calc(36px * var(--vv-text-scale)); }
  .landing-card-title { font-size: calc(18px * var(--vv-text-scale)); font-weight: 800; }
  .landing-card-desc { font-size: calc(13px * var(--vv-text-scale)); color: var(--muted); line-height: 1.6; }
  .landing-card-action { font-size: calc(13px * var(--vv-text-scale)); font-weight: 700; color: var(--accent-l); margin-top: 4px; }

  /* ── Landing top nav ── */
  .landing-nav {
    position: relative; flex-shrink: 0; z-index: 5;
    display: flex; align-items: center; justify-content: space-between;
    padding: 16px 24px; gap: 12px;
  }
  .landing-nav-brand { display: flex; align-items: center; gap: 8px; }
  .landing-nav-brand .ln-icon {
    width: 30px; height: 30px; object-fit: contain; flex-shrink: 0;
  }
  .landing-nav-brand .ln-text {
    font-size: calc(15px * var(--vv-text-scale)); font-weight: 900; letter-spacing: -0.4px;
    background: linear-gradient(135deg, var(--accent-l), var(--accent2));
    -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
  }
  .landing-nav-links { display: flex; align-items: center; gap: 4px; }
  .landing-nav-linkset { display: inline-flex; align-items: center; gap: 4px; }
  /* Brand logo image (theme-aware) replaces the icon + text when set */
  .ln-brand-img { display: none; height: 30px; width: auto; max-width: 200px; }
  .landing-nav-brand .ln-brand-img-dark  { display: block; }
  .landing-nav-brand .ln-brand-img-light { display: none; }
  [data-theme="light"] .landing-nav-brand .ln-brand-img-dark  { display: none; }
  [data-theme="light"] .landing-nav-brand .ln-brand-img-light { display: block; }
  /* Owner "Edit header" button — hidden unless owner */
  .hed-edit-btn { display: none; }
  .landing-nav.hd-can-edit .hed-edit-btn { display: inline-flex; align-items: center; }
  .landing-nav-link {
    padding: 8px 14px; border-radius: 9px; border: 1px solid transparent;
    background: transparent; color: var(--muted); font-size: calc(13px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit; transition: all 0.15s;
  }
  .landing-nav-link:hover { color: var(--text); background: var(--surface2); }
  .landing-nav-login {
    padding: 8px 14px; border-radius: 9px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text); font-size: calc(13px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit; transition: all 0.15s; margin-left: 6px;
  }
  .landing-nav-login:hover { border-color: rgba(124,58,237,0.5); }

  /* ── Landing footer ── */
  .landing-footer {
    position: relative; flex-shrink: 0; z-index: 5;
    display: flex; flex-direction: column; gap: 14px;
    padding: 20px 32px 22px;
    border-top: 1px solid var(--border);
    background: linear-gradient(to top, var(--surface), transparent);
    backdrop-filter: blur(2px);
  }
  .lf-top {
    display: flex; align-items: center; justify-content: space-between;
    gap: 24px; flex-wrap: wrap; max-width: 1100px; width: 100%; margin: 0 auto;
  }
  .lf-logos { display: flex; align-items: center; gap: 18px; flex-shrink: 0; }
  .lf-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; }
  .lf-logo { transition: opacity 0.15s; }
  .lf-logo:hover { opacity: 0.78; }
  /* Uploaded logo image — theme-aware: dark variant default, light variant in light mode */
  .lf-logo-img { display: none; height: 34px; width: auto; max-width: 220px; flex-shrink: 0; }
  .lf-logo .lf-logo-img-dark  { display: block; }
  .lf-logo .lf-logo-img-light { display: none; }
  [data-theme="light"] .lf-logo .lf-logo-img-dark  { display: none; }
  [data-theme="light"] .lf-logo .lf-logo-img-light { display: block; }
  /* Placeholder for a logo that has no image yet */
  .lf-logo-ph {
    display: inline-flex; align-items: center; padding: 7px 11px;
    border: 1px dashed var(--border); border-radius: 8px;
    font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--muted); letter-spacing: 0.4px; white-space: nowrap;
  }
  /* UBC built-in fallback — white in dark mode, UBC blue in light mode */
  .lf-logo-ubc { color: #ffffff; }
  [data-theme="light"] .lf-logo-ubc { color: #002145; }
  .lf-ubc-shield { display: block; flex-shrink: 0; }
  .lf-ubc-word {
    font-family: Georgia, 'Times New Roman', serif;
    font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; line-height: 1.3; letter-spacing: 1.3px;
    color: currentColor; text-align: left; white-space: nowrap; text-transform: uppercase;
  }
  .lf-divider { width: 1px; height: 34px; background: var(--border); }
  /* HIVE built-in fallback */
  .lf-hive-svg { display: block; flex-shrink: 0; }
  .lf-logo-word {
    font-size: calc(18px * var(--vv-text-scale)); font-weight: 900; letter-spacing: 1.5px; color: var(--text);
  }
  /* Owner "Edit footer" affordance */
  .lf-edit-btn {
    position: absolute; top: 10px; right: 16px; z-index: 6;
    display: none; align-items: center; gap: 6px;
    padding: 6px 12px; border-radius: 8px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text); font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit; transition: all 0.15s;
  }
  .lf-edit-btn:hover { border-color: var(--accent-l); background: var(--accent-dim); }
  .landing-footer.lf-can-edit .lf-edit-btn { display: inline-flex; }
  .lf-links {
    display: flex; align-items: center; flex-wrap: wrap; gap: 4px;
  }
  .lf-link {
    display: inline-flex; align-items: center; text-decoration: none;
    padding: 7px 12px; border-radius: 8px; border: 1px solid transparent;
    background: transparent; color: var(--muted); font-size: calc(12.5px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit; transition: all 0.15s; white-space: nowrap;
  }
  .lf-link:hover { color: var(--text); background: var(--surface2); }
  .lf-copy {
    max-width: 1100px; width: 100%; margin: 0 auto;
    font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); letter-spacing: 0.2px;
  }
  @media (max-width: 720px) {
    .landing-nav { flex-wrap: wrap; padding: 12px 16px; row-gap: 6px; }
    .landing-nav-links { flex-wrap: wrap; justify-content: flex-end; row-gap: 4px; }
    .landing-nav-linkset { flex-wrap: wrap; }
    .landing-nav-link { padding: 6px 10px; }
    .landing-inner { padding: 16px 16px 32px; gap: 18px; }
    .landing-title { font-size: calc(24px * var(--vv-text-scale)); }
    .landing-sub { margin-top: -8px; }
    .landing-cards { grid-template-columns: 1fr; gap: 12px; }
    .landing-card { padding: 22px 20px; }
    /* Phones: compact cards (title + action only) and a text-only hero logo. */
    .landing-card-desc { display: none; }
    .landing-logo-icon { display: none; }
    /* Center the logo, title and the two cards on the phone screen itself: the inner
       block fills the viewport below the nav (footer sits below the fold) and the
       bottom padding equals the nav height, so the content's middle is the screen's middle. */
    .landing-inner { min-height: calc(100vh - 58px); min-height: calc(100dvh - 58px); padding: 0 16px 58px; }
    .landing-footer { padding: 16px 18px 18px; gap: 12px; }
    .lf-edit-btn { position: static; align-self: flex-end; order: -1; }
    .lf-top { flex-direction: column; align-items: flex-start; gap: 14px; }
    .lf-links { gap: 2px; }
    .lf-link { padding: 6px 8px; }
  }

  /* ── Footer / header editor modal ── */
  #footer-editor, #header-editor {
    position: fixed; inset: 0; z-index: 210; display: none;
    align-items: center; justify-content: center; padding: 24px;
    background: rgba(0,0,0,0.55);
  }
  #footer-editor.show, #header-editor.show { display: flex; }
  .fed-panel {
    width: 100%; max-width: 680px; max-height: 88vh; max-height: 88dvh; display: flex; flex-direction: column;
    background: var(--surface); border: 1px solid var(--border); border-radius: 16px;
    box-shadow: 0 24px 70px rgba(0,0,0,0.5); overflow: hidden;
  }
  .fed-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 16px 20px; border-bottom: 1px solid var(--border); flex-shrink: 0;
  }
  .fed-title { font-size: calc(16px * var(--vv-text-scale)); font-weight: 800; }
  .fed-close {
    width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border);
    background: transparent; color: var(--muted); font-size: calc(16px * var(--vv-text-scale)); cursor: pointer; font-family: inherit;
  }
  .fed-close:hover { background: var(--surface2); color: var(--text); }
  .fed-body { padding: 18px 20px; overflow-y: auto; }
  .fed-section { margin-bottom: 22px; }
  .fed-section > h4 {
    margin: 0 0 10px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; text-transform: uppercase;
    letter-spacing: 0.6px; color: var(--muted);
  }
  .fed-card {
    border: 1px solid var(--border); border-radius: 12px; padding: 14px;
    margin-bottom: 12px; background: var(--bg);
  }
  .fed-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
  .fed-field { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 140px; }
  .fed-field label { font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; color: var(--muted); }
  .fed-input {
    padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text); font-size: calc(13px * var(--vv-text-scale)); font-family: inherit; width: 100%;
  }
  .fed-input:focus { outline: none; border-color: var(--accent-l); }
  .fed-imgs { display: flex; gap: 12px; margin-top: 12px; flex-wrap: wrap; }
  .fed-img-slot { flex: 1; min-width: 150px; }
  .fed-img-slot > span {
    display: block; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; color: var(--muted); margin-bottom: 6px;
  }
  .fed-preview {
    height: 56px; border-radius: 8px; border: 1px dashed var(--border);
    display: flex; align-items: center; justify-content: center; overflow: hidden; margin-bottom: 8px;
  }
  .fed-preview.light-bg { background: #eef1f8; }
  .fed-preview.dark-bg  { background: #0a0d14; }
  .fed-preview img { max-height: 48px; max-width: 100%; }
  .fed-preview .fed-empty { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); }
  .fed-mini {
    padding: 6px 10px; border-radius: 7px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text); font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit;
  }
  .fed-mini:hover { border-color: var(--accent-l); }
  .fed-mini.danger { color: #ef4444; }
  .fed-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
  .fed-card-head .fed-mini { padding: 4px 8px; }
  .fed-add {
    padding: 8px 12px; border-radius: 9px; border: 1px dashed var(--border);
    background: transparent; color: var(--accent-l); font-size: calc(12.5px * var(--vv-text-scale)); font-weight: 800;
    cursor: pointer; font-family: inherit; width: 100%;
  }
  .fed-add:hover { background: var(--accent-dim); }
  .fed-link-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
  .fed-link-row .fed-input { flex: 1; }
  .fed-foot {
    display: flex; justify-content: flex-end; gap: 10px;
    padding: 14px 20px; border-top: 1px solid var(--border); flex-shrink: 0;
  }
  .fed-save {
    padding: 9px 18px; border-radius: 9px; border: 1px solid transparent;
    background: linear-gradient(135deg, var(--accent-l), var(--accent2)); color: #fff;
    font-size: calc(13px * var(--vv-text-scale)); font-weight: 800; cursor: pointer; font-family: inherit;
  }
  .fed-cancel {
    padding: 9px 16px; border-radius: 9px; border: 1px solid var(--border);
    background: transparent; color: var(--muted); font-size: calc(13px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit;
  }

  /* ══════════════════════════════════════
     POLICY GATE — shown once per browser session (UI-201)
  ══════════════════════════════════════ */
  #policy-gate {
    position: fixed; inset: 0; z-index: 1000000;
    display: flex; align-items: center; justify-content: center;
    padding: 24px; overflow-y: auto;
    background: var(--bg);
    transition: opacity 0.25s ease;
  }
  /* UI-201: set by vanvrai-auth-gate.js (a blocking <head> script) before this
     element is even parsed, so a returning visitor within the same browser
     session never sees the gate flash on screen. */
  html.vv-policy-acked #policy-gate { display: none !important; }
  .pg-bg {
    position: absolute; inset: 0; pointer-events: none;
    background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(124,58,237,0.14) 0%, transparent 70%),
                radial-gradient(ellipse 50% 40% at 85% 100%, rgba(96,165,250,0.10) 0%, transparent 70%);
  }
  .pg-panel {
    position: relative; width: 100%; max-width: 920px; margin: auto;
    display: flex; flex-direction: column; align-items: center; gap: 22px;
    text-align: center; padding: 8px;
  }
  .pg-head { display: flex; flex-direction: column; align-items: center; gap: 8px; }
  .pg-brand {
    display: flex; align-items: center; gap: 8px;
    margin-bottom: 14px;   /* breathing room above the title */
  }
  .pg-brand-icon {
    width: 30px; height: 30px; object-fit: contain;
  }
  .pg-brand-text { font-size: calc(13px * var(--vv-text-scale)); font-weight: 800; letter-spacing: 0.3px; color: var(--muted); }
  .pg-title {
    font-size: calc(26px * var(--vv-text-scale)); font-weight: 900; letter-spacing: 0.5px; margin: 0; color: var(--text);
    text-transform: uppercase;
  }
  .pg-welcome { font-size: calc(14px * var(--vv-text-scale)); color: var(--accent-l); line-height: 1.6; margin: 0; font-weight: 600; }
  .pg-cards {
    display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; width: 100%;
    align-items: stretch; margin-top: 48px;   /* clearance for overhanging illustrations */
  }
  .pg-card {
    background: linear-gradient(135deg, var(--accent-l), var(--accent2));
    color: #fff; border-radius: 18px; padding: 22px 20px;
    display: flex; flex-direction: column; gap: 12px;
    text-align: left; font-size: calc(13px * var(--vv-text-scale)); line-height: 1.6;
    box-shadow: 0 10px 30px rgba(124,58,237,0.25);
  }
  .pg-card.tall { justify-content: flex-end; min-height: 230px; }
  .pg-card-col { display: flex; flex-direction: column; gap: 16px; }
  .pg-card.rule { flex-direction: row; align-items: center; gap: 14px; flex: 1; }
  .pg-card.rule .pg-rule-text { font-weight: 600; }
  /* Welcome card: figures illustration overlapping the top edge (above the text) */
  .pg-card.has-figures { position: relative; overflow: visible; padding-top: 26px; }
  .pg-figures {
    position: absolute; top: -60px; left: 50%; transform: translateX(-50%);
    width: 172px; height: auto; pointer-events: none; z-index: 3;
    filter: drop-shadow(0 12px 18px rgba(0,0,0,0.28));
  }
  @media (max-width: 720px) { .pg-figures { width: 132px; top: -46px; } }
  /* Computer illustration: 70% of the figures size, shorter overhang */
  .pg-fig-computer { width: 120px; top: -44px; }
  @media (max-width: 720px) { .pg-fig-computer { width: 94px; top: -34px; } }
  /* "no" icon: emoji with red circle + slash */
  .pg-no {
    position: relative; flex-shrink: 0;
    width: 52px; height: 52px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: calc(24px * var(--vv-text-scale)); background: rgba(255,255,255,0.16);
  }
  .pg-no::before {
    content: ''; position: absolute; inset: 0; border-radius: 50%;
    border: 3px solid #ff5a5a;
  }
  .pg-no::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: 3px; height: 58px; background: #ff5a5a; border-radius: 2px;
    transform: translate(-50%, -50%) rotate(45deg);
  }
  .pg-consent {
    display: inline-flex; align-items: center; gap: 10px; cursor: pointer;
    font-size: calc(15px * var(--vv-text-scale)); font-weight: 700; color: var(--text); user-select: none;
  }
  /* Sean (2026-08-11): "I understand" checkbox on the entry policy screen
     wasn't Tab-reachable, and there was no visible border when it was.
     Root cause of the first part: `width:0; height:0` (with opacity:0) is
     the classic "visually hide a native checkbox, keep .pg-box as the
     custom visual" trick, but a genuinely zero-size element is excluded
     from the Tab order entirely in some browsers (notably Safari/WebKit) —
     it's not just invisible, the browser skips it like `display:none`
     would. 1px keeps it imperceptible (still opacity:0, still absolutely
     positioned out of flow) while guaranteeing it stays focusable
     everywhere. Second part (border) is handled by :focus below, which
     replaces the old input:focus-visible + .pg-box rule this input never
     even reached before. */
  .pg-consent input { position: absolute; opacity: 0; width: 1px; height: 1px; }
  .pg-box {
    width: 22px; height: 22px; border-radius: 50%;
    border: 2px solid var(--muted); display: inline-flex; align-items: center; justify-content: center;
    transition: all 0.15s; flex-shrink: 0;
  }
  .pg-consent input:checked + .pg-box {
    background: var(--accent-l); border-color: var(--accent-l);
  }
  .pg-box::after {
    content: '✓'; color: #fff; font-size: calc(13px * var(--vv-text-scale)); font-weight: 900; opacity: 0; transition: opacity 0.15s;
  }
  .pg-consent input:checked + .pg-box::after { opacity: 1; }
  /* :focus (not :focus-visible) — Sean asked the border show "whenever
     it's the focus area," not gated behind the browser's own keyboard-vs-
     mouse heuristic, same call as the #three-canvas ring (P0-4). */
  .pg-consent input:focus + .pg-box { box-shadow: 0 0 0 3px var(--accent-l), 0 0 0 5px var(--accent-dim); }
  .pg-enter {
    padding: 14px 48px; border-radius: 30px; border: none;
    font-size: calc(16px * var(--vv-text-scale)); font-weight: 800; letter-spacing: 1px; font-family: inherit;
    background: var(--text); color: var(--bg); cursor: pointer; transition: all 0.2s;
    text-transform: uppercase;
  }
  .pg-enter:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 10px 30px rgba(0,0,0,0.25); }
  .pg-enter:disabled { opacity: 0.4; cursor: not-allowed; }
  @media (max-width: 720px) {
    .pg-cards { grid-template-columns: 1fr; margin-top: 24px; }
    .pg-card.tall { min-height: 0; }
    .pg-title { font-size: calc(20px * var(--vv-text-scale)); }
    /* Below the desktop breakpoint, cards stack full-width and there's no
       longer room for the illustration to overhang above the text without
       covering it — lay the card out as illustration-left / text-right
       instead, with the image back in normal flow (not absolutely
       positioned) so it can't overlap the copy. */
    .pg-card.has-figures {
      flex-direction: row; align-items: center; justify-content: space-between;
      gap: 14px; padding-top: 22px; overflow: hidden;
    }
    .pg-figures {
      position: static; top: auto; left: auto; transform: none; order: -1;
      width: 84px; margin: 0; filter: drop-shadow(0 6px 12px rgba(0,0,0,0.28));
    }
    .pg-fig-computer { width: 64px; }
  }

  /* ══════════════════════════════════════
     INFO PAGE (Team / Research)
  ══════════════════════════════════════ */
  #info-page {
    position: fixed; inset: 0; z-index: 195;
    display: none; flex-direction: column; background: var(--bg); overflow: hidden;
  }
  .ip-body { flex: 1; overflow-y: auto; }
  .ip-wrap { max-width: 860px; margin: 0 auto; padding: 40px 28px 80px; }
  .ip-meta { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-bottom: 18px; }
  .ip-content {
    font-size: calc(15px * var(--vv-text-scale)); line-height: 1.75; color: var(--text); word-wrap: break-word;
  }
  /* P0-4 follow-up (2026-08-11): this is a contenteditable region while
     editing (site-config.js ipEnterEdit()) — it's higher-specificity than
     the global :focus-visible ring, so the old bare `outline:none` won it
     and left the region with zero focus indicator, same fix pattern as
     .rte-editor:focus above. */
  .ip-content:focus { outline: none; border-radius: 6px; box-shadow: 0 0 0 2px var(--accent-l); }
  .ip-content[contenteditable="true"] {
    border: 1px dashed var(--border); border-radius: 12px;
    padding: 20px; min-height: 320px; background: var(--surface);
  }
  .ip-content h1 { font-size: calc(26px * var(--vv-text-scale)); font-weight: 900; margin: 0 0 14px; letter-spacing: -0.5px; }
  .ip-content h2 { font-size: calc(20px * var(--vv-text-scale)); font-weight: 800; margin: 28px 0 10px; }
  .ip-content h3 { font-size: calc(16px * var(--vv-text-scale)); font-weight: 800; margin: 22px 0 8px; }
  .ip-content p  { margin: 0 0 14px; }
  .ip-content ul, .ip-content ol { margin: 0 0 14px; padding-left: 24px; }
  .ip-content li { margin: 4px 0; }
  .ip-content a  { color: var(--accent-l); text-decoration: underline; }
  .ip-content img { max-width: 100%; height: auto; border-radius: 12px; margin: 8px 0; }
  .ip-content blockquote {
    margin: 0 0 14px; padding: 8px 16px; border-left: 3px solid var(--accent-l);
    background: var(--surface2); border-radius: 6px; color: var(--muted);
  }
  .ip-empty { color: var(--muted); font-size: calc(14px * var(--vv-text-scale)); text-align: center; padding: 60px 0; }
  /* edit toolbar */
  .ip-toolbar {
    display: none; align-items: center; gap: 4px; flex-wrap: wrap;
    padding: 10px 28px; background: var(--surface); border-bottom: 1px solid var(--border);
    position: sticky; top: 0; z-index: 2;
  }
  .ip-toolbar.show { display: flex; }
  .ip-tb-btn {
    min-width: 32px; height: 32px; padding: 0 8px; border-radius: 7px;
    border: 1px solid var(--border); background: transparent; color: var(--text);
    font-size: calc(13px * var(--vv-text-scale)); font-weight: 700; cursor: pointer; font-family: inherit;
    display: inline-flex; align-items: center; justify-content: center; transition: all 0.15s;
  }
  .ip-tb-btn:hover { background: var(--surface2); border-color: var(--accent-l); }
  .ip-tb-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
  .ip-edit-btn {
    padding: 7px 14px; border-radius: 9px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text); font-size: calc(13px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit; transition: all 0.15s;
  }
  .ip-edit-btn:hover { border-color: rgba(124,58,237,0.5); }
  .ip-save-btn {
    padding: 7px 14px; border-radius: 9px; border: 1px solid transparent;
    background: linear-gradient(135deg, var(--accent-l), var(--accent2)); color: #fff;
    font-size: calc(13px * var(--vv-text-scale)); font-weight: 800; cursor: pointer; font-family: inherit;
  }
  .ip-cancel-btn {
    padding: 7px 14px; border-radius: 9px; border: 1px solid var(--border);
    background: transparent; color: var(--muted); font-size: calc(13px * var(--vv-text-scale)); font-weight: 700;
    cursor: pointer; font-family: inherit;
  }

  /* ══════════════════════════════════════
     GRID LIBRARY
  ══════════════════════════════════════ */
  #grid-library, #academic-picker {
    position: fixed; inset: 0; z-index: 190;
    display: flex; flex-direction: column; background: var(--bg);
    overflow: hidden;
  }
  .gl-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 20px; height: 56px; background: var(--surface);
    border-bottom: 1px solid var(--border); flex-shrink: 0; gap: 12px;
  }
  .gl-header-left  { display: flex; align-items: center; gap: 12px; min-width: 0; }
  .gl-header-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
  .gl-back-btn {
    width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--border);
    background: transparent; color: var(--muted); font-size: calc(16px * var(--vv-text-scale)); cursor: pointer;
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    transition: all 0.15s; font-family: inherit;
  }
  .gl-back-btn:hover { background: var(--surface2); color: var(--text); }
  .gl-logo { display: flex; align-items: center; gap: 7px; flex-shrink: 0; }
  .gl-logo-icon {
    width: 28px; height: 28px; object-fit: contain; flex-shrink: 0;
  }
  .gl-logo-text {
    font-size: calc(14px * var(--vv-text-scale)); font-weight: 900;
    background: linear-gradient(135deg, var(--accent-l), var(--accent2));
    -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
  }
  /* CI v2: on phones the header brand is the symbol alone. The wordmark text is
     visually hidden but stays in the accessibility tree, so the link still has a name. */
  @media (max-width: 768px) {
    .landing-nav-brand .ln-text, .gl-logo-text, .logo-text {
      position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
      overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
    }
  }
  .gl-header-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
  .gl-title-wrap { display: flex; flex-direction: column; min-width: 0; }
  .gl-title { font-size: calc(14px * var(--vv-text-scale)); font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .gl-subtitle { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); }
  .gl-search-wrap {
    position: relative; display: flex; align-items: center;
  }
  .gl-search-icon { position: absolute; left: 10px; color: var(--muted); font-size: calc(15px * var(--vv-text-scale)); pointer-events: none; }
  .gl-search {
    background: var(--surface2); border: 1px solid var(--border); border-radius: 9px;
    color: var(--text); font-size: calc(13px * var(--vv-text-scale)); padding: 6px 12px 6px 30px;
    outline: none; font-family: inherit; width: 200px; transition: border-color 0.15s;
  }
  .gl-search:focus { border-color: var(--accent-l); }
  .gl-sort {
    background: var(--surface2); border: 1px solid var(--border); border-radius: 9px;
    color: var(--text); font-size: calc(12px * var(--vv-text-scale)); padding: 6px 10px; outline: none;
    font-family: inherit; cursor: pointer; transition: border-color 0.15s;
  }
  /* P0-4 follow-up (2026-08-11): same unconditional outline:none as
     .gl-search used to have, but this one had no :focus replacement at all —
     Tabbing to the sort dropdown showed no indicator whatsoever. */
  .gl-sort:focus { border-color: var(--accent-l); }
  .gl-admin-btn {
    padding: 6px 14px; border-radius: 9px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
    background: var(--accent-dim); color: var(--accent-l);
    border: 1px solid rgba(124,58,237,0.4); cursor: pointer; font-family: inherit;
    transition: all 0.15s;
  }
  .gl-admin-btn:hover { background: rgba(124,58,237,0.25); }
  /* 2026-10-05: phones. The right-hand group (200px search + sort + Login)
     is flex-shrink:0, so on a ~390px screen it ran over the logo and title.
     On narrow screens the header wraps into two rows — logo / title / Login
     on top, search (full width) + sort below — and the brand word is
     dropped to give the title room. Shared by #academic-picker, whose right
     group is just Login and stays on the first row. */
  @media (max-width: 720px) {
    .gl-header { flex-wrap: wrap; height: auto; padding: 10px 14px; row-gap: 8px; column-gap: 8px; }
    .gl-header-left { flex: 1 1 0; gap: 8px; }
    .gl-logo-text, .gl-header-sep { display: none; }
    #grid-library .gl-header-right { flex: 1 1 100%; order: 2; }
    #grid-library .gl-search-wrap { flex: 1 1 auto; min-width: 0; }
    #grid-library .gl-search { width: 100%; min-width: 0; box-sizing: border-box; }
    .gl-admin-btn, .gl-sort { flex-shrink: 0; white-space: nowrap; }
    .gl-filter-bar { padding: 8px 14px; }
  }
  .gl-filter-bar {
    display: flex; align-items: center; gap: 6px; padding: 10px 20px;
    background: var(--surface); border-bottom: 1px solid var(--border);
    flex-shrink: 0; overflow-x: auto; scrollbar-width: none;
  }
  .gl-filter-bar::-webkit-scrollbar { display: none; }
  .gl-filter-chip {
    padding: 4px 13px; border-radius: 20px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
    white-space: nowrap; cursor: pointer; border: 1px solid var(--border);
    background: transparent; color: var(--muted); font-family: inherit;
    transition: all 0.15s;
  }
  .gl-filter-chip:hover { background: var(--surface2); color: var(--text); }
  .gl-filter-chip.active { background: var(--accent-dim); color: var(--accent-l); border-color: rgba(124,58,237,0.4); }

  /* ── Tag filter (2026-08-18) — trigger + active chips live inline in
     .gl-filter-bar; the popover itself is a sibling positioned via JS
     against #grid-library (see glPositionTagPopover in site-config.js). ── */
  .gl-tags-btn {
    display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0;
    padding: 4px 12px; border-radius: 20px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 700;
    white-space: nowrap; cursor: pointer; border: 1px solid var(--border);
    background: transparent; color: var(--muted); font-family: inherit;
    margin-left: 4px; transition: all 0.15s;
  }
  .gl-tags-btn:hover { background: var(--surface2); color: var(--text); }
  .gl-tags-btn-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 16px; height: 16px; padding: 0 4px; border-radius: 20px;
    background: var(--accent-l); color: #fff; font-size: calc(10px * var(--vv-text-scale)); font-weight: 800;
  }
  .gl-active-tags { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; margin-left: 6px; }
  .gl-active-tag {
    display: inline-flex; align-items: center; gap: 5px;
    font-size: calc(12px * var(--vv-text-scale)); font-weight: 600; color: var(--accent-l);
    background: var(--accent-dim); border: 1px solid rgba(124,58,237,0.3);
    border-radius: 20px; padding: 3px 4px 3px 11px; white-space: nowrap;
  }
  .gl-active-tag-remove {
    display: inline-flex; align-items: center; justify-content: center;
    width: 15px; height: 15px; border-radius: 50%; border: none; cursor: pointer;
    background: transparent; color: var(--accent-l); opacity: 0.7; font-size: 12px; line-height: 1;
  }
  .gl-active-tag-remove:hover { opacity: 1; background: rgba(124,58,237,0.2); }

  .gl-tag-popover {
    position: absolute; z-index: 200;
    width: 240px; max-height: 340px; overflow-y: auto;
    background: rgba(20,23,32,0.97); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px; padding: 12px;
    box-shadow: 0 10px 34px rgba(0,0,0,0.5);
  }
  .gl-tag-popover.csp-hide { display: none; }
  .gl-tag-mode-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
  .gl-tag-mode-opt {
    display: flex; align-items: center; gap: 7px; font-size: calc(12px * var(--vv-text-scale));
    color: var(--text); cursor: pointer;
  }
  .gl-tag-mode-opt input[type="radio"]:disabled { cursor: not-allowed; }
  .gl-tag-mode-opt:has(input:disabled) { color: var(--muted); cursor: not-allowed; }
  .gl-tags-search-wrap { margin-bottom: 8px; }
  .gl-tags-search-input {
    width: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
    padding: 6px 8px; color: var(--text); font-size: calc(12px * var(--vv-text-scale)); font-family: inherit;
  }
  .gl-tags-search-input::placeholder { color: var(--muted); }
  .gl-tag-list { display: flex; flex-direction: column; gap: 2px; }
  .gl-tag-row {
    display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-radius: 7px;
    font-size: calc(12px * var(--vv-text-scale)); color: var(--text); cursor: pointer;
  }
  .gl-tag-row:hover { background: var(--surface2); }
  .gl-tag-row-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .gl-tag-row-count { color: var(--muted); font-size: calc(11px * var(--vv-text-scale)); font-weight: 600; }
  .gl-tag-empty { padding: 10px 4px; color: var(--muted); font-size: calc(12px * var(--vv-text-scale)); text-align: center; }
  .gl-tag-sep { height: 1px; background: var(--border); margin: 8px 0; }
  .gl-tag-footer { margin-top: 4px; }
  .gl-tag-clear-btn {
    width: 100%; background: transparent; border: 1px solid var(--border); border-radius: 7px;
    padding: 6px; color: var(--muted); font-size: calc(12px * var(--vv-text-scale)); font-weight: 600;
    font-family: inherit; cursor: pointer;
  }
  .gl-tag-clear-btn:hover { color: var(--text); background: var(--surface2); }
  .gl-grid-wrap {
    flex: 1; overflow-y: auto; padding: 20px;
  }
  .gl-grid-wrap::-webkit-scrollbar { width: 4px; }
  .gl-grid-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
  .gl-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
  }
  /* Model card */
  .gl-card {
    background: var(--surface); border: 1px solid var(--border); border-radius: 16px;
    overflow: hidden; cursor: pointer; transition: all 0.18s;
    display: flex; flex-direction: column;
  }
  .gl-card:hover { border-color: rgba(124,58,237,0.5); transform: translateY(-2px);
    box-shadow: 0 10px 32px rgba(0,0,0,0.35); }
  .gl-card-preview {
    height: 140px; background: linear-gradient(135deg, #141720, #1e2230);
    display: flex; align-items: center; justify-content: center;
    font-size: calc(42px * var(--vv-text-scale)); position: relative; flex-shrink: 0;
  }
  .gl-card-preview-label {
    position: absolute; top: 8px; left: 8px; font-size: calc(12px * var(--vv-text-scale)); font-weight: 800;
    padding: 2px 7px; border-radius: 20px; text-transform: uppercase; letter-spacing: 0.6px;
    background: rgba(0,0,0,0.5); color: var(--muted);
  }
  .gl-card-body { padding: 12px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
  .gl-card-name { font-size: calc(13px * var(--vv-text-scale)); font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .gl-card-file { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .gl-card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
  .gl-card-tag {
    font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; padding: 2px 7px; border-radius: 20px;
    border: 1px solid; white-space: nowrap;
  }
  /* Free-form tag chips — visually subordinate to .gl-card-tag (the filled,
     category-colored region chip): outline only, muted, so region stays the
     primary taxonomy at a glance and tags read as secondary facets. */
  .gl-card-tagchip {
    font-size: calc(11px * var(--vv-text-scale)); font-weight: 600; padding: 2px 7px; border-radius: 20px;
    border: 1px solid var(--border); color: var(--muted); white-space: nowrap; cursor: pointer;
  }
  .gl-card-tagchip:hover { color: var(--accent-l); border-color: rgba(124,58,237,0.4); }
  .gl-card-tagmore {
    font-size: calc(11px * var(--vv-text-scale)); font-weight: 600; padding: 2px 6px;
    color: var(--muted); white-space: nowrap;
  }
  .gl-card-meta {
    display: flex; align-items: center; gap: 6px;
    font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-top: auto; padding-top: 4px;
  }
  .gl-card-annot { color: var(--annot); font-weight: 700; }
  .gl-card-dot { opacity: 0.7; }
  /* Empty state */
  .gl-empty { display: flex; flex-direction: column; align-items: center; justify-content: center;
    height: 60vh; height: 60dvh; gap: 10px; }
  .gl-empty-icon { font-size: calc(48px * var(--vv-text-scale)); }
  .gl-empty-title { font-size: calc(16px * var(--vv-text-scale)); font-weight: 800; }
  .gl-empty-sub { font-size: calc(13px * var(--vv-text-scale)); color: var(--muted); }

  /* ══════════════════════════════════════
     ACADEMIC PICKER
  ══════════════════════════════════════ */
  .ap-body { flex: 1; overflow-y: auto; padding: 24px; }
  .ap-body::-webkit-scrollbar { width: 4px; }
  .ap-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

  /* Breadcrumb */
  .ap-breadcrumb { display: flex; align-items: center; gap: 4px; font-size: calc(12px * var(--vv-text-scale)); }
  .ap-bc-item { color: var(--muted); cursor: pointer; }
  .ap-bc-item:hover { color: var(--text); }
  .ap-bc-item.active { color: var(--text); font-weight: 700; cursor: default; }
  .ap-bc-sep { color: var(--border); }

  /* Level containers */
  /* Academic content sits in a centered 1040px column (church-site style). */
  .ap-level { animation: apFadeIn 0.18s ease; max-width: 1040px; margin: 0 auto; }
  @keyframes apFadeIn { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
  .ap-level-title { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--muted); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 16px; }

  /* School grid */
  .ap-school-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); gap: 12px; }
  .ap-school-card {
    background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
    padding: 18px 20px; cursor: pointer; transition: all 0.18s; display: flex; align-items: center; gap: 14px;
  }
  .ap-school-card:hover { border-color: rgba(124,58,237,0.5); transform: translateY(-2px); box-shadow: 0 8px 28px rgba(0,0,0,0.3); }
  .ap-school-card-icon { font-size: calc(28px * var(--vv-text-scale)); flex-shrink: 0; }
  .ap-school-card-name { font-size: calc(14px * var(--vv-text-scale)); font-weight: 800; }
  .ap-school-card-meta { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-top: 2px; }

  /* Course grid */
  .ap-course-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); gap: 12px; }
  .ap-course-card {
    position: relative;
    background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
    padding: 18px 20px; cursor: pointer; transition: all 0.18s;
  }
  .ap-course-card:hover { border-color: rgba(124,58,237,0.5); transform: translateY(-2px); box-shadow: 0 8px 28px rgba(0,0,0,0.3); }
  .ap-course-share {
    position: absolute; top: 12px; right: 14px; cursor: pointer;
    color: var(--muted); transition: color 0.15s; font-size: calc(13px * var(--vv-text-scale));
  }
  .ap-course-share:hover { color: var(--text); }
  .ap-course-code { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l); text-transform: uppercase; letter-spacing: 0.8px; }
  .ap-course-title { font-size: calc(14px * var(--vv-text-scale)); font-weight: 800; margin-top: 3px; }
  .ap-course-meta { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-top: 4px; }

  /* Week grid */
  .ap-week-grid { display: flex; flex-direction: column; gap: 8px; }
  .ap-week-card {
    background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
    padding: 14px 16px; cursor: pointer; transition: all 0.15s;
    display: flex; align-items: center; gap: 12px;
  }
  .ap-week-card:hover { border-color: rgba(124,58,237,0.4); background: var(--surface2); }
  .ap-week-num { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l);
    background: var(--accent-dim); border-radius: 6px; padding: 3px 9px; flex-shrink: 0; }
  .ap-week-info { flex: 1; min-width: 0; }
  .ap-week-card-title { font-size: calc(13px * var(--vv-text-scale)); font-weight: 700; }
  .ap-week-card-meta { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); margin-top: 2px; }
  .ap-week-arrow { color: var(--muted); font-size: calc(16px * var(--vv-text-scale)); flex-shrink: 0; }
  .ap-week-share { cursor: pointer; color: var(--muted); transition: color 0.15s; font-size: calc(13px * var(--vv-text-scale)); flex-shrink: 0; }
  .ap-week-share:hover { color: var(--text); }
  .ap-weeks-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }

  /* Week detail */
  .ap-detail-wrap { display: grid; grid-template-columns: 1fr 280px; gap: 20px; }
  @media (max-width: 900px) { .ap-detail-wrap { grid-template-columns: 1fr; } }
  .ap-detail-desc {
    background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
    padding: 20px 24px; font-size: calc(13px * var(--vv-text-scale)); line-height: 1.8; color: var(--text);
  }
  .ap-detail-desc h2 { font-size: calc(16px * var(--vv-text-scale)); font-weight: 800; color: var(--accent-l); margin: 12px 0 6px; }
  .ap-detail-desc h3 { font-size: calc(14px * var(--vv-text-scale)); font-weight: 700; margin: 10px 0 4px; }
  .ap-detail-desc p  { margin: 6px 0; }
  .ap-detail-desc ul, .ap-detail-desc ol { padding-left: 20px; margin: 6px 0; }
  .ap-detail-desc img { max-width: 100%; border-radius: 8px; margin: 8px 0; }
  .ap-detail-sidebar { display: flex; flex-direction: column; gap: 12px; }
  .ap-detail-section { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; }
  .ap-detail-section-title { font-size: calc(12px * var(--vv-text-scale)); font-weight: 800; color: var(--muted); text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 10px; }
  .ap-attach-item {
    display: flex; align-items: center; gap: 8px; padding: 6px 0;
    border-bottom: 1px solid var(--border); font-size: calc(12px * var(--vv-text-scale));
  }
  .ap-attach-item:last-child { border-bottom: none; }
  .ap-attach-link { flex: 1; color: var(--accent2); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ap-attach-link:hover { text-decoration: underline; }
  .ap-attach-size { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); flex-shrink: 0; }
  .ap-model-list { display: flex; flex-direction: column; gap: 6px; }
  .ap-model-item {
    display: flex; align-items: center; gap: 8px; padding: 7px 10px;
    background: var(--surface2); border-radius: 8px; cursor: pointer;
    border: 1px solid transparent; transition: all 0.14s; font-size: calc(12px * var(--vv-text-scale)); font-weight: 600;
  }
  .ap-model-item:hover { border-color: rgba(124,58,237,0.4); color: var(--accent-l); }
  .ap-all-btn {
    width: 100%; padding: 9px 12px; border-radius: 9px;
    background: var(--accent-dim); border: 1px solid rgba(124,58,237,0.35);
    color: var(--accent-l); font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; font-family: inherit;
    cursor: pointer; text-align: center; transition: all 0.15s;
  }
  .ap-all-btn:hover { background: rgba(124,58,237,0.25); }
  .ap-empty { grid-column: 1 / -1; text-align: center; padding: 60px 20px; color: var(--muted); }
  .ap-empty-icon { font-size: calc(48px * var(--vv-text-scale)); margin-bottom: 12px; }
  .ap-empty-note { max-width: 460px; margin: 14px auto 0; font-size: calc(13px * var(--vv-text-scale, 1)); line-height: 1.5; color: var(--text); opacity: .85; }
  .ap-empty-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 16px; }

  /* ══ Transform tool toolbar (Phase 1 gizmo) ══
     Unity's Scene-view tool strip: a vertical column pinned to the left edge
     of the canvas. Deliberately LEFT, where .draw-toolbar and .draw-fab are
     both bottom-RIGHT — the two can legitimately be open at once and must not
     stack on top of each other. Same surface/blur/radius language as
     .draw-toolbar so they read as one family.
     z-index 40 matches .draw-toolbar; it sits above the canvas but below the
     library panel and any modal. */
  /* The tool strip appears ONLY while transform mode is on (Sean,
     2026-08-19) — it's a mode, not permanent furniture. #xf-mode-btn below is
     the one always-visible way in. */
  /* Directly BELOW the mode rail, in the same left column (Sean, 2026-08-20).
     The two were briefly side by side — rail at left:14, strip at left:68
     under the Inspector — which read as two unrelated palettes sharing a
     corner. One column says what is true: the strip is Transform's second
     row, and Transform is a rail entry.

     --rail-bottom is the rail's measured bottom edge, written by
     measureRail() in js/mode-rail.js.

     It used to be the literal `top: 233px`, with a comment here instructing
     the next person to add 38 by hand if a sixth mode ever joined the rail.
     Puzzle joined on 2026-08-27 and brought a second .rail-sep with it, so
     the real figure was 49 (34 button + 4 gap + 7 separator + 4 gap). Neither
     number was ever added. Measured in Chromium before this change: the rail
     runs 14→272 and the strip started at 233, putting 39px of the strip
     behind the rail — and 77px during a live session, where the rail steps
     down and a literal cannot follow it.

     Hand-maintaining it could not have worked in any case, because the rail
     has no fixed height. refreshRole() in js/mode-rail.js drops Draw, Hands
     and Puzzle whenever their modules are absent from a deploy, which takes
     the rail to 220 and 144 in configurations this app really ships, and
     #vvLiveShareIndicator moves it 38px down for the length of a live
     session. Neither is visible from this file.

     The fallback is the measured height of the full six-button rail, so the
     strip still lands correctly if the script has not run yet. */
  .xf-toolbar {
    position: absolute; left: 14px; top: calc(var(--rail-bottom, 258px) + 10px);
    z-index: 40; display: none; flex-direction: column; align-items: center; gap: 4px;
    background: rgba(20,23,32,0.92); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 8px 7px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
  }
  .xf-toolbar.show { display: flex; }
  /* Transform is on, but Draw outranks it for left-click (Sean, 2026-08-20),
     so refresh() in js/viewer-gizmo.js has pulled the handles. The strip stays
     — the Inspector's numbers still drive the model, and the tool you pick
     here is the one that comes back the moment Draw goes off — but it must not
     look live while there is nothing on the canvas to grab. */
  .xf-toolbar.xf-suppressed { opacity: .45; }
  .xf-toolbar.xf-suppressed .xf-tool.active {
    background: var(--surface3); border-color: var(--border); color: var(--muted);
  }

  /* ══ Retired header controls (2026-08-20) ══
     Six buttons whose visible job moved to the mode rail, the ViewCube or the
     Share menu, but whose ELEMENTS have to stay: each is the anchor its own
     module binds behaviour to, or uses as a state mirror, or both — see the
     comments on them in VanVRAi.html. The new controls .click() through to
     them.

     A dedicated never-toggled class rather than csp-hide, because csp-hide is
     load-bearing on one of them: viewer-model.js STRIPS csp-hide off
     #share-btn when a model loads, as the "there is something to share now"
     flag that js/share-menu.js reads back. Hiding with the same class the flag
     uses is what put a second Share button in the header. */
  .vv-retired { display: none !important; }

  /* ══ Canvas mode rail (2026-08-20) ══
     One vertical strip of the five viewer MODES, top-left of the canvas. It
     replaced three scattered entry points — the Transform button that used to
     own this exact spot, the Draw FAB bottom-right, and the Hands and Compare
     buttons in the page header.

     Top-left rather than vertically centred: the middle of the left edge is
     free, but the eye goes to the top-left corner first and every other tool
     palette this app borrows from (Figma, Photoshop, Unity) anchors there. It
     also means the rail inherits the live-session step-down below, which the
     Transform button already needed for the same corner.

     The rail is 46px wide (34 button + 5 padding x 2 + 1 border x 2), so the
     panels that sit BESIDE it start at left:68px = 14 + 46 + 8 gutter:
     .xf-inspector and .compare-panel. Grep `left: 68px` before adding a third.
     The transform tool strip is the exception — it is Transform's own second
     row, so it stacks UNDER the rail in the same column rather than beside
     it. See the .xf-toolbar note above. */
  .mode-rail {
    position: absolute; left: 14px; top: 14px; z-index: 41;
    display: flex; flex-direction: column; gap: 4px;
    background: rgba(20,23,32,0.92); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 5px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
  }
  /* Boundary between the two kinds of rail button. Above it: Orbit /
     Transform / Draw, a radio group, because all three want the canvas's left
     mouse button. Below it: Compare / Hands, independent toggles that read no
     pointer input and so can be on at the same time as each other and as
     whatever is holding the pointer. Sean, 2026-08-20: "중복 허락해줘".
     Matches .xf-sep in the transform strip below it. */
  .rail-sep { height: 1px; align-self: stretch; background: var(--border); margin: 3px 4px; }
  .rail-btn {
    width: 34px; height: 34px; padding: 0; border-radius: 9px;
    border: 1px solid transparent; background: transparent; color: var(--muted);
    cursor: pointer; display: flex; align-items: center; justify-content: center;
    transition: background .12s, color .12s, border-color .12s;
    position: relative;
  }
  .rail-btn .icon { width: 17px; height: 17px; }
  .rail-btn:hover { background: var(--surface3); color: var(--text); }
  .rail-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
  /* Project rule: always :focus, never :focus-visible, and white on an
     accent fill or the purple ring disappears into the purple button. */
  .rail-btn:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 2px; }
  .rail-btn.active:focus { outline-color: #fff; }
  /* The two independent toggles get an outline fill rather than the solid
     accent the pointer group uses. Without this, Compare + Hands + Transform
     all lit solid reads as three winners of a radio group; the difference in
     weight is what says "one of these holds the pointer, the others are
     running alongside it". */
  #rail-compare.active, #rail-hands.active {
    background: var(--accent-dim, rgba(124,58,237,0.14));
    border-color: var(--accent); color: var(--accent-l, #a78bfa);
  }
  #rail-compare.active:focus, #rail-hands.active:focus {
    outline-color: var(--accent-l, #a78bfa);
  }
  /* Unavailable (usually: no model loaded yet). Dimmed in place rather than
     removed — a button vanishing out of a fixed strip reads as a bug, and the
     rail is also how you learn what this viewer can do. */
  .rail-btn.rail-off { opacity: .5; cursor: not-allowed; }
  .rail-btn.rail-off:hover { background: transparent; color: var(--muted); }
  /* Transform mode running with its Inspector minimised: the mode is on, but
     you are not seeing all of it. Without this the rail would claim the panel
     is open when it is tucked away. */
  /* Transform running with Draw on top of it: the mode is live and its
     numbers work, but Draw has left-click so the handles are gone. Softened
     rather than fully lit, so the rail is not claiming handles that are not
     there. Pairs with .xf-toolbar.xf-suppressed above. */
  .rail-btn.rail-outranked.active {
    background: var(--accent-dim, rgba(124,58,237,0.14));
    border-color: var(--accent); color: var(--accent-l, #a78bfa);
  }
  .rail-btn.rail-tucked::after {
    content: ''; position: absolute; right: 3px; bottom: 3px;
    width: 5px; height: 5px; border-radius: 50%;
    background: currentColor; opacity: .85;
  }
  /* #vvLiveShareIndicator ("visible to your instructor") also parks top-left,
     but only during a live session. Step below it rather than overlapping.
     A sibling selector, not a JS hook: the indicator is toggled purely by its
     `hidden` attribute and precedes the rail in the markup, so CSS reads the
     state on its own. Inherited from the Transform button this replaced. */
  #vvLiveShareIndicator:not([hidden]) ~ .mode-rail { top: 52px; }

  /* ══ Puzzle panel (L-03, 2026-08-27) ══
     Same corner/slot as .xf-inspector (left:68px — see the "Grep `left: 68px`
     before adding a third" note above) since Puzzle and Transform can never
     both be on (mode-rail.js's mutual-exclusion). Same dark glass look as
     every other floating canvas panel, deliberately not theme-variable-driven
     — these overlay the 3D canvas, not the page chrome, and stay dark in both
     themes so the model itself stays the visual focus. */
  .puzzle-panel {
    position: absolute; left: 68px; top: 14px; z-index: 42;
    width: 240px; max-height: calc(100% - 28px);
    background: rgba(20,23,32,0.94); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 10px 14px 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
    display: flex; flex-direction: column; gap: 8px;
    color: #e2e6f0; font-size: calc(12.5px * var(--vv-text-scale));
  }
  .puzzle-panel.csp-hide { display: none; }
  /* Once dragged, the saved left/top are written as inline styles; clear the
     default anchor so nothing here fights them (same contract .xf-inspector
     and .scene-panel have with js/panel-drag.js). */
  .puzzle-panel.puzzle-positioned { right: auto; bottom: auto; }
  #vvLiveShareIndicator:not([hidden]) ~ .puzzle-panel:not(.puzzle-positioned) { top: 52px; }
  /* The piece list runs to 34 rows, so the BODY scrolls, never the whole
     panel — otherwise the drag header would scroll out of reach. */
  .puzzle-panel-body {
    display: flex; flex-direction: column; gap: 10px;
    overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; min-height: 0;
  }
  /* Collapsed: header only. max-height must go too, or the panel keeps
     reserving the full height it had while open. */
  .puzzle-panel.puzzle-collapsed { max-height: none; }
  .puzzle-panel.puzzle-collapsed .puzzle-panel-body { display: none; }
  .puzzle-panel.puzzle-collapsed .xf-ins-head { margin-bottom: -2px; }
  /* Rotate the minimise bar into a "+" when collapsed, so the one button
     reads as both collapse and expand. */
  .puzzle-panel.puzzle-collapsed #puzzle-panel-min::before {
    box-shadow: 0 0 0 0 currentColor; width: 10px;
  }
  .puzzle-panel.puzzle-collapsed #puzzle-panel-min::after {
    content: ''; position: absolute; width: 1.5px; height: 10px;
    border-radius: 1px; background: currentColor;
  }
  #puzzle-panel-min { position: relative; }
  .puzzle-score { font-variant-numeric: tabular-nums; color: #9aa3bd; font-size: 12px; }
  .puzzle-sub { margin: 0; color: #9aa3bd; font-size: 11.5px; line-height: 1.5; }
  /* Live-session status line + Focus read-only chrome (2026-08-28).
     #puzzle-live-note is written by VVPuzzleSync (js/puzzle-mode.js) and is
     hidden for anyone not in a live session. */
  .puzzle-live-note {
    display: flex; align-items: center; gap: 6px;
    padding: 6px 9px; border-radius: 7px; font-size: 11.5px; font-weight: 700;
    color: #a78bfa; background: rgba(124, 58, 237, 0.14);
    border: 1px solid rgba(124, 58, 237, 0.4);
  }
  .puzzle-live-note.csp-hide { display: none; }
  /* Focus mode: the board belongs to the instructor, so every control that
     would change it is inert. Dragging pieces in the 3D scene is blocked
     separately and authoritatively by active() in js/puzzle-mode.js — this is
     only the panel half, so a student is not left clicking dead-looking
     buttons that silently do nothing. The collapse/drag header is deliberately
     NOT included: parking and folding the panel is still theirs. */
  .puzzle-panel.puzzle-mirrored .puzzle-panel-body button,
  .puzzle-panel.puzzle-mirrored .puzzle-panel-body input {
    pointer-events: none; opacity: 0.45;
  }
  .puzzle-piece-list { display: flex; flex-direction: column; gap: 3px; }
  .puzzle-piece-row {
    display: flex; align-items: center; gap: 8px; font-size: 12px;
    padding: 6px 8px; border-radius: 7px; background: rgba(255,255,255,0.03);
    border: 1px solid transparent; color: #9aa3bd; cursor: default;
    transition: background .15s, color .15s, border-color .15s;
  }
  .puzzle-piece-row .puzzle-dot {
    width: 7px; height: 7px; border-radius: 50%; background: #3a4055; flex: none;
  }
  .puzzle-piece-row .puzzle-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Eye / pencil: bare icon buttons that must not look like the row itself is
     a button, and must keep their own click away from the row's
     click / double-click gestures (handled with stopPropagation in JS). */
  .puzzle-eye, .puzzle-pen {
    flex: none; width: 18px; height: 18px; padding: 0; border: 0; border-radius: 4px;
    background: transparent; color: #6f778f; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
  }
  .puzzle-eye .icon, .puzzle-pen .icon { width: 13px; height: 13px; }
  .puzzle-eye:hover, .puzzle-pen:hover { background: rgba(255,255,255,0.10); color: #e2e6f0; }
  .puzzle-eye:focus-visible, .puzzle-pen:focus-visible {
    outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 1px;
  }
  /* Hidden piece: the row stays readable but clearly stood down, and the eye
     dims with it — there is no eye-slash glyph in the sprite, and dimming the
     whole row says "not on screen" more plainly than a second icon would. */
  .puzzle-piece-row.hidden-piece { opacity: 0.42; }
  .puzzle-piece-row.hidden-piece .puzzle-label { text-decoration: line-through; }
  /* Inline rename input, sized to sit exactly where the label was. */
  .puzzle-rename {
    flex: 1; min-width: 0; width: 100%; font: inherit; font-size: 12px;
    padding: 2px 6px; border-radius: 5px; color: #e2e6f0;
    background: rgba(0,0,0,0.45); border: 1px solid var(--accent, #7c3aed);
  }
  .puzzle-rename:focus { outline: none; }
  /* Owner tools live on their own line under the piece name — at 240px a name
     plus rename/category/colour on one row leaves nothing but an ellipsis. */
  .puzzle-sel-item { display: flex; flex-direction: column; }
  .puzzle-sel-tools {
    display: flex; align-items: center; gap: 5px;
    padding: 0 0 4px 28px;   /* aligns under the label, clear of the checkbox */
  }
  .puzzle-cat-select {
    flex: 1; min-width: 0; font: inherit; font-size: 10.5px;
    padding: 1px 2px; border-radius: 5px; color: #9aa3bd;
    background: rgba(255,255,255,0.05); border: 1px solid var(--border); cursor: pointer;
  }
  .puzzle-cat-select:hover, .puzzle-cat-select:focus { color: #e2e6f0; }
  /* Swatch. Browsers give type=color a chunky default chrome; strip it back to
     a plain square so it reads as a swatch beside the other small controls. */
  .puzzle-color {
    flex: none; width: 20px; height: 18px; padding: 0; cursor: pointer;
    border: 1px solid var(--border); border-radius: 5px;
    background: transparent; -webkit-appearance: none; appearance: none;
  }
  .puzzle-color::-webkit-color-swatch-wrapper { padding: 2px; }
  .puzzle-color::-webkit-color-swatch { border: 0; border-radius: 3px; }
  .puzzle-color::-moz-color-swatch { border: 0; border-radius: 3px; }
  .puzzle-color-reset {
    flex: none; width: 18px; height: 18px; padding: 0; line-height: 1;
    border: 0; border-radius: 4px; background: transparent; color: #6f778f;
    font-size: 11px; cursor: pointer;
  }
  .puzzle-color-reset:hover { background: rgba(255,255,255,0.10); color: #e2e6f0; }
  /* Placed pieces are clickable — click to send back to the tray (mirrors the
     standalone prototype's "click a placed piece's name to pick it up again"). */
  .puzzle-piece-row.placed {
    color: #e2e6f0; border-color: rgba(74,222,128,0.35); background: rgba(74,222,128,0.08);
    cursor: pointer;
  }
  .puzzle-piece-row.placed:hover { background: rgba(74,222,128,0.16); }
  .puzzle-piece-row.placed .puzzle-dot { background: var(--success, #4ade80); }
  .puzzle-piece-row.dragging { color: var(--accent-l, #a78bfa); }
  .puzzle-piece-row.dragging .puzzle-dot { background: var(--accent, #7c3aed); }
  .puzzle-fixed-op {
    display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: #9aa3bd;
  }
  .puzzle-fixed-op.csp-hide { display: none; }
  .puzzle-fixed-op span { flex: none; }
  .puzzle-fixed-op input[type=range] { flex: 1; min-width: 0; }
  .puzzle-fixed-op output { flex: none; width: 3.2em; text-align: right; font-variant-numeric: tabular-nums; }
  /* A display preference, not a board change: stays usable in Focus mode. */
  .puzzle-panel.puzzle-mirrored .puzzle-panel-body .puzzle-fixed-op input {
    pointer-events: auto; opacity: 1;
  }
  /* Four buttons (Targets, Lock placed, Shuffle, Pieces…) do not fit one row in
     the 240px panel: the row overflowed and the body scrolled sideways, which
     shifted every row out of line. Wrap into an even 2x2 grid instead. */
  .puzzle-actions { display: flex; flex-wrap: wrap; gap: 8px; }
  .puzzle-actions .btn {
    flex: 1 1 calc(50% - 4px); min-width: 0; font-size: 12px; padding: 7px 8px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* "Targets" is a toggle and defaults ON — it needs a visibly pressed state,
     unlike the one-shot buttons beside it. Tinted with the same amber the
     ghost wireframes themselves are drawn in (HINT_COLOR in
     js/puzzle-mode.js), so the button and what it controls read as one thing. */
  .puzzle-actions .btn.active {
    background: rgba(255,204,51,0.16); border-color: rgba(255,204,51,0.5); color: #ffcc33;
  }
  /* .xf-ins-title already takes the slack (flex:1), so these just sit between
     the title and the minimise button. */
  .puzzle-time { font-variant-numeric: tabular-nums; color: #6f778f; font-size: 11.5px; flex: none; }
  .puzzle-score { flex: none; }

  /* Both screens live inside .puzzle-panel's flex column, so each needs its
     own column context — otherwise their children collapse into the panel's
     single gap and the two screens can't space themselves differently. */
  #puzzle-select-screen, #puzzle-play-screen { display: flex; flex-direction: column; gap: 10px; }
  #puzzle-select-screen.csp-hide, #puzzle-play-screen.csp-hide { display: none; }

  .puzzle-presets { display: flex; flex-wrap: wrap; gap: 6px; }
  .puzzle-presets .btn { flex: 1 1 calc(50% - 3px); font-size: 11.5px; padding: 6px 4px; }
  /* No max-height/overflow of its own: .puzzle-panel-body is the one scroll
     container, and nesting a second one here made the mouse wheel land on
     whichever happened to be under the cursor. */
  .puzzle-cat-list { display: flex; flex-direction: column; gap: 8px; }
  .puzzle-cat { display: flex; flex-direction: column; gap: 2px; }
  .puzzle-cat-head {
    display: flex; align-items: center; gap: 7px; cursor: pointer;
    font-size: 11px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase;
    color: #6f778f; padding: 2px 0;
  }
  .puzzle-sel-row {
    display: flex; align-items: center; gap: 8px; cursor: pointer;
    font-size: 12px; color: #9aa3bd; padding: 3px 0 3px 14px; border-radius: 6px;
  }
  .puzzle-sel-row:hover { color: #e2e6f0; background: rgba(255,255,255,0.04); }
  .puzzle-sel-row .puzzle-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .puzzle-cat-head input, .puzzle-sel-row input { accent-color: var(--accent, #7c3aed); flex: none; margin: 0; }
  .puzzle-selcount { font-size: 11.5px; color: #6f778f; text-align: center; }
  #puzzle-start-btn:disabled { opacity: 0.45; cursor: not-allowed; }

  .puzzle-win {
    padding: 8px 10px; border-radius: 8px; text-align: center; font-weight: 700; font-size: 12.5px;
    background: rgba(74,222,128,0.14); border: 1px solid rgba(74,222,128,0.4); color: #4ade80;
  }
  .puzzle-win.csp-hide { display: none; }

  /* Cursor-following piece name. position:fixed against the viewport (the
     coordinates written to it are clientX/clientY) and pointer-events:none so
     it can never intercept the drag it is describing. */
  .puzzle-tooltip {
    position: fixed; z-index: 60; pointer-events: none;
    padding: 4px 9px; border-radius: 7px; max-width: 260px;
    background: rgba(20,23,32,0.96); border: 1px solid var(--border);
    color: #e2e6f0; font-size: calc(12px * var(--vv-text-scale)); white-space: nowrap;
    box-shadow: 0 4px 16px rgba(0,0,0,0.5);
  }
  .puzzle-tooltip.csp-hide { display: none; }

  .xf-tool, .xf-mini {
    width: 36px; height: 36px; border-radius: 9px; border: 1px solid var(--border);
    background: var(--surface2); color: var(--text);
    font: 600 10px inherit; cursor: pointer;
    display: flex; align-items: center; justify-content: center; transition: all .12s;
  }
  .xf-tool:hover, .xf-mini:hover { background: var(--surface3); }
  .xf-tool.active { background: var(--accent); border-color: var(--accent); color: #fff; }
  /* Project rule: always :focus, never :focus-visible, and an accent-filled
     control needs a WHITE ring or the purple-on-purple outline vanishes. */
  .xf-tool:focus, .xf-mini:focus {
    outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 2px;
  }
  .xf-tool.active:focus { outline-color: #fff; }
  /* #xf-space-btn carries a text label ("Global"/"Local") rather than an icon,
     so it needs a wider box and smaller type than its icon-only sibling. */
  #xf-space-btn { width: 36px; font-size: calc(9px * var(--vv-text-scale)); letter-spacing: -0.2px; }
  .xf-sep { height: 1px; align-self: stretch; background: var(--border); margin: 3px 2px; }
  /* On a phone the vertical strip would cover a third of the model. Lay it out
     horizontally along the bottom instead, clear of #fullscreen-btn. */
  @media (max-width: 768px) {
    .xf-toolbar {
      left: 50%; top: auto; bottom: 70px; transform: translateX(-50%);
      flex-direction: row;
    }
    .xf-sep { height: auto; width: 1px; align-self: stretch; margin: 2px 3px; }
    /* A six-button column costs a third of the model's height on a phone. Lay
       the rail along the top instead — and with it out of the left edge, the
       panels that were dodging it go back to left:14.
       The strip's `top: auto` above takes it out of the --rail-bottom column
       entirely here, which is correct: horizontal rail, horizontal strip, and
       they no longer stack. */
    .mode-rail {
      left: 50%; top: 10px; transform: translateX(-50%);
      flex-direction: row;
    }
    #vvLiveShareIndicator:not([hidden]) ~ .mode-rail { top: 48px; }
  }

  /* ══ Transform Inspector (Phase 2) ══
     Unity's Inspector Transform component. Parked at the canvas's TOP-LEFT,
     in the exact spot #xf-mode-btn occupies — the button is the panel's
     collapsed form, so the two share one corner and the panel expands in
     place over it (Sean, 2026-08-19). viewer-gizmo.js hides the button while
     the panel is up; the panel's own minimise bar brings it back.
     It sits INSIDE #canvas-wrap, which ends where aside.sidebar begins, so it
     can't collide with the sidebar at any width. */
  .xf-inspector {
    position: absolute; left: 68px; top: 14px; z-index: 42;
    background: rgba(20,23,32,0.94); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 8px 12px 10px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
    display: flex; flex-direction: column; gap: 6px;
    font-size: calc(11px * var(--vv-text-scale));
  }
  .xf-inspector.csp-hide { display: none; }
  /* Once dragged, js/panel-drag.js sets an explicit left/top via CSSOM.
     `right`/`bottom` are cleared defensively: the default anchor is left/top
     now, but the responsive rule below sets `right` and a stale `right`
     fighting an applied `left` is what stretches the panel. Same trap
     .draw-toolbar.dt-positioned documents. */
  .xf-inspector.xf-positioned { right: auto; bottom: auto; }
  /* #vvLiveShareIndicator ("visible to your instructor") shares this corner
     during a live session — step below it, exactly as #xf-mode-btn does.
     Skipped once the user has dragged the panel somewhere deliberate. */
  #vvLiveShareIndicator:not([hidden]) ~ .xf-inspector:not(.xf-positioned) { top: 52px; }
  #vvLiveShareIndicator:not([hidden]) ~ .compare-panel:not(.compare-positioned) { top: auto; }

  /* Drag header. The whole strip is the grip — a 16px dot cluster on its own
     is unusable on a touchscreen. */
  .xf-ins-head {
    display: flex; align-items: center; gap: 6px;
    cursor: grab; touch-action: none; user-select: none;
    margin: -2px -6px 2px; padding: 3px 6px; border-radius: 7px;
  }
  /* Pushes the minimise button to the far right of the header. */
  .xf-ins-head .xf-ins-title { flex: 1; }
  .xf-ins-min {
    width: 20px; height: 20px; padding: 0; flex-shrink: 0;
    border: 1px solid var(--border); border-radius: 5px;
    background: var(--surface2); color: var(--muted);
    cursor: pointer; display: flex; align-items: center; justify-content: center;
  }
  .xf-ins-min:hover { background: var(--surface3); color: var(--text); }
  .xf-ins-min:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 1px; }
  /* A 10x1.5px bar — there's no "minus"/"minimise" glyph in the icon sprite
     and adding one for a single flat line would be more markup than CSS. */
  .xf-ins-min::before {
    content: ''; width: 10px; height: 1.5px; border-radius: 1px; background: currentColor;
  }
  /* Same 20px chip as minimise, sitting beside it (Sean, 2026-08-20: minimise
     now only hides the numbers, so a distinct close is the only way left to
     drop the toolbar and handles too). An X drawn from two crossed bars,
     mirroring how .xf-ins-min draws its single bar. */
  .xf-ins-close {
    width: 20px; height: 20px; padding: 0; flex-shrink: 0; position: relative;
    border: 1px solid var(--border); border-radius: 5px;
    background: var(--surface2); color: var(--muted);
    cursor: pointer; display: flex; align-items: center; justify-content: center;
  }
  .xf-ins-close:hover { background: var(--surface3); color: var(--text); }
  .xf-ins-close:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 1px; }
  .xf-ins-close::before, .xf-ins-close::after {
    content: ''; position: absolute; width: 11px; height: 1.5px; border-radius: 1px; background: currentColor;
  }
  .xf-ins-close::before { transform: rotate(45deg); }
  .xf-ins-close::after { transform: rotate(-45deg); }
  .xf-ins-head:hover { background: var(--surface2); }
  .xf-ins-head.vv-dragging { cursor: grabbing; background: var(--surface3); }
  .xf-ins-head:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 1px; }
  /* Two rows of three dots, drawn entirely with box-shadow off one 3px dot —
     the same trick .dt-drag-handle uses, rotated to read as a horizontal grip. */
  .xf-ins-grip {
    width: 3px; height: 3px; border-radius: 50%; background: var(--muted);
    /* The element is 3px wide but PAINTS ~13px via the box-shadow dots below,
       and a shadow contributes nothing to layout — hence the outsized
       margin-right, without which the dots collide with the title text. */
    flex-shrink: 0; margin: 0 16px 3px 1px;
    box-shadow: 5px 0 0 var(--muted), 10px 0 0 var(--muted),
                0 5px 0 var(--muted), 5px 5px 0 var(--muted), 10px 5px 0 var(--muted);
  }
  .xf-ins-head:hover .xf-ins-grip, .xf-ins-head.vv-dragging .xf-ins-grip {
    background: var(--text);
    box-shadow: 5px 0 0 var(--text), 10px 0 0 var(--text),
                0 5px 0 var(--text), 5px 5px 0 var(--text), 10px 5px 0 var(--text);
  }
  .xf-ins-title { font-weight: 700; color: var(--text); letter-spacing: .2px; }
  .xf-ins-empty { color: var(--muted); font-style: italic; }
  .xf-ins-empty.csp-hide { display: none; }
  /* One grid so the X/Y/Z columns line up across all three rows the way
     Unity's do — a per-row flexbox would let each row size independently and
     the labels would stagger. */
  .xf-ins-row {
    display: grid; align-items: center; gap: 4px;
    grid-template-columns: 58px 14px 62px 14px 62px 14px 62px 24px;
  }
  .xf-ins-name { color: var(--muted); font-weight: 600; display: flex; align-items: center; gap: 4px; }
  .xf-ins-lab {
    text-align: center; font-weight: 700; cursor: ew-resize; user-select: none;
    touch-action: none;   /* or a scrub drag scrolls the page on a touch device */
    border-radius: 4px;
  }
  .xf-ins-lab.x { color: var(--axis-x); }
  .xf-ins-lab.y { color: var(--axis-y); }
  .xf-ins-lab.z { color: var(--axis-z); }
  .xf-ins-lab.scrubbing { background: var(--accent-dim); }
  .xf-ins-num {
    width: 100%; box-sizing: border-box;
    background: var(--surface2); color: var(--text);
    border: 1px solid var(--border); border-radius: 6px;
    padding: 3px 5px; font: inherit; font-variant-numeric: tabular-nums;
  }
  .xf-ins-num:disabled { opacity: .45; }
  /* Spinners steal width the numbers need and are useless next to a scrub
     label that does the same job better. */
  .xf-ins-num::-webkit-outer-spin-button,
  .xf-ins-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
  .xf-ins-num { -moz-appearance: textfield; appearance: textfield; }
  .xf-ins-rst {
    width: 22px; height: 22px; padding: 0; border-radius: 6px;
    border: 1px solid var(--border); background: var(--surface2); color: var(--muted);
    cursor: pointer; display: flex; align-items: center; justify-content: center;
  }
  .xf-ins-rst:hover { background: var(--surface3); color: var(--text); }
  /* Not a button any more. Scale is permanently uniform (anatomy models must
     not distort — Sean, 2026-08-19), so this is a status glyph saying "the
     three axes are tied together", with no way to untie them. */
  .xf-ins-lock {
    width: 16px; height: 16px; border-radius: 5px; flex-shrink: 0;
    background: var(--accent); color: #fff;
    display: flex; align-items: center; justify-content: center; cursor: help;
  }
  .xf-ins-rst .icon { width: 13px; height: 13px; }
  .xf-ins-lock .icon { width: 11px; height: 11px; }
  .xf-ins-note { color: var(--muted); font-size: calc(10px * var(--vv-text-scale)); margin-top: 1px; }
  /* "Save as initial view" row (2026-09-18) */
  .xf-ins-save-row {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    border-top: 1px solid var(--border); padding-top: 7px; margin-top: 2px;
  }
  .xf-ins-save-row.csp-hide, #xf-goto-initial.csp-hide { display: none; }
  .xf-ins-saved-note { color: var(--muted); font-size: calc(10px * var(--vv-text-scale)); margin-left: auto; }
  .xf-ins-saved-note.is-custom { color: var(--accent-l); }
  /* Project rule: always :focus, never :focus-visible; white ring on accent fills. */
  .xf-ins-num:focus, .xf-ins-rst:focus, .xf-ins-lab:focus {
    outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 1px;
  }
  /* Below ~820px the fixed number columns start crowding the model. Let them
     share the remaining width instead. A dragged panel keeps its own left/top
     (`.xf-positioned` wins on specificity order below). */
  @media (max-width: 900px) {
    .xf-inspector { left: 14px; right: 14px; top: 14px; }
    .xf-inspector.xf-positioned { right: auto; }
    .xf-ins-row { grid-template-columns: 58px 14px 1fr 14px 1fr 14px 1fr 24px; }
  }
  @media (max-width: 560px) {
    .xf-inspector { display: none; }   /* tool handles alone on a phone */
  }

  /* ══ Scene settings panel (2026-08-19 sidebar IA rework) ══
     Holds the Display / Lighting / Background sections that used to live in
     the sidebar. Top-right of the canvas — the corner the Inspector left when
     it moved to the top-left, so the two never fight.
     Reuses .xf-ins-head / .xf-ins-grip / .xf-ins-title / .xf-ins-min for its
     header so the two floating panels read as one family and there's only one
     drag-header style to maintain. */
  /* Bottom-right, directly above #fullscreen-btn (Sean, 2026-08-19). It first
     shipped at top-right and landed straight on top of #orient-gizmo — the
     104x104 Blender-style axis widget parked at top:16/right:16 — which is the
     overlap in Sean's screenshot.
     Matches #fullscreen-btn's 40x40 / radius-10 / translucent styling rather
     than the floating-panel look, because the two now read as one stack:
     fullscreen at bottom 16, this at 64, the draw FAB above at 112. */
  .scene-btn {
    position: absolute; right: 16px; bottom: 64px; z-index: 41;
    width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
    background: rgba(16,20,33,0.55); backdrop-filter: blur(4px);
    border: 1px solid var(--border); border-radius: 10px; color: var(--text);
    cursor: pointer; transition: background .15s, transform .1s;
  }
  .scene-btn:active { transform: scale(0.94); }
  .scene-btn:hover { background: rgba(16,20,33,0.85); }
  .scene-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
  .scene-btn:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 2px; }
  .scene-btn.active:focus { outline-color: #fff; }
  .scene-btn.csp-hide { display: none; }

  /* Opens upward from its button in the bottom-right stack, so the top-right
     corner stays clear for #orient-gizmo. */
  .scene-panel {
    position: absolute; right: 16px; bottom: 112px; z-index: 42;
    width: 268px; max-height: calc(100% - 128px);
    display: flex; flex-direction: column;
    background: rgba(20,23,32,0.94); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 8px 12px 10px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
    font-size: calc(11px * var(--vv-text-scale));
  }
  .scene-panel.csp-hide { display: none; }
  .scene-panel.scene-positioned { right: auto; bottom: auto; }
  /* The three sections together are taller than a short viewport; scroll the
     body, not the whole panel, so the drag header stays reachable. */
  .scene-panel-body { overflow-y: auto; overscroll-behavior: contain; }
  /* The moved markup still carries .sidebar-section / .divider, which were
     written for a wide fixed-width column. Neutralise their outer spacing here
     rather than editing the markup, so the sections stay byte-identical to
     what the sidebar had and any future sidebar styling change can't silently
     desync them. */
  .scene-panel .sidebar-section { padding: 6px 0; }
  .scene-panel .sidebar-section h3 { margin: 0 0 6px; }
  .scene-panel .divider { margin: 2px 0; }
  @media (max-width: 768px) {
    .scene-panel { left: 14px; right: 14px; width: auto; }
    .scene-panel.scene-positioned { right: auto; }
    /* On a phone the transform tool strip lays out horizontally along the
       bottom (bottom:70, centred) and on a narrow screen its right end reaches
       into this button's column. Sit BESIDE #fullscreen-btn instead of above
       it, leaving the whole vertical band free. */
    .scene-btn { bottom: 16px; right: 64px; }
  }
  /* On a short viewport the panel would run off the top; let it start below
     the transform column instead and scroll its body. */
  @media (max-height: 620px) {
    .scene-panel { top: 14px; bottom: 112px; max-height: none; }
  }

  /* ══ Compare panel (2026-08-19 sidebar IA rework) ══
     Bottom-left: clear of #controls-hint at bottom:14 and of the transform
     column up top. Shown/hidden by enableCompareMode() in js/viewer-model.js.
     Shares the .xf-ins-head header styling with the other floating panels. */
  .compare-panel {
    position: absolute; left: 68px; bottom: 56px; z-index: 42;
    width: 244px; display: flex; flex-direction: column;
    background: rgba(20,23,32,0.94); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 8px 12px 10px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
    font-size: calc(11px * var(--vv-text-scale));
  }
  .compare-panel.csp-hide { display: none; }
  .compare-panel.compare-positioned { right: auto; bottom: auto; }
  /* #compare-controls kept its sidebar markup verbatim; it has no styling of
     its own beyond csp-hide, so nothing to neutralise here — only make sure a
     long model name in the slot buttons can't push the panel wider. */
  .compare-panel #compare-controls { min-width: 0; }
  .compare-panel #compare-controls > * { max-width: 100%; }
  /* The rail goes horizontal along the top on a phone, so the left edge is
     free again and this can drop back from the 68px gutter it keeps on
     desktop. MUST stay after the base rule above — a media query adds no
     specificity, so an earlier copy of this would simply lose. */
  @media (max-width: 768px) {
    .compare-panel:not(.compare-positioned) { left: 14px; }
  }

  /* ══ Drawing toolbar ══ */
  /* DW-105: default parked position is bottom-right instead of centered-bottom
     (still fully drag-repositionable via js/draw-toolbar-drag.js). Raised
     above `bottom: 18px` to clear #fullscreen-btn (bottom:16), and raised
     again 70 -> 112 on 2026-08-19 when .scene-btn took the slot between them.
     The bottom-right column is now, upward: fullscreen 16, scene gear 64,
     this 112. */
  .draw-toolbar {
    position: absolute; right: 18px; bottom: 112px; left: auto; transform: none;
    display: none; align-items: center; gap: 8px; z-index: 40;
    background: rgba(20,23,32,0.92); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 8px 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
    max-width: calc(100% - 24px); flex-wrap: wrap; justify-content: center;
  }
  .draw-toolbar.show { display: flex; }
  /* Once the user drags the toolbar, it switches from its bottom-right-anchored
     default to an explicit top/left position (set via CSSOM in
     js/draw-toolbar-drag.js — CSP style-src blocks inline style="..."
     attributes but not el.style.x assignments, same pattern used throughout
     this app). `right` must be cleared here too, or the default `right: 18px`
     from the base rule above would fight the JS-applied `left`. */
  .draw-toolbar.dt-positioned { left: 0; right: auto; bottom: auto; transform: none; }

  /* DW-105: floating "Start Drawing" entry point — see #draw-fab-btn in
     VanVRAi.html. Sits in the same bottom-right spot as .draw-toolbar since
     the two are mutually exclusive (the FAB hides once drawing mode/the
     toolbar is on), and clears the fullscreen + scene-gear stack the same way. */
  .draw-fab {
    position: absolute; right: 18px; bottom: 112px; z-index: 39;
    display: flex; align-items: center; gap: 7px;
    background: var(--accent); color: #fff; border: 1px solid var(--accent);
    border-radius: 999px; padding: 10px 18px; font: 600 13px inherit;
    cursor: pointer; box-shadow: 0 8px 24px rgba(0,0,0,0.35);
    transition: background .15s, transform .1s;
  }
  .draw-fab:hover { background: var(--accent-l, var(--accent)); }
  .draw-fab:active { transform: scale(0.96); }
  .draw-fab.csp-hide { display: none; }
  .dt-drag-handle {
    width: 16px; height: 34px; flex-shrink: 0; cursor: grab; touch-action: none;
    border-radius: 6px; display: flex; align-items: center; justify-content: center;
  }
  .dt-drag-handle:active { cursor: grabbing; }
  .dt-drag-handle::before {
    content: ''; width: 4px; height: 4px; border-radius: 50%; background: var(--muted);
    box-shadow: 0 -7px 0 var(--muted), 0 7px 0 var(--muted),
                6px -7px 0 var(--muted), 6px 0 0 var(--muted), 6px 7px 0 var(--muted);
  }
  .dt-drag-handle:hover::before, .dt-drag-handle:active::before {
    background: var(--text);
    box-shadow: 0 -7px 0 var(--text), 0 7px 0 var(--text),
                6px -7px 0 var(--text), 6px 0 0 var(--text), 6px 7px 0 var(--text);
  }
  .dt-sep { width: 1px; align-self: stretch; background: var(--border); margin: 2px 2px; }
  .dt-tools { display: flex; gap: 4px; }
  .dt-tool, .dt-act {
    width: 34px; height: 34px; border-radius: 9px; border: 1px solid var(--border);
    background: var(--surface2); color: var(--text); font-size: calc(16px * var(--vv-text-scale)); cursor: pointer;
    display: flex; align-items: center; justify-content: center; transition: all .12s;
  }
  .dt-tool:hover, .dt-act:hover { background: var(--surface3); }
  .dt-tool.active { background: var(--accent); border-color: var(--accent); color: #fff; }
  .dt-close { color: var(--error); }

  /* ══ Canvas-options popover (gearshape in the draw toolbar) ══
     2026-08-16 — this component shipped with NO CSS at all. `.dt-canvas-opts-wrap`,
     `.dt-canvas-popover` and `.dt-canvas-opt` had zero rules anywhere in the stylesheet, so
     the browser's UA defaults rendered them: Arial 13px, #efefef background, black `outset`
     borders, square corners — nothing like the rest of the dark toolbar. Worse, the popover
     computed to `position: static`, so opening it pushed the whole toolbar out of shape
     instead of floating above the trigger. Verified live via getComputedStyle before writing
     this. Everything below matches the already-correct .dt-color-popover / .dt-tool patterns
     so the two popovers in this toolbar now look like one component family. */
  .dt-canvas-opts-wrap { position: relative; display: flex; }
  .dt-canvas-popover {
    position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
    z-index: 45; display: flex; flex-direction: column; gap: 6px;
    background: rgba(20,23,32,0.97); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px; padding: 10px;
    box-shadow: 0 10px 34px rgba(0,0,0,0.5);
    min-width: 200px; max-width: min(260px, calc(100vw - 32px));
  }
  .dt-canvas-popover.csp-hide { display: none; }
  /* Flipped below the trigger when there isn't room above — set by dwFlipPopover() in
     js/drawing-system.js, since only JS can measure the available space. */
  .dt-canvas-popover.dt-below, .dt-color-popover.dt-below {
    bottom: auto; top: calc(100% + 10px);
  }
  .dt-canvas-opt {
    display: flex; align-items: center; gap: 9px; width: 100%;
    min-height: 34px; padding: 0 11px; text-align: left; white-space: nowrap;
    border-radius: 9px; border: 1px solid var(--border);
    background: var(--surface2); color: var(--text);
    font: 600 calc(12px * var(--vv-text-scale)) inherit;
    cursor: pointer; transition: all .12s;
  }
  /* Explicit, so the two conditional rows (#dt-whiteboard-face / #dt-whiteboard-reset) stay
     hidden regardless of stylesheet order — `.csp-hide` lives in css/csp-inline.css and would
     otherwise be tied on specificity with the `display: flex` above. */
  .dt-canvas-opt.csp-hide { display: none; }
  .dt-canvas-opt:hover { background: var(--surface3); }
  /* Same treatment as .dt-tool.active above — "on" reads identically everywhere in the
     toolbar, whether it's a tool icon or one of these labeled rows. */
  .dt-canvas-opt.active { background: var(--accent); border-color: var(--accent); color: #fff; }
  .dt-canvas-opt .icon { width: 16px; height: 16px; flex-shrink: 0; }
  .dt-canvas-opt span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

  /* The trigger keeps a dot badge while free-space or whiteboard is on, so "something in here
     is enabled" is visible without opening the menu. dwUpdateCanvasOptsIndicator() in
     js/drawing-system.js has toggled `.has-active` since 2026-08-08 and its comment points
     here — but the rule itself was never written, so the badge never appeared. */
  #dt-canvas-opts-btn { position: relative; }
  #dt-canvas-opts-btn.has-active::after {
    content: ''; position: absolute; top: 3px; right: 3px;
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--accent-l, #a78bfa); border: 1.5px solid var(--surface2);
  }

  /* ══ Keyboard focus inside the draw toolbar ══
     Sean's request (2026-08-16): "탭에 지정된 곳에도 액티브 됐다는 뜻으로 보더를 보여줘".
     The global :focus-visible ring is `2px solid var(--accent-l)` — which disappears against
     an .active button, since those are already filled with the same accent purple. Tabbing
     onto the enabled Whiteboard row therefore looked exactly like tabbing onto nothing. A
     white outline with an accent halo outside it stays legible on all three backgrounds these
     controls use: the dark toolbar, the popover surface, and an active purple fill.

     Follow-up the same day — Sean: "키보드 탭이 이 영역을 지정할 때 유저가 어떻게 알어?
     아무 표시가 없어". :focus-visible alone was not enough. It is a browser heuristic: it
     suppresses the ring whenever the browser decides the focus came from a pointer, so after
     clicking the gear open with the mouse the toolbar looked completely unfocused even though
     a control genuinely held focus. Switched to plain :focus, so the indicator is shown
     whenever something here actually has focus, no heuristic involved. This matches the
     precedent Sean already set for .pg-consent on the entry policy gate ("show the border
     whenever it's focused"), and on these controls a lingering ring after a click is useful
     rather than noisy — it tells you which tool your keyboard shortcuts will act on. */
  .draw-toolbar .dt-tool:focus,
  .draw-toolbar .dt-act:focus,
  .draw-toolbar .dt-swatch:focus,
  .draw-toolbar .dt-color-wheel:focus,
  .dt-canvas-opt:focus {
    outline: 2px solid #fff;
    outline-offset: 2px;
    box-shadow: 0 0 0 4px var(--accent);
  }

  /* Region-level cue: while focus is anywhere inside the menu, the menu itself lights up, so
     "my keyboard is in here now" is readable at a glance from across the screen — not only
     from the 2px ring on one row. */
  .dt-canvas-popover:focus-within, .dt-color-popover:focus-within {
    border-color: var(--accent-l, #a78bfa);
    box-shadow: 0 10px 34px rgba(0,0,0,0.5), 0 0 0 1px var(--accent-l, #a78bfa);
  }
  /* Same idea one level up: the toolbar as a whole. Kept very quiet (a tinted border) so it
     reads as context, not as a second competing highlight next to the white focus ring. */
  .draw-toolbar:focus-within { border-color: var(--accent); }

  .dt-colors { display: flex; gap: 5px; }
  .dt-swatch {
    width: 20px; height: 20px; border-radius: 50%; cursor: pointer;
    border: 2px solid transparent; transition: transform .12s;
  }
  .dt-swatch:hover { transform: scale(1.15); }
  .dt-swatch.active { border-color: #fff; box-shadow: 0 0 0 2px var(--accent); }
  .dt-color-wheel-wrap { position: relative; width: 26px; height: 26px; flex-shrink: 0; }
  .dt-color-wheel {
    position: absolute; inset: 0; border-radius: 50%; cursor: pointer;
    background: conic-gradient(from 0deg, #ff3b30, #ff9500, #ffcc00, #34c759, #00c7be, #007aff, #af52de, #ff2d55, #ff3b30);
    border: 1px solid var(--border); transition: transform .12s;
  }
  .dt-color-wheel:hover { transform: scale(1.1); }

  /* ── DW-104: in-toolbar color-wheel popover ── */
  .dt-color-popover {
    position: absolute; bottom: calc(100% + 10px); right: -6px; z-index: 45;
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    background: rgba(20,23,32,0.97); backdrop-filter: blur(10px);
    border: 1px solid var(--border); border-radius: 14px; padding: 14px;
    box-shadow: 0 10px 34px rgba(0,0,0,0.5); width: 148px;
  }
  .dt-color-popover.csp-hide { display: none; }
  .dt-wheel-canvas-wrap { position: relative; width: 120px; height: 120px; touch-action: none; }
  .dt-wheel-canvas-wrap canvas {
    width: 120px; height: 120px; border-radius: 50%; cursor: crosshair;
    box-shadow: 0 0 0 1px var(--border);
  }
  .dt-wheel-cursor {
    position: absolute; top: 0; left: 0; width: 14px; height: 14px;
    margin: -7px 0 0 -7px; border-radius: 50%; border: 2px solid #fff;
    box-shadow: 0 0 0 1px rgba(0,0,0,0.5), 0 1px 4px rgba(0,0,0,0.4);
    pointer-events: none;
  }
  .dt-value-slider { width: 120px; }
  .dt-slider-row { display: flex; align-items: center; gap: 8px; width: 120px; }
  .dt-slider-label {
    width: 14px; flex-shrink: 0; font-size: calc(10px * var(--vv-text-scale)); font-weight: 700;
    color: var(--muted); text-align: center;
  }
  .dt-slider-row .dt-value-slider { width: auto; flex: 1; min-width: 0; }
  .dt-hex-row { display: flex; align-items: center; gap: 8px; width: 120px; }
  .dt-hex-swatch {
    width: 22px; height: 22px; border-radius: 6px; flex-shrink: 0;
    border: 1px solid var(--border);
  }
  .dt-hex-input {
    flex: 1; min-width: 0; background: var(--surface2); color: var(--text);
    border: 1px solid var(--border); border-radius: 7px; padding: 5px 7px;
    font: 600 12px ui-monospace, SFMono-Regular, Menlo, monospace;
    text-transform: lowercase;
  }
  .dt-hex-input:focus { outline: none; border-color: var(--accent); }
  .dt-size { display: flex; align-items: center; gap: 6px; }
  .dt-size input[type=range] { width: 80px; }
  #dt-size-val { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); min-width: 18px; text-align: center; }
  .draw-indicator {
    position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
    display: none; z-index: 40; background: var(--accent-dim); color: var(--accent-l);
    border: 1px solid var(--accent); border-radius: 20px; padding: 6px 14px;
    font-size: calc(12px * var(--vv-text-scale)); font-weight: 700; pointer-events: none;
  }
  .draw-indicator.show { display: block; }
  .canvas-wrap.draw-mode #three-canvas { cursor: crosshair; }
  .draw-text-input {
    position: absolute; display: none; z-index: 45; transform: translate(-50%, -50%);
    background: rgba(20,23,32,0.96); color: var(--text); border: 1px solid var(--accent);
    border-radius: 8px; padding: 6px 10px; font-size: calc(13px * var(--vv-text-scale)); font-weight: 600;
    outline: none; min-width: 160px; box-shadow: 0 6px 24px rgba(0,0,0,0.5);
  }
  .draw-text-input.show { display: block; }

  /* ══ Layers panel ══ */
  .draw-layer-list { display: flex; flex-direction: column; gap: 6px; }
  .draw-layer {
    display: flex; align-items: center; gap: 8px; padding: 8px 10px;
    background: var(--surface2); border: 1px solid var(--border); border-radius: 9px;
    transition: border-color .12s;
  }
  .draw-layer.active { border-color: var(--accent); background: var(--accent-dim); }
  /* An instructor's layer, shown read-only inside a student account
     (2026-08-20). It must read as "someone else's, and you may look but not
     touch" WITHOUT reading as disabled — the eye and the caret still work, and
     a greyed-out row would suggest they don't. So: a dashed left edge as the
     borrowed-content marker, the name in muted text with a default cursor
     (it is a real <input readonly>, and an I-beam over it invites a rename
     that cannot happen), and full opacity everywhere else. */
  .draw-layer.dl-readonly {
    border-style: dashed;
    border-left: 3px solid var(--accent2);
    background: var(--surface2);
  }
  .draw-layer.dl-readonly .dl-name { color: var(--muted); cursor: default; font-weight: 500; }
  .draw-layer.dl-readonly .dl-name:focus { border-bottom: none; }

  /* ── The student's read-only view of the instructor's layers ──
     Sidebar twin of .draw-layer, built by dwRenderStudentLayerList(). Smaller
     and flatter than the authoring rows: a student has one control here (the
     eye), so the row should not look like something you click into. Same
     dashed + accent-edge marker as .dl-readonly so "borrowed content" reads
     the same wherever it appears. */
  .sdb-layers { display: flex; flex-direction: column; gap: 4px; margin: 8px 0 4px; }
  .sdb-layer {
    display: flex; align-items: center; gap: 7px; padding: 5px 8px;
    background: var(--surface2);
    border: 1px dashed var(--border); border-left: 3px solid var(--accent2);
    border-radius: 7px;
  }
  /* A hidden layer stays listed — it is how the student gets it back. */
  .sdb-layer.off { opacity: .5; }
  .sdb-layer-vis {
    background: none; border: none; padding: 0; cursor: pointer;
    color: var(--text); display: flex; align-items: center; flex-shrink: 0;
  }
  .sdb-layer-vis:hover { color: var(--accent-l); }
  /* Which surface the layer was drawn on — `cube` for the model, `doc` for
     the whiteboard, matching the icons the draw toolbar already uses for
     those two ideas. Quiet: it is a classifier, not a control. */
  .sdb-layer-surf { display: flex; align-items: center; opacity: .5; flex-shrink: 0; }
  .sdb-layer-surf .icon { width: 12px; height: 12px; }
  /* The whiteboard is a different surface, so it gets a different edge colour
     rather than another badge — the row already carries an icon, a name, a
     count and a lock, and a fifth marker would just be noise. */
  .sdb-layer.sdb-layer-wb { border-left-color: var(--accent-l); }
  .sdb-layer-name {
    flex: 1; min-width: 0; font-size: calc(12px * var(--vv-text-scale));
    color: var(--muted); font-weight: 500;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .sdb-layer-count { font-size: calc(11px * var(--vv-text-scale)); color: var(--muted); flex-shrink: 0; }
  /* .6, not the .45 used on the wider authoring rows — at this row height the
     glyph is small enough that .45 read as a smudge rather than a lock. */
  .sdb-layer-lock { display: flex; align-items: center; opacity: .85; flex-shrink: 0; }
  /* ADR-0010: one block per instructor, their layers indented under a header
     that carries the instructor's name and an eye for all of their layers. */
  .sdb-author { display: flex; flex-direction: column; gap: 4px; }
  .sdb-author + .sdb-author, .sdb-author + .sdb-layer { margin-top: 6px; }
  .sdb-author > .sdb-layer { margin-left: 14px; }
  .sdb-author-head {
    display: flex; align-items: center; gap: 7px; padding: 2px 4px;
    font-size: calc(12px * var(--vv-text-scale)); font-weight: 600; color: var(--text);
  }
  .sdb-author-head.off { opacity: .55; }
  .sdb-author-vis {
    background: none; border: none; padding: 0; cursor: pointer;
    color: var(--text); display: flex; align-items: center; flex-shrink: 0;
  }
  .sdb-author-vis:hover { color: var(--accent-l); }
  .sdb-author-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .sdb-author-tag {
    font-size: calc(10px * var(--vv-text-scale)); font-weight: 700; letter-spacing: .02em;
    padding: 1px 6px; border-radius: 999px; flex-shrink: 0;
  }
  .sdb-author-live { background: rgba(248,113,113,0.15); color: var(--error); }
  /* Keyboard focus always visible (project rule: :focus, not :focus-visible). */
  .sdb-author-vis:focus, .sdb-layer-vis:focus { outline: 2px solid var(--accent-l); outline-offset: 2px; border-radius: 4px; }
  .draw-layer .dl-vis { background: none; border: none; cursor: pointer; font-size: calc(14px * var(--vv-text-scale)); opacity: .9; }
  .draw-layer .dl-name {
    flex: 1; background: none; border: none; color: var(--text); font-size: calc(12px * var(--vv-text-scale));
    font-weight: 600; cursor: pointer; outline: none; min-width: 0;
  }
  .draw-layer .dl-name:focus { border-bottom: 1px solid var(--accent); }
  .draw-layer .dl-count { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); }
  .draw-layer .dl-del { background: none; border: none; cursor: pointer; color: var(--error); font-size: calc(12px * var(--vv-text-scale)); opacity: .7; }
  .draw-layer .dl-del:hover { opacity: 1; }
  .draw-layer .dl-swatch { width: 12px; height: 12px; border-radius: 50%; flex: none; }
  /* expandable per-stroke list */
  .dl-block { display: flex; flex-direction: column; gap: 4px; }
  .draw-layer .dl-caret {
    background: none; border: none; cursor: pointer; color: var(--muted);
    font-size: calc(12px * var(--vv-text-scale)); width: 12px; flex: none; padding: 0; line-height: 1;
    transition: transform .12s;
  }
  .draw-layer .dl-caret.open { transform: rotate(90deg); }
  .dl-strokes {
    display: flex; flex-direction: column; gap: 3px;
    margin: 0 0 2px 24px; padding-left: 8px; border-left: 1px solid var(--border);
  }
  .dl-stroke {
    display: flex; align-items: center; gap: 7px; padding: 4px 7px;
    border-radius: 7px; background: var(--surface2); border: 1px solid transparent;
    cursor: pointer; font-size: calc(12px * var(--vv-text-scale));
  }
  .dl-stroke:hover { border-color: var(--border); }
  .dl-stroke.sel { border-color: var(--accent); background: var(--accent-dim); }
  .dl-stroke .ds-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
  .dl-stroke .ds-label { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .dl-stroke.hidden .ds-label { opacity: .45; text-decoration: line-through; }
  .dl-stroke .ds-vis, .dl-stroke .ds-del { background: none; border: none; cursor: pointer; font-size: calc(12px * var(--vv-text-scale)); opacity: .7; padding: 0; flex: none; }
  .dl-stroke .ds-del { color: var(--error); }
  .dl-stroke .ds-vis:hover, .dl-stroke .ds-del:hover { opacity: 1; }
  .dl-empty { font-size: calc(12px * var(--vv-text-scale)); color: var(--muted); padding: 3px 7px; }


/* ── Show/hide password toggle (auto-attaches to every password field) ── */
    .pw-wrap { position: relative; display: block; }
    .pw-wrap > input { padding-right: 40px !important; }
    .pw-toggle {
      position: absolute; top: 50%; right: 8px; transform: translateY(-50%);
      background: none; border: none; cursor: pointer; padding: 4px;
      line-height: 0; color: currentColor; opacity: .85;
    }
    .pw-toggle:hover { opacity: 1; }
    .pw-toggle svg { width: 18px; height: 18px; display: block; pointer-events: none; }

/* ── CSP: hover-hint tooltip (was set via style.cssText in hover-hints.js) ── */
.vv-hint-tip {
  position: fixed; z-index: 99999; max-width: 260px; padding: 8px 11px; border-radius: 9px;
  background: rgba(17,20,28,.97); color: #e8ebf3; border: 1px solid rgba(255,255,255,.14);
  font: 500 12px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  box-shadow: 0 8px 26px rgba(0,0,0,.5); pointer-events: none; opacity: 0; transition: opacity .12s;
  display: none;
}

/* ── CSP: hand-control overlay/pip/panel (was set via style.cssText in hand-control.js) ── */
.vv-hand-overlay {
  position: fixed; inset: 0; width: 100%; height: 100%; z-index: 9000;
  pointer-events: none; display: none;
}
.vv-hand-pip {
  position: fixed; right: 14px; bottom: 14px; width: 210px; border-radius: 12px;
  overflow: hidden; border: 1px solid rgba(255,255,255,.15); background: #000;
  z-index: 60; display: none; box-shadow: 0 10px 30px rgba(0,0,0,.5);
}
.vv-hand-pip-video { width: 210px; height: 158px; object-fit: cover; transform: scaleX(-1); display: block; }
.vv-hand-panel {
  position: fixed; left: 14px; bottom: 14px; z-index: 60; display: none;
  flex-direction: column; gap: 8px; background: rgba(20,23,32,.94);
  border: 1px solid rgba(255,255,255,.12); border-radius: 11px; padding: 11px 13px;
  font: 600 12px -apple-system,BlinkMacSystemFont,sans-serif; color: #e2e6f0;
  min-width: 215px; transition: min-width .3s ease;
}

/* LV-802: instructor's per-student watch PiPs, and (same class, see
   #vvl-teacher-pip in VanVRAi.html) a student's own "teacher view" PiP.
   Moved top-left (Sean, 2026-08-11) — was top-right, deliberately away from
   .vv-hand-pip/.vv-hand-panel (bottom corners) and the draw FAB/toolbar
   (bottom-right); top: 64px still clears #vvLiveShareIndicator (top: 12px,
   the P0-1 "visible to your instructor" pill), which is also top-left. */
.vvl-watch-pips {
  position: fixed; top: 64px; left: 14px; z-index: 55;
  display: flex; flex-direction: column; gap: 8px; pointer-events: none;
}
.vvl-watch-pip {
  width: 220px; border-radius: 10px; overflow: hidden; pointer-events: auto;
  border: 1px solid rgba(255,255,255,.15); background: #000;
  box-shadow: 0 8px 24px rgba(0,0,0,.5);
}
/* Dragged out of #vvl-watch-pips' flex flow onto its own fixed position —
   see _setupWatchPipDrag() in live-sync.js. */
.vvl-watch-pip.vvl-dragging { position: fixed; right: auto; z-index: 60; }
.vvl-watch-pip-header {
  display: flex; align-items: center; gap: 6px; padding: 5px 8px;
  background: rgba(20,23,32,.92); font: 700 calc(11px * var(--vv-text-scale)) -apple-system,BlinkMacSystemFont,sans-serif;
  color: #e2e6f0; cursor: grab; user-select: none;
}
.vvl-watch-pip-header:active { cursor: grabbing; }
.vvl-watch-pip-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vvl-watch-pip-close {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: calc(14px * var(--vv-text-scale)); line-height: 1; padding: 0 2px;
}
.vvl-watch-pip-close:hover { color: var(--text); }
/* Two stacked canvases: a WebGL 3D render of the watched student's own
   camera underneath, a transparent 2D pointer-dot overlay on top — see
   _ensureWatchPip()/_afterRender() in live-sync.js. */
.vvl-watch-pip-stage { position: relative; width: 220px; height: 140px; background: #000; }
.vvl-watch-pip-3d, .vvl-watch-pip-dot {
  position: absolute; inset: 0; display: block; width: 220px; height: 140px;
}
.vvl-watch-pip-dot { pointer-events: none; }
.vvl-watch-pip-waiting {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 0 10px; color: var(--muted); pointer-events: none;
  font: 600 calc(11px * var(--vv-text-scale)) -apple-system,BlinkMacSystemFont,sans-serif;
  background: rgba(0,0,0,.4);
}
/* Bottom-right resize handle — drag to scale the whole panel (see
   _setupPipResize()/_resizePip() in live-sync.js). A small diagonal-lines
   glyph via layered gradients, same spirit as a native OS resize grip. */
.vvl-watch-pip-resize {
  position: absolute; right: 0; bottom: 0; width: 16px; height: 16px;
  cursor: nwse-resize; z-index: 2;
  background:
    linear-gradient(135deg, transparent 0 40%, rgba(255,255,255,.55) 40% 46%, transparent 46% 60%,
                     rgba(255,255,255,.55) 60% 66%, transparent 66% 80%, rgba(255,255,255,.55) 80% 86%, transparent 86%);
}

/* ── CSP: course access-denied screen (was set via style.cssText in viewer-core.js) ── */
.vv-access-denied {
  position: fixed; inset: 0; z-index: 9999; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px;
  background: var(--bg, #0b0d17); text-align: center; padding: 24px;
}

/* ── UI contrast pass (2026-10-05) ───────────────────────────────────────
   Sean: "Show/Hide icon is hard to see — there are many buttons like that."
   Measured in a rendered build: outline buttons used --border (~1.3:1), so
   their shape vanished into the panel. Base-state override only; :hover,
   .active and .selected rules are more specific and still win. */
.btn-ghost, .btn-annot, .btn-close, .theme-toggle, .btn-annot-toggle, .icon-btn,
#lib-toggle-btn, #mob-sidebar-toggle, .vc-btn, #fullscreen-btn, .scene-btn,
.vlp-mode-btn, .vlp-reset-pins-btn, .vlp-watch-btn, .annot-popup-actions button,
.ac-subbtn, .ac-week-model-chip, .lf-edit-btn, .ip-tb-btn, .ip-edit-btn,
.ip-cancel-btn, .gl-back-btn, .gl-filter-chip, .gl-tag-clear-btn {
  border-color: var(--border-strong);
}
/* The 3D viewport stays dark in the light theme, and so do the controls
   floating on it (their backgrounds are hard-coded dark rgba). They used to
   pick up the light theme's dark-grey --muted and light --surface3 hover,
   i.e. dark icons on a dark rail (rail 2.3:1, ViewCube buttons 1.4:1).
   Pin them to the dark palette regardless of theme. */
[data-theme="light"] .mode-rail,
[data-theme="light"] .vc-btn,
[data-theme="light"] #fullscreen-btn,
[data-theme="light"] .scene-btn {
  --muted: #8f95ad; --text: #e2e6f0; --surface3: #262b3f;
  --border: #272c3f; --border-strong: #646c8c; --accent-l: #a78bfa;
}
/* --annot yellow text on its own 15% tint is 1.5:1 on the light theme's white
   panel. Same badge, darker amber text there (about 5.5:1). */
[data-theme="light"] .annot-count-badge { color: #8a5a00; border-color: rgba(138,90,0,.45); }

/* ══════════════════════════════════════
   SAFE AREAS (iPhone status bar / notch / home indicator)
   2026-10-06: on iPhone Safari the header was drawn under the status bar
   (clock, signal, battery covered Menu/Library/Login). VanVRApp.html now sets
   viewport-fit=cover so Safari reports the real insets, and each edge that
   touches the screen edge pads itself by env(safe-area-inset-*). On devices
   and browser layouts with no overlap every inset is 0, so nothing moves.
   Kept at the end of the file so it wins over the base and mobile rules.
══════════════════════════════════════ */
html { height: 100%; overflow: hidden; overscroll-behavior: none; }
body {
  overscroll-behavior: none;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
}
/* The header's own background fills the status-bar strip; its content
   starts below it. box-sizing keeps the 48px row height intact. */
header {
  box-sizing: content-box;
  padding-top: env(safe-area-inset-top);
}
#landing-screen {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
}
/* Fixed panels that were placed just under a 48px header. */
.vv-live-panel { top: calc(48px + env(safe-area-inset-top)); }
.vvl-watch-pips { top: calc(64px + env(safe-area-inset-top)); }

@media (max-width: 768px) {
  #vvAdminOverlay.admin-full { padding-top: env(safe-area-inset-top); }
  #vvAdminOverlay { padding-bottom: env(safe-area-inset-bottom); }
  .sidebar { padding-bottom: calc(24px + env(safe-area-inset-bottom)); }

  /* Header overflow on phones: the Share menu is already in the ☰ sheet's
     Quick Controls (the old #share-btn was hidden here for that reason; the
     2026-08-20 #share-menu-btn that replaced it never got the same rule), so
     it no longer pushes the ☰ button off the right edge. */
  header .share-wrap { display: none !important; }
}
@media (max-width: 430px) {
  .sidebar { padding-bottom: calc(20px + env(safe-area-inset-bottom)); }
}
/* Narrowest phones: Library becomes icon-only so Login and ☰ still fit. */
@media (max-width: 380px) {
  #lib-toggle-btn > span:last-child { display: none; }
}

/* V-02: Enter/Exit VR button (js/vr-mode.js). Only created where WebXR immersive-vr is supported. */
.vv-vr-btn {
  position: absolute; right: 16px; bottom: 16px; z-index: 40;
  padding: 10px 16px; min-height: 44px;
  background: var(--accent); color: #fff; border: 1px solid var(--accent-l);
  border-radius: 8px; font: 600 calc(14px * var(--vv-text-scale, 1)) system-ui, sans-serif;
  cursor: pointer;
}
.vv-vr-btn:hover { background: var(--accent-l); color: #0a0d14; }
.vv-vr-btn:focus { outline: 3px solid var(--accent2); outline-offset: 2px; }

/* V-02: the passthrough (immersive-ar) button sits to the left of the Enter VR button. */
.vv-vr-btn.vv-ar-btn { right: 150px; }

/* 2026-10-14: header dropdowns on phones and tablets.
   1) The library drawer (z 150) sits in the same stacking order as the header
      (z 20), so it painted over the Share menu. Lift the header above it.
   2) On phones the header scrolls sideways (overflow-x:auto forces overflow-y
      to clip too), which cut off an absolutely positioned menu hanging below
      it. Fixed positioning is not clipped by the header's overflow. */
@media (max-width: 1024px) {
  header { position: relative; z-index: 160; }
  header .share-menu {
    position: fixed; top: calc(48px + env(safe-area-inset-top) + 6px);
    right: 10px; left: auto;
  }
}

/* ══ Practice quiz (2026-10-22, SPEC_quiz-mode.md; js/quiz-mode.js) ══
   Panel sits where the Puzzle panel sits (left:68px, beside the rail) — the
   two never show together, the rail turns one off when the other starts.
   Dark glass in both themes, like every other canvas panel. Keyboard focus is
   always shown (:focus, never :focus-visible — Sean's standing rule). Colour
   is never the only signal: the feedback line also carries ✓ / ✗ text. */
.quiz-panel {
  position: absolute; left: 68px; top: 14px; z-index: 42;
  width: 280px; max-height: calc(100% - 28px); overflow-y: auto;
  background: rgba(20,23,32,0.94); backdrop-filter: blur(10px);
  border: 1px solid var(--border); border-radius: 14px;
  padding: 10px 14px 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.45);
  display: flex; flex-direction: column; gap: 8px;
  color: #e2e6f0; font-size: calc(12.5px * var(--vv-text-scale, 1));
}
.quiz-panel.csp-hide { display: none; }
#vvLiveShareIndicator:not([hidden]) ~ .quiz-panel { top: 52px; }
.quiz-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.quiz-title { font-weight: 700; }
.quiz-x {
  width: 28px; height: 28px; border-radius: 8px; border: 1px solid transparent;
  background: transparent; color: #e2e6f0; font-size: 18px; line-height: 1; cursor: pointer;
}
.quiz-x:hover { background: rgba(255,255,255,0.1); }
.quiz-x:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 2px; }
.quiz-screen { display: flex; flex-direction: column; gap: 8px; }
.quiz-screen.csp-hide { display: none; }
.quiz-hint, .quiz-note, .quiz-prompt { margin: 0; line-height: 1.4; color: #cbd2e3; }
.quiz-note { color: #fbbf24; }
.quiz-fs { border: 1px solid var(--border); border-radius: 10px; padding: 6px 8px 8px; margin: 0; }
.quiz-fs legend { padding: 0 4px; color: #aab2c8; font-size: calc(11.5px * var(--vv-text-scale, 1)); }
.quiz-opt { display: flex; gap: 8px; align-items: flex-start; padding: 4px 0; cursor: pointer; line-height: 1.35; }
.quiz-opt input { margin-top: 2px; }
.quiz-opt input:focus, .quiz-row select:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 2px; }
.quiz-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.quiz-row select {
  flex: 1; max-width: 170px; background: rgba(255,255,255,0.06); color: #e2e6f0;
  border: 1px solid var(--border); border-radius: 8px; padding: 4px 6px;
}
.quiz-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.quiz-go.quiz-off { opacity: .5; cursor: not-allowed; }
.quiz-progress { font-size: calc(11.5px * var(--vv-text-scale, 1)); color: #aab2c8; }
.quiz-tray { display: flex; flex-direction: column; gap: 6px; }
.quiz-card {
  display: block; width: 100%; text-align: left; padding: 9px 12px;
  background: rgba(255,255,255,0.08); color: #f1f4fb; font: inherit; font-weight: 600;
  border: 1px solid rgba(255,255,255,0.22); border-radius: 10px;
  cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none;
}
.quiz-card:hover { background: rgba(255,255,255,0.14); }
.quiz-card:active { cursor: grabbing; }
.quiz-card:focus { outline: 2px solid var(--accent-l, #a78bfa); outline-offset: 2px; }
.quiz-card.selected { background: var(--accent); border-color: #fff; color: #fff; }
.quiz-card.quiz-card-lifted { opacity: .35; }
.quiz-card.quiz-card-off { cursor: default; opacity: .6; }
.quiz-card.quiz-card-right { opacity: 1; background: #166534; border-color: #4ade80; }
.quiz-card.quiz-card-right::before { content: "\2713  "; }
.quiz-card.quiz-shake { animation: quiz-shake .4s; }
/* The floating copy that follows the pointer. Fixed + body-level so it rides
   above the pins (z 50) that sit outside the panel's stacking context. */
.quiz-ghost {
  position: fixed; left: 0; top: 0; z-index: 1000; pointer-events: none; margin: 0;
  box-shadow: 0 10px 28px rgba(0,0,0,0.55); background: var(--accent); border-color: #fff; color: #fff;
  width: auto; cursor: grabbing;
}
.quiz-feedback { margin: 0; min-height: 1.4em; font-weight: 600; }
.quiz-fb-good { color: #4ade80; }
.quiz-fb-bad  { color: #fca5a5; }
.quiz-result { margin: 0; font-weight: 700; }
.quiz-review { margin: 0; padding-left: 18px; color: #cbd2e3; }
.quiz-review.csp-hide { display: none; }
.quiz-panel .btn.csp-hide { display: none; }

/* Pins while a question is open. */
.annot-marker.quiz-target {
  box-shadow: 0 0 0 3px #fff, 0 0 14px var(--accent);
  animation: quiz-pulse 1.2s ease-in-out infinite;
}
.annot-marker.quiz-hover {
  transform: translate(-50%, -100%) rotate(-45deg) scale(1.4);
  box-shadow: 0 0 0 4px #fff, 0 0 18px var(--accent);
}
.annot-marker.quiz-correct { background: #22c55e; box-shadow: 0 0 0 4px #fff, 0 0 20px #22c55e; }
.annot-marker.quiz-wrong   { background: #ef4444; box-shadow: 0 0 0 4px #fff, 0 0 20px #ef4444; animation: quiz-pin-shake .4s; }
.annot-marker.quiz-answer  { box-shadow: 0 0 0 4px #fff, 0 0 22px #facc15; }
@keyframes quiz-pulse {
  0%, 100% { box-shadow: 0 0 0 3px #fff, 0 0 8px var(--accent); }
  50%      { box-shadow: 0 0 0 3px #fff, 0 0 24px var(--accent); }
}
@keyframes quiz-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); } 75% { transform: translateX(5px); }
}
@keyframes quiz-pin-shake {
  0%, 100% { transform: translate(-50%, -100%) rotate(-45deg) translateX(0); }
  25% { transform: translate(-50%, -100%) rotate(-45deg) translateX(-5px); }
  75% { transform: translate(-50%, -100%) rotate(-45deg) translateX(5px); }
}
@media (prefers-reduced-motion: reduce) {
  .annot-marker.quiz-target, .annot-marker.quiz-wrong, .quiz-card.quiz-shake { animation: none; }
}
/* Phones: the rail runs along the top, so the panel docks to the bottom edge. */
@media (max-width: 768px) {
  .quiz-panel { left: 10px; right: 10px; top: auto; bottom: 10px; width: auto; max-height: 55%; }
  #vvLiveShareIndicator:not([hidden]) ~ .quiz-panel { top: auto; }
}

/* U-52: reminder while the account still has a temporary password */
.vv-temp-pw-bar { position: fixed; left: 16px; bottom: 16px; z-index: 900; max-width: min(92vw, 420px); display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; padding: 10px 14px; border-radius: 10px; background: var(--surface, #1a2028); color: var(--text, #e6ebf2); border: 1px solid var(--accent, #6aa5f2); font-size: calc(13px * var(--vv-text-scale, 1)); box-shadow: 0 4px 16px rgba(0, 0, 0, .35); }
.vv-temp-pw-bar a { color: var(--accent, #6aa5f2); font-weight: 600; }

/* U-54 (2026-10-26): compare-scale banner, moved out of inline style attributes
   (style-src 'self' refused them, 3 console errors per page load). */
.vv-scale-banner { margin-top: 8px; padding: 8px 10px; border-radius: 8px; background: rgba(255,180,60,0.12); border: 1px solid rgba(255,180,60,0.35); font-size: 12.5px; line-height: 1.4; color: #e8c98a; }
.vv-scale-banner-dims { margin-top: 6px; opacity: 0.85; }
.vv-scale-banner-toggle { margin-top: 6px; padding: 2px 10px; font-size: 12px; }
