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

   The System 7 and Platinum language, without the period furniture.
   Black on white: frames are 1 px of `--line-firm`, which is `#000`.
   Selection inverts: a chosen row, a latched button and a pressed button swap
   their two colours. Shadows are hard: one offset, no blur. There is no
   decoration: 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. See `--chicago`.

   The layout works 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 is pure black. Greys mark 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. A list has no black rules in it, but the window around it does. */
  --line:       #c8c8c8;
  --line-firm:  #000000;

  /* There is no accent: a choice is marked by inverting it. The `--blue-*` names stay
     because many rules use them, and each resolves to ink. */
  --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, and there is no light
     here. */
  --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 is not
     on current Macs, so this lands on Geneva, which 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.
     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. This one rule
   declares it for every element. */
[hidden] { display: none !important; }

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

body {
  background: var(--screen);
  color: var(--emit-90);
  /* 13px: at 22px a panel holds 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. */
  background: rgba(20, 24, 28, .34);
}

/* A square, and a fixed one: a tab never resizes the modal, 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. */
.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 is too wide for a design's row. 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 as the inspector. */
.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; }

/* A list of designs is a list: one row a line, divided by a hairline, the name doing the work. */
.lib-list { display: flex; flex-direction: column; }

/* A label over a list, with its count in it. The count is in the heading, not in a sentence under
   an empty list: "INVITED 0" is one 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); }

/* SIZES in the Last Library: a row is the name and its keys, then the size, the reading and how it was set. */
.size-audit { padding-top: 16px; }
.size-row { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
.size-row + .size-row { border-top: 1px solid var(--line); }
.size-top { display: flex; align-items: center; gap: 10px; min-height: 28px; }
.size-name { font-family: var(--mono); font-size: var(--t-read); color: var(--emit-90); }
.size-kind { color: var(--emit-50); }
.size-keys { margin-left: auto; display: flex; gap: 6px; }
.size-facts { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: var(--t-small); color: var(--emit-70, var(--emit-90)); }
.size-flag { color: var(--err-90); }
.size-row > .size-reason { width: 100%; }
.size-row > .hint:empty { display: none; }
.lib-sizing { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }

.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 an empty list needs explaining rather than counting: see `.lib-heading .lib-count`.
   Smaller than the headings above it and the rows below it. */
.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 does not use `.lib-last`, which is a button and so takes a hover and a highlight. */
.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. An
     address with nowhere to break breaks anywhere. */
  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 placeholder says what a field is only
   until it has text 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, in a row with the fields beside it: 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;
}

/* A variant, under the shape it was made of: set in, with a rule down its left, so a shape and its variants
   read as one family. */
.lib-variant-row { margin-left: 14px; padding-left: 8px; border-left: 1px solid var(--line-firm); }
.lib-variant-row .lib-last { cursor: default; }
.lib-from { overflow: hidden; text-overflow: ellipsis; }

/* 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;
}

/* A library last's name as it is renamed, where its name was: in its row, or on its New Design card. */
.input.lib-name-field {
  flex: 1 1 180px;
  min-width: 0;
  width: 100%;
  text-align: left;
  font-family: var(--mono);
  font-size: var(--t-read);
  padding: 6px 8px;
}
/* Over the card's name, from its box on the screen: see `chooseNewDesign`. */
.input.lib-name-field.new-design-name-field { position: fixed; z-index: 2; width: auto; padding: 2px 4px; }
/* The line under a field a library last is named in, on a row of its own (`lastname.heldToALastName`). */
.lib-name-hint { flex: 1 1 100%; display: block; font-size: var(--t-body); color: var(--emit-50); }
.lib-name-hint.new-design-name-hint { position: fixed; z-index: 2; background: var(--raised); padding: 2px 4px; }

/* Last settings: each last's fields in a row under its name, with a rule between lasts. */
.lib-settings { padding: 10px 0; }
.lib-settings + .lib-settings { border-top: 1px solid var(--line); }
.lib-settings-head {
  flex: 1 1 100%;
  font-family: var(--mono);
  font-size: var(--t-read);
  color: var(--emit-100);
}
.lib-settings > .lib-field { flex: 1 1 120px; }
.lib-settings > .lib-field:first-of-type,
.lib-settings > .lib-field:nth-of-type(4) { flex: 2 1 200px; }

/* 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. */
.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.
   ══════════════════════════════════════════════════════════════════════ */

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

/* Two columns: the viewport, then the panel. Folding a panel never reflows the viewport into a
   different element, so the canvas keeps the one ResizeObserver already on it. */
.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. */
  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 and
   height attributes. With only `width: 100%` its height follows that ratio and not its container. */
/* The veil over the stage while a design is opening. It has the scene's own background colour, so
   a maker sees the viewport it is about to be. It fades and does not cut, because a hard cut draws
   the eye to the camera move it hides. */
.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. It must be a column:
   a body scrolls by being a flex child that can shrink, and as a block it sizes to its content, so
   the panel runs off the bottom of the screen. */
.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. If both grew, one panel would hold
   two half-height scrollers. */
.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. */
.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 job switcher, over the canvas ──
   Design has no panel, so the switcher floats top centre, clear of the undo pair at the left and
   the foot toggle at the right. A job is not a thing in the document: it is which question you ask
   about the pieces, so it is a switch and not a row. */
.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. With no column to fill, an equal share is the width of the widest box,
   and PATTERNS and SECTIONS elide. `overflow: hidden` is not set here: it zeroes a flex item's
   automatic minimum size, so the words could be squeezed below their own length. */
.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-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. */
.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. */
.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. The grouping separates the settings, 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. */
.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, not for a readout. */
.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. */
.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 keeps its words:
     the name beside it is what elides when the bar is full (see `.ctx-name`). */
  flex: 0 0 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. */
.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; }

/* The variant of its last the design is on, beside its name: as quiet as the saved note, and given up first. */
.ctx-variant {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 220px;
  overflow: hidden;
  font-family: var(--mono);
  font-size: var(--t-label);
  color: var(--emit-50);
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* FORM's variant block, first in the right-hand panel: the last's variant, then the keys that keep it. */
.variant-block {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
}
.variant-block[hidden] { display: none; }
.variant-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.variant-on { font-family: var(--mono); font-size: var(--t-label); color: var(--emit-90); }
.variant-changed { font-size: var(--t-label); color: var(--emit-50); }
.variant-keys { display: flex; flex-wrap: wrap; gap: 4px; }
.variant-keys .input.boxed.variant-name { flex: 1 1 12ch; width: auto; text-align: left; }

/* 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. Segments and not a dropdown: the modes
   are the top-level thing about the program, and a menu would hide some 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 in use would be white on near-white. Ink and not the accent: the accent means "a value you
   set", and the mode is where you are. */
.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.** 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; }

/* The eye beside each row: open while the piece is on the last, its lid shut while it is hidden for now. */
.rail-eye {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-left: 2px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--emit-50);
  cursor: pointer;
}
.rail-eye svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.25; stroke-linecap: round; }
.rail-eye:hover { color: var(--emit-100); }
.rail-eye:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: 1px; }
.rail-piece.shut .rail-eye { color: var(--emit-100); }
/* A hidden piece's row steps back, as its piece has. */
.rail-piece.shut .rail-dot { opacity: .45; }

/* 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 piece with no margin. */
.rail-raw {
  flex: 0 0 auto;
  font: var(--t-small)/1 var(--mono);
  color: var(--emit-50);
}
.rail-piece.on .rail-raw { color: var(--on-fill); }
.rail-piece.drawing .rail-raw { color: var(--mark); }

/* 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 is 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-eye { width: 32px; height: 40px; margin-left: 0; }
  .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);
}

/* The engine at work on an edit that has settled: a hairline under the dock with a short run across it. */
.engine-working {
  position: absolute;
  left: 50%;
  bottom: 8px;
  width: 56px;
  height: 2px;
  transform: translateX(-50%);
  overflow: hidden;
  background: var(--emit-30);
  pointer-events: none;
  animation: engine-in 160ms ease-out;
}
.engine-working[hidden] { display: none; }
.engine-working::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 18px;
  background: var(--emit-60);
  animation: engine-run 900ms ease-in-out infinite alternate;
}

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

@keyframes engine-run {
  from { left: 0; }
  to { left: 38px; }
}

@media (prefers-reduced-motion: reduce) {
  .engine-working::after { animation: none; left: 19px; }
}

/* 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 does not change the
   bar's width. A toolbar that changes width when you use it must be re-found 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 such as 7 mm is a number a maker knows. */
.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. Full
   width and not a fixed size, so it matches the field above it whatever that is set to. */
.dock-slider {
  width: 100%;
  margin: 4px 0 0;
  accent-color: var(--ink);
  cursor: ew-resize;
}

/* Whose numbers the Margins panel sets: two keys side by side, and the one pressed is the one in force. */
.dock-scope { display: flex; gap: 2px; padding: 2px 4px; }
.dock-scope-key {
  flex: 1 1 0;
  padding: 3px 8px;
  border: 1px solid var(--line);
  background: none;
  color: var(--emit-90);
  font: var(--t-small) var(--mono);
  letter-spacing: .04em;
  white-space: nowrap;
  cursor: pointer;
}
.dock-pop > .dock-scope-key { margin: 2px 4px 0; flex: none; }
.dock-scope-key:hover { background: var(--well); }
.dock-scope-key.on { background: var(--emit-100); border-color: var(--emit-100); color: var(--on-fill); }

/*
 * One named allowance inside the Margins panel: its name, its number, its slider.
 *
 * A grid and not a flex row, so the names line up under each other and the numbers do too.
 */
.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.
 */
.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); }

/* 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: 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
   mark of the menu button is already red. Not `.danger`, which dims an icon and so reads as
   unavailable. */
.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 too big for a text field in a side 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" is 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 reads 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);
}
/* With nothing open there is no last to show the foot in or to colour, and nothing to undo. */
.stage:has(> .stage-new:not([hidden])) > .canvas-tools { display: none; }

/* 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: two words in grey. */
.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. */
/* The two offers under the gate's button — a reset, and a confirmation sent again. Plain text and not keys:
   neither is the thing a maker came to the form to press, and a third full-width button would read as one. */
.gate-quiet-key {
  align-self: flex-start;
  margin-top: 8px;
  padding: 2px 0;
  border: 0;
  background: transparent;
  color: var(--emit-70);
  font: var(--t-small) var(--mono);
  text-decoration: underline;
  cursor: pointer;
}
.gate-quiet-key:hover { color: var(--emit-100); }
.gate-quiet-key[hidden] { display: none; }

#auth-hint,
#auth-waiting-hint,
#reset-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,
#reset-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 ── */

.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 ── */
/* Only in the Form panel, so it takes the panel's width. */
.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);
}

/* ── 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 round a correct
   value asks to be filled in. */
.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-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. 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, so the one thing that carries the state is the one thing
   with colour. */
.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: a square with a tick in it. The tick is drawn rather than typed, so it is the same
   mark at every size. */
.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 puts it over the toolbar and the
   design tree. */

/* ── Keys ── */
/* A button: 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. Every one carries a `title`. */
/* 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.
 *
 * Under and not beside, so the bar keeps its widths: a letter on the glyph's line makes every tool
 * half again as wide, and a toolbar that changes width when you use it must be re-found after every
 * action. The column costs the bar six pixels of height. Half-strength: it is a reminder, not a
 * label. A refused tool's letter fades with it. */
.dock-key.icon { flex-direction: column; gap: 1px; height: 34px; }

/* Except a reading, which puts its glyph *beside* the figures: a number stacked over a glyph is two
   things where the bar reads one. Here and not beside `.dock-value`, because the column is declared
   later and wins at the same specificity. */
.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. It holds the heel's shape;
   the dock holds what is done to the heel. */
.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. Measured on a 700 px window with a full stack of sole layers: 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; }

/* The shapes as their symbols; the one on the heel is inverted, as `.key.icon.on` is. */
.heel-shapes { display: grid; grid-template-columns: repeat(6, 1fr); gap: 2px; margin: 0 0 10px; }
.heel-shapes .key.icon { width: auto; height: 32px; }
.heel-shapes .key.icon svg { width: 22px; height: 22px; }

/* The HOW key and its card: the lines for printing and fitting this heel. */
.heel-how { margin: 10px 0 0; }
.heel-how .key { display: inline-flex; align-items: center; gap: 4px; }
.heel-how .key svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.25px; stroke-linecap: round; }
.heel-how-card { margin: 6px 0 0; padding: 6px 8px; list-style: none; border: 1px solid var(--line); border-radius: var(--r); background: var(--well); font-size: var(--t-small); line-height: 1.35; }
.heel-how-card li + li { margin-top: 3px; }
.heel-how-card[hidden] { display: none; }

/* Each number as a drawing of the heel that is dragged, with its value small under it. */
.heel-dials { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.heel-dial {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 4px 3px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  color: var(--emit-100);
  cursor: ns-resize;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.heel-dial[data-axis='x'] { cursor: ew-resize; }
.heel-dial:hover,
.heel-dial.held { background: var(--well); }
.heel-dial:focus-visible { outline: 2px solid var(--emit-100); outline-offset: -1px; }
.heel-dial svg {
  display: block;
  width: 100%;
  height: auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25px;
  stroke-linecap: round;
  stroke-linejoin: miter;
  vector-effect: non-scaling-stroke;
}
.heel-dial svg path { vector-effect: non-scaling-stroke; }
.heel-dial svg .ghost { stroke: var(--emit-40); stroke-dasharray: 2 2; }
.sole-dial svg .dim { stroke: var(--emit-70); stroke-width: 1px; }
.heel-dial-value {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-small);
  white-space: nowrap;
}

/* The screw: its size as three keys, then its place along the heel as a dial and as a distance to type. */
.heel-screw { display: grid; gap: 6px; margin: 10px 0 0; }
.heel-screw-sizes { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; }
.heel-screw-dial[aria-disabled='true'] { cursor: default; opacity: 0.5; }
.heel-screw-back { display: flex; align-items: center; gap: 4px; font-size: var(--t-small); }
.heel-screw-back .input { width: 64px; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.heel-screw-limit { font-family: var(--mono); font-size: var(--t-small); color: var(--warn); }
.heel-screw-limit[hidden] { display: none; }

/* ── The sole panel ──
   Over the canvas as the heel's is: a key per construction as its symbol, a live section of the stack, and the numbers
   that construction has as drawings of the section. It takes the heel panel's box (`.heel-panel`). */
.sole-choice { display: flex; flex-direction: column; gap: 6px; margin: 0 0 8px; }
.sole-families { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.sole-family {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 2px 3px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: transparent;
  color: var(--emit-100);
  font: var(--t-small) var(--mono);
  cursor: pointer;
}
.sole-family svg {
  width: 100%;
  height: auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25px;
  stroke-linejoin: miter;
  vector-effect: non-scaling-stroke;
}
.sole-family svg path { vector-effect: non-scaling-stroke; }
.sole-family svg .stitch,
.sole-family svg .glue { stroke-dasharray: 1.5 1.5; }
/* The four names on one line: each tile's width set its symbol's height, so Glued's name sat 6 px above Stitched's. */
.sole-family { justify-content: space-between; }
@media (min-width: 601px) { .sole-family svg { max-height: 18px; } }
.sole-family:hover { background: var(--well); }
.sole-family.on { background: var(--emit-100); color: var(--raised); }
.sole-variants,
.sole-materials { display: flex; flex-wrap: wrap; gap: 4px; }
.sole-variant {
  padding: 3px 8px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: transparent;
  color: var(--emit-100);
  font: var(--t-small) var(--mono);
  cursor: pointer;
}
.sole-variant:hover { background: var(--well); }
.sole-variant.on { background: var(--emit-100); color: var(--raised); }
.sole-controls { display: flex; flex-direction: column; gap: 6px; }
.sole-section { display: block; width: 100%; height: auto; color: var(--emit-100); }
.sole-section path { vector-effect: non-scaling-stroke; }
.sole-section .band { stroke: var(--emit-100); stroke-width: 1px; }
.sole-section .band.on { stroke-width: 2.25px; }
.sole-section .hit { fill: transparent; stroke: none; cursor: pointer; }
.sole-section .hit:focus-visible { outline: 2px solid var(--emit-100); outline-offset: -1px; }
.sole-section .grip { fill: var(--emit-100); stroke: var(--raised); stroke-width: 1px; cursor: ns-resize; touch-action: none; }
.sole-section .grip.x { cursor: ew-resize; }
.sole-section .ghost { fill: none; stroke: var(--emit-40); stroke-width: 1px; stroke-dasharray: 2 2; }
.sole-section .wall { fill: none; stroke: var(--emit-100); stroke-width: 1.5px; }
.sole-section .channel { fill: none; stroke: var(--emit-100); stroke-width: 1.25px; stroke-dasharray: 1.5 1.5; }
.sole-section text {
  pointer-events: none;
  fill: var(--emit-100);
  stroke: var(--raised);
  stroke-width: 2px;
  paint-order: stroke;
  font: 8px var(--mono);
}
.sole-total {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-small);
  text-align: center;
  color: var(--emit-70);
}
.sole-dials { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; }
.sole-dial-word {
  font: var(--t-small) var(--mono);
  color: var(--emit-70);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.sole-layer { display: flex; flex-direction: column; gap: 4px; }
.sole-layer-name { font: var(--t-small) var(--mono); color: var(--emit-100); }
.sole-layer-body { display: flex; gap: 8px; align-items: flex-start; }
.sole-sizes { display: flex; flex-direction: column; gap: 4px; width: 76px; flex: none; }
.sole-swatches { display: grid; grid-template-columns: repeat(5, 18px); gap: 4px; }
.sole-swatches .dock-swatch { width: 18px; height: 18px; }
.sole-more-key {
  align-self: flex-start;
  padding: 2px 0;
  border: 0;
  background: transparent;
  color: var(--emit-70);
  font: var(--t-small) var(--mono);
  text-decoration: underline;
  cursor: pointer;
}
.sole-more { display: flex; flex-direction: column; gap: 6px; }
.sole-more[hidden] { display: none; }
.sole-dial.set { border-color: var(--emit-100); }

/* ── 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. */
.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; }

/* ══════════════════════════════════════════════════════════════════════
   TOOLBAR
   ══════════════════════════════════════════════════════════════════════ */

/* One row, always. `flex-wrap: wrap` would put a second bar across the screen at half a window.
   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, centred on the bar. It is out of the flex flow: between two `.push` spacers it would
   be centred in the space left over, and the file buttons on one side are not the width of the modes
   on the other. 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. `.toolbar` is `overflow: hidden` with the modes as its
   last child, so without it the overflow comes off the modes. The modes are never 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. 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-name:disabled {
  color: var(--emit-50);
  border-bottom-color: transparent;
  cursor: default;
}

/* ── 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. */
.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 them at 180 px, a short address breaks and MAKE ADMIN is
     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 leaves 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); }
/* A last and its variants, across the grid, in a frame of their own: one shape and what was made of it. */
.new-design-family {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  padding: 6px;
  border: 1px solid var(--line);
  background: var(--well);
}
/* Where a variant came from, under its name: two lines at most, the rest in its tooltip. */
.new-design-from {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: var(--t-small);
  color: var(--emit-50);
}
/* A family's cards are as tall as its tallest, so each label fills its card. */
.new-design-family .new-design-label { flex: 1 1 auto; }
.new-design-card:focus .new-design-from { color: var(--on-fill); }
/* 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`. A row, because it is one number
   and one list. */
.ask-size {
  width: min(560px, 90vw);
  flex-flow: row wrap;
  align-items: center;
  gap: 10px;
}

.ask-size > .input { flex: 0 0 auto; }

/* The size boxed as FIT's THIS LAST IS field, with its system beside it; OPEN and CANCEL to the right. */
.ask-size > .input.boxed { flex: 0 0 72px; width: 72px; }

.ask-size > select.input { flex: 0 0 72px; width: 72px; text-align: left; }

/* No stepper: a size is typed or taken from a key, and the arrows read as a second control. */
.ask-size input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.ask-size input[type="number"]::-webkit-inner-spin-button,
.ask-size input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }

.ask-size > select.input + .key { margin-left: auto; }

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

/* FIT TO A FOOT (`ui/lastfit.js`): five columns, the silhouette on one wrapped line, the keys under it. */
.lastfit { display: flex; flex-direction: column; gap: 4px; padding: 0 0 6px; }
.lastfit-rows {
  display: grid;
  grid-template-columns: auto auto auto auto minmax(0, 1fr);
  align-items: baseline;
  column-gap: 12px;
  row-gap: 2px;
  padding: 0 4px;
}
.lastfit-head { font-size: var(--t-label); letter-spacing: .06em; color: var(--emit-50); }
.lastfit-name { font-size: var(--t-label); letter-spacing: .06em; color: var(--emit-90); }
.lastfit-rows:not(.judged) { grid-template-columns: auto auto auto auto; }
/* The mark sits under its row, from the NOW column across, so it is never cut off at the panel's edge. */
.lastfit-mark { grid-column: 2 / -1; font-size: var(--t-label); letter-spacing: .06em; }
.lastfit-rows .metric-value { white-space: nowrap; }
@media (max-width: 1100px) { .lastfit-rows { column-gap: 6px; } .lastfit-rows .metric-value { font-size: 10px; } }
.lastfit-changes { display: flex; flex-direction: column; padding: 2px 4px 0; font: var(--t-small) var(--mono); }
.lastfit-silhouette { display: flex; flex-wrap: wrap; gap: 2px 10px; padding: 0 4px; font: var(--t-small) var(--mono); color: var(--emit-50); }
.lastfit-measure.fit-error { color: var(--err-90); }
.lastfit-keys { padding: 2px 4px 0; }
.ask-lastfit { width: min(560px, 90vw); }

/* The reading and the file name's size, one line above the field; the reason a full line under it. */
.ask-size-choices { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 8px; }

.ask-size > .input.boxed.ask-size-reason { flex: 1 1 100%; width: 100%; text-align: left; }

.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. A sheet has paper of its own to sit on, so
   a pattern reads as a sheet of something and not 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;
  min-width: min(240px, 94vw);
  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: 0 0 0; }

/* The panel hugs a narrow piece, so the title gives way (and elides) before a button leaves the head. */
#sheet-title { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The codes the sheet prints inside its piece: after the title, and gone when the sheet has none. */
#sheet-codes { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sheet-codes:empty { display: none; }

/* ── 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. `.toolbar` is
   `overflow: hidden`, which is what makes the bar elide rather than wrap, so an absolutely
   positioned child is clipped to the height of the bar. `.menu-items` sits at `z-index: 1`, under
   the canvas. Fixed positioning leaves the clipping ancestor, and the stack here is the overlays'. */
#recent-items {
  position: fixed;
  right: auto;
  z-index: 62;
  --recent-row: 33px;
  overflow-y: auto;
}

/* 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. */
#shown-items {
  position: fixed;
  right: auto;
  z-index: 62;
  max-height: 34vh;
  overflow-y: auto;
}

/* The size chip: the number, and nothing else. */
#shown-btn { white-space: nowrap; }

/* While the answers for a new size are coming in. Only dimmed: no word, no spinner. */
#shown-btn.updating { opacity: 0.55; }

/* 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 > .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. */
.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 definite panel height 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 is 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. */
.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 run 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. */
.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. 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; }
  /* In a 280 px panel the target and the gaps give up 20 px, so STICK LENGTH and INSTEP GIRTH are read whole. */
  .tape-row { grid-template-columns: minmax(0, 1fr) auto 52px 26px; column-gap: 4px; }
  /* MONDO ran out of the panel: a choice of four goes under its label from here, not from 880 px. */
  .param:has(> .segments) { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}

@media (max-width: 880px) {
  .workspace { --right-w: 240px; }
  /* FIT keeps 280 px: at 240 every measure's name was cut to two letters. */
  .workspace:has(.tape-row) { --right-w: 280px; }
  /* 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 and the stage share the height. 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 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 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. 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 comes only under a pointer: on a touch screen
     the field is grey words on the page until it is 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. The desktop cap is worked out against the wordmark, which
     is not shown here. */
  .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 would be 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 has room for the modes or for the name, not both. */
@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 would run over undo
     and redo. */
  .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 is 785 px long and RELOAD is 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. */
  .updating.outdated > .key { flex: 0 0 auto; }
}

/* On its side the job tabs are 44 px tall. Both notes go under them, 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 row would be under undo, and a tap on it would press 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 { 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. */
  .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. */

  /* Above the dock however many lines it takes: `--dock-h` is its height, which `_measureTheDock`
     keeps. */
  .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); }

  /* 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 keys of a piece fit one line from 375 px up. A rule between groups would
     end one line or begin the next once the dock wraps. `--dock-balance` asks `deck._balanceTheDock`
     for as many keys on each line. */
  .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. `--covers` tells `deck._measureTheCover` to frame the
     last above 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;
  }
  /* Every key of the sole panel a thumb can hit. */
  .sole-variant,
  .sole-more-key { min-height: 44px; min-width: 44px; }
  .sole-swatches { grid-template-columns: repeat(4, 44px); }
  .sole-swatches .dock-swatch { width: 44px; height: 44px; }
  /* The shapes two lines of 44 px keys, and the dials four to a line. */
  .heel-shapes { margin-bottom: 6px; }
  .heel-shapes .key.icon { min-width: 0; height: 44px; }
  .heel-dials { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
  /* The screw dial a quarter of the line, as the other dials are, with the distance and the limit beside it. */
  .heel-screw { grid-template-columns: minmax(0, 1fr) minmax(0, 3fr); align-items: center; gap: 4px 8px; }
  .heel-screw-sizes { grid-column: 1 / -1; }
  .heel-screw-sizes .key { min-height: 44px; }
  .heel-screw-dial { grid-row: span 2; }
}

/* 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. */
@media (max-width: 720px) {
  .panel-right { width: auto; }
}

/* A long choice is wider than a 240 px panel's control column, so its label goes above it: MONDO and
   TABLE run 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. If its two bodies and the view controls each had a share of the
     screen, the tree would hide + ADD and the sizes after a grade would have 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 leaves a frame of the app round it to look
     past, and on its side the square is 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 would each fall 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-head .key.icon { width: 44px; height: 44px; }
  #sheet-export-items .menu-item { min-height: 44px; }
  .sheet-page { flex: 1 1 auto; min-height: 0; padding: 8px; }
}

/* ── 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 are 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;
  flex-wrap: wrap;
  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; user-select: none; }
.sculpt-pane:focus-visible .sculpt-svg { outline: 1px dotted #0b6aa8; outline-offset: -3px; }
.sculpt-was { fill: none; stroke: #9a9a9a; stroke-dasharray: 4 3; }
.sculpt-foot { fill: rgba(222, 170, 140, 0.2); stroke: rgba(176, 110, 80, 0.6); }
.sculpt-curve { fill: rgba(0, 0, 0, 0.03); stroke: #111; stroke-linejoin: round; }
.sculpt-held { fill: none; stroke: #d6d6d6; stroke-linecap: round; }
.sculpt-stopped { fill: none; stroke: #c81e1e; stroke-linecap: round; }
.sculpt-station-line { stroke: #0b6aa8; stroke-opacity: 0.4; stroke-dasharray: 5 3; }
/* The grid under each drawing is the 3D ground's, in the same two blues: one scale in every pane. */
.sculpt-grid-minor { fill: none; stroke: #d3dee7; }
.sculpt-grid-major { fill: none; stroke: #a9bdcf; }
.sculpt-tick, .sculpt-bar-text, .sculpt-dim-text { fill: #6a7b8a; font-family: var(--courier, monospace); letter-spacing: 0.02em; pointer-events: none; }
.sculpt-dim-text { fill: #3f4a54; }
.sculpt-bar { fill: none; stroke: #3f4a54; }
.sculpt-dim { fill: none; stroke: #8a97a3; }
/* A point is a plain dot: grey as found, black once moved. The selected point is a ring with its tangents. */
.sculpt-anchor { fill: #6b6b6b; stroke: none; cursor: grab; }
.sculpt-anchor.moved { fill: #000; }
.sculpt-anchor.hot { fill: #0b6aa8; }
.sculpt-anchor.picked { fill: #0b6aa8; }
.sculpt-hover { fill: rgba(11, 106, 168, 0.12); stroke: rgba(11, 106, 168, 0.5); }
.sculpt-ring { fill: #fff; fill-opacity: 0.85; stroke: #0b6aa8; cursor: grab; }
.sculpt-handle { fill: #fff; stroke: #0b6aa8; cursor: grab; }
.sculpt-handle.hot { fill: #0b6aa8; }
.sculpt-handle-line { stroke: #0b6aa8; }
/* The reach of a soft move: a disc about the place, and again where it is being put. */
.sculpt-halo { fill: rgba(11, 106, 168, 0.06); stroke: rgba(11, 106, 168, 0.55); stroke-dasharray: 5 4; pointer-events: none; }
.sculpt-halo.moved { fill: none; stroke-dasharray: none; stroke: rgba(11, 106, 168, 0.35); }
/* A lock where a place is held: filled for the maker's, hollow for the heel's end. */
.sculpt-lock { pointer-events: none; }
.sculpt-lock-body { fill: #fff; stroke: #0b6aa8; }
.sculpt-lock.kept .sculpt-lock-body { fill: #0b6aa8; }
.sculpt-lock-hasp { fill: none; stroke: #0b6aa8; }
.sculpt-lock-stem { stroke: #0b6aa8; stroke-opacity: 0.6; pointer-events: none; }

/* The pane's toolbar, top right, and the selected point's strip, bottom right. Over the drawing, out of its way. */
.sculpt-tools, .sculpt-point {
  position: absolute;
  right: 8px;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid var(--line);
}
.sculpt-tools { top: 34px; flex-direction: column; align-items: flex-end; gap: 2px; max-width: calc(100% - 16px); }
.sculpt-tools-row { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 2px; }
.sculpt-cleft { fill: rgba(11, 106, 168, 0.18); stroke: #0b6aa8; stroke-linejoin: round; pointer-events: none; }
.sculpt-split { display: grid; gap: 4px; margin: 8px 0; }
.sculpt-split-head { display: flex; align-items: center; justify-content: space-between; }
.sculpt-split-row { display: grid; grid-template-columns: 52px 1fr 54px; align-items: center; gap: 6px; }
.sculpt-split-dial { width: 100%; }
.sculpt-split-said { font-family: var(--courier, monospace); font-size: 10px; text-align: right; }
.sculpt-soft-ring { padding: 0 2px; }
.sculpt-radius { width: 90px; }
.sculpt-radius-said { font-family: var(--courier, monospace); min-width: 4.2em; text-align: right; font-size: 10px; }
.sculpt-tools-gap { width: 6px; }
.sculpt-zoom-said { font-family: var(--courier, monospace); min-width: 3.4em; text-align: center; }
.sculpt-point { bottom: 8px; gap: 4px; padding: 3px 4px; flex-wrap: wrap; justify-content: flex-end; max-width: calc(100% - 16px); }
.sculpt-point[hidden] { display: none; }
.sculpt-point-name { padding: 0 4px; }
.sculpt-measures { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-left: 6px; font-size: 10px; }
.sculpt-measure { display: inline-flex; gap: 4px; align-items: baseline; }
.sculpt-measure-now, .sculpt-measure-by { font-family: var(--courier, monospace); }
.sculpt-measure.moved .sculpt-measure-by { font-weight: 600; }
.sculpt-measures.draft { opacity: 0.85; }
.sculpt-tools .key.on, .sculpt-point .key.on { background: #000; color: #fff; }
.sculpt-tools .key.sm, .sculpt-point .key.sm { min-height: 0; height: 22px; padding: 0 6px; font-size: 10px; }

.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); }
  /* The canvas tools at the foot of the pane: at the head their four 44 px keys covered LATERAL. */
  .stage.sculpting:has(> .jobs.floating:not([hidden])) .canvas-tools { top: auto; bottom: calc(8px + env(safe-area-inset-bottom)); }
  /* And the drawing ends above them, so they never cover the toe. */
  .sculpt-svg { margin-bottom: calc(60px + env(safe-area-inset-bottom)); }
  .sculpt-point { bottom: 64px; }
}

/* A tablet or a phone: the pane's toolbar takes its own lines above the drawing. Over it, wrapped to three, it hid the
   top of the profile. */
@media (max-width: 880px) {
  .sculpt-tools { position: static; align-self: flex-end; margin: 0 8px 4px; }
  .sculpt-svg { order: 1; }
}

/* A control the plan does not allow: drawn like a disabled one but still pressable, so a press opens the plans
   (`plan.js`). Hidden or `disabled`, it would not say what it needs. */
.plan-locked {
  opacity: .45;
  cursor: pointer;
}

.plan-columns {
  display: grid;
  grid-template-columns: repeat(3, minmax(150px, 1fr));
  gap: 0;
  overflow: auto;
}

.plan-column {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px;
  border-left: 1px solid var(--line-firm);
}

.plan-column:first-child { border-left: 0; }
.plan-column.here { background: var(--well); }

.plan-name {
  font: var(--t-label) var(--label);
  letter-spacing: .1em;
}

.plan-price {
  font-family: var(--mono);
  font-size: var(--t-body);
}

.plan-fact {
  font-size: var(--t-body);
  color: var(--emit-70);
}

.plan-ask {
  margin-top: auto;
  text-align: center;
  text-decoration: none;
  opacity: .6;
}

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

.plan-said { font-size: var(--t-body); color: var(--emit-70); }

@media (max-width: 640px) {
  .plan-columns { grid-template-columns: 1fr; }
  .plan-column { border-left: 0; border-top: 1px solid var(--line-firm); }
  .plan-column:first-child { border-top: 0; }
}

/* PERSONAL and the workspace: one space shown at a time in Files. */
.lib-scope { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.sculpt-eye { padding: 2px 6px; line-height: 0; }

/* Which space a design lives in: a person for PERSONAL, the workspace's initials on its own colour. Quiet, and
   the same mark in the bar, the recent menu and Files. */
.space-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  min-width: 18px;
  height: 16px;
  padding: 0 4px;
  font: 600 9px/1 var(--label);
  letter-spacing: .04em;
  border: 1px solid var(--line-firm);
  border-radius: 3px;
  color: var(--emit-50);
}
.space-shared {
  --space-hue: 200;
  color: #fff;
  background: hsl(var(--space-hue) 45% 38%);
  border-color: hsl(var(--space-hue) 45% 38%);
}
#recent-items .menu-item .space-tag { margin-left: auto; }
#recent-items .menu-item .space-tag + .lib-when { margin-left: 0; }
.lib-last .space-tag { margin: 0 6px; }

/* The top bar's tag: the mark and the space's name, a press opens the one move. */
#space-menu { flex: none; }
.space-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: 0;
  padding: 2px 4px;
  font: var(--t-label) var(--label);
  letter-spacing: .04em;
  color: var(--emit-50);
  cursor: pointer;
}
.space-btn:disabled { cursor: default; }
.space-btn:not(:disabled):hover { color: var(--emit-100); }
.space-btn:focus-visible { outline: 1px dotted var(--emit-100); outline-offset: 1px; }
/* Fixed, from the button's box, as the recent menu is: the bar clips what hangs from it. */
#space-items { position: fixed; right: auto; min-width: 160px; z-index: 5; }
@media (max-width: 880px) { .space-word { display: none; } }

/* The two-state chip at the foot of NEW DESIGN, and the filter row of Files. */
.space-chip { display: flex; gap: 6px; margin-right: auto; }
.new-design-foot { gap: 10px; align-items: center; }

/* The wait of a download: a 2 px line along the foot of the key, filled as the server reports, or a slow shimmer.
   Nothing here changes the key's size. */
.progressing { position: relative; overflow: hidden; }
.progress-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  overflow: hidden;
  pointer-events: none;
}
.progress-line::before {
  content: "";
  display: block;
  height: 100%;
  width: calc(var(--fill, 0) * 100%);
  background: var(--emit-100);
}
.progress-line[data-phase="failed"]::before { background: var(--err-90); }
.progress-line.indeterminate::before {
  width: 30%;
  animation: progress-shimmer 1.8s ease-in-out infinite;
}
/* A key on the bar is 30 px wide, where a travelling 30 % dash reads as a speck: the whole foot pulses instead. */
.progress-line.indeterminate.narrow::before { width: 100%; animation: progress-pulse 1.4s ease-in-out infinite; }
.progress-line.indeterminate.still::before { width: 100%; opacity: .35; animation: none; }
@keyframes progress-shimmer {
  from { transform: translateX(-100%); }
  to { transform: translateX(340%); }
}
@keyframes progress-pulse {
  0%, 100% { opacity: .3; }
  50% { opacity: 1; }
}
.grade-action { position: relative; }
/* A key whose file the server keeps for the design as it stands (`ready.js`): a small square of the done colour in its
   corner, ringed in paper so it reads on a pressed key too. */
.key[data-ready="ready"],
.dock-key[data-ready="ready"],
.dock-item[data-ready="ready"] { position: relative; }
.key[data-ready="ready"]::before,
.dock-key[data-ready="ready"]::before,
.dock-item[data-ready="ready"]::before {
  content: "";
  position: absolute;
  top: 3px;
  right: 3px;
  width: 5px;
  height: 5px;
  background: var(--done);
  box-shadow: 0 0 0 1px var(--raised);
  pointer-events: none;
}
.progress-x {
  position: absolute;
  top: 13px;
  right: 8px;
  font: var(--t-label) var(--label);
  line-height: 1;
  cursor: pointer;
  color: var(--emit-100);
}
.progress-x:focus-visible { outline: 1px dotted var(--emit-100); }
