/* base.css — fonts, design tokens, reset, overall grid and shared components.
   Loaded first: everything else builds on the custom properties defined here. */

/* Self-hosted fonts, so the runtime loads no external resource and works offline. The swap
   display strategy falls back to a system font immediately if one has not loaded. */
/* Resolved from THIS sheet, which lives in css/ -- so the hop out is required: a bare
   `fonts/...` resolved to `/css/fonts/...` and every face 404'd in silence, the page
   falling back to a system font that looks close enough to go unnoticed. */
@font-face{font-family:'IBM Plex Mono';src:url('../fonts/plex-mono-400.woff2') format('woff2');font-weight:400;font-display:swap;}
@font-face{font-family:'IBM Plex Mono';src:url('../fonts/plex-mono-500.woff2') format('woff2');font-weight:500;font-display:swap;}
@font-face{font-family:'IBM Plex Mono';src:url('../fonts/plex-mono-600.woff2') format('woff2');font-weight:600;font-display:swap;}
@font-face{font-family:'IBM Plex Mono';src:url('../fonts/plex-mono-700.woff2') format('woff2');font-weight:700;font-display:swap;}
@font-face{font-family:'IBM Plex Sans';src:url('../fonts/plex-sans-400.woff2') format('woff2');font-weight:400;font-display:swap;}
@font-face{font-family:'IBM Plex Sans';src:url('../fonts/plex-sans-500.woff2') format('woff2');font-weight:500;font-display:swap;}
@font-face{font-family:'IBM Plex Sans';src:url('../fonts/plex-sans-600.woff2') format('woff2');font-weight:600;font-display:swap;}
:root{
  /* Declares the page dark to the browser itself, which is what makes scrollbars, form
     controls and the canvas behind the document follow instead of arriving white. Then the
     bar is toned down to the panel edge: a scrollbar states how far down a list is, it is
     not a control anyone aims at here. */
  color-scheme:dark;
  scrollbar-color:var(--dim2) transparent;
  scrollbar-width:thin;
  --bg:#0a0e12; --panel:#0f151b; --panel2:#10171e; --edge:#1d2731;
  --grid:#16202a; --ink:#c2d0da; --dim:#5d6e7c; --dim2:#3c4a56;
  --teal:#2dd4bf; --green:#41c463; --magenta:#d65db1; --amber:#f0a500;
  --red:#ef5a5a; --blue:#4aa8ff;
  /* The trace colour and a flat fill, never a vertical gradient: a fine trace over a grid,
     with no wash. */
  --trace:#37d0bf; --fill:rgba(55,208,191,.08);
  --mono:"IBM Plex Mono","SFMono-Regular",ui-monospace,"Cascadia Mono","Consolas",monospace;
  --sans:"IBM Plex Sans","Segoe UI",system-ui,-apple-system,sans-serif;
  /* The size below which the page stops being whole: four modules across, and the height its
     floors add up to. It is not a guess about screens -- it is what this layout needs, and the
     stylesheet test holds it against the floors themselves. A shorter screen gets the same page
     drawn smaller rather than a cramped one. */
  /* The one screen this instrument is drawn for: a maximized browser on a 1920x1080 display,
     which keeps 945 of those 1080 once the window furniture and the taskbar are out. Every
     other screen gets this same page, scaled -- never rearranged. */
  --fit-w:1920px; --fit-h:945px;
  /* Widths at which the instrument row gives up a column, measured on the width the PAGE is
     laid out in -- see the rules near .bottom. Declared here because the script that measures
     that width reads them from the sheet, so the two never drift apart. */
  --fold-2col:1360px; --fold-1col:760px;
  /* The smallest VIEWPORT this page is still drawn whole in. Under either of these the scale
     bottoms out and parts of the layout start giving way instead of merely shrinking, which is
     worth saying rather than leaving to be noticed. */
  --min-w:1366px; --min-h:720px;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0; background:radial-gradient(1200px 600px at 50% -10%,#0e161e,#070a0e 70%);
  color:var(--ink); font-family:var(--sans); font-size:13px;
  -webkit-font-smoothing:antialiased; overflow:hidden;
}
/* Layout: the page is a COLUMN OF SHARES, like every panel in it. The header and the control
   bar have no share and keep their natural height; the spectrum and the middle row trade theirs
   on a handle. It was a grid of four fixed rows, which made the boundary under the spectrum the
   only one on the page that could not be moved -- and so the only one that could not line up
   with its neighbours. Floors live on what needs them, never on a row: a row capped or floored
   in pixels sits at a different fraction of the page on every screen. */
.app{height:100vh; display:flex; flex-direction:column; gap:7px; padding:8px;}
.panel{background:linear-gradient(180deg,var(--panel2),var(--panel));
       border:1px solid var(--edge); border-radius:3px; position:relative;}
.cap{position:absolute; top:6px; left:9px; font-size:10px; letter-spacing:.14em;
     text-transform:uppercase; color:var(--dim); pointer-events:none;}
.cap .r{position:absolute; left:0;}
/* Panel corner toggle: spectrum centring, amplitude autoscale, time graph pause. Shared by
   several panels, so it lives here rather than in one widget's sheet. */
.spectrl{position:absolute; top:3px; right:8px; z-index:3; cursor:pointer; font-size:15px;
     line-height:1; color:var(--dim2); padding:1px 4px; border-radius:4px; user-select:none;}
.spectrl:hover{color:var(--ink); background:#172530;}
.spectrl.act{color:var(--teal);}
/* Receiver commands carried by a widget, in the FREE corner of its card: the top right is
   already the slot's mark and the widget's own toggles. Shared, since more than one widget
   carries commands, and smaller than a bar button -- it sits over an instrument, not under
   one. The lit and locked states come from `.btn`, which these are. */
.wcmd{position:absolute; top:3px; z-index:3; display:flex; gap:4px;}
.wcmd .btn{padding:3px 9px; font-size:10px; letter-spacing:.1em;}

/* What a keyboard shortcut just did. A key press has no visible origin, so the command names
   itself for a moment. Placed low and centred, over the page and never in it: it must not
   cover a reading, and it must not move one either -- hence fixed, out of the flow, and
   transparent to the pointer, so a message cannot swallow a click on what is underneath. */
.osd{position:fixed; left:50%; bottom:38px; transform:translateX(-50%);
     z-index:60; pointer-events:none;
     font-family:var(--mono); font-size:12px; letter-spacing:.14em; color:var(--teal);
     background:rgba(8,14,19,.92); border:1px solid #1f5048; border-radius:3px;
     padding:7px 16px; white-space:nowrap;
     opacity:0; transition:opacity .5s ease;}
/* Appearing is immediate and fading is slow: the answer has to land within the same gesture,
   while the message leaving must not pull the eye back to it. */
.osd.on{opacity:1; transition:opacity .06s ease;}
/* Continuous-trace meter with peak hold, and its watermark scale. What separates an
   instrument from a progress bar is the graduated scale and the peak marker that falls back
   slowly, so the look sits beside the code that drives it (`lMeter` and `lRuler`) rather than
   in one panel's sheet: several pages carry one of these. Panel-specific variants stay with
   their widget. */
.track{position:relative; height:13px; border-radius:2px; overflow:hidden;
       background:#0b1116; border:1px solid #182430; box-shadow:inset 0 1px 2px rgba(0,0,0,.6);}
.track .clip{position:absolute; top:0; left:0; height:100%; overflow:hidden;}
.track .grad{position:absolute; top:0; left:0; height:100%;}
.track.sig .grad{background:linear-gradient(90deg,#1f8f45 0%,var(--green) 42%,var(--amber) 74%,var(--red) 100%);}
.track.dev .grad{background:linear-gradient(90deg,var(--blue) 0%,var(--teal) 55%,var(--amber) 82%,var(--red) 100%);}
.track .peak{position:absolute; top:-1px; bottom:-1px; width:2px; background:#fff;
             box-shadow:0 0 5px rgba(255,255,255,.7); border-radius:1px;}
.track.dev .peak{background:var(--amber); box-shadow:0 0 6px var(--amber);}
/* A named landmark on a track: the one graduation a meter needs when a full scale would only
   repeat what a single mark already says. Drawn over the bar, so it stays readable once the
   trace has passed it -- which is precisely when it matters. */
.track .lim{position:absolute; top:0; bottom:0; width:1px; background:var(--ink); opacity:.55; z-index:2;}
.ruler{position:relative; height:11px; margin-top:2px; font-size:8px; color:var(--dim2);}
.ruler span{position:absolute; transform:translateX(-50%); top:3px;}
.ruler i{position:absolute; top:0; width:1px; height:3px; background:var(--dim2); transform:translateX(-.5px);}

canvas{display:block; width:100%;}
#spec,#fall{height:100%;}   /* overrides the implicit aspect ratio of a canvas element */

/* Header */
.hdr{display:flex; align-items:center; gap:14px; padding:8px 12px;}
.brand{font-weight:700; letter-spacing:.18em; font-size:13px; color:var(--ink);}
.brand b{color:var(--teal);}
/* The build, beside the name. Dim enough to read as a stamp rather than as a reading, and
   sized so it never competes with the instrument -- but present in any capture of the page. */
.ver{font-family:var(--mono); font-size:10px; color:var(--dim2); letter-spacing:.04em;
     margin-left:-8px; cursor:default;}
.badge{font-size:10px; color:var(--amber); border:1px solid #3a3217;
       background:#1a1709; border-radius:2px; padding:2px 6px; letter-spacing:.08em;}
/* The scale the page chose for itself, and the way back. Stated rather than applied in silence:
   a page that is not the size it was drawn at has to say so, or its type simply looks small for
   no reason anyone can check. Toned down to the panel palette -- it reports, it does not warn. */
.badge.fitz{color:var(--dim); border-color:var(--edge); background:#0d141b; cursor:pointer;
            font-family:var(--mono); white-space:nowrap;}
.badge.fitz:hover{color:var(--teal); border-color:#1f5048;}
/* Refused: the page is at full size on a screen that cannot hold it, which is the operator's
   call and stays visible as one. */
.badge.fitz.off{color:var(--dim2);}
/* Under the smallest screen the page is drawn for. It takes the warning colour every other
   badge already speaks, since here the scale alone no longer keeps the page whole. */
.badge.fitz.warn{color:var(--amber); border-color:#3a3217; background:#1a1709;}
.badge.fitz.warn:hover{color:var(--amber); border-color:#5c4f21;}
.hspacer{flex:1;}
.bigfreq{font-family:var(--mono); color:#eaf2f7; line-height:1; white-space:nowrap;}
.bigfreq b{font-size:34px; font-weight:600; letter-spacing:.02em;}
/* Tunable digits: hovering one and turning the wheel tunes that digit. */
.bigfreq .fd{cursor:ns-resize; border-radius:3px;}
.bigfreq .fd:hover{color:var(--teal); background:#13202b;}
.bigfreq .fdot{padding:0 1px;}
.bigfreq .fdvoid{visibility:hidden;}   /* integer padding: keeps the width, stays invisible */
.bigfreq i{font-size:13px; font-style:normal; color:var(--dim);
           letter-spacing:.18em; margin-left:8px;}
.freqline{display:flex; align-items:baseline; gap:12px; margin-bottom:8px;}
.stat{flex:0 0 auto; display:flex; align-items:center; gap:8px; font-family:var(--mono);}
.dot{width:8px; height:8px; border-radius:50%; background:var(--green);
     box-shadow:0 0 8px var(--green);}
.stat .mode{color:var(--dim); letter-spacing:.12em; font-size:12px;}
/* How far the named pass has got. Tabular figures, or the count jitters as the digits change
   under it and reads as a display that cannot settle. */
.stat .at{color:var(--teal); font-size:12px; letter-spacing:.06em; font-family:var(--mono);
     font-variant-numeric:tabular-nums;}
/* The two commands that end what the mode names. Scaled to the header line rather than to a
   bar, and given room from the readout so the state and its actions read as two things. */
.stat .btn{padding:4px 10px; font-size:10px; letter-spacing:.1em;}
.stat .btn:first-of-type{margin-left:6px;}

/* Containers of slots, side by side */
/* One module width, four of them across. A widget occupies exactly one within its family,
   wherever it sits, so moving it between slots reflows nothing inside it. The lateral panels
   take one module each; the middle column takes the two between them.
   ONE ROW. The grid places the three containers and stops there: every height inside them is a
   SHARE, traded on a boundary the operator drags. The row used to be split in two so the time
   graph could sit above the listening panel, which made that one boundary the only one on the
   page that could not be moved -- and the only one that could not line up with its neighbours.
   Floors live on the cards that need them, never here: a row capped in pixels sits at a
   different fraction of the page on every screen, so two lines that met at one height drift
   apart at another and an alignment the operator reached stops holding. */
/* A row item like the others -- it takes a share of the page's height and its boundary with the
   spectrum is dragged. Nothing mounts in it: it declares columns of its own, which is what keeps
   it out of the slots. */
.bottom{display:grid; gap:7px; grid-template-columns:repeat(4, minmax(0, 1fr));
        grid-template-rows:minmax(0,1fr);}
/* Its floor is what four containers of instruments need to stay readable. Three classes deep,
   because it has to outweigh the `min-height: 0` every row item carries -- and that rule lives
   in a sheet loaded after this one, so equal specificity would lose.
   A floor is stated in PIXELS only while it stays a minority of the page: past that it stops
   protecting a reading and starts pushing the bottom of the page out of a viewport that cannot
   scroll, which cuts in silence. Hence the second term -- the same floor, expressed as a share,
   whichever is smaller. The shares of the whole vertical chain are budgeted together, and the
   stylesheet test holds the sum. */
.app > .bottom[data-slot]{min-height:min(500px, calc(48 * var(--vh, 1vh)));}
.bottom > .left{grid-column:1; grid-row:1;}
.bottom > .midcol{grid-column:2 / 4; grid-row:1;}
/* The middle column is a container of slots like the lateral ones, so it carries the same
   class and would take the lateral placement -- which is more specific than its own rule
   above, and would drop it into the right-hand column. It is excluded by its own name. */
.bottom > .side:not(.left):not(.midcol){grid-column:4; grid-row:1;}
.ctxmenu{position:fixed; z-index:50; min-width:170px; padding:4px;
         background:var(--panel2); border:1px solid var(--edge); border-radius:7px;
         box-shadow:0 8px 24px rgba(0,0,0,.5); font-family:var(--mono); font-size:12px;}
.ctxitem{padding:7px 10px; border-radius:5px; color:var(--ink); cursor:pointer; white-space:nowrap;}
.ctxitem:hover{background:var(--teal); color:#04201c;}
/* Secondary, not refused: the entry is choosable, it simply is not free of consequence. */
.ctxitem.dim{color:var(--dim);}
.ctxitem.dim:hover{color:#04201c;}
/* A rule between two natures of choice, so a menu offering both does not read as one list. */
.ctxsep{height:1px; margin:4px 6px; background:var(--edge);}
/* A group carries a chevron, so an entry that opens a panel is told apart from one that acts
   before it is clicked rather than after. */
.ctxitem.sub{padding-right:22px; position:relative;}
.ctxitem.sub::after{content:"›"; position:absolute; right:9px; top:50%; transform:translateY(-50%);
                    color:var(--dim); font-size:14px; line-height:1;}
.ctxitem.sub:hover::after{color:#04201c;}
/* Four modules need the width of four modules -- four times the narrowest module the four-column
   state already accepts, plus the gaps and the page's padding. Below that the row folds rather
   than crushing every column at once: the middle column stays whole as long as it can, then
   everything stacks. Each step restates the placements, since a grid area declared for four
   columns is meaningless in two. The floor here is on the middle column, which holds two cards,
   and it takes the same share cap as every other floor on the page. */
/* Keyed on a CLASS and not on a viewport query, because the page is not always drawn at the
   viewport's size: scaled down, it is laid out in a box WIDER than the screen, and a media
   query cannot see that box -- it only ever reports the viewport. Left on a query, a screen
   1300px across folded the row away while the page it was drawing had 1645px of room for it,
   and the lateral panels disappeared for nothing. The script measures the box and says which
   state holds; exactly one of these classes is ever set. */
.app.cols2 .bottom{grid-template-columns:repeat(2, minmax(0, 1fr));
                   grid-template-rows:minmax(min(340px,calc(34 * var(--vh, 1vh))),2fr) minmax(0,1fr);}
.app.cols2 .bottom > .midcol{grid-column:1 / 3; grid-row:1;}
.app.cols2 .bottom > .left{grid-column:1; grid-row:2;}
.app.cols2 .bottom > .side:not(.left):not(.midcol){grid-column:2; grid-row:2;}
/* Everything stacks. It restates the placement of every container rather than leaning on the
   two-column rules above, since only one of the two classes is set at a time. */
.app.cols1 .bottom{grid-template-columns:1fr; grid-auto-rows:minmax(0,auto);
                   grid-template-rows:none;}
.app.cols1 .bottom > .side{grid-column:1; grid-row:auto;}
.app.cols1 .side{max-height:34vh;}
