/* ══════════════════════════════════════════════════════════════════════
   FORME — Macintosh, restrained

   The System 7 and Platinum *language*, without the period furniture. Four things
   carry all of it:

   * **Black on white, and lines that are actually black.** Frames are 1 px of
     `--line-firm`, which is `#000`. Nothing is a soft grey pretending to be an
     edge, and there is no colour doing a line's job.
   * **Selection inverts.** A chosen row, a latched button, a pressed button: the
     two colours swap. That is the whole gesture, and it is why none of it needs a
     gradient, a glow or a shadow to read.
   * **Shadows are hard.** One offset, no blur — `--shade` casts 2 px and stops.
     A blurred shadow is a lighting model, and this interface has no light in it.
   * **No decoration.** No rainbow bar, no pinstripes, no dithered scrollbars —
     those were the costume, and the costume is not the language. A colour in the
     interface means a material and nothing else.

   Type: **Chicago** for anything a maker presses or reads as a title, **Geneva**
   for prose, **Courier** for figures. Chicago has not shipped on a Mac since
   OS 9, so the stack falls to Geneva — which is still installed and is the other
   half of the same face. See `--chicago`.

   It has to work at half a window; see the breakpoints at the end.
   ══════════════════════════════════════════════════════════════════════ */

/* Courier Prime from this origin: Google's stylesheet held a cold browser's modules for 137-2299 ms. The files
   are @fontsource/courier-prime 5.3.0, named by their hash, under the licence in `fonts/OFL.txt`. Google's
   ranges, so a face is fetched only for text that uses it. */
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/CourierPrime-400-latin-ext.efbea074e3d8.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
    U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/CourierPrime-400-latin.f09844e1f4fe.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/CourierPrime-700-latin-ext.b7ce6854b908.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
    U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/CourierPrime-700-latin.5c7ae1cc9aae.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light;

  /* Paper. A window face is white. */
  --screen:     #ffffff;
  --raised:     #ffffff;
  --well:       #eeeeee;
  --viewport:   #f2f2f2;

  /* Ink. Body text was pure black, and greys were for what you cannot press. */
  --emit-100:   #000000;
  --emit-90:    #000000;
  --emit-80:    #1c1c1c;
  --emit-70:    #333333;
  --emit-60:    #4d4d4d;
  --emit-50:    #5c5c5c;
  --emit-40:    #949494;
  --emit-30:    #d4d4d4;
  --on-fill:    #ffffff;

  /* `--line` separates inside a window; `--line-firm` is a frame, and a frame is
     black. That distinction is the whole of why this reads as crisp rather than
     as heavy: a list has no black rules in it, but the window around it does. */
  --line:       #c8c8c8;
  --line-firm:  #000000;

  /* There is no accent. Aqua's blue is 2001; this era marks a choice by inverting
     it. The `--blue-*` names stay because a hundred rules use them, and every one
     of them now resolves to ink — see the header. */
  --blue-100:   #000000;
  --blue-90:    #000000;
  --blue-70:    #000000;
  --blue-50:    #949494;
  --blue-30:    #e4e4e4;

  /* The three states that mean something. `--viewport` is read by `viewer.js` for the canvas ground, so
     it is used from JavaScript rather than from a rule here. */
  /* The mark. One colour the program owns, on the favicon and on the button that opens the menu — and
     nowhere else, so it is never mistaken for a state. Not `--err-90`: a brand mark in the error colour
     makes every alert look like the logo. */
  --mark:       #e02b1d;

  --err-90:     #aa0000;
  --err-50:     #d0453c;   /* a lighter red, for a message a maker reads rather than one that stops them */
  --warn:       #8a6212;
  --done:       #006600;

  /* Ink does not emit. */
  --glow-text:  none;
  --glow-text-dim: none;
  --glow-blue:  none;
  --glow-blue-bright: none;
  --glow-err:   none;

  /* Hard, and offset. A blurred shadow is a lighting model; there is no light
     here. This is the same `2px 2px 0` that was pulled out earlier for looking
     dated — it is the point now. */
  --shade:      2px 2px 0 rgba(0, 0, 0, .32);
  --lift:       1px 1px 0 rgba(0, 0, 0, .28);

  /* Chicago for what you press and what titles a thing; Geneva for prose. Chicago
     has not shipped since OS 9, so this lands on Geneva, which is still on every
     Mac and is the same drawing at a smaller size. */
  --chicago: Chicago, 'ChiKareGo2', 'Charcoal', Geneva, Verdana, sans-serif;
  --geneva:  Geneva, Verdana, 'DejaVu Sans', Tahoma, sans-serif;
  --mono:    'Courier Prime', 'Courier New', Courier, monospace;
  --sans:    var(--geneva);
  --label:   var(--chicago);

  --t-label: 11px;
  --t-small: 11px;
  --t-body:  12px;
  --t-read:  13px;
  --t-title: 15px;

  /* Square. Everything. A rounded push button among square fields and square lists is a mix, and the
     mix is what reads as unfinished — one shape, and a button is told apart by its outline and by
     inverting when pressed. */
  --r: 0;

  --paper-sheet: #ffffff;
}

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

/* `hidden` is a UA `display: none`, which any author `display` outranks. Declared once here rather than
   per element: it was fourteen `.thing[hidden]` rules and the fifteenth was missing, which is why a
   mode with no tools showed an empty box floating over the canvas. */
[hidden] { display: none !important; }

html, body { height: 100%; overflow: hidden; }

body {
  background: var(--screen);
  color: var(--emit-90);
  /* 13px, not 22. The old size was a readout on a terminal; this is an interface, and at 22px a
     panel held four settings. */
  font: var(--t-body)/1.45 var(--sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  display: flex;
  flex-direction: column;
}

/* Numbers are read in columns, so they line up. Everything else is prose. */
.mono, .metric-value, .param-value { font-family: var(--mono); }


@media (prefers-reduced-motion: reduce) {
  /* Asked for no motion, so there is none — not a shorter one. */
  *, *::before, *::after {
    animation-duration: 0s !important;
    animation-delay: 0s !important;
    transition-duration: 0s !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   DESIGN LIBRARY OVERLAY
   ══════════════════════════════════════════════════════════════════════ */

.library {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Neutral, like everything else. This was the old blue theme's. */
  background: rgba(20, 24, 28, .34);
}

/* A square, and a fixed one. It took its height from whatever section was showing, so choosing a tab
   resized the whole modal under the pointer. One size, and only the right-hand side scrolls.
   `86vh` is in the min so the square shrinks on a short window rather than running off the bottom. */
.library-panel {
  --files: min(620px, 92vw, 86vh);
  width: var(--files);
  height: var(--files);
  display: flex;
  flex-direction: column;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
}

/* Sections down the left, what they hold on the right. */
.library-split {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: 128px minmax(0, 1fr);
}

.lib-rail {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 8px 0;
  border-right: 1px solid var(--line-firm);
  background: var(--well);
  overflow-y: auto;
}

/* A row in a rail, not a segment in a control: full width, left-aligned, and never elided — the labels
   were being cut off because three of them were sharing the width of a title bar. */
.lib-tab {
  padding: 6px 10px 6px 9px;
  border: 0;
  border-left: 3px solid transparent;
  background: none;
  color: var(--emit-70);
  font: var(--t-body) var(--sans);
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.lib-tab:hover { background: var(--raised); color: var(--emit-100); }
.lib-tab:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: -2px; }

/* The one showing: a bar down its edge and the page's own ground, so the rail reads as attached to it. */
.lib-tab.on {
  border-left-color: var(--emit-100);
  background: var(--raised);
  color: var(--emit-100);
  font-weight: 700;
}

/* On a phone the rail took 128 of a 359 px panel and left a design's row no room for its name. The sections
   go across the top, and the panel is taller than it is wide, still one size whatever the section. */
@media (max-width: 600px), (max-height: 500px) {
  .library-panel { height: min(640px, 86vh); }
  .library-split { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  .lib-rail {
    flex-direction: row;
    flex-wrap: wrap;
    padding: 0 6px;
    border-right: 0;
    border-bottom: 1px solid var(--line-firm);
    overflow: visible;
  }
  .lib-tab { border-left: 0; border-bottom: 3px solid transparent; padding: 8px 8px 5px; }
  .lib-tab.on { border-bottom-color: var(--emit-100); }
}

.library-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-firm);
}

.library-head .push { flex: 1 1 auto; }

.library-body {
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow-y: auto;
}

/* One section's worth. The rows inside use the same `.param` grid the inspector does — the overlay had
   spacing of its own and read as a different program. */
.lib-block { display: flex; flex-direction: column; gap: 2px; }

.lib-block .param { grid-template-columns: 96px minmax(0, 1fr); }

/* A fact, as against a control: right of its label, in the figures face, so a column reads down. */
.lib-value {
  font-family: var(--mono);
  font-size: var(--t-body);
  color: var(--emit-100);
  overflow: hidden;
  text-overflow: ellipsis;
}

.lib-block .param-actions { margin-top: 6px; }

.lib-onboard {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.lib-onboard .input {
  flex: 1 1 180px;
  min-width: 0;
}

/* A list of designs is a list: one row a line, divided by a hairline, the name doing the work. Eight
   cards with 8 px between them is eight things to look at instead of one list to read down. */
.lib-list { display: flex; flex-direction: column; }

/* A label over a list, with its count in it. Several lists in one section read as one long run of
   unrelated rows without these.
   The count is *in the heading* rather than a sentence under an empty list. "NOBODY INVITED" was a line of
   prose at 15 px — larger than this heading and larger than the rows it described — where "INVITED 0" is
   the same fact in the place you are already reading. */
.lib-heading {
  margin: 16px 0 4px;
  font-size: var(--t-label);
  color: var(--emit-50);
  letter-spacing: .08em;
}
.lib-heading:first-child { margin-top: 0; }
.lib-heading .lib-count { margin-left: 6px; color: var(--emit-90); }

/* What a list is, under its heading, so Designs and Lasts do not read as one kind of thing. */
.lib-caption { margin: -2px 0 4px; font-size: var(--t-small); color: var(--emit-50); }

.lib-list > .lib-last-row + .lib-last-row { border-top: 1px solid var(--line); }

.lib-last {
  font-family: var(--mono);
  font-size: var(--t-read);
  color: var(--emit-90);
  background: none;
  border: 0;
  border-radius: var(--r);
  padding: 9px 10px;
  text-align: left;
  cursor: pointer;
}

.lib-last:hover { background: var(--well); color: var(--emit-100); }

/* Only where a list being empty needs explaining rather than counting — see `.lib-heading .lib-count`.
   It was `--t-title`, 15 px: bigger than the headings above it and the rows below it, which is what made
   the section read as random sizes thrown together. */
.lib-empty {
  font-size: var(--t-body);
  color: var(--emit-50);
  padding: 8px 0;
}

/* A row that is text rather than a thing to press: a member, a design somebody else owns.
   It used `.lib-last`, which is a button — so it took a hover and a highlight, and a maker could
   half-select their own name in a list that does nothing when clicked. */
.lib-row-text {
  font-family: var(--mono);
  font-size: var(--t-read);
  color: var(--emit-90);
  padding: 9px 10px;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  /* The words, then the note (a role, a date) under them where both do not fit on one line, and an address
     with nowhere to break broken anywhere: kept on one line, "member" ran on under MAKE ADMIN. */
  flex-wrap: wrap;
  overflow-wrap: anywhere;
  column-gap: 18px;
  row-gap: 2px;
}
/* The gap above is the space beside the words, so a note on a line of its own starts under them. */
.lib-row-text > .lib-when { margin-left: 0; }

/* A labelled field: the label above, both hard left. A row of bare inputs with placeholders inside them
   says what a field is only until it has something in it. */
.lib-field {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  flex: 1 1 200px;
  min-width: 0;
}
.lib-field > .dl {
  font-size: var(--t-label);
  color: var(--emit-50);
  letter-spacing: .06em;
  text-align: left;
}
.lib-field > .input { width: 100%; text-align: left; }

/* The file picker, made to sit in a row with the fields beside it. It was three loose controls — a CHOOSE
   FILE button, the chosen name floating off to the right, and an add button — which is what "the UI is a
   little weird and janky" was. One labelled control, and the name field fills itself in from the file. */
.lib-field > input[type="file"] {
  width: 100%;
  padding: 2px 0;
  border: 0;
  border-bottom: 1px solid transparent;
  background: none;
  color: var(--emit-70);
  font: var(--t-label) var(--mono);
}
.lib-field > input[type="file"]:hover { border-bottom-color: var(--line-firm); }
.lib-field > input[type="file"]::file-selector-button {
  margin-right: 8px;
  padding: 3px 8px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raised);
  color: var(--emit-80);
  font: var(--t-label) var(--mono);
  letter-spacing: .06em;
}

/* The forms that hold them: stacked, so a label is over its own field and not beside somebody else's. */
.lib-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  padding: 2px 0 6px;
}

/* Where a list of addresses is pasted. Two lines high, so it reads as taking more than one without being a
   box that dominates the form it sits in. */
.lib-many {
  width: 100%;
  min-height: 42px;
  padding: 4px;
  text-align: left;
  resize: vertical;
  line-height: 1.4;
}

/* Which last a design is drawn on, beside its name. Quiet: it is the answer to "what is this", which is
   read after the name rather than instead of it. */
.lib-tag {
  margin-left: 8px;
  padding: 1px 5px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  font-family: var(--mono);
  font-size: var(--t-label);
  color: var(--emit-50);
  white-space: nowrap;
}

/* Naming a design as it is started, in the shelf row it is started from. */
.lib-naming {
  flex: 1 1 180px;
  min-width: 140px;
  width: auto;
  text-align: left;
  padding: 3px 4px;
}

/* The foot of a shelf: ADD NEW LAST, and the fields it becomes. Its own row so the button sits under the
   list it adds to rather than beside the last row in it. */
.lib-add {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 8px 0 2px;
}
.lib-add > .lib-form { flex: 1 1 100%; padding: 0; }

/* An invitation's code, on a line of its own under the address it is for: read aloud or pasted, so in the
   figures face and wide enough to show whole. */
.lib-last-row:has(> .lib-code) { flex-wrap: wrap; }
.lib-code {
  flex: 1 1 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 0 10px 8px;
}
/* Two classes, to outweigh `.input`'s 60 px, which comes later in the sheet and cut the code to half. */
.input.lib-code-value {
  width: 18ch;
  font-family: var(--mono);
  color: var(--emit-100);
  letter-spacing: .06em;
  text-align: left;
}
.lib-code .lib-caption { flex: 1 1 100%; margin: 0; }

/* An invitation to answer: the workspace on one line, and under it the code's field with ACCEPT, which sends
   it, and DECLINE. All on one line, DECLINE was pushed onto a line of its own. */
.lib-last-row.lib-answer { flex-wrap: wrap; align-items: flex-end; padding-bottom: 8px; }
.lib-answer > .lib-row-text { flex: 1 1 100%; padding-bottom: 2px; }
.lib-answer .lib-field { flex: 0 1 18ch; margin-left: 10px; }

/* Where a shape is shown after it is added, or when its row is pressed. Holds its height whether or not
   anything is in it, so pressing a row does not push the list under the cursor. */
.lib-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 130px;
  padding: 4px 0;
}
.lib-thumb {
  width: 260px;
  height: 130px;
  background: var(--viewport);
  border: 1px solid var(--line);
  border-radius: var(--r);
}

/* ══════════════════════════════════════════════════════════════════════
   WORKSPACE — design tree, viewport, inspector, all live at once.
   Replaces the bottom step deck: the document is the program, not the steps.
   ══════════════════════════════════════════════════════════════════════ */

.app {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Three columns either way, so folding a panel never reflows the viewport into a
   different element — the canvas keeps the one ResizeObserver already on it. */
/* Two columns: the viewport, then the panel. It was three, and the left one stayed after the panel in
   it was deleted — a 260 px empty column beside a squeezed viewport, which is 560 px of a laptop's
   width gone with 300 px of it showing anything. */
.workspace {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  display: grid;
  /* `auto`, and the panel carries its own width. A track sized `var(--right-w)` reserves those 320 px
     whether or not anything is in it — so hiding the panel in Design left a 320 px strip of nothing beside
     the canvas, which is the panel still taking the room after it had gone. */
  grid-template-columns: minmax(0, 1fr) auto;
}

.stage {
  position: relative;
  min-width: 0;
  overflow: hidden;
}

/* Pinned to the stage box on both axes.
   A canvas has an intrinsic aspect ratio from its width/height attributes, and with
   only `width: 100%` set its height followed that ratio instead of its container:
   folding a side panel widened the stage to 1228px and the canvas grew to 1076px
   tall inside a 430px box, so the last was rendered off the bottom of the screen and
   the viewport looked empty. */
/* The veil over the stage while a design is opening.
   The same colour as the scene's own background, so what a maker sees is the viewport it is about to be —
   not a panel over it. It goes when the last is there and framed, and it fades rather than cutting, because
   the thing it is hiding is a camera move and a hard cut draws the eye to exactly what it is covering. */
.veil {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  place-items: center;
  background: var(--viewport, #e4e7ea);
  animation: veil-in 120ms ease-out;
}

.veil .dl { opacity: 0.5; letter-spacing: .12em; }

@keyframes veil-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

#viewport {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* ── Side panels ── */

/* A column of three: what is in the design, what is selected, and its settings. A *column*, because
   everything below depends on it — a body scrolls by being a flex child that is allowed to shrink, and
   as a block it sizes to its content instead, so the panel runs off the bottom of the screen with no way
   to reach the end of it. The rule was lost to an over-greedy regex earlier and nothing said so: no
   error, no missing element, just a panel that would not scroll. */
.panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  background: var(--raised);
  position: relative;
}

/* Exactly one of the two bodies grows, and it is the settings. Both growing made two half-height
   scrollers out of one panel: a list of five rows got as much room as the piece being worked on. */
.panel-grow { flex: 1 1 0; }

/* The tree takes what its rows need, capped so a long list cannot push the settings off the panel.
   `:not()` and not a plain `.panel-body` cap that `.panel-grow` overrides: at equal specificity the
   later rule wins, and `.panel-grow` is declared first — so the cap applied to the settings body as
   well and the bottom half of the panel was blank. */
.panel-body:not(.panel-grow) { max-height: 45%; }

/* Between the two halves of the one panel: what is in the design, then what is selected in it. */
.panel-split {
  flex: 0 0 auto;
  padding: 10px 14px 6px;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--well);
}

.panel-split .dl { font-size: var(--t-label); letter-spacing: .06em; color: var(--emit-90); font-weight: 600; }

.panel-right {
  width: var(--right-w, 320px);
  border-left: 1px solid var(--line-firm);
}

.panel-note {
  flex: 0 0 auto;
  padding: 8px 12px 0;
  font-size: var(--t-body);
  line-height: 1.3;
  color: var(--emit-50);
}

.panel-note:empty { display: none; }

/* Neither body grows by default and neither is capped here: `.panel-grow` grows the settings and
   `.panel-body:not(.panel-grow)` caps the tree, both above, so the two decisions are in one place. */
.panel-body {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 8px 10px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* ── The design tree ── */

/* The three jobs of designing, as a switcher at the top of the panel. A job is not a thing in the
   document — it is which question you are asking about the pieces — so it is a switch and not a row in a
   list, and switching costs one click from anywhere. */
/* ── The job switcher, over the canvas ──
   Design has no panel, so the switcher cannot live in one: three buttons do not justify a quarter of the
   window. Top centre, clear of the undo pair at the left and the foot toggle at the right. */
.jobs.floating {
  position: absolute;
  left: 50%;
  top: 12px;
  transform: translateX(-50%);
  margin: 0;
  z-index: 3;
  box-shadow: var(--shade);
  animation: jobs-in .16s ease-out;
}

@keyframes jobs-in {
  from { opacity: 0; transform: translate(-50%, -6px); }
  to { opacity: 1; transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .jobs.floating { animation: none; }
}

.jobs {
  display: flex;
  gap: 1px;
  margin: 0 0 6px;
  padding: 2px;
  background: var(--well);
  border: 1px solid var(--line-firm);
}

/* As wide as its own word. It was `flex: 1 1 0`, an equal third each — right for a switcher filling a
   panel, and wrong the moment this floated: with no column to fill, the third was the width of the widest
   *box*, and after 4 px of padding PATTERNS and SECTIONS both ran out of their own 57 px and elided while
   DRAW sat in space. `overflow: hidden` is what let them: it zeroes a flex item's automatic minimum size,
   so the words could be squeezed below their own length. Neither is here now — there is nothing to elide
   when a button is the size of its label. */
.job {
  flex: 0 0 auto;
  padding: 4px 12px;
  border: 0;
  background: none;
  color: var(--emit-50);
  font: var(--t-label)/1 var(--label);
  letter-spacing: .06em;
  cursor: pointer;
  white-space: nowrap;
}

/* The sign-in switcher does fill a panel: two tabs of equal width across a fixed column read as one
   control, where two words centred in it read as two buttons that happen to be adjacent. */
#auth-jobs .job { flex: 1 1 0; padding: 4px 2px; }

.job:hover { color: var(--emit-100); }

/* The one you are in inverts, like everything else chosen here. */
.job.on { background: var(--emit-100); color: var(--on-fill); }
.job:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: -2px; }
/* A job others do not all have, which only the owner is shown: a dot after its word, its title saying who has it. */
.job[data-limited]::after {
  content: '';
  display: inline-block;
  width: 4px;
  height: 4px;
  margin-left: 5px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: middle;
}

.tree-heading {
  padding: 12px 4px 4px;
  border-top: 1px solid var(--line);
  margin-top: 6px;
}

.tree-heading:first-child { border-top: 0; margin-top: 0; }

.tree-row {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 8px;
  background: none;
  border: 0;
  border-radius: var(--r);
  color: var(--emit-90);
  font: var(--t-body) var(--mono);
  text-align: left;
  cursor: pointer;
}

.tree-row:hover { background: var(--well); }

/* Selected inverts. That is how a Macintosh marked a chosen row, and it is unmistakable at any size
   without a colour, a frame or a shadow. */
.tree-row.selected {
  background: var(--emit-100);
  color: var(--on-fill);
}

.tree-row.selected .tree-detail { color: var(--on-fill); }

/* The name above its detail: "needs bottom of the cut, bolt centre" does not fit
   beside a name in a 260px panel, and side by side the name was squeezed away. */
.tree-row > .tree-name,
.tree-name-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
}

.tree-name { flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.tree-detail {
  flex: 0 0 auto;
  font-family: var(--label);
  font-size: var(--t-label);
  line-height: 1.35;
  color: var(--emit-50);
  text-shadow: none;
}

/* A modifier row is a toggle, a name and a remove, so it is a div of buttons. */
.tree-row.modifier { padding: 0; align-items: stretch; }

.tree-name-btn {
  flex: 1 1 auto;
  justify-content: center;
  padding: 6px 4px;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.tree-toggle,
.tree-remove {
  flex: 0 0 auto;
  padding: 6px 8px;
  background: none;
  border: 0;
  color: var(--emit-80);
  font: var(--t-body) var(--mono);
  cursor: pointer;
}

.tree-toggle[aria-pressed="false"] { color: var(--emit-40); }
.tree-remove:hover { color: var(--err-90); }

/* On the selected row, which is inverted, the controls take the row's own ink: in grey they were not there. */
.tree-row.selected .tree-toggle,
.tree-row.selected .tree-remove,
.tree-row.selected .tree-move { color: var(--on-fill); }
.tree-row.selected .tree-move:disabled { color: var(--emit-50); }
.tree-row.selected .tree-remove:hover { color: #ffb3ad; }
.tree-row.modifier[data-state="refused"] .tree-detail { color: var(--err-90); }
.tree-row.selected.modifier[data-state="refused"] .tree-detail { color: #ffb3ad; }

/* Moving a row moves the cut, so the control belongs on the row itself. */
.tree-order {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.tree-move {
  padding: 0 4px;
  background: none;
  border: 0;
  color: var(--emit-70);
  font: var(--t-label) var(--mono);
  line-height: 1.1;
  cursor: pointer;
}

.tree-move:hover:not(:disabled) { color: var(--emit-100); }
.tree-move:disabled { color: var(--emit-30); cursor: default; }

/* A modifier past the rollback bar is in the design but not in the view. */
.tree-row.modifier.rolled-back { opacity: .4; }
.tree-row.modifier.rolled-back .tree-name { text-decoration: line-through; }

/* ── The rollback bar ── */

.timeline {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--line);
}

.timeline .dl { flex: 1 1 auto; }

.timeline-at {
  font: var(--t-read) var(--mono);
  color: var(--emit-70);
  font-variant-numeric: tabular-nums;
}

/* Rolled back is a state the user must not mistake for the finished model. */
.timeline.active { border-top-color: var(--err-90); }
.timeline.active .dl { color: var(--err-90); text-shadow: var(--glow-err); }
.timeline.active .timeline-at { color: var(--err-90); }

/* ── HINGES: the stack, then the modifier selected in it ── */

.modify-stack { display: flex; flex-direction: column; gap: 2px; }
.modify-one { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line); }
.modify-head { display: flex; flex-direction: column; gap: 2px; padding: 0 0 6px; }

/* ── Add menu ── */

.tree-add { position: relative; margin-top: 6px; }
.tree-add > .key { width: 100%; }

.add-menu {
  position: fixed;
  z-index: 70;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
}

.add-menu[hidden] { display: none; }

.add-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 10px;
  background: var(--well);
  border: 0;
  border-top: 1px solid var(--line);
  text-align: left;
  cursor: pointer;
}

.add-item:hover:not(:disabled) { background: var(--emit-30); }

.add-item:disabled { cursor: not-allowed; opacity: .5; }

.add-title { font: var(--t-read) var(--mono); color: var(--emit-90); }

.add-why { font: var(--t-body) var(--mono); color: var(--emit-50); line-height: 1.25; }

/* ── Inspector parameters ── */

/* ── One setting, on one line ──
   Label left at a fixed width, control right. As a column with the label above it, every setting was
   two lines tall and bordered off from the next: eleven of them on a piece came to a panel a maker had
   to scroll to find DELETE in, with no two of them looking related. The grouping does the separating
   now, so a row has no rule of its own. */
.param {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  align-items: center;
  gap: 4px 8px;
  padding: 3px 0;
}


/* Every control starts at the same x, whatever it is. A row of controls that each begin where their own
   width happens to put them is the awkwardness — nothing lines up with anything. */
.param > .segments,
.param > .swatches,
.param > .check,
.param > .param-actions { justify-self: start; }

/* A setting whose control needs the width — a slider, a row of chips — puts the label above it. */
.param.wide { grid-template-columns: minmax(0, 1fr); gap: 2px; }

.param-label {
  font-family: var(--label);
  font-size: var(--t-label);
  letter-spacing: .08em;
  color: var(--emit-50);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.param.needed .param-label { color: var(--err-90); text-shadow: var(--glow-err); }

/* A setting's control fills its column and is sized for a row: 20 px of text in a form is a number a
   maker types once, and 8 of them down a panel is the bulk this was about. */
.param .input {
  width: 100%;
  text-align: left;
  font-size: var(--t-body);
  padding: 2px 6px;
  border-width: 1px;
}

.param-point { display: flex; align-items: center; gap: 8px; }

.param-readout {
  flex: 1 1 auto;
  font-size: var(--t-read);
  color: var(--emit-70);
  font-variant-numeric: tabular-nums;
}

.param-clear {
  background: none;
  border: 0;
  color: var(--emit-50);
  font: var(--t-read) var(--mono);
  cursor: pointer;
}

.param-clear:hover { color: var(--err-90); }

/* A field's unit, after its label: "Depth mm". */
.param-unit { color: var(--emit-40); letter-spacing: .04em; text-transform: none; }

.param-help {
  font-size: var(--t-small);
  line-height: 1.35;
  color: var(--emit-50);
  padding: 1px 0 3px;
}

.param-actions { display: flex; gap: 4px; flex-wrap: wrap; padding: 2px 0; align-items: center; }

/* One button per axis, each showing where that axis is turned to. The angles were a fourth thing on the
   row — `X0° Y0° Z0°` with no column of its own, so they fell under the label and floated there. */
.axes .axis {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  padding: 4px 7px;
  background: var(--well);
  font-family: var(--mono);
}

.axes .axis b {
  font-weight: 400;
  color: var(--emit-50);
  letter-spacing: 0;
}

.axes .axis:hover b { color: var(--emit-90); }

/* The saved note, beside the design's name. Quiet on purpose: it is there to be glanced at, not read.
   A program that autosaves should say so once and never ask for anything. */
.saved {
  font-size: var(--t-label);
  letter-spacing: .04em;
  color: var(--emit-50);
  white-space: nowrap;
  /* As wide as its words, from 96 px so a short note appearing does not shift what follows. It may be given
     up to an ellipsis, because a reassurance is worth less than the modes it was pushing off the bar. */
  flex: 0 1 auto;
  min-width: 96px;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The bar's two quiet notes, one treatment: when it saved, and whose it is. Over `.dl`, which the saved note
   also carries and which set it black and widely tracked; the states below still colour it. */
.toolbar .saved,
.toolbar .held {
  font: var(--t-label) var(--label);
  letter-spacing: .04em;
  color: var(--emit-50);
  text-shadow: none;
}

.saved[data-state="failed"] { color: var(--err-90); }
/* A conflict, or a design gone from the server, waits on the maker, and clicking the note asks again. */
.saved[data-state="conflict"],
.saved[data-state="gone"] { color: var(--err-90); cursor: pointer; }

/* Nowhere to save is the state a maker most needs to see, so it reads like the error it is rather than like
   a note. It was reachable for a whole session with nothing on screen saying so. */
.saved[data-state="nowhere"] {
  color: var(--err-90);
  font-weight: 600;
}

/* Saving and unsaved are transient and must not shout — what they are for is telling a saved design from one
   that only looks saved. */
.saved[data-state="saving"],
.saved[data-state="pending"] { opacity: .7; }

/* Whose a workspace design is. As quiet as the saved note, and the first thing in the bar to give up room.
   The wordmark is centred over the bar and out of the flex flow, so the cap is what holds this off it: half
   the bar, less half the wordmark and the size and mode buttons to its right. */
.held {
  font-family: var(--label);
  font-size: var(--t-label);
  letter-spacing: .04em;
  color: var(--emit-50);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 0;
  max-width: calc(50vw - 410px);
}
/* Narrower, "you can edit" alone; narrower still, nothing, until 880 px, where the bar has no wordmark. */
@media (max-width: 1180px) { .held-lead { display: none; } }
@media (min-width: 881px) and (max-width: 1000px) { .held { display: none; } }
@media (max-width: 880px) { .held { max-width: none; } }

/* When a design was last worked on, at the right of its row in Files. */
.lib-when { margin-left: 8px; color: var(--emit-50); font-size: var(--t-small); }

/* ── The mode switcher ──
   In the title bar, where a drawing program puts its modes. Three segments and not a dropdown: there
   are three, they are the top-level thing about the program, and a menu would hide two of them. */
/* A segmented control, the way a title bar carries modes. Padding of its own, so the selected segment
   is a filled pill inside the track rather than a block butted against its neighbours. */
.modes {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--well);
}

.mode {
  padding: 6px 14px;
  border: 0;
  border-radius: var(--r);
  background: none;
  color: var(--emit-50);
  font: var(--t-label)/1 var(--label);
  letter-spacing: .1em;
  cursor: pointer;
  transition: background .12s ease, color .12s ease;
}

.mode:hover { color: var(--emit-100); }

/* Filled in ink, not in `--raised` with a `--glow-box` — that token is `none` in this theme, so the
   mode you were in was white on near-white and the most important control on screen said nothing.
   Ink and not the accent: the accent means "a value you set", and which mode you are in is where you
   are. Two jobs, two colours, so a segmented control in a panel is never mistaken for navigation. */
.mode.on {
  background: var(--emit-100);
  color: var(--on-fill);
}

.mode.on:hover { background: var(--emit-90); color: var(--on-fill); }

/* ── The mode's own tools ──
   Floating at the foot of the canvas rather than in a panel: what a maker reaches for while drawing
   belongs where they are looking. Centred, clear of the status bar, and never over the last itself —
   the viewport frames the last well above this. */
/* ══════════════════════════════════════════════════════════════════════
   THE PIECES LIST

   The pieces, down the right of the canvas: one row each, with the piece's name and a tab in the leather it is
   made of. The last is the picture: a click on a row focuses the piece, and the last dims the others.

   **Nothing in here moves unless the maker moved it.** A maker: "I don't like the maximum view, like it scales it
   up when you're trying to select it." So each kind of row has one size in every state: a state changes the
   frame, the fill and the ink, never the box. A drag opens a gap where the piece will go.
   ══════════════════════════════════════════════════════════════════════ */

.rail {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  /* Against the canvas: a tall design must not run off the top and bottom of it. */
  max-height: calc(100% - 96px);
  overflow-y: auto;
  overflow-x: visible;
  /* No card. A panel drawn round the rows is the side panel this replaces, moved. */
  background: none;
  border: 0;
  padding: 2px;
}

.rail-piece { position: relative; display: flex; align-items: center; justify-content: flex-end; }

/* One piece: a button, because it picks the piece. The tab is 6 px from the left edge. */
.rail-dot {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  width: 160px;
  height: 28px;
  padding: 0 7px 0 5px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raised);
  color: var(--emit-90);
  font: var(--t-body)/1.2 var(--label);
  text-align: left;
  /* Grab, not pointer: the list reorders by drag. */
  cursor: grab;
  /* No transition on size or transform, because neither changes. */
  transition: border-color .12s ease;
}

/* The leather, shorter than the row. The inner frame shows a white piece on a white row. */
.rail-tab {
  flex: 0 0 auto;
  width: 6px;
  height: 16px;
  background: var(--dot, var(--emit-30));
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .2);
}

.rail-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* A long name: its start is cut short and its end always shows, so names that start the same differ on the row. */
.rail-name:has(> .rail-name-end) { display: flex; text-overflow: clip; }
.rail-name-start { flex: 0 1 auto; min-width: 0; overflow: hidden; white-space: pre; text-overflow: ellipsis; }
.rail-name-end { flex: 0 0 auto; white-space: pre; }

/* How many sub-pieces are under a row while they are hidden. */
.rail-count {
  flex: 0 0 auto;
  font: var(--t-small)/1 var(--mono);
  color: var(--emit-50);
}

/* A cut-out is a hole in the last's paint, so its tab is a frame with nothing in it. White inside, also on a picked
   row, where a black piece's filled tab and its hollow one would be the same black. */
.rail-piece.cut-out .rail-tab {
  background: var(--raised);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .2), inset 0 0 0 2px var(--dot, var(--emit-30));
}

/* A sub-piece: under its parent, 14 px in for each parent above it, smaller and in smaller type. */
.rail-piece.sub .rail-dot {
  width: calc(160px - 14px * var(--depth, 1));
  height: 22px;
  gap: 5px;
  font-size: var(--t-small);
}
.rail-piece.sub .rail-tab { width: 5px; height: 12px; }
.rail-piece.sub .rail-name-field { font-size: var(--t-small); padding-left: 16px; }

.rail-dot:hover { border-color: var(--emit-60); }
.rail-dot:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: 2px; }

/* Picked: the row inverts, as a chosen row does everywhere in this interface. */
.rail-piece.on .rail-dot {
  background: var(--emit-100);
  border-color: var(--line-firm);
  color: var(--on-fill);
}
.rail-piece.on .rail-tab { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5); }
.rail-piece.on.cut-out .rail-tab {
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5), inset 0 0 0 2px var(--dot, var(--emit-30));
}
.rail-piece.on .rail-count { color: var(--on-fill); }

/* Being drawn: a state, not a choice, so it is told apart at a glance. Not a pulse: motion beside the work. */
.rail-piece.drawing .rail-dot {
  background: var(--raised);
  border-color: var(--mark);
  color: var(--mark);
}
.rail-piece.drawing .rail-tab { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .2); }
.rail-piece.drawing.cut-out .rail-tab {
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .2), inset 0 0 0 2px var(--dot, var(--emit-30));
}
.rail-piece.drawing .rail-count { color: var(--mark); }

/* The name, open to change: a field of the row's size over the row, its text where the name was. The tab shows
   over it. */
.rail-name-field {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  padding: 0 7px 0 17px;
  border: 1px solid var(--line-firm);
  border-radius: var(--r);
  background: var(--raised);
  color: var(--emit-90);
  font: var(--t-body)/1.2 var(--label);
  outline: none;
}
.rail-piece:has(> .rail-name-field) .rail-tab { position: relative; z-index: 2; }
.rail.says .rail-name-field { pointer-events: auto; }

/* What the engine said about a piece, beside its row. A piece that will not cut says why in the error red and a red
   frame, always; a warning is a dot at the right end, and its words show only for the picked piece.
   The note is inside the button, so the row's box is still the button's, and the rail makes room for it on the
   left. Clicks go through that room to the model: only the rows are for pressing. */
.rail.says { padding-left: 250px; pointer-events: none; }
.rail.says .rail-dot { pointer-events: auto; }
.rail-dot[data-says="refused"] { border-color: var(--err-50); }
.rail-dot[data-says="warn"] { padding-right: 15px; }
.rail-dot[data-says="warn"]::after {
  content: '';
  position: absolute;
  top: calc(50% - 2.5px);
  right: 5px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--warn);
}
.rail-piece.on .rail-dot[data-says="warn"]::after { box-shadow: 0 0 0 1px var(--on-fill); }
.rail-note {
  position: absolute;
  right: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%);
  width: max-content;
  max-width: 232px;
  padding: 0 4px;
  background: color-mix(in srgb, var(--viewport) 88%, transparent);
  color: var(--err-90);
  /* One short line in the face of the findings list; the engine's sentences are the row's tooltip. */
  font: var(--t-body)/1.35 var(--sans);
  text-align: right;
  white-space: nowrap;
  pointer-events: none;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rail-note.warn { color: var(--emit-50); }

/* ── Dragging one up or down the order ──
   The row follows the pointer and the others open a gap where it would land. A gap, not a rule: a 2 px black line
   between two rows was the loudest thing on the screen. The others stay as they are. */
.rail-piece.lifting { z-index: 2; }
.rail-piece.lifting .rail-dot {
  cursor: grabbing;
  transform: translateY(var(--lift, 0));
  /* No transition while dragging, or the row lags the pointer. */
  transition: none;
  border-color: var(--line-firm);
  box-shadow: 0 1px 12px rgba(0, 0, 0, .13);
}

/* Where it would land: a gap the height of what is dragged, which the drag sets as `--room`. */
.rail-piece.above { margin-top: var(--room, 32px); }
.rail-piece.below { margin-bottom: var(--room, 32px); }
.rail-piece:not(.lifting) { transition: margin .12s ease; }
/* Or the row it would go into: a drop on the middle of a row puts the piece in it. */
.rail-piece.into .rail-dot { outline: 2px solid var(--emit-100); outline-offset: 1px; }

@media (max-width: 720px) {
  .rail { right: 6px; gap: 3px; }
  .rail-dot { width: 104px; height: 40px; }
  .rail-piece.sub .rail-dot { width: calc(104px - 10px * var(--depth, 1)); height: 32px; }
  .rail-piece.above { margin-top: var(--room, 43px); }
  .rail-piece.below { margin-bottom: var(--room, 43px); }
}

@media (prefers-reduced-motion: reduce) {
  /* The lift still follows the pointer: that is the gesture. The gap does not animate. */
  .rail-piece:not(.lifting) { transition: none; }
}

.dock {
  position: absolute;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Against the canvas, not the screen: a dock wider than the viewport runs off both sides of it and
     the outer buttons cannot be reached. It wraps rather than overflows. */
  max-width: calc(100% - 24px);
  flex-wrap: wrap;
  gap: 3px;
  padding: 4px;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  border-radius: var(--r);
  box-shadow: var(--lift);
}

/* Armed. The bar itself says the pen is live, so the state is not something to read off one button in
   the middle of it. */
.dock.drawing {
  border-color: var(--blue-50);
  box-shadow: var(--lift), 0 0 0 3px var(--blue-30);
}

/* Between an action and the piece it acts on. A hairline and not a gap, because a gap between two
   things in a four-item bar reads as two bars. */
.dock-rule {
  width: 1px;
  height: 18px;
  margin: 0 2px;
  background: var(--line);
}


/* ── A control that opens, without moving the bar ──
   Anchored to its own button and taken out of the flex flow, so opening one changes nothing about the bar's
   width. They opened *along* it before — eight colour swatches in a row, a number between two step buttons —
   and every press shoved the whole bar sideways under the pointer. A toolbar that changes width when you use it
   is one you have to re-find after every action. */
.dock-held { position: relative; display: flex; align-items: center; }

.dock-pop {
  position: absolute;
  /* Upward: the bar sits at the bottom of the canvas, so there is room above it and none below. */
  bottom: calc(100% + 7px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 4px;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
  /* It opens over the model, so it must not be scrolled past the canvas by a long list. */
  max-height: 60vh;
  overflow-y: auto;
  animation: dock-pop .14s ease-out;
}

/* Grown out of the button rather than appearing above it, so the eye follows one thing opening. The transform
   is repeated in both frames because it also carries the centring — dropped from either, the panel jumps. */
@keyframes dock-pop {
  from { opacity: 0; transform: translate(-50%, 5px); }
  to { opacity: 1; transform: translateX(-50%); }
}

/* What is open reads as pressed, the way a latched tool does. */
.dock-held.open > .dock-key,
.dock-held.open > .dock-dot { background: var(--well); }

/* One row of a menu: a shape's own profile, and what it is called. Left-aligned, because it is a list of
   names and a centred list of names has no edge to read down. */
.dock-item {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 8px 3px 5px;
  border: 0;
  background: none;
  color: var(--emit-90);
  font: var(--t-body) var(--sans);
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.dock-item:hover { background: var(--well); }
.dock-item.on { background: var(--emit-100); color: var(--on-fill); }
.dock-item svg { width: 15px; height: 15px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.25; }

/* A number typed rather than stepped to. A width is a number a maker knows — 7 mm, 12 mm — and reaching it by
   pressing a step button five times is five presses to say one thing. */
.dock-field {
  width: 72px;
  font-family: var(--mono);
  font-size: var(--t-read);
  text-align: center;
}

/* The steps, under the field, for the case of nudging one. Side by side because they are a pair. */
/* ── The slider in a dock number's panel ──
   Wide enough to be worth dragging: the panel is about 120 px and the slider takes all of it, because a
   control you have to aim at is a control you would rather type into. Full width and not a fixed size, so it
   matches the field above it whatever that is set to.

   The step buttons that were here are gone — see `_dockNumber`. A maker reaching 0.45 from 1.0 at two
   hundredths a press was twenty-seven presses, each an undo step and a re-cut, and none of them showed the
   shape on the way. */
.dock-slider {
  width: 100%;
  margin: 4px 0 0;
  accent-color: var(--ink);
  cursor: ew-resize;
}

/*
 * One named allowance inside the Margins panel: its name, its number, its slider.
 *
 * A grid and not a flex row, so the three names line up under each other and the three numbers do too — a
 * panel of rows whose columns do not agree reads as three unrelated controls, which is what the two loose
 * numbers on the bar were.
 */
.dock-row {
  display: grid;
  grid-template-columns: auto 72px;
  align-items: center;
  gap: 4px 8px;
  padding: 2px 4px;
}

.dock-row .dock-slider {
  grid-column: 1 / -1;
}

.dock-row-name {
  display: flex;
  align-items: center;
  gap: 6px;
  justify-self: start;
  white-space: nowrap;
  cursor: help;
}

/*
 * The colour this allowance is drawn in, beside its name.
 *
 * A square and not a round dot: a piece's colour is a round swatch — `.dock-dot` — and these are not pieces.
 * The distinction is the point, since both appear on the same bar.
 */
.dock-row-dot {
  width: 9px;
  height: 9px;
  flex: none;
  background: var(--dot, var(--emit-40));
  border: 1px solid var(--line);
}

/* The value on the bar, in figures. Tabular, so the bar does not twitch as the digits change. */
.dock-value {
  min-width: 34px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

.dock-swatch {
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  background: var(--dot);
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .28);
  transition: transform .12s ease;
}

.dock-swatch:hover { transform: scale(1.22); }
.dock-swatch.on { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .28), 0 0 0 2px var(--emit-100); }

/* The sole's colours: a row for each layer a maker sees, its name and then its colours. A grid, so the colours of
   every row line up under each other whatever the names are. */
.dock-sole-row {
  display: grid;
  grid-template-columns: auto repeat(8, 16px);
  align-items: center;
  gap: 5px;
  padding: 3px 4px;
}

.dock-sole-name {
  padding-right: 6px;
  color: var(--emit-90);
  font: var(--t-body) var(--sans);
  white-space: nowrap;
}

/* The layers' names, to pick the one row a phone shows. A wider screen shows every row and has no use for them. */
.dock-sole-layers { display: none; }

/* Pressed: the swatch reads as a held button while its colours are out. */
.dock-held.open .dock-dot { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .28), 0 0 0 2px var(--emit-100); }

/* A button now, not a label: it says what the piece is made of and it is how that is changed. */
.dock-dot {
  width: 16px;
  height: 16px;
  margin: 0 3px 0 2px;
  padding: 0;
  border: 0;
  background: var(--dot, var(--emit-40));
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .28);
  transition: transform .12s ease, box-shadow .12s ease;
}

.dock-dot:hover { transform: scale(1.14); }
.dock-dot:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: 2px; }

.dock-key {
  position: relative;
  padding: 6px 11px;
  border: 0;
  border-radius: var(--r);
  background: none;
  color: var(--emit-70);
  font: var(--t-small)/1 var(--label);
  letter-spacing: .08em;
  cursor: pointer;
  transition: background .12s ease, color .12s ease;
}

.dock-key:hover { background: var(--well); color: var(--emit-100); }
.dock-key.primary {
  border: 0;
  background: none;
  color: var(--emit-100);
  box-shadow: none;
}
.dock-key:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: 1px; }
.dock-key.primary:hover { background: var(--well); color: var(--emit-100); }
.dock-key.on {
  background: none;
  color: var(--emit-100);
}

.dock-key.on::after {
  content: '';
  position: absolute;
  left: 4px;
  right: 4px;
  bottom: 1px;
  height: 2px;
  background: var(--emit-100);
}

/* One piece's colour, as the few it can be. Dots rather than squares, because a row of eight small
   rectangles is eight more boxes, and a colour is the one thing here that needs no label. */
.swatches { display: flex; flex-wrap: wrap; gap: 5px; }

.swatch-dot {
  width: 17px;
  height: 17px;
  padding: 0;
  border: 0;
  background: var(--dot);
  cursor: pointer;
  /* A hairline inside every swatch. It carries the light end of the set: the last's shades are neutrals
     and the lightest is white, which without this is an invisible square on a white panel. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .22);
}

.swatch-dot:hover { transform: scale(1.12); }

/* The chosen one, said with a ring off the dot rather than a tick on it: a tick has to be light on a
   dark colour and dark on a light one, and there are eight of them. */
.swatch-dot.on {
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .22), 0 0 0 2px var(--raised), 0 0 0 3px var(--emit-100);
}

/* ── A colour, chosen ──
   The native picker, stripped of the chrome browsers put round it: what should read as a swatch is a
   block of the colour and a hairline, not a button with a colour in it. */
.swatch {
  appearance: none;
  -webkit-appearance: none;
  width: 42px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--line);
  background: none;
  cursor: pointer;
}

.swatch::-webkit-color-swatch-wrapper { padding: 0; }
.swatch::-webkit-color-swatch { border: 0; }
.swatch::-moz-color-swatch { border: 0; }


.param-value {
  font-family: var(--mono);
  font-size: var(--t-small);
  color: var(--emit-90);
  text-align: right;
  white-space: nowrap;
}


/* Red only once it is under the pointer. A red mark sitting in every piece's panel reads as a warning
   about the piece rather than as a button. */
/* The plan has ended. A wash behind the mark and a hairline round it, not a colour on it: the button
   that carries this is the menu, and its mark is already red — recolouring it would say nothing. Not
   `.danger` either, which dims an icon and so reads as unavailable, and this is the one thing on the bar
   that has to catch an eye that was not looking for it. */
.key.icon.danger { color: var(--emit-40); }
.key.icon.danger:hover:not(:disabled) { background: #f6e7e6; color: var(--err-90); }

.key.danger { border-color: var(--err-90); color: var(--err-90); }
.key.danger:hover:not(:disabled) { background: var(--err-90); color: var(--on-fill); }

/* A name is read, not dialled. Twenty-pixel mono is right for a readout and far too big for a text
   field in a side panel — it made a piece called "vamp" fill the width of the panel. */
.input.compact {
  font-size: var(--t-body);
  padding: 3px 4px;
  text-align: left;
}

/* What the last cut measured — read-only, beside the numbers that produced it. */
.param.metrics { gap: 2px; }

/* Every figure in the value column: placed in turn, every other one would fall into the label's 92 px. */
.param.metrics > .metric { grid-column: 2; }

.metric {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: var(--t-read);
  color: var(--emit-70);
}

.metric-value { font-variant-numeric: tabular-nums; color: var(--emit-90); }

/* ── Fit: what the last measures, beside what it is asked to be ── */

/* The value now, then the field, on one line: a target with nothing to compare it
   against is a number somebody has to trust. */
/* The name takes what the value and the field leave: in the 92 px column "GIRTH AT THE INSTEP" was cut. */
.param.fit-row { grid-template-columns: minmax(0, 1fr) 176px; }

.fit-row .param-actions {
  align-items: center;
  padding: 4px 4px 2px;
}

.fit-row .metric-value {
  flex: 0 0 5.4em;
  font-size: var(--t-read);
  white-space: nowrap;
  color: var(--emit-60);
}

.fit-row .input { flex: 1 1 5em; min-width: 0; }

/* A missed target is not a warning about the software, it is a fact about the last. */
.fit-error { color: var(--err-90); text-shadow: var(--glow-err); }

.written-file.fit-error,
.written-file.fit-warn { white-space: normal; }

.written-file.fit-error .written-name,
.written-file.fit-warn .written-name { white-space: normal; overflow: visible; }

.written-file.fit-warn { color: var(--emit-90); }

/* ── FIT: the last's size, its tape rows, the band, and the drag's readout ── */

/* THIS LAST IS EU [38] and what it measures as, then ALL VALUES and LAST. */
.fit-head { display: flex; flex-direction: column; gap: 6px; padding: 0 0 6px; }
.fit-size { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; }
.fit-size-label { font-size: var(--t-label); letter-spacing: .06em; color: var(--emit-90); }
.fit-size-system { font: var(--t-label) var(--label); letter-spacing: .06em; color: var(--emit-90); }
/* Over `.input.boxed`'s full width: the field holds a size, and the words run on beside it. */
.fit-size .input.boxed.fit-size-field { flex: 0 0 64px; width: 64px; }
.fit-size-read { font: var(--t-small) var(--mono); color: var(--emit-50); }
.fit-size-read.warn { color: var(--warn); }
.fit-keys { display: flex; gap: 4px; }
/* LAST's pop: SHADE and ORIENT, under the head while it is open. */
.fit-look-pop { margin: 0 0 6px; padding: 4px 8px; border: 1px solid var(--line); background: var(--well); }
.fit-graded { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin: 0 0 4px; }

.fit-groups { display: flex; flex-direction: column; }
.fit-group {
  margin-top: 8px;
  padding: 2px 4px 3px;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-label);
  letter-spacing: .08em;
  color: var(--emit-50);
}

/* A measure on one line: its name, the value now, the target and the lock. What the fit did goes under it. */
.tape-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 66px 26px;
  align-items: center;
  column-gap: 6px;
  padding: 1px 4px;
}
.tape-row.hover { background: var(--well); }
/* The selected one carries the blue it is drawn in on the last. */
.tape-row.on { background: var(--well); box-shadow: inset 2px 0 0 #0b6aa8; }

.tape-pick {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 4px 0;
  border: 0;
  background: none;
  color: var(--emit-80);
  font: var(--t-label) var(--label);
  letter-spacing: .08em;
  text-align: left;
  cursor: pointer;
}
.tape-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tape-row.on .tape-pick { color: var(--emit-100); }

.tape-dot { flex: 0 0 7px; width: 7px; height: 7px; border-radius: 50%; background: var(--emit-30); }
.tape-dot.band-ok { background: var(--done); }
.tape-dot.band-warn { background: var(--warn); }
.tape-dot.band-hard { background: var(--err-90); }

.tape-now {
  font: var(--t-body) var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--emit-90);
  text-align: right;
  white-space: nowrap;
}

/* A target is a field a maker fills in, so it is boxed: an unboxed one read as grey text. */
.input.boxed {
  width: 100%;
  min-width: 0;
  padding: 2px 4px;
  border: 1px solid var(--emit-40);
  background: var(--raised);
  text-align: right;
}
.input.boxed:hover { border-color: var(--emit-100); }
.input.boxed:focus { border-color: var(--emit-100); background: var(--raised); }
.input.boxed:disabled { border-style: dashed; color: var(--emit-50); }
.tape-field.band-ok:not(:placeholder-shown) { border-color: var(--done); color: var(--done); }
.tape-field.band-warn { border-color: var(--warn); color: var(--warn); }
.tape-field.band-hard { border-color: var(--err-90); color: var(--err-90); }

.tape-lock { width: 26px; height: 24px; }
.tape-reading { grid-column: 3 / 5; font: 9px/1 var(--label); letter-spacing: .08em; color: var(--emit-40); text-align: right; }
.tape-outcome { grid-column: 1 / -1; padding: 0 0 2px 13px; font: var(--t-small) var(--mono); color: var(--emit-50); }

/* The band of the heel change: the one thing in FIT that says whether this is still the same last. */
.fit-band {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 2px 0 4px;
  padding: 5px 8px;
  border: 1px solid currentColor;
}
.fit-band.band-ok { color: var(--done); }
.fit-band.band-warn { color: var(--warn); }
.fit-band.band-hard { color: var(--err-90); }
.fit-band-word { font: var(--t-label) var(--label); letter-spacing: .1em; font-weight: 600; }
.fit-band-by { font: var(--t-small) var(--mono); color: var(--emit-60); }

.fit-legacy { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; margin-top: 8px; }
.fit-warn { color: var(--warn); }
.fit-tools { margin-top: 6px; }
/* The report of a fit whose last is not on screen: there, and plainly not about what is shown. */
.fit-groups.not-shown { opacity: .45; }

/* A fold for what only a specialist reads. */
.fold { margin-top: 10px; padding-top: 6px; border-top: 1px solid var(--line); }
.fold > summary { cursor: pointer; font-size: var(--t-label); letter-spacing: .08em; color: var(--emit-60); }
.fold[open] > summary { margin-bottom: 4px; }

/* Beside the handle while a measure is dragged, in the colour of its band. */
.tape-readout {
  position: absolute;
  z-index: 20;
  padding: 3px 6px;
  background: var(--raised);
  border: 1px solid currentColor;
  box-shadow: var(--lift);
  color: var(--emit-100);
  font: var(--t-label) var(--label);
  letter-spacing: .06em;
  white-space: nowrap;
  pointer-events: none;
}
.tape-readout.band-warn { color: var(--warn); }
.tape-readout.band-hard { color: var(--err-90); }

/* ── The station table ── */

.stations {
  width: 100%;
  margin-top: 10px;
  border-collapse: collapse;
  font: var(--t-body) var(--mono);
  font-variant-numeric: tabular-nums;
}

.stations th {
  padding: 4px 2px;
  border-bottom: 1px solid var(--line);
  font-family: var(--label);
  font-size: var(--t-label);
  letter-spacing: .1em;
  color: var(--blue-70);
  text-shadow: var(--glow-blue);
  text-align: right;
}

.stations th:first-child,
.stations td:first-child { text-align: left; }

.stations td {
  padding: 3px 2px;
  color: var(--emit-80);
  text-align: right;
}

.stations tbody tr:hover td { color: var(--emit-100); }

/* ── The pick banner, over the viewport ── */

/* Undo and redo, over the canvas — floating, because the canvas is the thing they act on. A hairline
   and a shadow, like every other thing that sits over the viewport rather than beside it. */
.canvas-tools {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 3;
  display: flex;
  gap: 1px;
  padding: 3px;
  max-width: calc(100% - 24px);
  background: var(--raised);
  border: 1px solid var(--line-firm);
  border-radius: var(--r);
  box-shadow: var(--lift);
}

/* The foot toggle, first beside undo and redo and the same size. On, it is not filled as a latched icon is: the
   glyph at full ink with a 2 px ink bar under it. */
/* The last's colour: its key wears the colour, and its pop of swatches opens under the tools. */
.canvas-tools #last-colour .last-dot {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--dot, #f0f0f2);
  box-shadow: inset 0 0 0 1px var(--line-firm);
}
.last-colour-pop {
  position: absolute;
  top: 52px;
  right: 10px;
  z-index: 4;
  display: grid;
  grid-template-columns: repeat(4, auto);
  gap: 8px;
  padding: 10px;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  border-radius: var(--r);
  box-shadow: var(--lift);
}
.last-colour-pop[hidden] { display: none; }
.canvas-tools #see-foot { position: relative; }
.canvas-tools #see-foot.on,
.canvas-tools #see-foot.on:hover:not(:disabled) { background: none; color: var(--emit-100); }
.canvas-tools #see-foot.on::after {
  content: '';
  position: absolute;
  left: 5px;
  right: 5px;
  bottom: 1px;
  height: 2px;
  background: var(--emit-100);
}

/* What is left to place, above the dock. Text and nothing else: it was a boxed, bordered, full-width
   announcement — and it kept the old theme's colours, so it was black text on a dark translucent panel
   by the time the palette changed under it. Two words in grey is the whole job. */
.pick-banner {
  position: absolute;
  left: 50%;
  bottom: 56px;
  transform: translateX(-50%);
  z-index: 45;
  padding: 0;
  background: none;
  border: 0;
  color: var(--emit-50);
  font: var(--t-label) var(--mono);
  white-space: nowrap;
  pointer-events: none;
}

/*
 * What was just refused. Bottom-centre over the canvas, above the dock: a refusal belongs where the action
 * was, not in a panel on the far side of the window. Clickable, so it can be got rid of — the one thing on
 * the canvas that is allowed to take a click without being a tool.
 */
.say {
  position: absolute;
  left: 50%;
  bottom: 96px;
  transform: translateX(-50%);
  z-index: 46;
  max-width: min(560px, 76%);
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raised);
  color: var(--emit-70);
  font: var(--t-label) var(--mono);
  text-align: center;
  cursor: pointer;
}

.say.bad {
  border-color: var(--err-50);
  color: var(--err-90);
}

/* Over `.dl`, which the note carries for its face and which comes later: a label's `nowrap` ran a long note out of
   its box. `max-content`, or the box shrinks to the half of the stage right of `left: 50%` and wraps a short note;
   `anywhere`, for an address longer than the box. */
.say.dl {
  width: max-content;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Over the stage while what is drawn on the last is a previous version's answer, until the current one lands. Quiet,
   and never in the way of a click on the model. */
.updating {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  padding: 4px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raised);
  color: var(--emit-70);
  font: var(--t-label) var(--mono);
  letter-spacing: .12em;
  pointer-events: none;
}

/* Under it, once the server serves a newer client than this page: the one note over the stage that takes a click. */
.outdated {
  top: 44px;
  display: flex;
  gap: 12px;
  align-items: center;
  color: var(--emit-100);
  pointer-events: auto;
}

/* ── Status bar ── */

.mesh-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }

/* ── The lock screen ── */

.gate {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--screen);
}

/* A labelled field on the gate: the label above the input, because a sign-in form is read top to bottom
   and not scanned like a panel of settings. */
.field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 100%;
}

.field .input {
  width: 100%;
  padding: 5px 6px;
  border-bottom: 1px solid var(--line-firm);
  text-align: left;
}

.field .input:focus { border-bottom-color: var(--emit-100); background: var(--well); }

.gate-panel .jobs { width: 100%; margin-bottom: 4px; }
.gate-panel .key.primary { width: 100%; margin-top: 4px; }

/* Why the sign-in did not work, under the button that tried it.
   Overrides `.dl`, which the shared hint style carries: a label's letter-spacing and `nowrap` are right for
   two words beside a control and wrong for a sentence, which has to wrap rather than run off the panel. */
#auth-hint,
#auth-waiting-hint {
  width: 100%;
  margin-top: 8px;
  font-family: var(--mono);
  font-size: var(--t-body);
  letter-spacing: 0;
  white-space: normal;
  text-align: left;
  text-shadow: none;
  color: var(--emit-50);
}
#auth-hint.error,
#auth-waiting-hint.error { color: var(--err-50); }

/* The waiting screen's sentences: read, not scanned, so the body face and left-aligned like the hint. */
.gate-note {
  width: 100%;
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-body);
  color: var(--emit-100);
}
.gate-note.quiet { color: var(--emit-50); }
/* Under the centred brand, the waiting and signing-out lines are centred too; the brand's own right margin, for the
   title bar, would set it 4 px off their centre. */
#auth-waiting .gate-note,
#auth-leaving .gate-note { text-align: center; }
.gate-panel .brand { margin-right: 0; }
.gate-panel #auth-waiting-out,
.gate-panel #auth-waiting-have { width: 100%; margin-top: 4px; }

.gate-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 28px 32px;
  gap: 8px;
  width: min(320px, 88vw);
  border: 1px solid var(--line-firm);
  background: var(--raised);
}

/* ── Narrow screens: one panel at a time, viewport first ── */

/* The wordmark. Its selector and opening brace were lost in an earlier edit, so
   these declarations sat orphaned after a media query and the browser skipped the
   lot — the brand had no styling at all and nothing said so. */
.brand {
  font-family: var(--label);
  font-size: var(--t-read);
  letter-spacing: .18em;
  color: var(--blue-100);
  text-shadow: var(--glow-blue-bright);
  white-space: nowrap;
  margin-right: 8px;
}

/* Section label */
.dl {
  font-family: var(--label);
  font-size: var(--t-label);
  letter-spacing: .15em;
  color: var(--blue-70);
  text-shadow: var(--glow-blue);
  white-space: nowrap;
}

/* ── Drop tray ── */
/* A last is loaded: this is its name. It was the same dashed drop box either way, which puts a dashed
   rectangle round a filename — a box asking to be filled in with the file already in it. */
.file-named {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 4px;
  border: 0;
  border-radius: var(--r);
  background: none;
  color: var(--emit-90);
  font: var(--t-body) var(--mono);
  text-align: left;
  cursor: pointer;
}

.file-named:hover { background: var(--well); }
.file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Now only ever in the Form panel, so it takes the panel's width instead of the width it had beside the
   modes in the bar. */
.drop-tray {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 22px 10px;
  border: 1px dashed var(--line-firm);
  background: var(--well);
  color: var(--emit-50);
  text-shadow: var(--glow-text-dim);
  font-size: var(--t-title);
  white-space: nowrap;
  cursor: pointer;
}

.drop-tray > span:last-child {
  overflow: hidden;
  text-overflow: ellipsis;
}

.drop-tray:hover,
.drop-tray.drag {
  border-color: var(--emit-90);
  color: var(--emit-90);
  text-shadow: var(--glow-text);
}

.drop-tray.has-file {
  border-style: solid;
  border-color: var(--line-firm);
  background: var(--raised);
  color: var(--emit-90);
  text-shadow: var(--glow-text);
}

/* ── Input wells ── */
/* A field is the value, written where the value goes. The affordance arrives with the pointer and
   focus adds a line under what is being typed — nothing is boxed at rest.
   A box drawn permanently around a value that is already correct is a box asking to be filled in, and
   twenty of them down a panel is the bulk that was complained about. */
.input,
.pass-input {
  width: 60px;
  padding: 3px 4px;
  border: 0;
  border-bottom: 1px solid transparent;
  border-radius: 0;
  background: none;
  color: var(--emit-90);
  font: var(--t-body) var(--mono);
  text-align: center;
  outline: none;
}

.input:hover,
.pass-input:hover { border-bottom-color: var(--line-firm); }

.input:focus,
.pass-input:focus {
  border-bottom-color: var(--blue-70);
  background: var(--well);
}

.input::placeholder { color: var(--emit-40); }

.pass-input { width: 180px; font-size: var(--t-read); text-align: left; }

/* ── Size chips ── */
.sizes-row {
  display: flex;
  gap: 5px;
  align-items: center;
  flex-wrap: wrap;
}

.size-chip {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 26px;
  padding: 0 4px;
  border: 0;
  border-radius: var(--r);
  background: var(--well);
  color: var(--emit-50);
  font: var(--t-body) var(--mono);
  cursor: pointer;
  user-select: none;
}

.size-chip:hover { color: var(--emit-90); }

.size-chip.checked {
  background: var(--emit-100);
  color: var(--on-fill);
}

.size-chip input { display: none; }

/* ── Radio / checkbox chips ── */
.radio-row { display: flex; gap: 10px; flex-wrap: wrap; }

.radio-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--emit-50);
  text-shadow: var(--glow-text-dim);
  font: var(--t-read) var(--mono);
  cursor: pointer;
  user-select: none;
}

.radio-chip input { display: none; }

.radio-chip .box {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border: 1px solid var(--line-firm);
  background: var(--well);
}

.radio-chip input:checked ~ .box {
  border-color: var(--emit-90);
  background: var(--emit-90);
}

.radio-chip input:checked ~ .box::after {
  content: '';
  display: block;
  width: 6px;
  height: 6px;
  margin: 4px;
  background: var(--on-fill);
}

.radio-chip:has(input:checked) {
  color: var(--emit-90);
  text-shadow: var(--glow-text);
}

/* ── A choice, as one control instead of three ──
   Two or three mutually exclusive options are a segmented control everywhere else in a drawing program,
   and they were three 18px radio boxes here — three lines of chrome to say one word. The radio inputs
   stay, hidden, so the choice is still a radio group to the keyboard and to a screen reader. */
/* A track, and the chosen segment filled. It used to mark the choice with `--raised` on `--well` and a
   `box-shadow` of `--glow-box` — which is `none` in this theme — so white sat on near-white and nothing
   on screen said which of the three was set. That is the blandness: not too little colour in general,
   but no colour on the one thing that carries the state. */
.segments {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 0;
  border-radius: var(--r);
  background: var(--well);
}

.segments .radio-chip {
  gap: 0;
  padding: 3px 9px;
  border-radius: var(--r);
  font: var(--t-label) var(--label);
  letter-spacing: .08em;
  color: var(--emit-50);
  text-shadow: none;
}

.segments .radio-chip .box { display: none; }

.segments .radio-chip:hover { color: var(--emit-100); }

.segments .radio-chip:has(input:checked) {
  background: var(--blue-70);
  color: var(--on-fill);
}

.segments .radio-chip:has(input:focus-visible) {
  outline: 1px dotted var(--emit-100);
  outline-offset: 1px;
}

/* A checkbox. There were no toggle switches on a Macintosh — a setting that is on or off was a square
   with a tick in it, and the tick is drawn rather than typed so it is the same mark at every size.
   The sliding switch this replaces was the one piece of 2010s phone UI left in the panel. */
.check {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  justify-self: start;
}

.check input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.check-box {
  position: relative;
  width: 13px;
  height: 13px;
  border: 1px solid var(--emit-100);
  background: var(--raised);
}

.check:hover .check-box { background: var(--well); }

/* Two lines at an angle, which is a tick. Hidden until checked rather than drawn and faded, so an
   unchecked box is an empty box. */
.check-box::after {
  content: '';
  position: absolute;
  left: 2px;
  top: 1px;
  width: 4px;
  height: 8px;
  border: solid var(--emit-100);
  border-width: 0 2px 2px 0;
  transform: rotate(40deg);
  opacity: 0;
}

.check input:checked + .check-box::after { opacity: 1; }
.check input:focus-visible + .check-box { outline: 1px dotted var(--emit-100); outline-offset: 2px; }

/* ══════════════════════════════════════════════════════════════════════
   MEASUREMENT HUD — the numbers a grade exists to produce
   ══════════════════════════════════════════════════════════════════════ */

/* Anchored inside the stage, not the window: fixed positioning put it over the
   toolbar and the design tree, which is where it was covering the brand. */

/* ── Keys ── */
/* A button, not a key on a machine. The offset drop-shadow, the 22px mono and the wide letter-spacing
   were the single biggest thing making this read as a toy: every action on screen looked like a chunky
   plastic keycap. This is a hairline, a radius and a shadow you notice only on hover. */
/* A glyph button. Square, because a row of glyphs of different widths reads as words rather than as a
   row of controls — and every one carries a `title`, since a symbol nobody can name needs its name
   somewhere. */
/* Frameless. A labelled button is outlined; an icon in a toolbar is not, on a Macintosh or here. */
.key.icon {
  border: 0;
  background: none;
  box-shadow: none;
  color: var(--emit-70);
}

.key.icon:hover:not(:disabled) { background: var(--well); color: var(--emit-100); }
.key.icon:active:not(:disabled) { background: var(--emit-30); color: var(--emit-100); }
.key.icon.on { background: var(--emit-100); color: var(--on-fill); }
.key.icon:disabled { border: 0; background: none; color: var(--emit-40); }

.key.icon,
.dock-key.icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 24px;
  padding: 0;
  font-size: var(--t-read);
  line-height: 1;
  letter-spacing: 0;
}

.dock-key.icon { width: 30px; height: 28px; font-size: var(--t-read); }

/* The key that presses a tool, printed under its glyph.
 *
 * A maker: "there are all these different things like clear, etc. I want you to add letters to that." Under and not
 * beside, so the bar keeps its widths — a letter on the same line as a glyph makes every tool half again as wide, and a
 * toolbar that changes width when you use it is one you have to re-find after every action.
 *
 * The column costs the bar six pixels of height and is what buys the letters being visible at all. Half-strength: it is
 * a reminder, not a label, and a maker who knows the key should not have to read past it. A refused tool's letter fades
 * with it, because a key that will not act should not look like one that will. */
.dock-key.icon { flex-direction: column; gap: 1px; height: 34px; }

/* A worded key: its glyph and its word on one line, with the printed letter under them from the column above. The bar's
   keys are glyphs, so the one that has to say *which* thing it is — the sole's construction — is the only one with a word
   on it, and a word is wider than 30 px. */
.dock-key.icon.dock-named { width: auto; padding: 0 7px; }
.dock-face { display: flex; align-items: center; gap: 5px; font: var(--t-small)/1 var(--label); letter-spacing: .08em; }

/* Except a reading, which puts its glyph *beside* the figures: the column above is for a letter caption under a glyph,
   and a number stacked over one is two things where the bar reads one. Here rather than beside `.dock-value`, which is
   where it was and where the cascade ate it — same specificity, and the column is declared seven hundred lines later. */
.dock-key.icon.dock-value {
  flex-direction: row;
  gap: 4px;
  width: auto;
  height: 34px;
  min-width: 0;
  padding: 0 7px;
  font-size: var(--t-small);
}

.dock-key kbd {
  font: 9px/1 var(--label);
  /* No letter-spacing: it is one character, so all a tracking value does is pad its right and shift it off centre. */
  opacity: .62;
}

.dock-key:hover kbd,
.dock-key.on kbd { opacity: .8; }
.dock-key:disabled kbd { opacity: .22; }

/* Stroked, not filled, and at the weight of a hairline: an icon drawn in the same line as the rules
   around it belongs to the interface rather than sitting on top of it. */
.key.icon svg,
.dock-key.icon svg {
  display: block;
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: miter;
}

.dock-key.icon svg { width: 16px; height: 16px; }

/* The one thing in the bar that is a mark and not a glyph: the brand's red dot, as the landing page and the
   favicon draw it, the same red whatever the key is doing. `.key.icon` in the selector, so it outranks the
   stroked rule above: without it the key shows a grey ring. The red stroke keeps the dot the size of that ring. */
.key.icon[data-icon="files"] svg {
  fill: var(--mark);
  stroke: var(--mark);
}

/* A push button, the way a 1990s Macintosh drew one: a 1 px ink outline, a white face, the label in ink,
   and a radius that says "button". Pressed, it **inverts** — the whole state change is two colours
   swapping places, which is why it stays crisp at any size and needs no bevel, gradient or shadow.
   `.key.icon` opts out below: a Mac toolbar icon had no frame, and only a labelled button did. */
.key {
  padding: 4px 12px;
  border: 1px solid var(--emit-100);
  border-radius: 0;
  background: var(--raised);
  color: var(--emit-100);
  font: var(--t-label) var(--label);
  letter-spacing: .1em;
  white-space: nowrap;
  cursor: pointer;
  transition: background .1s ease, color .1s ease;
}

.key:hover:not(:disabled) { background: var(--well); }

/* Inverted, not tinted. This is the one gesture the whole button language rests on. */
.key:active:not(:disabled) {
  background: var(--emit-100);
  color: var(--on-fill);
}

.key:focus-visible {
  outline: 1px dotted var(--emit-100);
  outline-offset: 1px;
}

/* The default button: the one Return presses. A Macintosh marked it with a second line inside the
   first rather than by filling it or colouring it, so the row still reads as a row of buttons with one
   of them spoken for. Drawn inward with `inset`, so it takes no room and cannot be clipped by a panel
   that scrolls. */
.key.primary {
  box-shadow: inset 0 0 0 1px var(--raised), inset 0 0 0 2px var(--emit-100);
}

/* Latched. On a Macintosh a button that stays down *is* a pressed button, so it is drawn as one — the
   same inversion, held. Colour is reserved for what a value is set to; ink is for what a button does. */
.key.on {
  background: var(--emit-100);
  color: var(--on-fill);
}

.key.on:hover:not(:disabled) { background: var(--emit-90); color: var(--on-fill); }

.key:disabled {
  border-color: var(--emit-40);
  color: var(--emit-40);
  background: var(--raised);
  box-shadow: none;
  cursor: default;
}

/* Off because its write is out (`guard.js`), not because it is refused: the pointer says it is working. The look
   waits 150 ms (`WITHIN.sayMs`), so a write that lands sooner does not flash it. */
.key[aria-busy='true'],
button[aria-busy='true'] { cursor: progress; }

:is(button, input, textarea)[aria-busy='true'],
[aria-busy='true'] kbd {
  transition: color 0s .15s, background-color 0s .15s, border-color 0s .15s, box-shadow 0s .15s, opacity 0s .15s;
}

/* A menu or a panel held out of sight while its answer is out, for at most `WITHIN.sayMs` (`quietly`). */
.answer-due { opacity: 0; }

.key.sm { padding: 3px 9px; }

/* ── The heel panel ──
   Floating at the right of the viewport, over the last rather than beside it: the heel is drawn on the
   underside and a maker turning the last to see it wants the whole width for the model.

   Every control here is *named on screen*. The dock is glyphs with the name on hover, which works for an
   action — a pen is a pen — and does not for a number: three figures in a row are three questions you have to
   hover over to tell apart. A maker: "that you have to mandatorily look at the tooltip while hovering over a
   button is ridiculous." */
.heel-panel {
  position: absolute;
  top: 64px;
  right: 12px;
  z-index: 4;
  display: none;
  width: 232px;
  padding: 10px 12px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raised);
  box-shadow: var(--shade);
  /* Bounded against the stage, which the rail always was and this never had to be — until the sole's layers moved in.
     Measured on a 700 px window with the eight layers a stack may hold: the panel ran 71 px off the bottom of the
     screen and took the two numbers the whole sole is priced on with it. Sixty-four at the top, twelve at the foot,
     which is the inset it keeps at its own right edge. */
  max-height: calc(100% - 76px);
  overflow-y: auto;
}

.heel-panel.show { display: block; }

/* One row per number: its name, its value, and the slider under both. The name and the value sit on one
   line with the value hard right, so the column of figures reads down. */
.heel-row { margin: 0 0 10px; }

.heel-row-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.heel-name {
  font: var(--t-label) var(--label);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--emit-50);
}

.heel-value {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-body);
}

/* The shape, as words. A heel shape has a name a maker knows — cuban, wedge — and a glyph for it would be a
   picture of a heel five times over. */
.heel-shapes { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 10px; }

/* What the numbers come to, which is the point of setting them: the girth at the seat and at the ground, and
   how many layers that is to cut. Read from the engine, except while a slider is moving. */
.heel-sums {
  margin: 0;
  padding-top: 8px;
  border-top: 1px solid var(--line);
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--emit-50);
}

/* ── The foot panel ──
   The suggested fixes, at the top left of the viewport in FORM and DRAW: the heel panel, the rail and the canvas
   tools are on the right. */
.foot-panel {
  position: absolute;
  /* Under the job tabs, which are centred at the top and reach this far on a narrow window. */
  top: 56px;
  left: 12px;
  z-index: 4;
  width: 240px;
  max-height: calc(100% - 68px);
  overflow-y: auto;
  padding: 8px 12px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--raised);
  box-shadow: var(--shade);
}

.foot-panel[hidden] { display: none; }

.foot-title { display: block; margin: 0 0 6px; font-size: var(--t-label); letter-spacing: .06em; color: var(--emit-90); }

.foot-fixes { margin: 0 0 8px; }

.foot-fix { margin: 0 0 3px; font-size: var(--t-body); line-height: 1.35; color: var(--emit-100); }

.foot-apply { width: 100%; }

.foot-why { margin: 0; font: var(--t-small) var(--mono); line-height: 1.35; color: var(--emit-70); }

/* ── Mesh controls ── */
/* The view controls, at the foot of the panel. They were a bar across the bottom of the screen that
   showed nothing in two of the three modes and still drew its border and its padding — which is the
   bar that appeared to be there for no reason. */
.mesh-ctls {
  display: none;
  flex: 0 0 auto;
  flex-direction: column;
  gap: 8px;
  padding: 8px 12px 10px;
  border-top: 1px solid var(--line);
  background: var(--raised);
}

.mesh-ctls.show { display: flex; }

/* Wrapping is right here and wrong in a bar: in a fixed-width panel a second row of view buttons is
   expected, where in a full-width bar it is a mystery band across the screen. */
.mesh-views { display: flex; flex-wrap: wrap; gap: 4px; }

/* One size a line, its slider at the same x on every line. */
.mesh-ctls .mesh-row { flex-direction: column; align-items: stretch; gap: 2px; }
.mesh-ctls .mesh-row > .mesh-ctl { flex: 0 0 auto; }
.mesh-ctl { display: flex; align-items: center; gap: 6px; }
.mesh-ctls .mesh-ctl label { flex: 0 0 96px; overflow: hidden; text-overflow: ellipsis; }
.mesh-ctls .mesh-ctl input[type=range] { flex: 1 1 auto; min-width: 0; width: auto; }

.mesh-ctl label {
  font-family: var(--label);
  font-size: var(--t-label);
  color: var(--emit-50);
  text-shadow: var(--glow-text-dim);
  white-space: nowrap;
}

.mesh-ctl .swatch {
  width: 10px;
  height: 10px;
  flex: 0 0 auto;
  border: 1px solid rgba(255, 255, 255, .3);
}

.mesh-toggle {
  font-family: var(--label);
  font-size: var(--t-label);
  color: var(--emit-80);
  background: none;
  border: 0;
  border-radius: var(--r);
  padding: 2px 5px;
  cursor: pointer;
  line-height: 1;
  flex: 0 0 auto;
}
.mesh-toggle:hover { background: var(--well); color: var(--emit-100); }
.mesh-ctl.off .mesh-toggle { color: var(--emit-40); }
.mesh-ctl.off label { opacity: .35; }
.mesh-ctl.off .swatch { opacity: .25; }
.mesh-ctl.off input[type=range] { opacity: .35; }

input[type=range] {
  -webkit-appearance: none;
  appearance: none;
  width: 100px;
  height: 6px;
  border: 1px solid var(--line-firm);
  background: var(--emit-30);
  outline: none;
}

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 16px;
  border: 0;
  background: var(--blue-70);
  cursor: pointer;
}

input[type=range]::-moz-range-thumb {
  width: 12px;
  height: 16px;
  border: 0;
  border-radius: 0;
  background: var(--blue-70);
  cursor: pointer;
}

/* ── Status, hints, links ── */

.hint { font-size: var(--t-body); color: var(--emit-50); text-shadow: none; }

.hint.ok { color: var(--done); }

.hint.error { color: var(--err-90); text-shadow: var(--glow-err); }

.hint.warn { color: var(--warn); }

/* What the engine reported that is about no one piece: one quiet word beside the saved note, in the colour of
   the worst thing said, and a short list behind it over the top-left of the canvas. See `ui/findings.js`. */
.findings-chip {
  appearance: none;
  background: none;
  border: 0;
  padding: 0 2px;
  font-family: var(--label);
  font-size: var(--t-label);
  letter-spacing: .04em;
  color: var(--warn);
  white-space: nowrap;
  cursor: pointer;
}
.findings-chip[hidden] { display: none; }
.findings-chip.error { color: var(--err-90); }
.findings-chip.info { color: var(--emit-50); }
.findings-chip:hover,
.findings-chip[aria-expanded="true"] { text-decoration: underline; }

.findings {
  position: absolute;
  top: 8px;
  left: 10px;
  z-index: 47;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(440px, calc(100% - 20px));
  max-height: 50%;
  overflow-y: auto;
  padding: 10px 12px;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
  font: var(--t-body)/1.4 var(--sans);
  color: var(--emit-80);
}
.findings[hidden] { display: none; }
.finding { display: flex; gap: 8px; align-items: baseline; }
.finding .sev { flex: 0 0 1em; text-align: center; color: var(--emit-50); }
.finding-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
.finding-fix { color: var(--emit-50); }
.finding-where { flex: 0 0 auto; color: var(--emit-50); font-size: var(--t-label); }
.finding.warn .sev { color: var(--warn); }
.finding.error .sev,
.finding.error .finding-message { color: var(--err-90); }
.finding-dismiss { align-self: flex-end; }

.push { margin-left: auto; }

/* ══════════════════════════════════════════════════════════════════════
   CONTEXT + PUBLISH TOOLBAR (filesystem-style last/branch switching)
   ══════════════════════════════════════════════════════════════════════ */

/* One row, always. `flex-wrap: wrap` is what put a second bar across the screen at half a window: the
   modes fell onto their own line and the maker had a strip of chrome with no way to account for it.
   Nowrap plus `min-width: 0` on the children means things narrow and then drop out by breakpoint,
   which is a decision rather than a reflow. */
.toolbar {
  /* The frame the wordmark is centred in. */
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: nowrap;
  width: 100%;
  min-width: 0;
  padding: 6px 10px;
  background: var(--raised);
  border-bottom: 1px solid var(--line-firm);
  overflow: hidden;
}

.toolbar > * { flex: 0 0 auto; }

/* The wordmark, in the middle. The one place in the bar nothing else wants: the left is which file this
   is, the right is what you are doing to it. */
/* The wordmark, centred on the bar.
   Taken out of the flex flow to get there. Between two `.push` spacers it was centred in the space *left
   over* — and the file buttons on one side are not the width of the modes on the other, so it sat off to
   the right by half the difference. A spacer cannot centre a thing against unequal neighbours; only the bar
   itself can say where its middle is. */
.wordmark {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  font: var(--t-body) var(--label);
  letter-spacing: .22em;
  color: var(--emit-50);
  user-select: none;
  white-space: nowrap;
  /* It lies over the middle of the bar, so it must not take a click meant for what is under it. */
  pointer-events: none;
}
/* The notes beside the name reach it: see `clearTheWordmark`. */
.wordmark.crowded { visibility: hidden; }

.ctx {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  /* The one thing in the bar allowed to give up room, because a name elides legibly and a button does not.
     Shrink only: `.push` is what holds the modes to the right, and two mechanisms doing one job is the one
     that goes stale. */
  flex: 0 1 auto;
  min-width: 0;
}

/* The design's name reads as the title it is. `field-sizing` shrinks it to the name where the browser has
   it; the width is the fallback, and a cap so a long name cannot push the modes off the bar.

   `min-width: 0` is the load-bearing line. At `8ch` the field stopped giving room while the bar was still
   too narrow, and `.toolbar` is `overflow: hidden` with the modes as its last child — so the overflow came
   off the modes, silently, one letter at a time. The modes are the top-level thing about the program and
   are never the thing that gets clipped: the name elides instead, which is what a name is for. */
.ctx-name {
  width: 22ch;
  /* Two caps, and the tighter one wins. `34vw` keeps a long name from taking the bar; the second keeps it
     clear of the wordmark, which is centred on the bar and absolutely positioned — so nothing in the flex
     flow holds it off, and at 1000 px a 34vw name ran straight under the wordmark with both illegible.
     It allows a wordmark up to 179 px of 12 px Chicago at .22em, after two icons and their gaps. */
  max-width: min(34vw, calc(50vw - 204px));
  field-sizing: content;
  min-width: 0;
  text-overflow: ellipsis;
  text-align: left;
  font-size: var(--t-title);
  color: var(--emit-100);
}

#ctx-branch .ctx-name { width: 90px; }

.ctx-name:disabled {
  color: var(--emit-50);
  border-bottom-color: transparent;
  cursor: default;
}

.ctx-drop {
  padding: 4px 8px;
  font-size: var(--t-body);
}

.ctx-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 70;
  min-width: 230px;
  max-height: 40vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
}

.ctx-menu .ctx-head {
  font-family: var(--label);
  font-size: var(--t-label);
  letter-spacing: .12em;
  color: var(--blue-70);
  text-shadow: var(--glow-blue);
  padding: 8px 10px 4px;
}

.ctx-item {
  font-family: var(--mono);
  font-size: var(--t-title);
  text-align: left;
  color: var(--emit-90);
  background: var(--well);
  border: 0;
  border-top: 1px solid var(--line);
  padding: 8px 10px;
  cursor: pointer;
  text-shadow: var(--glow-text-dim);
  white-space: nowrap;
}

.ctx-item:hover { background: var(--emit-30); color: var(--emit-100); }

.ctx-item.current { color: var(--blue-90); text-shadow: var(--glow-blue); }

.ctx-item.new { color: var(--blue-100); text-shadow: var(--glow-blue-bright); }

.ctx-empty {
  font-size: var(--t-read);
  color: var(--emit-50);
  padding: 8px 10px;
}

/* ── Library delete affordance ── */
.lib-last-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* One line a row, whatever the name: the name shortens to an ellipsis, but not below its floor (its own
   length up to ten characters, `--name-floor`, set by library.js); then the tag; the date last. The tiny
   shrink factors make the order strict: a factor of 1 against one a millionth takes all of the shortfall. */
.lib-last-row .lib-last {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  white-space: nowrap;
  overflow: hidden;
}
.lib-name {
  flex: 0 1 auto;
  min-width: var(--name-floor, 0px);
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The tag keeps its words until the name is at its floor: it says which last, which is why it is there. */
.lib-last-row .lib-tag {
  flex: 0 0.000001 auto;
  min-width: 0;
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A date is one word; broken over two lines it reads as two. Shortened last of all. */
.lib-last-row .lib-when {
  white-space: nowrap;
  flex: 0 0.000000001 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A last's size tells lasts of one name apart, so it is never shortened. Its file says less than its name and
   shortens first: "ada …" kept "EU 41 · last41.stl", and a cut name with no size read the same three times. */
.lib-last-row .lib-size { flex-shrink: 0; }
.lib-last-row .lib-file { flex-shrink: 1000000; margin-left: .5em; }

/* A phone's panel is too narrow for a row on one line. The name has the first line to itself and the tag and
   date go under it, where the tag has the width the name had; keys that do not fit beside go under, right. */
@media (max-width: 600px), (max-height: 500px) {
  .lib-last-row:has(> .lib-last) { flex-wrap: wrap; justify-content: flex-end; }
  .lib-last-row > .lib-last { flex: 1 1 180px; flex-wrap: wrap; row-gap: 2px; padding: 6px 10px; }
  .lib-last-row .lib-name { flex: 1 1 100%; }
  /* A basis of nothing, so the date is not wrapped to a third line; grown back to its words where they fit. */
  .lib-last-row .lib-tag { flex: 1 1 0; max-width: max-content; }
  .lib-last-row .lib-name + .lib-tag,
  .lib-last-row .lib-name + .lib-when { margin-left: 0; }
  /* A row of text (a member, a task, an invitation): its words have the row's width, and its keys go under
     them, right, together. Beside, at 180 px, a short address was broken and MAKE ADMIN was parted from
     REMOVE. An invitation to answer has its own layout, under `.lib-answer`. */
  .lib-last-row:has(> .lib-row-text):not(.lib-answer) { flex-wrap: wrap; justify-content: flex-end; }
  .lib-last-row:not(.lib-answer) > .lib-row-text { flex: 1 1 100%; }
  .lib-last-row:has(> .lib-row-text):has(> button):not(.lib-answer) { padding-bottom: 8px; }
  .lib-last-row:has(> button):not(.lib-answer) > .lib-row-text { padding-bottom: 2px; }
}

/* The whole row lights, not only its name, where the row is what opens. */
.lib-last-row:has(> button.lib-last):hover { background: var(--well); }
.lib-last-row .lib-last:hover { background: none; }
/* The same row, to read and not to press: a design the asker cannot open. */
.lib-last.lib-still { cursor: default; }

/* Where a section's key that ends something sits: below everything a maker came to use. */
.lib-foot { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }

/* A row's own actions, quiet until pointed at: the row carries the name, not a bank of keys. */
.lib-act {
  flex: 0 0 auto;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--r);
  background: none;
  color: var(--emit-50);
  font: var(--t-label) var(--label);
  letter-spacing: .08em;
  cursor: pointer;
}
.lib-act:hover,
.lib-act:focus-visible { color: var(--emit-100); border-color: var(--line-firm); outline: none; }

/* Naming a design as it is started: the field takes the room the size and file were using. */
.lib-last-row.naming .lib-last { flex: 0 1 auto; }
.lib-last-row.naming .lib-when { display: none; }
/* Over `.input`, which is centred for a number in a panel and comes later in the sheet. */
.lib-last-row .lib-naming { text-align: left; }

.lib-del {
  font-family: var(--mono);
  font-size: var(--t-read);
  line-height: 1;
  color: var(--emit-40);
  background: none;
  border: 0;
  border-radius: var(--r);
  padding: 7px 10px;
  cursor: pointer;
  flex: 0 0 auto;
}

.lib-del:hover {
  background: #fbecea;
  color: var(--err-90);
}

/* A row with a write out: its keys are off until the answer lands (`guard.js`), and the row reads as busy. */
.lib-last-row:has([aria-busy='true']) { opacity: .6; transition: opacity 0s .15s; }

/* ── New design: which last to start on ──
   Over Files (60) and under a confirmation (80), which leaving the open design may ask. */
.new-design-back {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(20, 24, 28, .34);
}

.new-design {
  width: min(640px, calc(100vw - 32px));
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
}

/* The panel's one heading, a size above the list labels under it. */
.new-design-title {
  margin-bottom: 8px;
  font-size: var(--t-read);
  letter-spacing: .12em;
  color: var(--emit-100);
  text-shadow: none;
}

/* The shelves scroll inside the panel rather than pushing its foot off the screen. */
.new-design-shelves {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.new-design-shelves > .lib-heading { margin: 12px 0 6px; }
.new-design-shelves > .lib-heading:first-child { margin-top: 0; }

/* A shelf: a card for each last, three to a row. */
.new-design-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

/* A card: the last's image on the viewport's ground, then its name and size. The whole card is the button. */
.new-design-card {
  appearance: none;
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 0;
  font: inherit;
  text-align: left;
  color: var(--emit-90);
  background: var(--raised);
  border: 1px solid var(--line);
  cursor: pointer;
}
.new-design-picture {
  display: block;
  aspect-ratio: 3 / 2;
  /* Not the image's height as a flex item's least: its 3:2 is the box's less the rule, so a card would grow
     1 px as its image lands and move every card under it. */
  min-height: 0;
  background: var(--viewport);
  border-bottom: 1px solid var(--line);
}
/* Cover, so the studio picture's paper reaches the card's edges: both images are 3:2, the box is 3:2 less its
   1 px rule, and contain left a hairline of the ground at its sides. */
.new-design-picture > img { display: block; width: 100%; height: 100%; object-fit: cover; }
.new-design-label {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 5px 8px 6px;
}
/* A last's name in the face the Last Library lists it in, on one line. */
.new-design-name {
  overflow: hidden;
  font-family: var(--mono);
  font-size: var(--t-read);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.new-design-size { min-height: 1.2em; font-size: var(--t-small); color: var(--emit-50); }
/* The import: a card like the others, a thin plus where the image would be. */
.new-design-import .new-design-picture { position: relative; background: var(--raised); }
.new-design-import .new-design-picture::before,
.new-design-import .new-design-picture::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 24px;
  height: 1px;
  background: var(--emit-50);
  transform: translate(-50%, -50%);
}
.new-design-import .new-design-picture::after { width: 1px; height: 24px; }
.new-design-import .dl { overflow: hidden; color: var(--emit-90); text-shadow: none; text-overflow: ellipsis; }
/* A last the server draws no image of (it failed, or no worker draws): said, not left as one being drawn. */
.new-design-none .new-design-picture { display: flex; align-items: center; justify-content: center; }
.new-design-none .new-design-picture::after {
  content: 'NO IMAGE';
  font-family: var(--label);
  font-size: var(--t-label);
  letter-spacing: .15em;
  color: var(--emit-50);
}

/* Focus shows however the panel was opened, with a click as with a key: the chosen card's label inverts, and
   Enter takes it. */
.new-design-card:focus { outline: none; border-color: var(--line-firm); }
.new-design-card:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: 2px; }
.new-design-card:focus .new-design-label { background: var(--emit-100); color: var(--on-fill); }
.new-design-card:focus .new-design-size,
.new-design-import:focus .dl { color: var(--on-fill); }

.new-design-foot { display: flex; justify-content: flex-end; margin-top: 10px; }

/* The empty stage's one control, where the last will be. */
.stage-new {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
}

/* ══════════════════════════════════════════════════════════════════════
   CONFIRMATION PANE (single shared destructive-action modal)
   ══════════════════════════════════════════════════════════════════════ */

.confirm {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(20, 42, 60, .38);
}

.confirm-panel {
  width: min(440px, 90vw);
  background: var(--raised);
  border: 1px solid var(--err-90);
  box-shadow: var(--shade);
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* The size a mesh is asked for on the way in — see `asksize.js`. Laid out as a row, because it is one
   number and one list and a column of them reads as a form with more to fill in than there is. */
.ask-size {
  flex-flow: row wrap;
  align-items: center;
  gap: 10px;
}

.ask-size > .input { flex: 1 1 90px; }

.ask-size > .hint { flex: 1 1 100%; }

.confirm-msg {
  font-size: var(--t-title);
  color: var(--emit-90);
  text-shadow: var(--glow-text);
  white-space: pre-wrap;
}

.confirm-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
}

/* ══════════════════════════════════════════════════════════════════════
   CRT-STYLED SCROLLBARS (dropdowns, library, any overflow)
   ══════════════════════════════════════════════════════════════════════ */

* {
  scrollbar-width: thin;
  scrollbar-color: var(--emit-70) var(--well);
}

::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

::-webkit-scrollbar-track {
  background: var(--well);
  border-left: 1px solid var(--line);
}

::-webkit-scrollbar-thumb {
  background: var(--emit-50);
  border: 1px solid var(--line);
}

::-webkit-scrollbar-thumb:hover {
  background: var(--emit-90);
}

::-webkit-scrollbar-corner {
  background: var(--well);
}

/* ══════════════════════════════════════════════════════════════════════
   2D DRAWINGS — sheet thumbnails, and one sheet full size.

   The drawings are black on transparent so they print. That was invisible on the
   amber CRT, so a sheet was given paper of its own to sit on; it keeps it here,
   a shade warmer and brighter than the panels, so a pattern still reads as a
   sheet of something rather than as part of the interface.
   ══════════════════════════════════════════════════════════════════════ */

/* ── One sheet, full size ── */

.sheet-view {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(20, 42, 60, .38);
}

.sheet-panel {
  display: flex;
  flex-direction: column;
  max-width: min(1200px, 94vw);
  max-height: 90vh;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
}

.sheet-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line-firm);
}

.sheet-head .push { flex: 1 1 auto; }

/* ── EXPORT AS ▾ ──
   A menu and not three buttons in a row. Printing is what a maker wants and the other two are for a
   plotter and for CAD, so they go behind one word rather than competing with it. */
.menu { position: relative; }

/* Fixed, not absolute, and placed from the button's own position at open time.
   Two things were hiding it. `.toolbar` is `overflow: hidden` — that is what makes the bar elide rather
   than wrap — so an absolutely positioned child was clipped to the height of the bar. And `.menu-items`
   sits at `z-index: 1`, which is under the canvas. Fixed positioning leaves the clipping ancestor
   entirely, and the stack here is the overlays'. */
#recent-items {
  position: fixed;
  right: auto;
  z-index: 62;
}

/* The size chip's own menu, fixed for the same reason and placed the same way — see `_renderShownSize`.
   Every size the system has, so it is a list to scroll rather than a field to type in, capped at a third of
   the window: forty-odd rows would otherwise run off the bottom of a laptop screen. */
#shown-items {
  position: fixed;
  right: auto;
  z-index: 62;
  max-height: 34vh;
  overflow-y: auto;
}

/* The size chip: the number, and nothing else. It carried a word saying whether the size was the source or a
   grade of it, and that is what a maker asked to have taken away. */
#shown-btn { white-space: nowrap; }

/* A size that could not be graded. The chip keeps showing the last it fell back to, and says why on hover. */
.key.warn { color: var(--warn); border-color: var(--warn); }

#shown-items .menu-item { white-space: nowrap; }

/* A design's name, then how long ago it was worked on, pushed to the right. */
#recent-items .menu-item { display: flex; align-items: baseline; gap: 12px; }
#recent-items .menu-item .lib-when { margin-left: auto; }

/* The recent designs, five in sight at a time: the list scrolls under the wheel, a finger and the arrow keys, and
   the keys under it stay in place. A row has a set height, so five rows are exactly the list's height. */
#recent-items { --recent-row: 33px; overflow-y: auto; }
#recent-items > .menu-item { flex: none; }
.recent-list {
  display: flex;
  flex-direction: column;
  flex: 0 1 auto;
  max-height: calc(5 * var(--recent-row));
  overflow-y: auto;
  overscroll-behavior: contain;
  /* `auto` gives the bar to `::-webkit-scrollbar` in Chrome and Safari, which draw it at all times: an overlay bar
     shows only while the list moves, and five rows in a box of five then look like the whole list. */
  scrollbar-width: auto;
  scrollbar-color: auto;
}
.recent-list > .menu-item { flex: none; height: var(--recent-row); }

/* The way to the full list, set apart from the designs above it. */
.menu-item.more {
  border-top: 1px solid var(--line);
  color: var(--emit-50);
}

/* A note where a list would be: not pressable, so it does not look it. */
.menu-item.note { color: var(--emit-50); cursor: default; }

.menu-items {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-width: 190px;
  background: var(--raised);
  border: 1px solid var(--line-firm);
  box-shadow: var(--shade);
}

.menu-item {
  appearance: none;
  background: none;
  border: 0;
  font: inherit;
  font-size: var(--t-read);
  color: var(--emit-90);
  text-align: left;
  padding: 7px 12px;
  cursor: pointer;
}

.menu-item:hover { background: var(--well); color: var(--emit-100); }
.menu-item:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: -2px; }

/* Paper, and it scrolls: a plan outline is 240 mm long and will not always fit. */
/* The sheet carries its own white ground, so the page it sits on adds none. The cream tint here was
   the off-white *behind* the white paper — `.sheet-paper` in the side panel was only half of it. */
.sheet-page {
  overflow: auto;
  padding: 16px;
  background: var(--paper-sheet);
}

/* Width here, height in script. `max-height: 100%` resolves against `.sheet-page`, whose own height comes
   from its content, so the percentage binds nothing: a 70 x 240 mm sheet came out 907px tall in a 722px box
   and scrolled. Giving the panel a definite height instead would letterbox a small drawing in a tall empty
   box, so the height is bounded in pixels from the window — see `boundTheHeight` in `ui/sheet.js`.
   `overflow: auto` above stays as the backstop for the moment before script has measured. */
.sheet-page img { display: block; margin: 0 auto; max-width: 100%; height: auto; }

/* A modifier whose geometry the engine refuses. "Ready" only ever meant "has its
   picks", and a row that said so while the cut failed was actively misleading. */
.tree-row.modifier.invalid { border-color: var(--err-90); }
.tree-row.modifier.invalid .tree-detail { color: var(--err-90); text-shadow: var(--glow-err); }

/* What the last cut reported, above the parameters that caused it. */
.param.verdict { gap: 4px; border-bottom-color: var(--err-90); }

.verdict-line {
  font-size: var(--t-body);
  line-height: 1.35;
}

.verdict-line.error { color: var(--err-90); text-shadow: var(--glow-err); }
.verdict-line.warn { color: var(--warn); }

/* ── GRADE: the run, then its download ── */

/* Labels a little wider than the other panels': SIZE SYSTEM and FEATHERLINE MM are cut at 92 px. */
.grade-panel .param { grid-template-columns: 108px minmax(0, 1fr); }
.grade-panel .param.wide { grid-template-columns: minmax(0, 1fr); gap: 4px; padding-top: 8px; }

/* An action across the panel. In the label's column its words ran out of its border. */
.grade-action { display: grid; gap: 4px; padding: 10px 0 4px; }
.grade-action .key { width: 100%; }
.grade-action .param-help { margin: 0; }

.grade-download { margin-top: 10px; padding-top: 6px; border-top: 1px solid var(--line); }

/* Three columns that fit the panel: the k and the two deltas of their own ran past its edge. */
.grade-run { table-layout: fixed; margin-top: 2px; }
.grade-run th:first-child, .grade-run td:first-child { width: 30%; }
.grade-run td { white-space: nowrap; }
.grade-moved { color: var(--emit-50); font-size: var(--t-label); }

.written {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 26vh;
  overflow-y: auto;
}

.written-file {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: var(--t-body);
  color: var(--emit-80);
}

.written-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.written-kind {
  flex: 0 0 auto;
  font-family: var(--label);
  font-size: var(--t-label);
  color: var(--emit-50);
}

/* ══════════════════════════════════════════════════════════════════════
   HALF A WINDOW

   The program has to be usable at about 640–700 px wide: half a laptop screen,
   an iPad, an iPad in split view. Not a phone. Three things break at that width
   unless something says otherwise, and all three did:

   * a bar wraps and becomes a second bar,
   * the panel keeps its full width and takes half of what is left,
   * anything floating over the canvas runs off the edge of it.

   The rule is that the viewport is the point of the program, so it is the last
   thing to give up room. Chrome narrows, then drops out, then floats.
   ══════════════════════════════════════════════════════════════════════ */

/* The wordmark goes before anything that works does. It is centred on the bar and out of its flow, so from
   here down "Last saved a moment ago", the longest note an edit writes, runs into it. A longer note or the
   findings chip hides it at any width: see `clearTheWordmark`. */
@media (max-width: 940px) {
  .wordmark { display: none; }
}

@media (max-width: 1100px) {
  .workspace { --right-w: 280px; }
  .toolbar { gap: 8px; }
}

@media (max-width: 880px) {
  .workspace { --right-w: 240px; }
  /* Autosave is not news. At this width the room it takes is worth more than the reassurance, and the
     title bar still says "Not saved" if something is actually wrong — that is what `data-state` is. */
  .saved[data-state="saved"] { display: none; }
  .mode { padding: 5px 9px; }
  .toolbar { padding: 6px 8px; }
  .dock { bottom: 12px; }
  .canvas-tools { top: 8px; right: 8px; }
}

/* Below this the panel stops being a column and becomes a sheet over the canvas. At 220 px of panel
   beside 420 px of viewport neither one is usable, and of the two it is the viewport that matters. */
@media (max-width: 720px) {
  /* Stacked, not overlaid. The panel used to become a sheet over the canvas that folded away to a rail —
     and with the fold gone there would be no way to get the canvas back, so the two share the height
     instead. The stage keeps the larger share because the model is the point. */
  .workspace {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
  }

  .stage { min-height: 45vh; }

  .panel {
    max-height: 42vh;
    border-left: 0;
    border-top: 1px solid var(--line-firm);
  }

  .mode { padding: 5px 7px; letter-spacing: .04em; }
  .ctx-name { font-size: var(--t-read); }
}

/* ══════════════════════════════════════════════════════════════════════
   A PHONE

   Narrower than 600 px, or held on its side: short, and touched. Every rule
   from here down is under one of those queries, so a desktop window is laid
   out as before. On a phone a control is at least 44 px each way, a field is
   set at 16 px, which is what stops iOS zooming into it, and nothing sits under
   the notch or the home indicator, where `viewport-fit=cover` puts the page.
   ══════════════════════════════════════════════════════════════════════ */

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  .key { min-height: 44px; min-width: 44px; }
  .key.icon { width: 44px; height: 44px; }
  .job,
  .lib-field > input[type="file"] { min-height: 44px; }
  .swatch-dot { width: 44px; height: 44px; }
  /* A link drawn as a key centres its word the way a button does. */
  a.key { display: inline-flex; align-items: center; justify-content: center; }

  .input,
  .pass-input,
  .param .input,
  .input.compact { min-height: 44px; font-size: 16px; }

  /* A slider a finger can hold: the thumb was 12 x 16 px. The track is drawn inside a taller control, so the
     thumb stays inside the box that takes the touch. */
  input[type=range] { height: 44px; border: 0; background: none; }
  input[type=range]::-webkit-slider-runnable-track {
    height: 6px;
    border: 1px solid var(--line-firm);
    background: var(--emit-30);
  }
  input[type=range]::-webkit-slider-thumb { width: 22px; height: 28px; margin-top: -12px; }
  input[type=range]::-moz-range-track { height: 6px; border: 1px solid var(--line-firm); background: var(--emit-30); }
  input[type=range]::-moz-range-thumb { width: 22px; height: 28px; }
}

/* A tap leaves `:hover` on what it pressed until the next tap lands somewhere else, so a key stayed grey after
   it had done its work. Where nothing hovers, a key under the finger looks as it does at rest. */
@media (hover: none) {
  .key:hover:not(:disabled) { background: var(--raised); }
  .key.icon:not(.on):hover:not(:disabled) { background: none; color: var(--emit-70); }
  .key.icon.on:hover:not(:disabled) { background: var(--emit-100); color: var(--on-fill); }
  .key.icon.danger:hover:not(:disabled) { color: var(--emit-40); }
  .key.danger:hover:not(:disabled) { background: var(--raised); color: var(--err-90); }
  .dock-key:not(.primary):hover { background: none; color: var(--emit-70); }
  .dock-key.on:hover,
  .dock-key.primary:hover { background: none; color: var(--emit-100); }
  .dock-held.open > .dock-key:hover { background: var(--well); }
  /* A password field is drawn by its underline, which came only under a pointer: on a touch screen the field was
     grey words on the page until it was tapped. */
  .pass-input { border-bottom-color: var(--line-firm); }
}

/* ── The title bar ── */

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  .toolbar {
    gap: 4px;
    padding: max(4px, env(safe-area-inset-top)) max(6px, env(safe-area-inset-right)) 4px
      max(6px, env(safe-area-inset-left));
  }

  .mode { min-height: 44px; }

  /* Context rather than news, so it is the first thing in the bar to go: see `.held`. */
  .held { display: none; }

  /* The name takes what the bar has left. Its cap was worked out against the wordmark, which is not shown here,
     and on a phone it came to less than nothing: the field was 8 px wide and showed a colon. */
  .ctx {
    flex: 1 1 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: 26px 18px;
    column-gap: 6px;
  }
  .ctx-name {
    grid-area: 1 / 1 / 3 / 3;
    width: 100%;
    max-width: none;
    height: 44px;
    field-sizing: fixed;
  }
  /* The saved note and the findings chip go on a line under the name, inside the field's own 44 px: beside it, a
     "Last saved" squeezed the name away. The field keeps the taps, except on a note that asks again. */
  .ctx:has(> .saved:not([hidden]):not([data-state="saved"]), > .findings-chip:not([hidden])) .ctx-name {
    padding-bottom: 18px;
  }
  .ctx > .saved,
  .ctx > .findings-chip { grid-row: 2; z-index: 1; align-self: center; }
  .ctx > .saved { grid-column: 1; min-width: 0; max-width: none; padding-left: 4px; pointer-events: none; }
  .ctx > .saved[data-state="conflict"],
  .ctx > .saved[data-state="gone"] { pointer-events: auto; }
  /* The chip's box is one line of text; the band round it takes the tap. It stays on the chip's line: taller, it
     reached the middle of a 150 px name at 320 px, and a tap meant for the name opened the findings. */
  .ctx > .findings-chip { grid-column: 2; position: relative; }
  .ctx > .findings-chip::before { content: ''; position: absolute; inset: -3px -6px -4px; }

  /* A design's name and its age, as Files lists them: the name shortens, and the age stays one word. */
  #recent-items { --recent-row: 44px; right: max(8px, env(safe-area-inset-right)); }
  #recent-items .menu-item { min-height: 44px; align-items: center; }
  #recent-items .menu-item > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #recent-items .menu-item .lib-when { white-space: nowrap; }
  #shown-items .menu-item { min-height: 44px; }
}

/* On its side a third of the screen was three sizes: the list runs to the foot of the screen instead. */
@media (max-height: 500px) and (pointer: coarse) {
  #shown-items { max-height: calc(100vh - 72px); }
  /* As wide as its names, as at a desk, and 8 px short of the edge at most: held to the edge, a row ran across the
     screen. `--recent-left` is the menu's left, set where it is placed (`_wireRecent`). */
  #recent-items { right: auto; max-width: calc(100vw - var(--recent-left, 0px) - max(8px, env(safe-area-inset-right))); }
}

/* Upright, the modes have a line of their own under the file, a third of it each. On one line the bar had room
   for the modes or for the name, and the name lost. */
@media (max-width: 600px) {
  .toolbar { flex-wrap: wrap; row-gap: 4px; }
  .modes { order: 1; flex: 1 1 100%; display: flex; }
  .mode { flex: 1 1 0; }
}

/* ── The job tabs, and what shares the top of the canvas with them ── */

@media (max-width: 600px) {
  /* Across the top of the canvas, a quarter each. Centred at their own width they ran over undo and redo, and at
     320 px a tap on undo landed on PATTERNS. */
  .jobs.floating {
    left: 8px;
    right: 8px;
    top: 8px;
    transform: none;
    animation: none;
  }
  .jobs.floating .job { flex: 1 1 0; padding: 0 4px; }

  /* The foot toggle, undo and redo at the right, under the tabs where there are tabs: the foot of the canvas is the
     dock's and nothing else's. */
  .canvas-tools { top: 8px; right: 8px; }
  .stage:has(> .jobs.floating:not([hidden])) .canvas-tools { top: 64px; }

  /* Left of the canvas tools, and never on the tabs: both were at the top, centred. */
  .updating { top: 20px; max-width: calc(100% - 200px); }
  .stage:has(> .jobs.floating:not([hidden])) .updating { top: 76px; }
  /* A sentence and a key, wrapped inside the canvas: on one line it was 785 px long and RELOAD was off screen. */
  .stage .updating.outdated { top: 68px; }
  .stage:has(> .jobs.floating:not([hidden])) .updating.outdated { top: 124px; }
  .updating.outdated {
    left: 8px;
    right: 8px;
    max-width: none;
    transform: none;
    justify-content: space-between;
    white-space: normal;
    letter-spacing: .06em;
    line-height: 1.4;
  }
  /* The sentence gives way to RELOAD, never the other way round: the key was squeezed to 44 px round its word. */
  .updating.outdated > .key { flex: 0 0 auto; }
}

/* On its side the job tabs are 44 px tall, and both notes sat on them, at the desktop's 12 px and 44 px. Under them
   instead, and the long one wraps between the notch and the rail. */
@media (max-height: 500px) and (min-width: 601px) and (pointer: coarse) {
  .stage:has(> .jobs.floating:not([hidden])) .updating { top: 70px; }
  .updating.outdated {
    top: 70px;
    left: max(8px, env(safe-area-inset-left));
    right: calc(80px + env(safe-area-inset-right));
    max-width: none;
    transform: none;
    justify-content: space-between;
    white-space: normal;
    letter-spacing: .06em;
    line-height: 1.4;
  }
  .updating.outdated > .key { flex: 0 0 auto; }
}

/* ── The pieces rail, between undo and what is at the foot of the canvas ── */

/* Centred on the whole canvas, the first of twelve rows was under undo, and a tap on it pressed redo. `--rail-top`
   and `--rail-foot` are what the rail keeps clear of. */
@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  .rail {
    top: calc(50% + (var(--rail-top) - var(--rail-foot)) / 2);
    max-height: calc(100% - var(--rail-top) - var(--rail-foot));
  }
}

@media (max-width: 600px) {
  .rail { --rail-top: 68px; --rail-foot: calc(var(--dock-h, 44px) + 16px + env(safe-area-inset-bottom)); }
  .stage:has(> .jobs.floating:not([hidden])) .rail { --rail-top: 124px; }
}

/* On its side, above the foot toggle, in the phone's rows. */
@media (max-height: 500px) and (min-width: 601px) and (pointer: coarse) {
  .rail { --rail-top: 68px; --rail-foot: calc(88px + env(safe-area-inset-bottom)); gap: 3px; }
  .rail-dot { width: 104px; height: 40px; }
  .rail-piece.sub .rail-dot { width: calc(104px - 10px * var(--depth, 1)); height: 32px; }
}

/* ── The dock, its panels, and the heel panel ── */

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  /* As wide as its keys, up to the canvas less a margin. Centred from `left: 50%` it could only be as wide as the
     half of the canvas right of its middle, so it wrapped at 190 px. Over the heel panel, which its own panels open
     across. */
  .dock {
    left: max(8px, env(safe-area-inset-left));
    right: max(8px, env(safe-area-inset-right));
    bottom: max(8px, env(safe-area-inset-bottom));
    width: fit-content;
    max-width: none;
    margin: 0 auto;
    transform: none;
    z-index: 5;
  }
  .dock-key { min-height: 44px; }
  .dock-key.icon,
  .dock-key.icon.dock-value,
  .dock-key.icon.dock-named { height: 44px; }
  .dock-key.icon { width: 44px; gap: 0; }
  .dock-key.dock-value { min-width: 44px; }
  /* Letters for keys that a phone has no keyboard to press. The mark count was drawn over one. */
  .dock-key kbd { display: none; }
  /* The swatch stays the size it is; the transparent border round it takes the tap. */
  .dock-dot { width: 44px; height: 44px; margin: 0; border: 14px solid transparent; background-clip: padding-box; }
  .dock-held.open .dock-dot { outline: 2px solid var(--emit-100); outline-offset: -14px; }

  /* A panel opens over the middle of the dock rather than over its own key, and wraps its words inside the screen:
     over a key at the end of the bar, the sole's list started 40 px off the left edge. */
  .dock-held { position: static; }
  .dock-pop { width: max-content; max-width: calc(100vw - 16px); max-height: 50vh; }
  .dock-item { min-height: 44px; white-space: normal; }
  .dock-pop:has(> .dock-swatch) { display: grid; grid-template-columns: repeat(4, 44px); gap: 4px; }
  .dock-swatch { width: 44px; height: 44px; }
  /* One layer's colours, four to a line as the piece's colours are, under the layers' names. */
  .dock-sole-layers { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }
  .dock-sole-layer { min-width: 44px; justify-content: center; }
  .dock-sole-row { grid-template-columns: repeat(4, 44px); gap: 4px; }
  .dock-sole-row:not(.shown),
  .dock-sole-name { display: none; }

  /* Above the dock however many lines it takes: `--dock-h` is its height, which `_measureTheDock` keeps. The count
     of marks sat on the first of two lines of keys. */
  .pick-banner { bottom: calc(var(--dock-h, 44px) + 18px + env(safe-area-inset-bottom)); }
  .say { bottom: calc(var(--dock-h, 44px) + 40px + env(safe-area-inset-bottom)); max-width: calc(100% - 24px); }

  .heel-shapes { flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain; }
  .heel-shapes .key { flex: 0 0 auto; }
  /* The width of their panel, as `.dock-slider` asks: `input[type=range]` outweighed it and held them at 100 px. */
  input[type=range].dock-slider { width: 100%; }
}

@media (max-width: 600px) {
  /* Keys edge to edge, so the eight a piece has fit one line from 375 px up. A rule between groups ended one line
     or began the next once the dock wrapped. `--dock-balance` asks `deck._balanceTheDock` for as many keys on each
     line: at 360 px the bin was alone on the second. */
  .dock { gap: 0; padding: 2px; --dock-balance: 1; }
  .dock-rule { display: none; }
  /* A list ends below undo: the title bar, the job tabs and undo take 250 px, plus the notch and home-bar insets. */
  .dock-pop {
    max-height: min(50vh, calc(100dvh - 250px - var(--dock-h, 44px) - env(safe-area-inset-top)
      - env(safe-area-inset-bottom)));
  }

  /* Across the foot of the canvas, above the dock: at the top right it covered half the last. The numbers two to a
     line, and the shapes one line that scrolls. `--covers` tells `deck._measureTheCover` to frame the last above it,
     where it was framed behind it. Tall enough for the whole panel on a 320 x 640 screen. */
  .heel-panel {
    top: auto;
    left: 8px;
    right: 8px;
    bottom: calc(var(--dock-h, 44px) + 16px + env(safe-area-inset-bottom));
    width: auto;
    max-height: 45%;
    --covers: foot;
  }
  #heel-panel-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 16px; }
  #heel-panel-body > .heel-shapes,
  #heel-panel-body > .heel-sums { grid-column: 1 / -1; }
  #heel-panel-body > .heel-shapes { margin-bottom: 6px; }
  .heel-row { margin-bottom: 4px; }
  /* Up under its name: the thumb is 28 px high in a 44 px control, and the top of the control was empty. */
  .heel-row .dock-slider { margin: -6px 0 0; }
}

/* Where the side panel is under the canvas, FORM holds the foot panel at its head (`deck._renderFootPanel`), over the
   submode's content. */
@media (max-width: 720px) {
  .panel-right > .foot-panel {
    position: static;
    flex: 0 0 auto;
    width: auto;
    max-height: 40%;
    border: 0;
    border-bottom: 1px solid var(--line-firm);
    border-radius: 0;
    box-shadow: none;
  }
}

/* On a phone in DRAW the foot panel is a sheet across the foot of the canvas, above the dock: the foot toggle is at the
   top right there, so the sheet never covers it. `--covers` frames the last above it (`deck._measureTheCover`). */
@media (max-width: 600px) {
  .stage > .foot-panel {
    top: auto;
    left: 8px;
    right: 8px;
    bottom: calc(var(--dock-h, 44px) + 16px + env(safe-area-inset-bottom));
    width: auto;
    max-height: 40%;
    padding: 4px 12px 8px;
    --covers: foot;
  }
  .stage:not(:has(> .dock:not([hidden]))) > .foot-panel { bottom: calc(8px + env(safe-area-inset-bottom)); }
  .foot-apply { min-height: 44px; }
}

/* On its side: down the left, clear of the foot toggle at the bottom right and above the dock, with the last framed in
   what is left of the canvas (see `--covers` above). */
@media (max-height: 500px) and (min-width: 601px) and (pointer: coarse) {
  .heel-panel {
    top: 8px;
    left: max(8px, env(safe-area-inset-left));
    right: auto;
    bottom: calc(var(--dock-h, 44px) + 16px + env(safe-area-inset-bottom));
    max-height: none;
    --covers: left;
  }
  /* And the dock clear of the foot toggle: centred on the whole width, the ten keys of HEEL ran under it at 667 px. */
  .dock { right: calc(80px + max(8px, env(safe-area-inset-right))); }
}

/* An iPhone SE on its side has no room for the heel panel, the job tabs and undo on one line, and the panel covered
   DRAW: it starts under the tabs. Its dock is keys edge to edge, as upright, so HEEL's ten keys stay on one line. */
@media (max-height: 500px) and (min-width: 601px) and (max-width: 800px) and (pointer: coarse) {
  .heel-panel { top: 70px; }
  .dock { gap: 0; padding: 2px; }
  .dock-rule { display: none; }
}

/* ── The panel, in FORM and GRADE ── */

/* Under the canvas it is the width of the screen. It kept the column's 240 px and left a blank strip beside it. */
@media (max-width: 720px) {
  .panel-right { width: auto; }
}

/* A choice of four is wider than a 240 px panel's control column, so its label goes above it: MONDO and TABLE ran
   out of the panel on an iPad and on a phone on its side. */
@media (max-width: 880px) {
  .param:has(> .segments) { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  /* A size's row is the panel's width and its slider takes what the row has left: at 100 px it ran past. */
  .mesh-ctl { flex: 1 1 100%; min-width: 0; }
  .mesh-ctl input[type=range] { flex: 1 1 60px; min-width: 0; width: auto; }
}

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  /* One scroll for the whole panel. Its two bodies and the view controls each had a share of 42 % of the screen:
     the tree hid + ADD, and after a grade the sizes had a strip 100 px tall. */
  .panel { overflow-y: auto; overscroll-behavior: contain; padding-bottom: env(safe-area-inset-bottom); }
  .panel-body,
  .panel-body:not(.panel-grow),
  .panel-grow { flex: none; max-height: none; overflow: visible; }

  .tree-row,
  .tree-name-btn,
  .tree-toggle,
  .tree-remove,
  .add-item { min-height: 44px; }
  .tree-move { min-height: 32px; min-width: 44px; }
  .size-chip { min-width: 44px; height: 44px; }
  /* FIT's rows stay one line each: the field and the lock at a finger's size, and nothing else grows. */
  .tape-row { grid-template-columns: minmax(0, 1fr) auto 76px 44px; column-gap: 4px; padding: 0 2px; }
  .tape-pick { min-height: 40px; }
  .tape-row .input.boxed { min-height: 40px; }
  .tape-lock { width: 44px; height: 40px; }
  .segments .radio-chip { min-height: 44px; padding: 0 12px; }
  .check { min-height: 44px; padding: 0 12px 0 0; }
  .swatches { gap: 8px; }
  .mesh-toggle { min-width: 44px; min-height: 44px; }
}

/* ── Files ── */

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  /* The whole screen. A square in the middle of a phone left a frame of the app round it to look past, and on its
     side the square was 336 px of an 844 px screen. */
  .library { align-items: stretch; justify-content: stretch; }
  .library-panel {
    width: 100%;
    height: 100%;
    border: 0;
    box-shadow: none;
    padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
  }
  .library-head { padding: 4px 8px 4px 16px; }
  .library-body { padding-bottom: calc(14px + env(safe-area-inset-bottom)); }

  /* A design's name and date on the left and its keys at the right, on one row whatever the width: at 360 px
     DUPLICATE and the cross each fell to a line of their own. The name wraps rather than shortens. */
  .lib-last-row:has(> .lib-last) { flex-wrap: nowrap; }
  .lib-last-row > .lib-last { flex: 1 1 0; }
  .lib-last-row .lib-name { white-space: normal; overflow-wrap: anywhere; min-width: 0; }
  /* A last's file wraps under its name rather than shortening: lasts that share a name differ only in it. */
  .lib-last-row .lib-file { flex: 1 1 auto; white-space: normal; overflow-wrap: anywhere; }
  .lib-last,
  .lib-act,
  .lib-del { min-height: 44px; }
  .lib-del { min-width: 44px; }
  .lib-tab { min-height: 44px; }

  /* The address is what says whose account this is, so it wraps rather than shortens. */
  .lib-block .param { grid-template-columns: 80px minmax(0, 1fr); }
  .lib-value { overflow-wrap: anywhere; }
  .pass-input { width: 100%; max-width: 360px; }
  .lib-code .input.lib-code-value { width: 100%; }
}

/* Upright, the sections are one line that scrolls sideways, never two: a second line left the chosen tab hanging
   below the first. The shadow at either end says there is more that way, and shows only where there is: at 320 px
   the row can end exactly on a tab, and a 2 px line there did not say that a tab was hidden past it. */
@media (max-width: 600px) {
  .lib-rail {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    background:
      linear-gradient(to right, var(--well) 60%, transparent) left / 28px 100% no-repeat local,
      linear-gradient(to left, var(--well) 60%, transparent) right / 28px 100% no-repeat local,
      linear-gradient(to right, rgba(0, 0, 0, .3), rgba(0, 0, 0, .12) 3px, transparent) left / 18px 100% no-repeat scroll,
      linear-gradient(to left, rgba(0, 0, 0, .3), rgba(0, 0, 0, .12) 3px, transparent) right / 18px 100% no-repeat scroll,
      var(--well);
  }
  .lib-rail::-webkit-scrollbar { display: none; }
  .lib-tab { flex: 0 0 auto; padding: 0 12px; }
}

/* On its side the screen is wide and short, so the sections go back down the left, as on a desktop. */
@media (max-height: 500px) and (min-width: 601px) and (pointer: coarse) {
  .library-split { grid-template-columns: 128px minmax(0, 1fr); grid-template-rows: none; }
  .lib-rail {
    flex-direction: column;
    flex-wrap: nowrap;
    padding: 4px 0;
    border-right: 1px solid var(--line-firm);
    border-bottom: 0;
    overflow-y: auto;
  }
  .lib-tab { border-bottom: 0; border-left: 3px solid transparent; padding: 0 10px 0 9px; }
  .lib-tab.on { border-left-color: var(--emit-100); }
}

/* ── A drawing, full size ── */

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  /* The whole screen, and the sheet scrolls inside it. `!important` over the width `ui/sheet.js` sets to hug the
     drawing, which is wider than a phone. The cap is in px units because `boundTheHeight` reads it as a number. */
  .sheet-view { padding: 0; align-items: stretch; }
  .sheet-panel {
    width: 100% !important;
    max-width: none;
    height: 100%;
    max-height: 100vh;
    max-height: 100dvh;
    border: 0;
    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  }
  .sheet-head { gap: 6px 8px; padding: 6px 8px 8px 14px; }
  #sheet-export-items .menu-item { min-height: 44px; }
  .sheet-page { flex: 1 1 auto; min-height: 0; padding: 8px; }
}

/* Upright, two lines: which piece and CLOSE, then what can be done with it. On one line CLOSE was off the screen. On
   its side the four fit one line, and a second took a third of the height. */
@media (max-width: 600px) {
  .sheet-head { flex-wrap: wrap; }
  #sheet-title { flex: 1 1 auto; }
  #sheet-close { order: 1; }
  /* The spacer, made the break between the two lines. */
  .sheet-head .push { order: 2; flex: 0 0 100%; height: 0; }
  #sheet-print { order: 3; flex: 1 1 auto; }
  #sheet-export { order: 4; }
}

/* ── Questions: a confirmation, the size of a new last, a new design ── */

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  .confirm-panel { padding: 16px; max-height: calc(100vh - 16px); overflow-y: auto; }
  /* Answers wrap inside the panel, sharing each line: at 320 px KEEP MINE started left of the panel. */
  .confirm-actions { flex-wrap: wrap; gap: 8px; }
  .confirm-actions .key { flex: 1 1 auto; }

  /* The size and its system on one line, OPEN and CANCEL together on the next. */
  .ask-size > input.input { flex: 1 1 calc(100% - 110px); }
  .ask-size > select.input { flex: 0 0 100px; }
  .ask-size > .key { order: 1; flex: 1 1 0; }
  .ask-size > .hint.dl { order: 2; }

  /* The shelves take what the panel has left, and scroll in it; nothing under the notch or the home indicator. */
  .new-design-back {
    padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right))
      max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
  }
  .new-design { width: min(640px, 100%); max-height: 100%; padding: 12px; }
  .new-design-grid { gap: 8px; }
}

/* Two cards to a row on a phone held upright; on its side it is wide enough for three. */
@media (max-width: 600px) {
  .new-design-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ── The gate and the waiting screen ── */

@media (max-width: 600px), (max-height: 500px) and (pointer: coarse) {
  /* It scrolls, and is centred only while it fits: centred on a short screen, a two-line message or the keyboard
     pushed the brand and the tabs off the top with no way back to them. */
  .gate {
    align-items: flex-start;
    overflow-y: auto;
    padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
      max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  }
  .gate-panel { margin: auto; width: min(360px, 100%); padding: 24px 20px; }
  /* A label wraps before it runs past its field, and an address with nothing to break at breaks anywhere. */
  .field > .dl { white-space: normal; }
  .gate-note { overflow-wrap: anywhere; }
}

/* ── The notch, on its side ── */

/* On its side the notch is at one end of the screen and the home bar at the foot, and undo, the rail, the foot toggle and
   the panel's rows were 8 to 16 px from the edge, under them. They keep clear, as the title bar and the dock do. */
@media (max-height: 500px) and (min-width: 601px) and (pointer: coarse) {
  .canvas-tools { right: max(8px, env(safe-area-inset-right)); }
  .rail { right: max(6px, env(safe-area-inset-right)); }
  .findings { left: max(10px, env(safe-area-inset-left)); }
  /* And no wider than on a desktop, so a long refusal is not drawn across the rail. */
  .say { max-width: min(560px, calc(100% - 24px - env(safe-area-inset-left) - env(safe-area-inset-right))); }
  .panel { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
}

/* Where the panel is beside the canvas it has the right of the screen to itself, wider by the notch, and the right
   of the canvas is not the right of the screen. */
@media (max-height: 500px) and (min-width: 721px) and (pointer: coarse) {
  .panel { padding-left: 0; }
  .panel-right { width: calc(var(--right-w, 320px) + env(safe-area-inset-right)); }
  .workspace:has(> .panel-right:not([hidden])) .canvas-tools { right: 8px; }
}

/* ── SCULPT: the profiles in a 2x2 grid over the stage, the 3D last in the fourth cell ──
   The canvas is put in that cell, so the viewer's own ResizeObserver frames it there. The tabs over the stage keep
   their place: the grid starts under them. */
.stage { --sculpt-top: 56px; }

.sculpt-grid {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  top: var(--sculpt-top);
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  pointer-events: none;
}

.sculpt-grid[hidden] { display: none; }
.sculpt-tabs { display: none; }

.sculpt-pane {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  pointer-events: auto;
  background: var(--viewport);
  border-top: 1px solid var(--line-firm);
  outline: none;
}

.sculpt-side, .sculpt-section { border-right: 1px solid var(--line-firm); }
.sculpt-pane.focus { box-shadow: inset 0 2px 0 #0b6aa8; }
.sculpt-3d { background: transparent; pointer-events: none; }
.sculpt-3d .sculpt-head { pointer-events: auto; align-self: flex-start; }

.sculpt-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 4px 8px;
}

.sculpt-toggles { display: flex; gap: 1px; }
.sculpt-station { flex: 1 1 auto; min-width: 60px; max-width: 220px; }
.sculpt-station-said { font-family: var(--courier, monospace); min-width: 3.5em; }
.sculpt-reach { flex: 0 0 auto; width: 4.6em; max-width: 4.6em; }
.sculpt-reach-said { margin-left: 4px; }

.sculpt-svg { flex: 1 1 0; width: 100%; min-height: 0; touch-action: none; cursor: default; }
.sculpt-was { fill: none; stroke: #a8a8a8; stroke-dasharray: 2 1.5; }
.sculpt-foot { fill: rgba(222, 170, 140, 0.22); stroke: rgba(176, 110, 80, 0.55); stroke-width: 0.4; }
.sculpt-curve { fill: none; stroke: #000; stroke-linejoin: round; }
.sculpt-held { fill: none; stroke: #cfcfcf; stroke-linecap: round; }
.sculpt-stopped { fill: none; stroke: #c81e1e; stroke-linecap: round; }
.sculpt-station-line { stroke: #0b6aa8; stroke-opacity: 0.35; stroke-dasharray: 3 2; }
/* The grid under each drawing, the 3D ground's lines; and each anchor a plain dot: grey as found, black once moved. */
.sculpt-grid-minor { fill: none; stroke: rgba(0, 0, 0, 0.06); }
.sculpt-grid-major { fill: none; stroke: rgba(0, 0, 0, 0.13); }
.sculpt-anchor { fill: #6b6b6b; stroke: none; cursor: grab; }
.sculpt-anchor.moved { fill: #000; }
.sculpt-anchor.picked { fill: #0b6aa8; }
.sculpt-handle { fill: #fff; stroke: #0b6aa8; cursor: grab; }
.sculpt-handle-line { stroke: #0b6aa8; }

.stage.sculpting #viewport {
  inset: auto;
  left: 50%;
  top: calc(var(--sculpt-top) + (100% - var(--sculpt-top)) / 2);
  width: 50%;
  height: calc((100% - var(--sculpt-top)) / 2);
}

/* FIT's fixes are FIT's: SCULPT holds its curves off the foot as they are drawn. */
.stage.sculpting > .foot-panel { display: none; }

/* SCULPT's panel. */
.sculpt-size, .sculpt-suggest, .sculpt-files { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 4px 2px; }
.sculpt-size-own { font-weight: 600; }
.sculpt-moved { display: grid; grid-template-columns: 1fr auto auto; gap: 2px 10px; padding: 6px 2px; border-top: 1px solid var(--line); }
.sculpt-moved-row { display: contents; }
.sculpt-moved-now, .sculpt-moved-by { font-family: var(--courier, monospace); text-align: right; }
.sculpt-files { border-top: 1px solid var(--line); }
.sculpt-finding { margin: 4px 2px; }

/* A phone: one pane at a time, chosen by its tab. */
@media (max-width: 600px) {
  .stage { --sculpt-top: 60px; }
  .sculpt-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  .sculpt-tabs { display: flex; gap: 1px; padding: 2px 8px; pointer-events: auto; background: var(--raised); }
  .sculpt-tab { flex: 1 1 0; }
  .sculpt-tab.on { background: #000; color: #fff; }
  .sculpt-grid .sculpt-pane { display: none; border-right: 0; }
  .sculpt-grid[data-phone="side"] .sculpt-side,
  .sculpt-grid[data-phone="plan"] .sculpt-plan,
  .sculpt-grid[data-phone="section"] .sculpt-section,
  .sculpt-grid[data-phone="3d"] .sculpt-3d { display: flex; }
  .stage.sculpting #viewport { left: 0; width: 100%; top: calc(var(--sculpt-top) + 40px); height: calc(100% - var(--sculpt-top) - 40px); }
  /* Under the panes' tabs, at the right of the pane's own head, which wraps clear of them. */
  .sculpt-head { flex-wrap: wrap; padding-right: 136px; }
  /* Under the panes' tabs, at the right of the pane's own head. */
  .stage.sculpting:has(> .jobs.floating:not([hidden])) .canvas-tools { top: calc(var(--sculpt-top) + 44px); }
}
