:root { color-scheme: light; --rail-w:300px; --header-h:44px; --topbar-h:52px; --split:56%;
        /* Brand palette — these nine colours are the source of truth. Any other
           shade is an opacity of one of them, never a fresh hex. */
        --c-white:#FFFFFF; --c-off-black:#080808; --c-dark-grey:#3D3C3C;
        --c-graphite:#BDCACC; --c-stone:#BEBFA3; --c-genki-green:#68E287;
        --c-moss:#013500; --c-mint:#D8FDD4; --c-zest:#FFF400;
        /* A drafting instrument on paper: off-black ink on white, graphite
           hairlines for structure, stone dotted rules for editable blanks.
           Green is the signal and lives in fills and marks (the one CTA, the
           focused floor, progress); moss is the accent as ink, where text must
           still pass AA on white. */
        --accent:var(--c-genki-green); --accent-ink:var(--c-moss);
        --ink:var(--c-off-black); --faded:var(--c-dark-grey); --dim:rgba(61,60,60,0.75);  /* ≥4.5:1 on white */
        --hairline:rgba(189,202,204,0.85);       /* graphite: structure rules */
        --hairline-soft:rgba(189,202,204,0.45);
        --blank:rgba(190,191,163,0.95);  /* the dotted rule under an editable blank — stone */
        --danger:#e5484d;  /* off-palette, confirmed with the user: true errors only */
        --obstacle:#E86A00;      /* off-palette, confirmed with the user (FRA-519): safety
                                    orange for what a band makes — the gauge, the 3D columns,
                                    the raster overlay. Legibility outranks the brand in the
                                    editor, and nothing else here reads hazard. */
        --obstacle-ink:#9A4600;  /* the same signal as small text; ≥4.5:1 on white */
        --unknown:#CDCDCD;       /* unknown cells: the PGM's own 205 grey, hueless beside the
                                    orange — the unknown you see is the unknown you ship */
        /* Floor-height overlay ramp (FRA-545): blue below the median floor,
           neutral on it, orange above — the research heatmap palette
           (bench/floor_field.py). Data viz, not brand. The overlay samples
           these at paint time, so a theme that retunes them is followed. */
        --heat-below:#2F6FA0; --heat-level:#F0EFEC; --heat-above:#C4611A;
        --doubt:#3E8FA0;         /* off-palette (FRA-551): desaturated teal for floor-doubt
                                    suggestions — stamped obstacles whose evidence hugs the
                                    local floor. Cool against the safety orange, darker than
                                    the unknown grey, so a suggestion never reads as a decided
                                    obstacle. Mirrored by PREVIEW_RGB.doubt in grid-view.js. */
        --caution:var(--c-zest);  /* zest is a fill or a mark on light ground, never text ink */
        --sans:"Suisse BP Int'l", "Suisse BP Neue", "Helvetica Neue", Arial, sans-serif;
        --serif:"Times Now", Times, Georgia, serif;
        /* Glass, only where a surface floats over live data: the two gauges
           (each overlaps its pane's render), the zoom pill over the raster,
           and the transient status chip.
           One recipe, high-white and quiet; glass never sits on glass. */
        --glass:rgba(255,255,255,0.66);
        --glass-stroke:rgba(189,202,204,0.7);
        --glass-blur:blur(12px) saturate(120%);
        --glass-shadow:0 2px 14px rgba(8,8,8,0.07); }
* { box-sizing: border-box; }
/* App shell: a top bar over a two-pane bench (point cloud │ occupancy grid).
   One sheet of paper — panes are zones divided by hairline rules, not cards. */
body { margin:0; height:100vh; overflow:hidden; color:var(--ink);
       background:var(--c-white);
       font:14px/1.5 var(--sans); font-variant-numeric:tabular-nums; }
.app { height:100vh; display:flex; flex-direction:column; }
/* Top bar: a running head — one hairline, no panel. Shared controls only. */
#header { flex:0 0 var(--topbar-h); display:flex; align-items:center; gap:14px; padding:0 18px;
          border-bottom:1px solid var(--hairline); }
.spring { flex:1 1 auto; }
.topbar #progress { margin-left:0; }
#floorslot { display:inline-flex; align-items:center; gap:12px; }
/* The wordmark: the page's one serif — Times Now carries the rare expressive
   headline against Suisse's neutrality. */
#brand { font:italic 400 17px/1 var(--serif); color:var(--ink); }
/* Build stamp: Show commit page was built from. Click to copy */
#build { color:var(--faded); font:12px var(--sans); font-style:normal; cursor:pointer;
         user-select:all; -webkit-user-select:all; }
#build::before { content:"/"; margin:0 2px; color:var(--dim);
                 user-select:none; -webkit-user-select:none; }
#build:hover { color:var(--accent-ink); }
/* Import control in the header (FRA-313): joined Open + URL-caret. */
#open { display:inline-flex; align-items:baseline; gap:6px; }
/* Buttons are phrases you can act on: a dotted stone rule that turns solid
   moss on approach. */
#header button { border:0; border-bottom:1px dotted var(--blank); border-radius:0;
                 padding:0 1px; cursor:pointer; font:inherit; font-size:13.5px;
                 background:none; color:var(--ink); white-space:nowrap;
                 transition:color 0.12s, border-color 0.12s; }
#header button:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
#header #urltoggle { border-bottom-color:transparent; padding:0 3px; }
#header #urltoggle:hover { border-bottom-color:var(--accent-ink); }
#urlbar { display:flex; gap:10px; align-items:baseline; }
#urlbar[hidden] { display:none; }
#header #url { background:none; border:0; border-bottom:1px dotted var(--blank); border-radius:0;
               color:var(--ink); padding:1px 2px; font:inherit; font-size:13px; min-width:240px; }
#header #url:focus { outline:none; border-bottom:1px solid var(--accent); color:var(--accent-ink); }
/* One header load indicator (FRA-315). */
#progress { display:flex; align-items:center; gap:8px; margin-left:auto; color:var(--faded); font-size:12px; }
#progress[hidden] { display:none; }
#progresslabel { white-space:nowrap; min-width:96px; text-align:right; }
#progresstrack { width:150px; height:5px; border-radius:3px; background:rgba(8,8,8,0.10); overflow:hidden; }
#progressbar { height:100%; width:0; background:var(--accent); border-radius:3px; transition:width 0.15s linear; }
#progressbar.indet { width:35%; transition:none; animation:indet 1.1s ease-in-out infinite; }
@keyframes indet { 0% { margin-left:-35% } 100% { margin-left:100% } }
/* The 3D viewport fills the cloud pane: white paper, no wash — the stipple of
   the scan is the only thing with visual weight here. */
#viewport { position:relative; flex:1 1 auto; min-height:0; overflow:hidden;
            background:var(--c-white); }
#viewport canvas { display:block; }
/* Shown in place of the canvas when WebGL won't start (FRA-403). */
#nowebgl { position:absolute; inset:0; display:flex; flex-direction:column;
           align-items:center; justify-content:center; text-align:center;
           padding:24px; color:var(--faded); font-size:13px; line-height:1.7; }
.lbl { color:var(--faded); white-space:nowrap; }
.field { display:flex; flex-direction:column; gap:4px; }
.row { display:flex; gap:6px; align-items:center; }
/* Pass knobs render into each pane's toolbar; only the "Advanced" disclosure
   survives, as a popover anchored to its summary (FRA-312, FRA-317). */
summary::-webkit-details-marker { display:none; }
.passhead { color:var(--faded); font-size:11px; font-weight:500;
            letter-spacing:0.12em; text-transform:uppercase; }
details.advanced { position:relative; }
details.advanced > summary { cursor:pointer; list-style:none; color:var(--faded);
       font-size:12px; font-style:italic; text-transform:lowercase; }
details.advanced > summary:hover { color:var(--accent-ink); }
details.advanced > summary::before { content:"\25B8 "; }
details.advanced[open] > summary::before { content:"\25BE "; }
.advbody { position:absolute; right:0; top:22px; z-index:20; min-width:200px;
       display:flex; flex-direction:column; gap:8px; padding:12px; border-radius:4px;
       background:var(--c-white); border:1px solid var(--hairline);
       box-shadow:0 4px 16px rgba(8,8,8,0.10); }
.tools input:disabled, .advbody input:disabled,
.tools select:disabled, #floorslot select:disabled { opacity:0.4; }
/* Editable values are blanks in a sentence: a dotted rule, no box. Focus
   turns the rule solid green and the ink moss. */
.tools input[type=text], .tools input[type=number], .tools select,
.advbody input[type=text], .advbody input[type=number], .advbody select, #floorslot select {
       background:none; border:0; border-bottom:1px dotted var(--blank); color:var(--ink);
       padding:1px 2px; border-radius:0; font:inherit; font-size:inherit;
       font-variant-numeric:tabular-nums; }
.tools input[type=text], .tools input[type=number],
.advbody input[type=text], .advbody input[type=number] { text-align:center; }
.tools input:focus, .tools select:focus,
.advbody input:focus, .advbody select:focus, #floorslot select:focus {
       outline:none; border-bottom:1px solid var(--accent); color:var(--accent-ink); }
.tools select:hover, .advbody select:hover, #floorslot select:hover { border-bottom-color:var(--accent-ink); }
.advbody .field { flex-direction:row; align-items:center; justify-content:space-between; gap:8px; }
.row > input { flex:1 1 0; min-width:0; }
/* The one filled action on the page: Download .h5, genki-green chip with
   off-black label (brand.md Themes). Load stays a phrase. */
#header #load { color:var(--ink); border-bottom:1px dotted var(--blank); flex:0 0 auto; }
#header #load:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
#header #hd5 { background:var(--accent); color:var(--c-off-black); border:0; border-radius:4px;
               padding:5px 12px; font-size:13px; font-weight:500; flex:0 0 auto;
               transition:background-color 0.12s; }
#header #hd5:hover { background:var(--c-mint); }
#header #hd5:disabled { background:rgba(189,202,204,0.35); color:var(--dim); cursor:default; }
label.chk { display:inline-flex; gap:6px; align-items:center; cursor:pointer; }
/* Checkboxes: a small square blank; the check is ink, not chrome. */
input[type=checkbox] { appearance:none; -webkit-appearance:none; width:12px; height:12px;
       border:1px solid var(--blank); background:none; margin:0; cursor:pointer;
       position:relative; vertical-align:-1px; }
input[type=checkbox]:checked::after { content:"\2713"; position:absolute; inset:-4px 0 0 0;
       font-size:13px; line-height:1; color:var(--ink); text-align:center; }
input[type=checkbox]:hover { border-color:var(--accent-ink); }
input[type=checkbox]:disabled { opacity:0.4; }
input[type=range] { accent-color:var(--accent); }
/* The cloud's data sentence: a quiet line on the top edge, twin to #gridhead.
   A soft white veil keeps it legible where points pass beneath. */
#cloudhead { position:absolute; left:226px; top:10px; z-index:3; color:var(--faded); font-size:12.5px;
             background:rgba(255,255,255,0.8); padding:3px 9px;
             max-width:calc(100% - 240px);
             white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
#cloudhead:empty { display:none; }
/* Transient feedback only — hints, errors, action results. Docked at the foot
   of the viewport, clear of the cloud. The page's one glass element. */
#status { position:absolute; bottom:14px; left:50%; transform:translateX(-50%); z-index:10;
          background:var(--glass); padding:7px 11px; border-radius:4px; font-size:12.5px;
          white-space:pre-wrap; max-width:min(440px, calc(100% - 28px)); color:var(--faded);
          border:1px solid var(--glass-stroke);
          backdrop-filter:var(--glass-blur); -webkit-backdrop-filter:var(--glass-blur);
          box-shadow:var(--glass-shadow); }
#status b { color:var(--ink); font-weight:600; }
/* Observable log: a durable, copyable record docked at the foot of the app,
   collapsed by default; only errors badge the summary, so it stays quiet
   until it matters. */
#logpanel { flex:0 0 auto; border-top:1px solid var(--hairline); background:var(--c-white); }
#logpanel > summary { cursor:pointer; list-style:none; display:flex; align-items:center; gap:8px;
            padding:6px 18px; color:var(--faded); }
#logpanel > summary:hover { background:rgba(189,202,204,0.18); }
#logpanel > summary::-webkit-details-marker { display:none; }
#logpanel > summary::before { content:"\25B8 "; color:var(--faded); }
#logpanel[open] > summary::before { content:"\25BE "; }
/* Error count: the danger hue as a fill, off-black ink on it for contrast. */
#logbadge { background:var(--danger); color:var(--c-off-black); font-size:10px; font-weight:600;
            border-radius:9px; padding:1px 7px; line-height:1.5; }
#logbadge[hidden] { display:none; }
.logbody { display:flex; flex-direction:column; gap:6px; padding:0 18px 10px; }
.logbody button { border:0; border-bottom:1px dotted var(--blank); background:none; color:var(--ink);
           font:inherit; font-size:13px; padding:0 1px; border-radius:0; cursor:pointer; }
.logbody button:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
/* The rows stay monospaced: a log is a console, not prose. (Suisse Int'l Mono
   once licensed — brand.md flags the stopgap.) */
#logrows { max-height:180px; overflow-y:auto; display:flex; flex-direction:column; gap:3px;
           font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:11px; color:var(--faded); }
.logrow { display:flex; gap:6px; white-space:pre-wrap; word-break:break-word; }
.logrow .logt { color:var(--dim); flex:0 0 auto; }
.logrow .loglvl { flex:0 0 auto; font-weight:600; letter-spacing:0.08em; font-size:10px; }
/* Severity is a mark plus the level word, never color alone: errors carry the
   danger bar and wash, warnings the caution ones. Ink stays off-black for AA. */
.logrow.error { color:var(--ink); border-left:3px solid var(--danger);
                background:rgba(229,72,77,0.08); padding-left:5px; }
.logrow.warn { color:var(--ink); border-left:3px solid var(--caution);
               background:rgba(255,244,0,0.16); padding-left:5px; }
.logrow.debug { color:var(--dim); }   /* PLY_DEBUG output, only when ?debug is on */
#logcopy { align-self:flex-start; }
/* Pipeline strip: one text row per pane, stages joined by arrows. Hovering a
   stage rings the control that configures it (.pipehot). */
#pipepanel { flex:0 0 auto; border-top:1px solid var(--hairline); background:var(--c-white); }
#pipepanel > summary { cursor:pointer; list-style:none; display:flex; align-items:baseline; gap:8px;
            padding:6px 18px; color:var(--faded); }
#pipepanel > summary:hover { background:rgba(189,202,204,0.18); }
#pipepanel > summary::-webkit-details-marker { display:none; }
#pipepanel > summary::before { content:"\25B8 "; color:var(--faded); }
#pipepanel[open] > summary::before { content:"\25BE "; }
#pipesum { color:var(--dim); font-size:12px; }
#pipebody { display:flex; flex-direction:column; gap:5px; padding:2px 18px 10px; font-size:12.5px; }
.piperow { display:flex; flex-wrap:wrap; gap:2px 8px; align-items:baseline; }
.prowlbl { flex:0 0 108px; color:var(--dim); font-style:italic; font-size:11.5px; text-align:right; }
.pstage .pn { color:var(--ink); }
.pstage .pp { color:var(--faded); }
.pstage .pm { color:var(--dim); font-size:11.5px; }
.pstage[data-links]:hover .pn { color:var(--accent-ink); }
.parrow { color:var(--dim); }
.phand { color:var(--dim); font-style:italic; }
/* A failed stage: the danger wash; the stage text itself names the failure. */
.pstage.perr { color:var(--ink); background:rgba(229,72,77,0.10); padding:0 3px; }
.pipehot { outline:1px dotted var(--accent-ink); outline-offset:3px; }
#obsrail.pipehot { border-color:rgba(104,226,135,0.9); }
/* Whole-window drop target: a mint invitation, not an alarm. */
#drop { position:fixed; inset:0; z-index:20; display:none; align-items:center; justify-content:center;
        background:rgba(216,253,212,0.55); border:3px dashed var(--c-moss); font-size:20px; color:var(--c-moss); }
#drop.on { display:flex; }
/* Grid-guard confirm: the one modal — plain paper on a dimmed page, the
   question in running text, the choice as the app's phrase-buttons. Cancel
   (keep the edits) takes focus first as the safe default. */
#confirmdlg { border:1px solid var(--hairline); border-radius:0; background:var(--c-white);
              color:var(--ink); font:inherit; max-width:400px; padding:18px 20px;
              box-shadow:0 6px 28px rgba(8,8,8,0.14); }
#confirmdlg::backdrop { background:rgba(8,8,8,0.28); }
#confirmmsg { margin:0 0 14px; line-height:1.55; }
.dlgactions { display:flex; justify-content:flex-end; gap:20px; margin:0; }
.dlgactions button { border:0; border-bottom:1px dotted var(--blank); border-radius:0;
                     padding:0 1px; cursor:pointer; font:inherit; font-size:13.5px;
                     background:none; color:var(--ink);
                     transition:color 0.12s, border-color 0.12s; }
.dlgactions button:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
/* The occupancy raster stage: fills the grid pane body, canvas centered on the
   paper; a hairline frame delimits the map's extent. JS toggles display
   (flex when shown, none when the preview is off). The panel anchors the
   pinned chrome (head, legend, band strip); the scroll stage inside it pans
   an oversized raster with native scrolling (FRA-510). */
#gridpanel { position:relative; flex:1 1 auto; min-height:0; display:none; }
/* margin:auto on the canvas centers a small raster AND keeps every edge of an
   oversized one reachable — justify-content:center would push the leading
   edge past the scroll origin, where no scrollbar reaches. */
#gridscroll { position:absolute; inset:0; overflow:auto; display:flex; padding:16px; }
/* The grid's statistics, set as one quiet data sentence. The zoom pill sits in
   the bottom corner now, so the whole top edge is the sentence's, but for its
   own right margin. */
#gridhead { position:absolute; left:234px; top:10px; z-index:3; color:var(--faded); font-size:12.5px;
            background:rgba(255,255,255,0.8); padding:3px 9px;
            max-width:calc(100% - 246px);
            white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Zoom pill (FRA-521): a Google-Maps control in the bottom-right corner, where
   a map reader reaches for zoom. + and − stacked with the scale readout
   between, which clicks back to fit. Glass, the recipe reserved for surfaces
   over live data; hairlines divide the rows as they divide the panes.
   overflow:hidden clips each row's hover wash to the rounded corners. */
#gridzoom { position:absolute; right:12px; bottom:12px; z-index:3; display:flex; flex-direction:column;
            border:1px solid var(--glass-stroke); border-radius:10px; overflow:hidden;
            background:var(--glass); -webkit-backdrop-filter:var(--glass-blur);
            backdrop-filter:var(--glass-blur); box-shadow:var(--glass-shadow); }
#gridzoom button { width:38px; display:flex; align-items:center; justify-content:center;
                   border:0; border-radius:0; cursor:pointer; background:none; color:var(--ink);
                   font:inherit; font-variant-numeric:tabular-nums; }
#gridzoom button:hover { background:var(--c-mint); color:var(--accent-ink); }
#gridzoom button:active { background:var(--accent); color:var(--accent-ink); }
#gridzoom button:focus-visible { outline:2px solid var(--accent-ink); outline-offset:-2px; }
/* + and − are the tall tap targets; the readout is a quieter row between two
   hairlines, doubling as the fit reset. */
#zoomin, #zoomout { height:34px; font-size:19px; line-height:1; }
#zoomlvl { height:26px; font-size:11.5px; color:var(--dim);
           border-top:1px solid var(--hairline); border-bottom:1px solid var(--hairline); }
#zoomlvl:hover { color:var(--accent-ink); }
#editgrid { border:0; border-bottom:1px dotted var(--blank); padding:0 1px; border-radius:0;
            cursor:pointer; font:inherit; font-size:13px; background:none; color:var(--ink);
            white-space:nowrap; flex:0 0 auto; }
#editgrid:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
#editgrid:disabled { opacity:0.5; cursor:default; border-bottom-color:transparent; color:var(--faded); }
#editgrid.on { color:var(--accent-ink); border-bottom:1px solid var(--accent); background:none; }
/* flex:none — a flex item shrinks to its container by default, which would
   swallow the overflow the scroll stage exists to expose. */
#gridcanvas { display:block; image-rendering:pixelated; background:var(--c-white);
              border:1px solid var(--hairline); border-radius:0;
              margin:auto; flex:none; }
#gridcanvas.painting { cursor:crosshair; touch-action:none; }
/* Crop marquee: a mint window onto the region the crop will keep; the giant
   shadow dims everything outside it (ink only — shadows never grow the
   scrollable area). Sits above the canvas, below the pinned chrome, and
   ignores pointers so the drag keeps flowing to the canvas. */
#cropbox { position:absolute; z-index:2; pointer-events:none;
           border:1px dashed var(--accent-ink); background:rgba(104,226,135,0.10);
           box-shadow:0 0 0 100000px rgba(8,8,8,0.18); }
/* Legend: the swatch key, held to the bottom edge but shifted left of the zoom
   pill that now owns the corner. */
#gridlegend { position:absolute; right:62px; bottom:10px; z-index:3; display:flex; gap:12px; align-items:center;
              color:var(--faded); font-size:12px; background:rgba(255,255,255,0.8); padding:3px 9px; }
#gridlegend .sw { display:inline-block; width:12px; height:12px; margin-right:4px;
                  border:1px solid rgba(8,8,8,0.25); vertical-align:middle; }
/* Swatches mirror the preview palette (previewRGB): free light, obstacle
   dark, unknown between — the same order as the PGM export. */
.sw.free { background:var(--c-white); } .sw.obs { background:var(--obstacle); } .sw.unk { background:var(--unknown); }
.sw.doubt { background:var(--doubt); }
/* Paint palette + brush (FRA-376): a 3-value swatch picker reusing the legend
   colours, shown only in edit mode. Pickers, tools, and actions are all text
   phrases; the selected one takes moss ink and a solid green rule. */
#gridpaint { display:none; align-items:baseline; gap:8px 14px; flex-wrap:wrap;
             padding:9px 16px; border-top:1px solid var(--hairline);
             color:var(--faded); font-size:12.5px; }
#gridpaint.on { display:flex; }
#gridpaint .pick { display:inline-flex; align-items:center; gap:5px; cursor:pointer;
                   border:0; border-bottom:1px dotted var(--blank); border-radius:0; padding:0 1px;
                   background:none; color:var(--ink); font:inherit; font-size:12.5px; }
#gridpaint .pick:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
#gridpaint .pick.sel { color:var(--accent-ink); border-bottom:1px solid var(--accent); background:none; }
#gridpaint .pick .sw { display:inline-block; width:12px; height:12px;
                       border:1px solid rgba(8,8,8,0.25); vertical-align:middle; }
/* Tool-mode segment (FRA-378): brush/fill/rect/line. The active tool takes
   the same moss ink and solid green rule as a selected .pick. */
#gridpaint .seg { display:inline-flex; gap:10px; }
#gridpaint .tool { cursor:pointer; border:0; border-bottom:1px dotted var(--blank); border-radius:0;
                   padding:0 1px; background:none; color:var(--ink); font:inherit; font-size:12.5px; }
#gridpaint .tool:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
#gridpaint .tool.sel { color:var(--accent-ink); border-bottom:1px solid var(--accent); background:none; }
/* Crop confirm/cancel ride with the tool: hidden until crop is selected. */
#cropactions { display:none; gap:14px; }
#cropactions.on { display:inline-flex; }
#gridpaint label { display:inline-flex; align-items:center; gap:5px; }
#gridpaint input[type=range] { width:78px; vertical-align:middle; }
#gridpaint .bsz { min-width:1.4em; text-align:right; color:var(--ink); }
#gridpaint button.act { border:0; border-bottom:1px dotted var(--blank); border-radius:0;
                        padding:0 1px; cursor:pointer; font:inherit; font-size:12.5px;
                        background:none; color:var(--ink); }
#gridpaint button.act:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
#gridpaint button.act:disabled { opacity:0.4; cursor:default; border-bottom-color:transparent; }
/* GIMP round-trip help: the threshold bands, shown only in edit mode. */
#gridiohelp { display:none; color:var(--faded); font-size:11.5px; font-style:italic; padding:0 16px 8px; }
#gridiohelp.on { display:block; }
/* Inline input warnings next to the offending knob (FRA-315): zest is a
   highlighter mark behind off-black ink, never yellow text on white. */
.warn { color:var(--ink); background:var(--caution); padding:0 4px; font-size:11px; }
.warn[hidden] { display:none; }
/* Elevation rail: a drafting margin on the left edge — the legend for floor
   cuts, so hue is freed for the cloud. It floats over the render, so it
   takes the glass: the cloud ghosts through instead of being occluded. */
#elevrail { position:absolute; z-index:3; left:10px; top:10px; bottom:10px;
            width:206px; border-radius:0; padding:6px 8px; overflow:hidden;
            background:var(--glass); border:1px solid var(--hairline);
            backdrop-filter:var(--glass-blur); -webkit-backdrop-filter:var(--glass-blur);
            box-shadow:var(--glass-shadow); }
#elevrail[hidden] { display:none; }
#elevsvg { width:100%; height:100%; display:block; font-family:var(--sans); }
/* Obstacle-band strip: a floor-relative z-gauge on the grid pane's left edge,
   mirroring the elevation rail. Bands are shaded above/below the floor line. */
#obsrail { position:absolute; z-index:3; left:10px; top:10px; bottom:10px;
           width:210px; border-radius:0; padding:8px 10px; overflow:hidden;
           display:flex; flex-direction:column; gap:8px;
           background:var(--glass); border:1px solid var(--hairline);
           backdrop-filter:var(--glass-blur); -webkit-backdrop-filter:var(--glass-blur);
           box-shadow:var(--glass-shadow); }
#obsrail[hidden] { display:none; }
/* While the rail is up, reserve its footprint: the scroll stage starts beside
   the bands, so the raster never sits or scrolls under them. The stage is
   inset-positioned, so panel padding could not move it; its own left inset
   can. 238 = 10 offset + 212 rail + 16 gap. */
#gridpanel:has(> #obsrail:not([hidden])) > #gridscroll { left:238px; }
/* One line: the tracked title, then the unit as a quiet annotation beside it. */
#obshead { color:var(--faded); font-size:10px; font-weight:500; letter-spacing:0.08em;
           text-transform:uppercase; line-height:1.4; white-space:nowrap; }
#obshead .unit { text-transform:none; letter-spacing:0; font-weight:400;
                 font-style:italic; color:var(--dim); }
#obssvg { flex:1 1 auto; width:100%; min-height:0; display:block; font-family:var(--sans); }
/* The band list: labelled [min] [max] ✕ rows and the "+ band" button. */
#obslist { flex:0 0 auto; display:flex; flex-direction:column; gap:6px; }
#obsrows { display:flex; flex-direction:column; gap:5px; max-height:170px; overflow-y:auto; }
.obsrow { display:grid; grid-template-columns:1fr 1fr 18px; gap:6px; align-items:center; }
.obsrow.obshdr { color:var(--dim); font-size:11px; font-style:italic; padding:0 2px; }
.obsrow input[type=number] { width:100%; box-sizing:border-box; font:inherit; font-size:12.5px;
  padding:1px 2px; text-align:center; color:var(--ink); background:none;
  border:0; border-bottom:1px dotted var(--blank); border-radius:0; -moz-appearance:textfield; }
.obsrow input[type=number]::-webkit-outer-spin-button,
.obsrow input[type=number]::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
.obsrow input:focus { outline:none; border-bottom:1px solid var(--accent); color:var(--accent-ink); }
.rmrow { background:none; border:0; color:var(--faded); cursor:pointer; font-size:13px; padding:0; line-height:1; }
.rmrow:hover { color:var(--accent-ink); }
#obsadd { align-self:flex-start; border:0; border-bottom:1px dotted var(--blank); background:none;
          color:var(--ink); border-radius:0; padding:0 1px; font:inherit; font-size:12px; cursor:pointer; }
#obsadd:hover { color:var(--accent-ink); border-bottom:1px solid var(--accent-ink); }
/* In-scene floor-name tags, projected from the 3D footprints each frame. */
#floorlabels { position:absolute; inset:0; z-index:4; pointer-events:none; }
/* Floor name tag: italic ink over the scene, lifted by a paper halo, no pill. */
.floorlabel { position:absolute; transform:translateY(-50%);
              font:italic 13px/1.2 var(--sans); color:var(--ink);
              white-space:nowrap;
              text-shadow:0 0 4px rgba(255,255,255,0.95), 0 0 2px rgba(255,255,255,0.9); }
/* Drafting dimension text: centred on its dimension line, minimal chrome. */
.measlabel { position:absolute; transform:translate(-50%,-50%); color:var(--faded);
             font:italic 11px/1 var(--sans); white-space:nowrap;
             text-shadow:0 0 4px rgba(255,255,255,0.95), 0 0 2px rgba(255,255,255,0.9); }
.floorlabel.dim, .measlabel.dim { opacity:0.34; }
/* ---- the bench: two paper zones on a hairline splitter, no boxes ---------- */
.bench { flex:1 1 auto; display:flex; min-height:0; }
.pane { display:flex; flex-direction:column; min-width:0; min-height:0;
        background:var(--c-white); overflow:hidden; }
.pane.cloud { flex:0 0 var(--split); }
.pane.grid { flex:1 1 0; }
.panehead { flex:0 0 auto; display:flex; align-items:center; gap:10px; padding:12px 18px 0; }
/* Pane titles: technical labels set as uppercase Suisse with tracking. */
.panetitle { font-size:11px; font-weight:500; letter-spacing:0.14em;
             text-transform:uppercase; color:var(--faded); }
/* Per-pane toolbar: the knobs that shape this view, set as one line of
   editable phrases under the title (FRA-312). */
.tools { flex:0 0 auto; display:flex; align-items:baseline; gap:4px 16px; flex-wrap:wrap;
         padding:4px 18px 10px; font-size:13px; border-bottom:1px solid var(--hairline); }
.tools .field, #floorslot .field { flex-direction:row; align-items:baseline; gap:5px; }
.tools .field input[type=number] { width:58px; }
.tools .field input[type=text] { width:100px; }
.tools .advanced { margin-left:auto; }
/* The splitter, carrying the transform label — cloud → grid, source → output. */
.splitter { flex:0 0 14px; cursor:col-resize; position:relative; z-index:6;
            display:flex; align-items:center; justify-content:center; }
.splitter::before { content:""; position:absolute; inset:0 6px; background:var(--hairline); }
.splitter:hover::before, .splitter.drag::before { background:var(--accent); }
.xform { position:absolute; z-index:7; background:var(--c-white);
         color:var(--faded); font:italic 12px var(--sans);
         padding:8px 2px; white-space:nowrap;
         writing-mode:vertical-rl; text-orientation:mixed; letter-spacing:0.04em; user-select:none; }
.splitter:hover .xform, .splitter.drag .xform { color:var(--accent-ink); }
:focus-visible { outline:1px dotted var(--accent-ink); outline-offset:2px; }
@media (prefers-reduced-motion:reduce) { * { transition:none !important; } }
/* Section probe (FRA-549): the ink cut mark over the raster and the F(s)
   plot strip under it. The overlay ignores pointers — the drag flows through
   the canvas — and lives in the scroll stage's content coordinates, so it
   pans and zooms with the raster like the crop marquee. */
#sectionline { position:absolute; z-index:2; pointer-events:none; overflow:visible; }
#sectionline[hidden] { display:none; }
#sectionpanel { flex:0 0 auto; height:168px; display:flex; flex-direction:column; gap:4px;
                padding:8px 16px 6px; border-top:1px solid var(--hairline); }
#sectionpanel[hidden] { display:none; }
#sectionhead { color:var(--faded); font-size:10px; font-weight:500; letter-spacing:0.08em;
               text-transform:uppercase; line-height:1.4; white-space:nowrap; }
#sectionhead .unit { text-transform:none; letter-spacing:0; font-weight:400;
                     font-style:italic; color:var(--dim); }
#sectionmeta { text-transform:none; letter-spacing:0; font-weight:400; margin-left:10px;
               color:var(--dim); font-variant-numeric:tabular-nums; }
#sectionsvg { flex:1 1 auto; width:100%; min-height:0; display:block; font-family:var(--sans); }
