/* NAMING. One stylesheet serves three consoles, so a class name is global and a second
   definition of one silently wins — that has happened five times (`#whoami`, `.landing`,
   `.seg`, `.pw-note`, `.ord-ref`). `tests/test_css.py` now fails the build on it.
   
   Before inventing a name, grep for it. And prefix anything that belongs to ONE console:
   `pw-` for the workshop, `a-` for admin/ops, `myord-`-style specifics for the customer.
   Unprefixed names are a claim that the thing is shared by all three — buttons, dialogs,
   the header menu, `.dim`. Splitting this file per console would NOT fix this: four of the
   five collisions were inside a single surface. */
* { box-sizing: border-box; margin: 0; }
[hidden] { display: none !important; }
:root {
  --bg: #0f1216; --panel: #171c22; --card: #1d242c; --line: #2a333d;
  --text: #e6ebf0; --dim: #8b98a5; --accent: #26a0f0; --ok: #3ecf8e;
  --warn: #e8b23c; --err: #ef5f6b; --hi: #ff8a3d;
  /* Neutral translucent surfaces + anything that must invert with the theme. */
  --overlay: rgba(23,28,34,.92);
  --overlay-soft: rgba(23,28,34,.85);
  --scrim: rgba(15,18,22,.7);
  --sunken: rgba(15,18,22,.4);
  --btn-text: #06121c;
  /* Khmer needs naming explicitly: the Latin system stack has no Khmer, so the browser
     falls back to whatever it finds and the stacked diacritics come out cramped. Defined
     once here because the interface itself is going Khmer next, not only the wordmark. */
  --font-khmer: "Noto Sans Khmer", "Khmer OS Battambang", "Khmer Sangam MN", "Khmer MN",
                "Khmer UI", "Leelawadee UI", sans-serif;
  /* The wordmark, and only the wordmark. Geometric so it reads as a made object rather than
     as the interface it sits in — the system stack's whole job is to disappear, which is the
     opposite of what a logo does.
     Futura is macOS-only, so the stack lands on the nearest geometric each platform has:
     Century Gothic on Windows, URW Gothic on Linux. They are close but not identical, which
     is exactly why the finished mark should be outlined to SVG rather than set in type. */
  --font-brand: Futura, "Century Gothic", "Avenir Next", "URW Gothic", "Trebuchet MS",
                sans-serif;
  /* The Khmer half of the wordmark, kept SEPARATE from --font-khmer above. That one is the
     app's reading face and wants whatever renders body text best on each platform; this one
     is a logo and wants one specific shape. Conflating them would mean a future change to
     interface typography silently redrawing the mark. */
  --font-brand-khmer: "Khmer MN", "Noto Sans Khmer", "Khmer Sangam MN", "Khmer UI",
                      "Leelawadee UI", sans-serif;
  --viewer-bg: #0f1216;
  --grid-major: #2a333d;
  --grid-minor: #1d242c;
  /* A lift under the drop zone, so the middle of the page is where the eye lands. */
  --grid-glow: rgba(38, 160, 240, .10);
  /* Drawn ON the paper, so a shade up from the ruling rather than the same colour. */
  --paper-ink: #3a4653;
  --metal: #9fb2c4;
  --sky: #dfe8f0; --ground: #35404c;
}

/* Tuned for contrast rather than inverted: dark text on a soft grey (less glare than
   white), and accents darkened so they stay legible on a light background. */
:root[data-theme="light"] {
  --bg: #eef1f5; --panel: #ffffff; --card: #ffffff; --line: #ccd4dd;
  --text: #14181d; --dim: #55636f; --accent: #0a66c2; --ok: #147a4a;
  --warn: #8a5a00; --err: #b3202f; --hi: #c2410c;
  --overlay: rgba(255,255,255,.94);
  --overlay-soft: rgba(255,255,255,.9);
  --scrim: rgba(238,241,245,.75);
  --sunken: rgba(0,0,0,.03);
  --btn-text: #ffffff;
  --viewer-bg: #e4e9ef;
  --grid-major: #b9c4d0;
  --grid-minor: #d4dbe3;
  --grid-glow: rgba(255, 255, 255, .55);
  --paper-ink: #9fadbc;
  --metal: #aab6c3;
  --sky: #ffffff; --ground: #b9c4d0;
}
/* Khmer.
   `--font-khmer` has been defined since the wordmark and was never used for reading text —
   the Latin system stack has no Khmer at all, so the browser falls back to whatever it finds
   and the stacked diacritics come out cramped and misaligned. This is the switch.
   And Khmer is written WITHOUT SPACES between words, so a sentence is one long token with
   nowhere to break: without `overflow-wrap: anywhere` it pushes every container it is in
   wider than the screen. `line-height` is raised because the stacked marks above and below
   the baseline need the room; at the Latin 1.5 they collide with the line above. */
.lang-km body, .lang-km input, .lang-km button, .lang-km textarea, .lang-km select {
  font-family: var(--font-khmer);
}
.lang-km body { line-height: 1.75; overflow-wrap: anywhere; }
/* "ខ្មែរ" on the language button has to be set in Khmer even while the interface is
   English — it is the label of the thing you are switching TO. `lang="km"` on the button
   says so, and this is the rule that honours it. */
[lang="km"] { font-family: var(--font-khmer); }
/* The wordmark keeps its own faces in both languages — it is a logo, not interface text. */
.lang-km .brand { font-family: var(--font-brand); }
.lang-km .brand-kh { font-family: var(--font-brand-khmer); }

html, body { height: 100%; }
body {
  background: var(--bg); color: var(--text);
  font: 14px/1.5 -apple-system, "Segoe UI", Roboto, Helvetica, sans-serif;
  display: flex; flex-direction: column; overflow: hidden;
}
header {
  display: flex; align-items: center; gap: 16px;
  padding: 10px 18px; border-bottom: 1px solid var(--line); background: var(--panel);
}

/* The whole point: a LARGER gap between groups than within them. Six controls evenly
   spaced read as one undifferentiated row; the same six in three clusters read as three
   things, and the eye has somewhere to stop. */
/* Pushed right on every console — this is the rule that does it, so a page without a
   .hdr-right leaves its controls stranded mid-header. All three have one. */
.hdr-right { margin-left: auto; display: flex; align-items: center; gap: 20px; }
.hdr-group { display: flex; align-items: center; gap: 10px; }
.hdr-group + .hdr-group { border-left: 1px solid var(--line); padding-left: 20px; }
/* The language pair, out in the open. Both flags are shown with the current one lit rather
   than one button offering the other, because a single flag reads two opposite ways — "you
   are on English" and "press for English" — and the one person this control is for cannot
   read the caption that would settle it. */
.hdr-lang { gap: 4px; }
.hdr-lang button {
  display: flex; align-items: center; gap: 6px;
  padding: 3px 8px 3px 4px; border: 1px solid transparent; border-radius: 999px;
  background: none; color: var(--dim); font: inherit; font-size: 12px; cursor: pointer;
  /* Off-language flags read as decoration at full strength and compete with the cart. The
     one you are on is the only one that should look switched on. */
  opacity: .55;
}
.hdr-lang button:hover { opacity: 1; color: var(--fg); }
.hdr-lang button[aria-pressed="true"] {
  opacity: 1; color: var(--fg); border-color: var(--line); background: var(--panel);
}
.hdr-lang .flag { border-radius: 2px; flex: none; display: block; }
/* Khmer carries marks above and below the letter, so it needs both more room and more size
   than Latin to read as equally clear — at a matched font-size it looks like a footnote. */
.hdr-lang button span { line-height: 1.7; }
.hdr-lang button[lang="km"] span { font-size: 14px; }
/* An address is long and is not a control. It identifies, quietly, and gets out of the way
   of the things you can actually press. */
#whoami {
  color: var(--dim); font-size: 12px; max-width: 12rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.brand {
  font-family: var(--font-brand);
  /* Futura runs wide and its geometric bowls need air; the negative tracking that suited a
     UI face closes it up. */
  font-size: 20px; font-weight: 700; letter-spacing: 0;
  color: inherit; text-decoration: none;
  /* inline-flex so the gaps either side of the tilde are margins I set, not collapsed
     whitespace I am guessing at. Baseline, so the two names sit on one line the way they
     would in running text. */
  display: inline-flex; align-items: baseline;
}
.brand span { color: var(--accent); }
/* The Khmer name, set alongside rather than instead of. It is the company's actual name to
   the people who will use this, and burying it in a tooltip would be the wrong way round —
   but it sits quieter than the wordmark so the two read as one mark, not two. */
/* The join. Light, wide-set and quiet — it is a hinge between two names, not a character
   in either of them, so it takes no weight and none of the accent. */
.brand-tilde {
  /* Quiet, not absent. At --line (the border colour) it read as a gap with a smudge in it;
     --dim at reduced opacity keeps it subordinate to both names while still being a mark
     somebody can see.
     
     MEASURED, not eyeballed. A tilde's ink sits 11.85px above the baseline at this size
     while "Bongkum"'s ink centre is at 5.72px, so on the baseline it floats about six
     pixels high. 0.383em brings it level. Aligned to the LATIN's centre rather than to the
     midpoint of both names: the Khmer's ink centre is dragged down to 1.08px by its
     subscript marks, and centring on that drops the tilde below the line the eye actually
     reads along. */
  color: var(--dim) !important; opacity: .55; font-weight: 400; font-size: 16px;
  transform: translateY(0.383em);
  /* Equal both sides. The Khmer used to carry a margin-left of its own, which made the gaps
     1px and 8px — the tilde was nearer one name than the other. */
  margin: 0 6px;
}

.brand-kh {
  /* Larger than the Latin beside it on purpose: Khmer stacks marks above and below the
     line, so at matched point size it reads noticeably smaller and the detail closes up.
     Khmer MN runs tighter and darker than Sangam, so it takes another point to hold its
     own next to Futura. */
  font-family: var(--font-brand-khmer); font-size: 18px; font-weight: 400; line-height: 1;
  color: var(--dim) !important; letter-spacing: 0;
}
.tagline {
  color: var(--dim); font-size: 12px;
  /* Yields first. The controls are what somebody came to press; the tagline is scenery,
     and it truncating is better than it pushing them off the edge. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* --- project rail ------------------------------------------------------
   A vertical strip left of the viewer: every part in the job, its price, and the
   running total. `flex-shrink: 0` because the part list scrolls inside itself —
   without it a twenty-part project squeezes the viewer away. */
#project-bar {
  width: 240px; flex-shrink: 0; background: var(--panel);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column; min-height: 0;
}
.pb-head { display: flex; gap: 6px; padding: 10px; border-bottom: 1px solid var(--line); }
/* The buttons' own row, so the name above gets the full width. Its top border would be a
   second line immediately under the name's, so it goes. */
.pb-acts { border-top: 0; padding-top: 0; padding-bottom: 8px; flex-wrap: wrap; }
.pb-head:not(.pb-acts) { border-bottom: 0; padding-bottom: 4px; }
/* A TITLE that happens to be editable, not a text field that happens to hold the name.
   At 13px in the app's normal weight it read as a value in a form; the thing at the top of
   a panel naming what the panel is about should look like a heading. */
.pb-name {
  flex: 1; min-width: 0; background: transparent; color: var(--text);
  border: 1px solid transparent; border-radius: 6px; padding: 2px 6px;
  /* LONGHANDS. `font: 700 16px/1.35 inherit` is invalid — `inherit` is a global keyword and
     cannot stand in for the family inside the shorthand, so the browser threw the whole
     declaration away. The original was `font: 600 13px/1.4 inherit`, equally invalid, which
     is why this input had been rendering in the UA's 13.33px Arial since M6c: not the app's
     typeface, not the app's size, in the one place naming what the panel is about.
     `font: inherit` ALONE is fine, and is what the rest of this file uses. */
  font: inherit;
  font-size: 16px; font-weight: 700; line-height: 1.35; letter-spacing: -0.2px;
}
.pb-title-row { align-items: center; gap: 2px; }
/* The chevron: quiet until wanted, like the rest of the rail's chrome. */
.pb-switch {
  flex: 0 0 auto; background: transparent; color: var(--dim); border: 1px solid transparent;
  border-radius: 6px; width: 24px; height: 24px; padding: 0; font-size: 12px; line-height: 1;
}
.pb-switch:hover { color: var(--accent); border-color: var(--line); filter: none; }
.pb-name:hover { border-color: var(--line); }
.pb-name:focus { border-color: var(--accent); outline: none; background: var(--card); }
.pb-add {
  background: var(--card); color: var(--accent); border: 1px solid var(--line);
  border-radius: 6px; padding: 3px 8px; font-size: 12px; flex-shrink: 0;
}
.pb-add:hover { border-color: var(--accent); filter: none; }

.pb-parts { flex: 1; overflow-y: auto; padding: 6px; display: flex; flex-direction: column; gap: 4px; }
.pb-part {
  /* The right padding is permanent, not applied on hover: the delete button lives in
     that gutter, and shifting the price sideways as the mouse arrives reads as a jump. */
  position: relative; cursor: pointer; border-radius: 8px; padding: 7px 26px 7px 9px;
  border: 1px solid transparent; display: grid; gap: 1px 8px;
  grid-template-columns: 1fr auto;
  /* The price occupies the FIRST row only. Spanning both was what centred it. */
  grid-template-areas: "file price" "meta meta";
}
.pb-part:hover { background: var(--card); }
.pb-part.sel { background: var(--card); border-color: var(--accent); }
.pb-part.bad .pb-file { color: var(--dim); }
.pb-file {
  grid-area: file; font-size: 12.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pb-meta { grid-area: meta; font-size: 10.5px; color: var(--dim); }
/* On the NAME's line, not floating between the two.
   `align-self: center` put the price at the midpoint of a two-line card, level with neither
   the filename above it nor the process below — money arriving from nowhere rather than
   belonging to the thing it prices. Baseline-aligned with the name it belongs to. */
.pb-price {
  grid-area: price; align-self: start; font-size: 12.5px; font-weight: 600;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.pb-noprice { color: var(--warn); font-size: 10.5px; }
/* Delete sits on top of the price and only appears on hover — a mis-click that removes
   a part the customer just uploaded is worse than an extra hover step. */
.pb-x {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 19px; height: 19px; padding: 0; border-radius: 5px; line-height: 1;
  background: var(--card); color: var(--dim); border: 1px solid var(--line);
  opacity: 0; font-size: 14px;
}
.pb-part:hover .pb-x { opacity: 1; }
.pb-x:hover { color: var(--err); border-color: currentColor; filter: none; }

.pb-foot { border-top: 1px solid var(--line); padding: 10px; display: grid; gap: 8px; }
.pb-total { display: grid; gap: 1px; }
.pb-total-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: .7px; color: var(--dim);
}
#pb-total-value { font-size: 20px; font-weight: 700; }
.retention {
  color: var(--dim); font-size: 11px; line-height: 1.5; max-width: 380px;
  margin-top: 2px; text-align: center;
}
/* The primary takes the room; the ghost is sized by its label. width:100%
   on .pb-share made the two fight and the important one lost. */
.pb-share-row { display: flex; gap: 6px; }
.pb-share-row .pb-share { flex: 1; width: auto; }
.pb-share-row .pb-share.ghost { flex: 0 0 auto; padding: 7px 10px; }
/* Filled red, the way Copy link is filled blue. It was a grey outline that only turned red
   once you were already pointing at it — a warning that arrives after the decision. The two
   buttons at the foot of this panel are the two ends of what you can do with a project, and
   they should look like it. */
.pb-delete {
  background: var(--err); color: #fff; border: 0;
  border-radius: 7px; padding: 7px; font-size: 12.5px; font-weight: 600; width: 100%;
}
.pb-delete:hover { filter: brightness(1.08); }
/* Read-only: the controls that would change something are simply not there, rather than
   present-and-disabled, which invites clicking and then explains why not. */
body.read-only #pb-add,
body.read-only #pb-delete,
body.read-only .pb-x,
body.read-only #pb-share-ro { display: none; }
body.read-only .pb-name { pointer-events: none; }
.ro-badge {
  display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px;
  background: var(--sunken); border: 1px solid var(--line);
  color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .6px;
}
.pb-share {
  background: var(--accent); color: var(--btn-text); border: 0; border-radius: 7px;
  padding: 7px; font-size: 12.5px; font-weight: 600; width: 100%;
}

main { flex: 1; display: flex; min-height: 0; }
/* overflow:hidden is load-bearing. #dim-lines runs overflow:visible so an arrowhead is
   not clipped at the SVG's own edge, and the labels are absolutely positioned children —
   so on a zoomed-in part the dimension lines used to run straight across the panel. The
   clip belongs on the wrapper, not on the SVG. */
#viewer-wrap { flex: 1; position: relative; min-width: 0; overflow: hidden; }
#viewer { position: absolute; inset: 0; background: var(--viewer-bg); }
#viewer canvas { display: block; }

#drop-hint {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  pointer-events: none; overflow-y: auto;
}

/* Figures in the paper's margins: a gear, a bolt circle, a stepped shaft, a section.
 *
 * Graph paper alone is a texture, not a drawing — it makes the page look ruled rather than
 * used. These make it a sheet somebody has been working on.
 *
 * The geometry is GENERATED, not hand-written: twenty trapezoidal gear teeth typed by hand
 * is how you get a gear that is subtly wrong in a way everybody feels and nobody can name.
 * Centrelines are drawn long-dash-short-dash and the section is hatched at 45°, because
 * those two conventions are most of what separates a drafted sketch from clip art.
 *
 * They sit in the gutters either side of the 620px hero and NEVER behind it — a drawing
 * under a headline is noise whatever its opacity. Below 1040px there are no gutters, so
 * they go entirely rather than being squeezed.
 */
.paper-marks { position: absolute; inset: 0; pointer-events: none; }
/* Landing only. This layer is positioned, so it paints ABOVE the viewer canvas — leave it
   on and the gear would be floating over somebody's part. */
body:not(.landing) .paper-marks { display: none; }
.mark {
  position: absolute; aspect-ratio: 1; fill: none; stroke: var(--paper-ink);
  /* Mitred, not rounded: rounding turns every corner of a drawing into a blob. */
  stroke-width: 1.6; stroke-linejoin: miter; opacity: .5;
}
/* ONE PEN ACROSS THE WHOLE SHEET.
 *
 * stroke-width is in USER UNITS, so it multiplies by whatever width the seat gives the
 * figure — the same 1.6 came out at 3.3px in a 288px corner and 1.3px in a 115px band. That
 * did not read as "one is bigger"; it read as "one is drawn in a heavier pen", which is a
 * different claim and the wrong one. Making the seats deliberately unequal made it worse,
 * because it widened exactly that spread.
 *
 * non-scaling-stroke takes the stroke out of the user-space transform: 1.6 means 1.6 device
 * pixels at any size. Size changes, ink does not — which is what a drawing sheet looks like,
 * and it is also what lets the thin weights inside a figure (hatching at .8, a bow string at
 * .8) stay reliably HALF the outline instead of drifting with the seat.
 *
 * Not inherited — it is not an inheriting property — so it has to be set on the shapes
 * rather than on the <svg>. */
.mark * { vector-effect: non-scaling-stroke; }
/* Arrowheads are the one thing on a drawing that IS filled. */
.mark .ah { fill: var(--paper-ink); stroke: none; }
/* Font size is in USER UNITS, so it multiplies by the figure's own scale — 11 units on a
   250px figure lands at 27px on screen and the number shouts over the drawing it belongs to.
   Each size below is chosen to come out near 11px rendered, which is why they differ: these
   figures are not all displayed at the same size. */
.mark text { fill: var(--paper-ink); stroke: none; font-weight: 500; font-size: 5px; }
/* Font size is in USER UNITS, so it multiplies by whatever width the SEAT gives the figure
   — which is why this follows the slot and not the drawing. 5 units in a 280px corner and
   8 units in a 120px band both land near 12px on screen, and a number that lands at 20px
   shouts over the drawing it belongs to. */
.slot-ml text, .slot-mr text { font-size: 6px; }
.slot-nl text, .slot-nr text, .slot-top text { font-size: 8px; }
/* The widest of the small seats, so it needs the smallest number to land in the same
   place: 8 units across 187px is 15px on screen, which is a caption shouting. */
.slot-bot text { font-size: 6px; }
/* The one exception, and it is about MEANING rather than size: a detail letter is the one
   mark here meant to be read rather than noticed, so it stays large in any seat. */
.mark-bubble text { font-size: 13px; }
/* POSITION BELONGS TO THE SLOT, NOT TO THE DRAWING.
 *
 * Every figure used to name its own corner, which meant the page could only ever vary
 * WITHIN a slot — the gear corner stayed the gear corner. There are seventeen figures and
 * the margins hold eight, so the interesting variable was never which drawing exists, it
 * was where it lands. app.js deals them out at load; these rules only say where the seats
 * are. Nothing below mentions a figure by name.
 *
 * Sized in vw, so a wide screen gets bigger drawings rather than the same eight stamps
 * marooned in more empty paper. The lower bound is set by the gutter at the breakpoint
 * below: 620px of hero in a 1150px window leaves 265px a side, and nothing here may cross
 * into that column. The upper bound is taste — past this they stop reading as marginalia.
 */
/* Placement is a COLUMN problem, not a scatter.
 *
 * The gutter beside a 620px hero is only ~265px wide, and a figure is ~200px of that — so
 * nothing can sit BESIDE a figure, only above or below it. Every slot is therefore part of
 * a stack: a large drawing top and bottom of each gutter, and whatever the remaining
 * vertical band will hold in between. "Random" here means which drawing takes which seat,
 * never a random x and y — that would put a gear across somebody's headline.
 */
/* Pushed hard into the corners. Not for the look — it is what opens the middle band. Every
   percent these come inward is a percent off whatever has to fit between them.

   THE SEATS ARE DELIBERATELY UNEQUAL, on a diagonal: top-left and bottom-right are the big
   ones, top-right and bottom-left a size down. Four corner figures at one size read as
   wallpaper — a repeat, evenly spaced — where a real sheet has a main view and a couple of
   smaller ones, and the difference is what makes it look composed rather than tiled. The
   diagonal rather than a side, because two big ones stacked in the same gutter eat the band
   between them.

   Which figure is big changes with the deal, so nothing is permanently the important one. */
.slot-tl { left: 2.5%; top: 4%;    width: clamp(220px, 20vw,   400px); }
.slot-tr { right: 3%;  top: 6%;    width: clamp(150px, 13.5vw, 240px); }
.slot-bl { left: 2%;   bottom: 5%; width: clamp(180px, 17vw,   300px); }
.slot-br { right: 2%;  bottom: 7%; width: clamp(210px, 20vw,   380px); }

/* The middle band of each gutter, and the two things competing for it. A tall window gets
   a second pair of DRAWINGS; a laptop-height one gets annotations instead, which are half
   the height. One band, two answers, never both — so the deal fills all four seats and the
   media queries below decide which pair is actually on screen. */
/* Deliberately smaller than the corner seats: these have to FIT BETWEEN them, and the band
   narrows as the corners grow. A middle figure sized like a corner figure is one that will
   overlap the moment somebody opens a tall window. */
/* And unequal in here too, the other way round — the small middle sits under the big
   corner, so each gutter reads big-then-small rather than two of a size. */
.slot-ml { left: 3%;  top: 38%; width: clamp(120px, 10vw,   170px); }
.slot-mr { right: 3%; top: 40%; width: clamp(150px, 12.5vw, 215px); }
.slot-nl { left: 7%;  top: 41%; width: clamp(104px, 9vw,    160px); }
.slot-nr { right: 6%; top: 39%; width: clamp(112px, 9.5vw,  175px); }
@media (max-height: 940px) { .slot-ml, .slot-mr { display: none; } }
@media (min-height: 941px) { .slot-nl, .slot-nr { display: none; } }

/* The two bands ABOVE and BELOW the hero — the only horizontal space on the page nothing
   else wants — and off centre, because a mark centred under a headline reads as placed
   rather than as left there. */
.slot-top { left: 33%; top: 3%;    width: clamp(96px,  8vw,  150px); }
.slot-bot { left: 41%; bottom: 3%; width: clamp(150px, 13vw, 240px); }
/* Both bands close up on a short window before anything else does. */
@media (max-height: 820px) { .slot-top, .slot-bot { display: none; } }

@media (max-width: 1150px) { .paper-marks { display: none; } }
/* Short windows put the hero over the lower pair; there is no gutter vertically either. */
@media (max-height: 700px) { .mark-shaft, .mark-section { display: none; } }

/* The front door's background: graph paper.
 *
 * An empty page reads as unfinished, and the two obvious fixes are both wrong here. A stock
 * photograph of a machining centre would be a picture of somebody else's workshop — we are a
 * distribution service, so the only honest photograph is of a partner's own floor, and we do
 * not have one yet. A gradient would be decoration that says nothing.
 *
 * A drawing grid says what this is, and it costs one declaration and no bytes. It is ruled in
 * `--grid-major` and `--grid-minor` — the same two colours `makeGrid()` in app.js hands to
 * the viewer's GridHelper — so the paper somebody drops a file onto and the floor the part
 * stands on afterwards are literally the same surface, seen from above and then from the
 * side. That is why those variables are shared rather than a second pair being invented.
 *
 * BACKGROUND LAYERS, NOT A MASK. A mask on this element would fade its CHILDREN — the
 * headline, the drop box — at the same edges. The vignette is instead the topmost background
 * layer, painting the grid out into the viewer's own colour, so it touches nothing in front
 * of it. Layers stack first-on-top, hence the order below.
 *
 * `background-attachment: scroll` is the default and is what we want on a scrolling element:
 * it pins the paper to the box, so on a short window the hero scrolls and the grid does not.
 */
body.landing #drop-hint {
  background-image:
    radial-gradient(125% 105% at 50% 45%, transparent 42%, var(--viewer-bg) 88%),
    radial-gradient(46% 38% at 50% 46%, var(--grid-glow), transparent 72%),
    linear-gradient(var(--grid-major) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-major) 1px, transparent 1px),
    linear-gradient(var(--grid-minor) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-minor) 1px, transparent 1px);
  /* Minor every 26px, major every fifth line, the way squared paper is ruled. */
  background-size: 100% 100%, 100% 100%, 130px 130px, 130px 130px, 26px 26px, 26px 26px;
  background-position: center;
}

/* The landing hero. NOT `.landing` — that is the state class on <body>, and sharing the
   name applied this 620px centred column to the whole page. */
.landing-hero {
  pointer-events: auto; display: flex; flex-direction: column; align-items: center;
  gap: 18px; padding: 28px 20px; max-width: 620px; width: 100%; margin: auto;
}
.landing-title {
  margin: 0; font-size: 26px; font-weight: 700; letter-spacing: -0.4px; text-align: center;
}
/* A fixture, not an offer. See initDev() in app.js — `?dev=1` brings both this and the
   in-viewer sample switcher back. */
.landing-samples {
  display: flex; flex-direction: column; gap: 7px; align-items: center; width: 100%;
}
body:not(.dev) .landing-samples { display: none; }
.landing-samples em { color: var(--dim); font-size: 12px; }

.drop-box {
  pointer-events: auto; text-align: center; padding: 28px 56px; width: 100%;
  border: 2px dashed var(--line); border-radius: 14px; background: var(--panel);
  display: flex; flex-direction: column; gap: 10px; align-items: center;
}
.drop-box.dragover { border-color: var(--accent); background: rgba(38,160,240,.08); }
.drop-box strong { font-size: 17px; }
.drop-box span, .drop-box em { color: var(--dim); font-size: 12px; }
.drop-box code { color: var(--accent); }
/* Twenty-three of these were the tallest thing on the page, on a page whose job is to get
   somebody to drop their own file. They are a demo aid, so they read like one now: small,
   tight, and capped at a few rows with the rest a scroll away. */
.samples {
  display: flex; gap: 5px; flex-wrap: wrap; justify-content: center;
  max-width: 100%; max-height: 5.6rem; overflow-y: auto;
}
.samples button {
  background: transparent; color: var(--dim); border: 1px solid var(--line);
  font-size: 11px; padding: 3px 9px; font-weight: 400;
}
.samples button:hover { color: var(--accent); border-color: var(--accent); }
button {
  background: var(--accent); color: var(--btn-text); border: 0; border-radius: 8px;
  padding: 8px 22px; font-weight: 600; font-size: 14px; cursor: pointer;
}
button:hover { filter: brightness(1.1); }

/* Single row that scrolls sideways — the sample list grows, the viewer must not shrink. */
#switcher {
  position: absolute; left: 12px; top: 12px; right: 106px; z-index: 4;
  display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;
  padding: 7px 10px; background: var(--overlay);
  border: 1px solid var(--line); border-radius: 9px;
}
/* Without the fixtures this bar holds one button, so it stops being a bar: the chips and
   their label go, and it shrinks to the width of "Upload…". That control is NOT a fixture —
   with a single part on screen and no project rail, it is the only way to price another
   one. */
body:not(.dev) #switcher { right: auto; }
body:not(.dev) .sw-label,
body:not(.dev) #switcher-chips { display: none; }
.sw-label {
  color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .7px;
  flex-shrink: 0;
}
#switcher-chips {
  display: flex; gap: 5px; flex-wrap: nowrap;
  overflow-x: auto; scrollbar-width: thin; flex: 1; padding-bottom: 1px;
  /* fade the clipped edge so it reads as "scroll for more", not a broken chip */
  mask-image: linear-gradient(to right, #000 calc(100% - 18px), transparent 100%);
}
#switcher-chips::-webkit-scrollbar { height: 4px; }
#switcher-chips::-webkit-scrollbar-thumb { background: var(--line); border-radius: 2px; }
#switcher-chips button { flex-shrink: 0; }
#switcher-upload, #add-to-project { flex-shrink: 0; }
#switcher-chips button {
  background: var(--bg); color: var(--dim); border: 1px solid var(--line);
  font-size: 11.5px; font-weight: 500; padding: 4px 9px; border-radius: 6px;
}
#switcher-chips button:hover { color: var(--text); border-color: var(--accent); filter: none; }
#switcher-chips button.sel {
  background: rgba(38,160,240,.12); color: var(--accent); border-color: var(--accent);
}
/* Both, in ONE rule. `#add-to-project` had no styling at all, so it fell through to the
   app's default button — filled, blue and a size larger — sitting next to a quiet outlined
   one. Two peers in a floating toolbar, neither more important than the other, so both take
   the quiet style: a filled primary over the viewer competes with the part. */
#switcher-upload, #add-to-project {
  background: var(--card); color: var(--text); border: 1px solid var(--line);
  font-size: 11.5px; font-weight: 500; padding: 4px 10px; border-radius: 6px;
}
#switcher-upload:hover, #add-to-project:hover {
  border-color: var(--accent); color: var(--accent); filter: none;
}

#part-badge {
  position: absolute; left: 12px; bottom: 12px; padding: 8px 12px;
  background: var(--overlay); border: 1px solid var(--line); border-radius: 8px;
  font-size: 12px; color: var(--dim);
}
#part-badge b { color: var(--text); }
.badge-dl {
  margin-left: 8px; color: var(--accent); text-decoration: none;
  font-size: 14px; line-height: 1; padding: 0 3px;
}
.badge-dl:hover { color: var(--text); }
.proc {
  display: inline-block; margin-left: 6px; padding: 1px 8px; border-radius: 999px;
  background: rgba(62,207,142,.15); color: var(--ok); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .4px;
}
.proc.none { background: rgba(232,178,60,.15); color: var(--warn); }
.route-row.offered-no .bar i { background: var(--warn); }

#busy {
  position: absolute; inset: 0; display: flex; gap: 12px; align-items: center;
  justify-content: center; background: var(--scrim); z-index: 5;
}
.spinner {
  width: 22px; height: 22px; border: 3px solid var(--line);
  border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Nothing loaded: the panel would be 400px of "no part loaded" beside the one thing there
   is to do. It is not hidden to save pixels — it is hidden because an empty panel next to a
   drop zone reads as something broken. */
body.landing #panel { display: none; }
#panel {
  width: 400px; flex-shrink: 0; overflow-y: auto; border-left: 1px solid var(--line);
  background: var(--panel); padding: 14px; display: flex; flex-direction: column; gap: 12px;
}
/* Don't let the fixed panel starve the 3D viewer in a narrow window. */
@media (max-width: 1100px) { #panel { width: 340px; } }
@media (max-width: 900px)  { #panel { width: 300px; } .sw-label { display: none; } }
#empty-panel { color: var(--dim); padding: 18px 6px; }
#empty-panel .dim { margin-top: 10px; font-size: 12px; }
#results { display: flex; flex-direction: column; gap: 12px; }

.card {
  background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px;
}
.card h2 {
  font-size: 12px; text-transform: uppercase; letter-spacing: .8px; color: var(--dim);
  margin-bottom: 10px; display: flex; align-items: center; gap: 8px;
}
.pill {
  background: var(--line); border-radius: 999px; padding: 0 8px; font-size: 11px; color: var(--text);
}

.route-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.route-row .name { width: 150px; font-size: 12.5px; }
.route-row .bar { flex: 1; height: 6px; background: var(--line); border-radius: 3px; overflow: hidden; }
.route-row .bar i { display: block; height: 100%; background: var(--accent); }
.route-row.best .bar i { background: var(--ok); }
.route-reason { font-size: 11px; color: var(--dim); margin: -4px 0 8px 0; }

.hole {
  display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 7px;
  cursor: pointer; border: 1px solid transparent;
}
.hole:hover { background: rgba(38,160,240,.07); }
.hole.sel { border-color: var(--hi); background: rgba(255,138,61,.08); }
.hole .dia { font-weight: 600; min-width: 62px; }
/* Pockets read as W×L, which is wider than a Ø, and they earn a marker so a glance
   separates "cut out of the face" from "drilled". */
.hole.pkt .dia { min-width: 78px; font-size: 13px; }
.hole.pkt { border-left: 2px solid var(--accent); border-radius: 0 10px 10px 0; }
.hole .meta { color: var(--dim); font-size: 12px; flex: 1; }
.hole .thread { color: var(--ok); font-size: 11.5px; }

.dfm-item { display: flex; gap: 8px; padding: 6px 0; font-size: 12.5px; align-items: flex-start; }
.dfm-item .dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0; }
.dfm-item.error .dot { background: var(--err); }
.dfm-item.warning .dot { background: var(--warn); }
.dfm-item.info .dot { background: var(--accent); }
.dfm-ok { color: var(--ok); font-size: 13px; }

.field { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.field label { width: 78px; color: var(--dim); font-size: 12px; }
.field select, .field input {
  flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 7px 9px; font-size: 13px;
}
.seg { display: flex; flex: 1; gap: 6px; }
.seg button {
  flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--line);
  padding: 6px 4px; font-size: 11.5px; font-weight: 500; border-radius: 7px;
}
.seg button small { display: block; color: var(--dim); font-size: 10px; }
.seg button.sel { border-color: var(--accent); color: var(--accent); }

/* The pinned bar: the price and the one button, held against the bottom of the panel.
   `margin: auto -14px -14px` does two jobs — `auto` on top pushes it down when the content
   is short enough not to scroll, and the negative sides cancel the panel's padding so it
   spans the full width and sits flush, like a bar rather than a card that happens to be
   last. */
#buy-bar {
  position: sticky; bottom: -14px; z-index: 3;
  margin: auto -14px -14px; padding: 10px 14px 12px;
  background: var(--panel); border-top: 1px solid var(--line);
  display: grid; grid-template-columns: auto 1fr; align-items: center;
  column-gap: 12px; row-gap: 0;
}
#price-main { font-size: 22px; font-weight: 700; white-space: nowrap; }
#price-main .cur { font-size: 13px; color: var(--dim); font-weight: 500; }
/* Side by side, so the bar costs one row instead of two. The number is as wide as it needs
   to be and the button takes the rest — the reverse starves whichever the window is
   narrowest for. */
#buy-bar .add-cart, #buy-bar .add-done { margin-top: 0; }
/* The qualifier spans both columns under them, because it qualifies the pair. */
#buy-bar .disclaimer { grid-column: 1 / -1; margin: 8px 0 0; text-align: center; }
/* Nothing to buy: the reason takes the whole bar and reads as a sentence rather than as a
   price that went wrong. Keyed off the panel, since #price-main no longer sits inside the
   card that carries the class. */
#panel.blocked #price-main {
  grid-column: 1 / -1; text-align: center; white-space: normal;
  color: var(--err); font-size: 14px; font-weight: 600; line-height: 1.4;
}
#ops-toggle {
  margin-left: auto; display: flex; align-items: center; gap: 4px; cursor: pointer;
  font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: .6px;
}
#ops-toggle input { accent-color: var(--warn); cursor: pointer; }
#ops-panel {
  margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--warn);
}
.ops-box {
  background: rgba(232,178,60,.07); border: 1px solid rgba(232,178,60,.3);
  border-radius: 8px; padding: 8px 10px; margin-bottom: 10px; font-size: 11.5px;
}
.ops-line { display: flex; justify-content: space-between; padding: 2px 0; color: var(--dim); }
.ops-line b { color: var(--text); font-variant-numeric: tabular-nums; }
.ops-line.good b { color: var(--ok); }

#cust-breaks { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 4px; }
#cust-breaks th {
  text-align: left; color: var(--dim); font-weight: 500; font-size: 10px;
  text-transform: uppercase; letter-spacing: .5px; padding: 4px; border-bottom: 1px solid var(--line);
}
#cust-breaks td { padding: 7px 4px; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; }
#cust-breaks td:first-child { color: var(--dim); width: 34px; }
#cust-breaks td:last-child { text-align: right; color: var(--dim); }

.ranking-note {
  background: rgba(38,160,240,.08); border: 1px solid rgba(38,160,240,.3);
  border-radius: 8px; padding: 7px 10px; margin-bottom: 9px;
  color: var(--accent); font-size: 11.5px; line-height: 1.5;
}

/* --- one card per capable workshop, cheapest first --- */
.shop {
  border: 1px solid var(--line); border-radius: 9px; padding: 10px 11px; margin-bottom: 9px;
  background: var(--sunken);
}
.shop.best { border-color: var(--ok); background: rgba(62,207,142,.05); }
.shop-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.shop-name { font-size: 13px; font-weight: 600; }
.shop-name .tag {
  margin-left: 6px; font-size: 9.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .5px; color: var(--ok); background: rgba(62,207,142,.15);
  padding: 1px 6px; border-radius: 999px; vertical-align: 1px;
}
.shop-machine { color: var(--dim); font-size: 11.5px; margin-top: 1px; }
.shop-price {
  font-size: 14px; font-weight: 700; white-space: nowrap; text-align: right;
  font-variant-numeric: tabular-nums;
}
.shop-price .cur { font-size: 11px; color: var(--dim); font-weight: 500; }
.shop-meta { color: var(--dim); font-size: 11px; margin: 6px 0 8px; line-height: 1.45; }
.uncal { color: var(--warn); }

.breaks { width: 100%; border-collapse: collapse; font-size: 12px; }
.breaks th {
  text-align: left; color: var(--dim); font-weight: 500; font-size: 10px;
  text-transform: uppercase; letter-spacing: .5px; padding: 3px 4px;
  border-bottom: 1px solid var(--line);
}
.breaks td { padding: 5px 4px; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.breaks td:first-child { color: var(--dim); width: 30px; }
.breaks td:last-child { text-align: right; color: var(--dim); }

.notes { list-style: none; margin-top: 8px; }
.notes li { color: var(--dim); font-size: 11px; padding: 2px 0 2px 11px; position: relative; }
.notes li::before { content: "·"; position: absolute; left: 2px; color: var(--accent); }

/* --- shops that cannot make it, and precisely why --- */
#rejected { margin-top: 4px; }
.rej-title {
  color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .7px;
  margin: 10px 0 6px;
}
.rej-row {
  border-left: 2px solid var(--err); padding: 4px 0 4px 9px; margin-bottom: 7px; font-size: 12px;
}
.rej-why { color: var(--dim); font-size: 11px; margin-top: 2px; line-height: 1.45; }
#shop-badge { text-transform: none; letter-spacing: 0; font-weight: 500; }

details { margin-top: 8px; }
summary { cursor: pointer; color: var(--dim); font-size: 12px; }
.breakdown { width: 100%; margin-top: 8px; font-size: 11.5px; border-collapse: collapse; }
.breakdown td { padding: 3px 4px; border-bottom: 1px solid var(--line); color: var(--dim); }
.breakdown td:last-child { text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }
.disclaimer { margin-top: 10px; color: var(--warn); font-size: 11px; }
.small { font-size: 12px; }
.dim { color: var(--dim); }


/* Turned-profile rows reuse the hole-row layout */
.hole .thread.groove { color: var(--ok); }
.hole .thread.bore   { color: var(--accent); }
.hole .thread.chamfer, .hole .thread.taper { color: var(--dim); }
.hole .thread.od     { color: var(--dim); }

/* --- dimensions drawn as a technical drawing would --- */
/* width/height are required: an SVG is a REPLACED element, so inset:0 alone leaves it
   at its intrinsic 300x150 and every leader gets clipped away. */
#dim-lines {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 2; overflow: visible;
}
.dim-leader, .dim-line, .dim-ext {
  fill: none; stroke: var(--text); stroke-width: 1; opacity: .8;
}
.dim-ext { opacity: .45; }
.dim-arrowhead { fill: var(--text); opacity: .8; }
.dim-arrowhead.sel { fill: var(--hi); opacity: 1; }
.dim-leader.sel, .dim-line.sel { stroke: var(--hi); stroke-width: 1.6; opacity: 1; }
.dim-line.sel { marker-start: url(#dim-arrow-sel); marker-end: url(#dim-arrow-sel); }

.dim-label {
  position: absolute; z-index: 3; cursor: pointer; white-space: nowrap;
  pointer-events: auto; font-variant-numeric: tabular-nums; color: var(--text);
  padding: 0 2px 1px; line-height: 1.15;
  text-shadow: 0 0 3px var(--viewer-bg), 0 0 3px var(--viewer-bg), 0 0 3px var(--viewer-bg);
}
.dim-label .dv { font-size: 12.5px; font-weight: 600; letter-spacing: .2px; }
/* Deviations stack upper-over-lower beside the value, as on a drawing. */
.dim-label .tol {
  display: inline-block; vertical-align: -0.35em; margin-left: 3px;
  font-size: 8.5px; line-height: 1.05; color: var(--dim); text-align: left;
}
.dim-label .tol i { display: block; font-style: normal; }
.dim-label.linear { transform-origin: 50% 100%; }
/* Pocket callouts read as a size, not a diameter — tint them like the
   pocket rows in the feature list so the two obviously pair up. */
.dim-label.pkt .dv { color: var(--accent); }

/* --- process choice ---------------------------------------------------
   A blocked option stays clickable on purpose: selecting it is how the customer
   sees the constraint that stops it, which is the only way to know what to fix. */
/* This row is a set of choices, not one control, so it takes the card's full width
   instead of squeezing into the ~150px the label column leaves. */
#process-field { display: block; }
#process-field > label { display: block; width: auto; margin-bottom: 5px; }
.proc-opts { display: flex; flex-wrap: wrap; gap: 6px; }
.proc-opt {
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 8px; padding: 5px 10px; font-size: 12.5px; font-weight: 500;
  display: flex; flex-direction: column; align-items: flex-start; line-height: 1.25;
}
.proc-opt small { color: var(--dim); font-size: 10.5px; font-weight: 400; }
.proc-opt:hover { border-color: var(--accent); filter: none; }
.proc-opt.sel { border-color: var(--accent); background: rgba(38,160,240,.12); }
.proc-opt.blocked { color: var(--dim); border-style: dashed; }
.proc-opt.blocked small { color: var(--warn); }
.proc-opt.blocked.sel { border-color: var(--warn); background: rgba(232,178,60,.12); }
#cust-breaks td.why {
  color: var(--warn); font-size: 12px; line-height: 1.5; padding: 5px 0;
  border-bottom: 1px solid var(--line); white-space: normal;
}
.dim-label:hover .dv { color: var(--accent); }
.dim-label.sel .dv { color: var(--hi); }
.dim-label.sel .tol { color: var(--hi); }

/* --- per-feature spec panel --- */
.spec-meta { color: var(--dim); font-size: 11.5px; margin-bottom: 10px; }
.fit-readout { margin-top: 4px; }
.fit-band {
  display: flex; justify-content: space-between; align-items: baseline;
  background: var(--sunken); border: 1px solid var(--line); border-radius: 7px;
  padding: 7px 10px; font-variant-numeric: tabular-nums; font-size: 13px;
}
.fit-band span { color: var(--accent); font-weight: 600; }
.fit-note { color: var(--dim); font-size: 11px; margin-top: 6px; line-height: 1.5; }
.hole .dia .xn { color: var(--dim); font-weight: 500; margin-left: 4px; font-size: 12px; }
#spec-card { border-color: var(--accent); }
#spec-card h2 { color: var(--accent); }

/* --- M5: customer view --- */
.sum-line { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
.sum-line span { color: var(--dim); }
.sum-line b { font-weight: 600; }
.sum-none { color: var(--warn); font-size: 13px; line-height: 1.5; }
.sum-none .dim { font-size: 11.5px; }

#dfm-card.has-errors { border-color: var(--err); }
#dfm-card.has-errors h2 { color: var(--err); }
#ship-line { text-align: center; margin-top: 8px; }

/* --- M5: ops-only card --- */
.card.ops { border-color: rgba(232,178,60,.45); background: rgba(232,178,60,.04); }
.card.ops h2 { color: var(--warn); }
.ops-sub {
  color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .7px;
  margin: 12px 0 6px;
}
.ops-sub:first-child { margin-top: 0; }

/* --- M5: stack on a phone; the fixed side panel would otherwise crush the viewer --- */
@media (max-width: 700px) {
  body { overflow: auto; }
  /* The header cannot stay one row at this width — the settings were being pushed off the
     right edge entirely. It wraps, and the account address goes: it is the longest thing in
     there and the least useful, since "Sign out" already says you are signed in. */
  header { flex-wrap: wrap; gap: 8px 12px; padding: 8px 12px; }
  .brand { font-size: 17px; }
  .hdr-right { gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
  .hdr-group { gap: 8px; }
  .hdr-group + .hdr-group { padding-left: 12px; }
  /* Both are identity labels out in the header, both are the longest thing in it, and
     neither can be pressed. The menu still names the account by way of Sign out being in
     it, and a workshop knows which shop it is. */
  #whoami, #pw-shop { display: none; }
  /* The flags carry it alone here. Two names plus a cart plus a burger do not fit, and a
     flag is the part that does not need reading. */
  .hdr-lang button span { display: none; }
  .hdr-lang button { padding: 3px 5px; }
  main { flex-direction: column; }
  #viewer-wrap { height: 52vh; flex: none; }
  /* On the landing there is no panel below to make room for. */
  body.landing #viewer-wrap { height: auto; flex: 1; }
  .landing-title { font-size: 21px; }
  /* Stacked layout: the rail becomes a horizontal strip above the viewer, and the
     part list scrolls sideways instead of down. */
  #project-bar { width: 100%; border-right: 0; border-bottom: 1px solid var(--line); }
  .pb-parts { flex-direction: row; overflow-x: auto; overflow-y: hidden; padding: 6px 8px; }
  /* Narrow columns cannot fit name and price side by side, so the price drops to the
     second line beside the process label. */
  .pb-part { flex: 0 0 165px; grid-template-areas: "file file" "meta price"; }
  .pb-price { align-self: end; justify-self: end; }
  .pb-foot { grid-template-columns: 1fr auto; align-items: center; }
  /* The project bar's own look is at the top level and needs no restating here — this
     block held a byte-identical copy of ten rules, .retention through .ro-badge, that
     changed nothing at any width. See test_css.py. */
.pb-share { width: auto; padding: 7px 14px; }
  #panel {
    width: 100%; border-left: 0; border-top: 1px solid var(--line); overflow-y: visible;
  }
  #switcher { left: 8px; right: 8px; top: 8px; }
  .drop-box { padding: 28px 22px; }
  .field { flex-wrap: wrap; }
  .field label { width: 100%; margin-bottom: 2px; }
  label.row { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Admin console (/admin)
   ========================================================================== */
body.admin { overflow: auto; }
.admin-tag {
  font-size: 11px; font-style: normal; text-transform: uppercase; letter-spacing: 1px;
  color: var(--warn); background: rgba(232,178,60,.14); padding: 2px 8px;
  border-radius: 999px; vertical-align: 3px;
  /* .brand is a flex container, and flex drops pure-whitespace text nodes — so the space
     that used to separate this from the mark has to be a margin now. */
  margin-left: 10px; align-self: center;
}
.back-link { margin-left: auto; color: var(--accent); text-decoration: none; font-size: 12px; }
.back-link:hover { text-decoration: underline; }

#side {
  width: 260px; flex-shrink: 0; border-right: 1px solid var(--line); background: var(--panel);
  padding: 14px 12px; display: flex; flex-direction: column; gap: 6px; overflow-y: auto;
}
.side-head {
  color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .8px;
  margin: 10px 0 4px;
}
.nav-shop, .side-btn {
  display: block; width: 100%; text-align: left; background: var(--card); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; font-size: 13px;
  font-weight: 500; margin-bottom: 5px;
}
.nav-shop:hover, .side-btn:hover { border-color: var(--accent); filter: none; }
.nav-shop.sel { border-color: var(--accent); background: rgba(38,160,240,.1); }
.nav-name { display: block; }
.nav-sub { display: block; color: var(--dim); font-size: 11px; margin-top: 1px; font-weight: 400; }
.side-btn { color: var(--accent); }
#side-note { font-size: 11px; margin-top: 12px; line-height: 1.5; }

#editor { flex: 1; padding: 20px 24px; overflow-y: auto; max-width: 760px; }
/* ONE EMPTY STATE, UNSCOPED. There were four: `#editor .empty` here, `.a-clear` in the ops
   queue, `.acct-empty` in the customer's project and order lists, and `.empty.sm` inline.
   They said the same thing in four paddings. Unscoped from #editor because it only ever
   worked in there by accident — every use happened to land in that one container, and the
   next one somewhere else would have rendered as unstyled body text. */
.empty { color: var(--dim); padding: 34px 0; text-align: center; }
/* Inside a card or a table, where 34px of nothing is a hole rather than a pause. */
.empty.sm { padding: 14px 0; text-align: left; font-size: 12px; }
/* ONE LOADING LINE. The spinner below is a different thing and stays: it is for the one
   operation slow enough to need a moving part — the kernel opening a STEP file — and a
   spinner on a 40ms fetch is a flicker, not feedback. */
.loading { color: var(--dim); font-size: 12px; }
.ed-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: 8px; flex-wrap: wrap;
}
.ed-head h1 { font-size: 20px; font-weight: 700; }
.ed-actions { display: flex; gap: 8px; }
.lede { color: var(--dim); font-size: 12.5px; margin-bottom: 14px; line-height: 1.55; max-width: 60ch; }

.group {
  border: 1px solid var(--line); border-radius: 10px; background: var(--card);
  padding: 12px 14px; margin-bottom: 12px;
}
.group-title {
  font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--dim);
  margin-bottom: 10px; display: flex; align-items: center; gap: 10px;
}
.add-machine { margin-left: auto; display: flex; gap: 6px; }

label.row {
  display: grid; grid-template-columns: 180px 1fr; gap: 4px 12px;
  align-items: center; padding: 5px 0;
}
.row .lbl { color: var(--dim); font-size: 12.5px; }
.row .ctl { display: flex; align-items: center; gap: 7px; }
.row input[type="text"], .row input[type="number"], .row select {
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 6px 9px; font-size: 13px; width: 100%; max-width: 240px;
}
.row input[type="checkbox"] { accent-color: var(--accent); width: 15px; height: 15px; }
.row input:disabled { opacity: .5; }
.row .unit { color: var(--dim); font-size: 11.5px; }
.row .hint {
  grid-column: 2; color: var(--dim); font-size: 11px; line-height: 1.45; opacity: .85;
}
.vec3 { display: flex; gap: 6px; }
.vec3 input { max-width: 76px !important; }

.mats { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 4px; }
.mat { display: flex; align-items: center; gap: 7px; font-size: 12.5px; cursor: pointer; }
.mat input { accent-color: var(--accent); }

.machine {
  border: 1px solid var(--line); border-radius: 9px; padding: 10px 12px; margin-bottom: 10px;
  background: var(--sunken);
}
.machine-head { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.machine-head code { color: var(--dim); font-size: 11.5px; }
.machine-head button { margin-left: auto; }
.proc-tag {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px;
  padding: 2px 8px; border-radius: 999px;
}
.proc-tag.milling { color: var(--accent); background: rgba(38,160,240,.15); }
.proc-tag.turning { color: var(--ok); background: rgba(62,207,142,.15); }
.proc-tag.laser   { color: var(--hi); background: rgba(255,138,61,.15); }

/* One thickness box per material family — a laser's limit is a property of the
   material as much as of the machine. */
.thickmap { display: flex; flex-wrap: wrap; gap: 6px 10px; }
.thickmap label {
  display: flex; align-items: center; gap: 5px;
  font-size: 11.5px; color: var(--dim);
}
.thickmap input { width: 62px; }

/* THE BASE BUTTON IS THE PRIMARY BUTTON. `button.primary` used to restate the two
   declarations directly above it and was therefore a no-op — a class you could add or
   remove with no effect, which is worse than no class at all because it reads as intent.
   The vocabulary is: a plain button is the important one, .ghost steps back, .danger
   destroys, .sm is compact. Four words, and the workshop console now uses the same four. */
button.ghost {
  background: transparent; color: var(--dim); border: 1px solid var(--line);
  font-weight: 500;
}
button.ghost:hover { color: var(--text); border-color: var(--accent); filter: none; }
button.danger { background: transparent; color: var(--err); border: 1px solid var(--err); }
button.sm { padding: 4px 10px; font-size: 11.5px; background: var(--bg); color: var(--accent);
  border: 1px solid var(--line); font-weight: 500; }

/* The one place any console says something that is not a question. Built by ui.js rather
   than sitting in three HTML files, so there is nothing to keep in step. */
#notice {
  position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%);
  background: var(--ok); color: var(--btn-text); padding: 9px 18px; border-radius: 8px;
  font-size: 13px; font-weight: 600; z-index: 50; cursor: pointer;
  /* A failure can be a list — one line per file that would not load — so it has to wrap
     and it has to stay inside the window on a phone. */
  max-width: min(92vw, 460px); text-align: center; line-height: 1.5;
}
#notice div + div { margin-top: 2px; font-weight: 500; }
#notice.bad { background: var(--err); color: #fff; }

/* The audit trail. Narrow first column so the times line up as a column of times rather
   than as the start of five sentences, and the note last because it is the only cell whose
   length nobody controls. */
.ord-trail td:first-child { white-space: nowrap; font-variant-numeric: tabular-nums; }
.ord-trail td:last-child { width: 100%; }

/* Scoped to body.admin — an unscoped `main { flex-direction: column }` here also hit the
   quoting page and collapsed its 3D viewer to zero height between 700 and 820 px. */
@media (max-width: 820px) {
  body.admin main { flex-direction: column; }
  body.admin #side { width: 100%; border-right: 0; border-bottom: 1px solid var(--line); }
  body.admin label.row { grid-template-columns: 1fr; }
  body.admin .row .hint { grid-column: 1; }
}

/* --- calibration (admin) --------------------------------------------- */
.cal-facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px; margin-bottom: 8px;
}
.cal-facts div { display: grid; gap: 1px; }
.cal-facts span { font-size: 11px; }
.cal-facts b { font-size: 17px; font-weight: 700; }
.cal-facts b.ok { color: var(--ok); }
.cal-facts b.warn { color: var(--warn); }
.cal-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.cal-table th {
  text-align: left; color: var(--dim); font-weight: 500; font-size: 10.5px;
  text-transform: uppercase; letter-spacing: .6px; padding-bottom: 5px;
}
.cal-table td { padding: 6px 8px 6px 0; border-top: 1px solid var(--line); vertical-align: top; }
.cal-table td.ok { color: var(--ok); }
.cal-table td.warn { color: var(--warn); }
.cal-table .dim { font-size: 11px; }

/* --- DFM markers on the model -----------------------------------------
   A finding in the side panel names a measurement; it does not say which of thirty
   holes it means. The flag sits on the offending geometry, and clicking it brings the
   number and the reason to that spot rather than making the eye travel back and forth.
   Centred on its anchor via translate(-50%,-50%), because the anchor IS the defect. */
.dfm-flag {
  position: absolute; z-index: 4; display: flex;
  align-items: center; justify-content: center;
  width: 18px; height: 18px; margin: 0; padding: 0;
  transform: translate(-50%, -50%);
  border-radius: 50%; cursor: pointer;
  font: 700 12px/1 system-ui, sans-serif; color: #fff;
  background: var(--warn); border: 1.5px solid var(--viewer-bg);
  box-shadow: 0 1px 5px rgba(0, 0, 0, .45);
  transition: transform .12s ease;
}
/* Red for both severities, deliberately. In the panel amber-vs-red grades how bad it is;
   on the model the only question is "there is a problem, and it is HERE", and a marker
   that blends into the amber dimension text is a marker nobody notices. */
.dfm-flag, .dfm-flag.error { background: var(--err); }
.dfm-flag:hover { transform: translate(-50%, -50%) scale(1.25); }
/* Kept scaled while its popup is open so it is obvious which flag you are reading. */
.dfm-flag.open { transform: translate(-50%, -50%) scale(1.25); }

.dfm-pop {
  position: absolute; z-index: 5; max-width: 260px;
  padding: 8px 10px; border-radius: 8px;
  background: var(--card); color: var(--text);
  border: 1px solid var(--warn);
  box-shadow: 0 4px 18px rgba(0, 0, 0, .35);
  font-size: 12px; line-height: 1.45; white-space: normal;
}
.dfm-pop, .dfm-pop.error { border-color: var(--err); }
/* The measurement is the headline — it is the thing the customer has to change. */
.dfm-pop .v {
  font-size: 15px; font-weight: 700; margin-bottom: 3px;
  color: var(--warn); font-variant-numeric: tabular-nums;
}
.dfm-pop .v, .dfm-pop.error .v { color: var(--err); }
.dfm-pop .m { color: var(--dim); }

/* The measured span, drawn only while a marker is open. Ticks square to the line so 4 mm
   still reads as a dimension rather than a scratch; the dashed lead ties the offset badge
   back to the span it is describing. */
.dfm-span { stroke: var(--err); stroke-width: 2; opacity: 1; }
.dfm-span-tick { stroke: var(--err); stroke-width: 2; opacity: 1; }
.dfm-span-lead { stroke: var(--err); stroke-width: 1; stroke-dasharray: 3 3; opacity: .55; }

/* --- accounts --------------------------------------------------------- */

.link-btn {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--accent); font-size: 12.5px; font-weight: 500;
}
.link-btn:hover { text-decoration: underline; filter: none; }

#auth-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 0;
  background: var(--card); color: var(--text); max-width: 360px; width: calc(100% - 32px);
  /* A native modal <dialog> centres itself with `margin: auto`, and the reset on line 1
     sets `margin: 0` on everything — which pinned this to the top-left corner. Restoring
     the margin is the whole fix; `inset: 0` is what gives auto-margin room to work in. */
  inset: 0; margin: auto;
}
#auth-dialog::backdrop { background: rgba(0, 0, 0, .55); }
#auth-form { display: flex; flex-direction: column; gap: 10px; padding: 20px; }
#auth-form h2 { margin: 0; font-size: 17px; }
#auth-form label { display: flex; flex-direction: column; gap: 4px; font-size: 12px;
                   color: var(--dim); }
/* One rule for every dialog field in the app. #ask-form was written without it and its
   input came out unstyled and inline with its label, which is what made the New project
   box look like a different product. */
#auth-form input, #ask-form input {
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 8px 10px; font-size: 13.5px; font-family: inherit;
  width: 100%;
}
#auth-form input:focus, #ask-form input:focus { outline: none; border-color: var(--accent); }
.auth-why { margin: 0; font-size: 12px; color: var(--dim); line-height: 1.5; }
/* Named for where it goes, not for the first form that needed it — it was `.auth-error`
   and it is used by sign-in, checkout, the DXF thickness question and the workshop's
   concern box. `.pw-tg-err` was a fourth copy of the same three declarations. */
.form-error { margin: 0; font-size: 12.5px; color: var(--err); line-height: 1.45; }
.auth-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px;
                margin-top: 2px; }
.auth-actions button[type="submit"] {
  background: var(--accent); color: #fff; border: none; border-radius: 7px;
  padding: 7px 14px; font-size: 13px; font-weight: 600; cursor: pointer;
}
.auth-actions button[type="submit"]:disabled { opacity: .6; cursor: default; }
.auth-switch { margin: 0; font-size: 12px; color: var(--dim); }
/* Deliberately visible rather than a "forgot password?" link: there is no email provider,
   so a link would go nowhere and a dead end is worse than an honest sentence. */
.auth-note { margin: 0; font-size: 11.5px; color: var(--dim); line-height: 1.5;
             border-top: 1px solid var(--line); padding-top: 10px; }

/* --- saving a project to an account ----------------------------------- */
.pb-save {
  margin: 0 10px 8px; padding: 6px 10px; width: calc(100% - 20px);
  background: var(--accent); color: #fff; border: none; border-radius: 7px;
  font-size: 12.5px; font-weight: 600; cursor: pointer;
}
#dxf-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 20px; inset: 0; margin: auto;
  background: var(--card); color: var(--text); max-width: 380px; width: calc(100% - 32px);
}
#dxf-dialog::backdrop { background: rgba(0, 0, 0, .55); }
#dxf-form { display: flex; flex-direction: column; gap: 12px; }
#dxf-form h2 { margin: 0; font-size: 17px; }
/* The assumption a unitless file forced on us, said where it cannot be missed. */
/* Not an error: an explanation. Neutral, so arriving on the partner page with a customer
   account does not read as having done something wrong.
   NOT `.pw-note` — that name was already taken further down this file by a warn-coloured
   rule, and the later definition simply won. Third time this file has produced that bug
   (`.landing`, `.seg`); grep before naming. */
.pw-explain {
  margin: 0; padding: 8px 10px; font-size: 12px; color: var(--dim);
  border: 1px solid var(--line); border-radius: 8px; background: var(--sunken);
}
.pw-explain .link-btn { font-size: 12px; }

.dxf-assumed {
  margin: 0 10px 8px; padding: 7px 10px; font-size: 12px;
  color: var(--warn); border: 1px solid var(--warn); border-radius: 8px;
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}

#projects-dialog, #orders-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 0; inset: 0; margin: auto;
  background: var(--card); color: var(--text); max-width: 460px; width: calc(100% - 32px);
}
#projects-dialog::backdrop, #orders-dialog::backdrop { background: rgba(0, 0, 0, .55); }
#projects-dialog .dlg, #orders-dialog .dlg {
  display: flex; flex-direction: column; gap: 12px; padding: 20px;
}
#projects-dialog h2, #orders-dialog h2 { margin: 0; font-size: 17px; }

/* An order row is two lines, not one: the reference is what somebody is scanning for and it
   earns a line of its own, with everything they would check afterwards under it. */
.myord-row {
  display: grid; grid-template-columns: 1fr auto; gap: 1px 12px; align-items: center;
  padding: 9px 10px; border-radius: 7px; text-decoration: none; color: var(--text);
}
.myord-row:hover { background: var(--bg); }
.myord-ref { font-weight: 600; font-size: 13px; }
.myord-meta { grid-column: 1; color: var(--dim); font-size: 11.5px; }
/* The same pill as the order's own page, so a status looks the same wherever it is read. */
.myord-row .order-status { grid-column: 2; grid-row: 1 / span 2; font-size: 11px; padding: 2px 9px; }
.proj-list { display: flex; flex-direction: column; gap: 2px; max-height: 50vh;
             overflow-y: auto; }
.proj-row {
  display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: baseline;
  padding: 8px 10px; border-radius: 7px; text-decoration: none; color: var(--text);
  font-size: 13px; position: relative;
}
.proj-row:hover { background: var(--bg); }
.proj-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* One project expanded at a time: this is a list to scan, not a tree to browse. */
.proj-item { border-radius: 8px; }
.proj-item.open { background: var(--bg); padding-bottom: 6px; }
.proj-item .proj-row { cursor: pointer; }
.proj-parts { display: flex; flex-direction: column; gap: 2px; padding: 0 10px 4px 18px; }
.proj-part {
  display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: baseline;
  background: none; border: 0; padding: 5px 8px; border-radius: 6px; width: 100%;
  text-align: left; font: inherit; font-size: 12.5px; color: var(--text); cursor: pointer;
}
.proj-part:hover { background: var(--card); filter: none; }
.proj-acts { display: flex; gap: 14px; padding: 4px 10px 2px 18px; }
.proj-acts .link-btn { font-size: 12px; }
/* Red on the word, not a box around it — the outlined danger button is for a button. */
.danger-text { color: var(--err); }
.proj-meta, .proj-when { color: var(--dim); font-size: 11.5px; white-space: nowrap; }

/* --- choosing where an upload goes -------------------------------------- */
#pick-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 0; inset: 0; margin: auto;
  background: var(--card); color: var(--text); max-width: 420px; width: calc(100% - 32px);
}
#pick-dialog::backdrop { background: rgba(0, 0, 0, .55); }
/* .dlg carries no padding of its own — each dialog sets it, so this one has to too. */
#pick-dialog .dlg { display: flex; flex-direction: column; gap: 12px; padding: 20px; }
#pick-dialog h2 { margin: 0; font-size: 17px; }
.pick-list { display: flex; flex-direction: column; gap: 6px;
             max-height: 52vh; overflow-y: auto; }
.pick-row {
  display: grid; grid-template-columns: 1fr auto auto; gap: 10px; align-items: baseline;
  width: 100%; text-align: left; padding: 10px 12px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--text); font: inherit;
}
.pick-row:hover { border-color: var(--accent); }
/* Green, and the only coloured thing in the list: it is the one row that MAKES something
   where every other row picks something that exists. It also lost its explanatory line —
   naming happens in the field that opens, so a caption guessing at a name was answering a
   question a moment before asking it properly. */
.pick-new { grid-template-columns: 1fr; border-color: var(--ok); color: var(--ok); }
.pick-new:hover { background: color-mix(in srgb, var(--ok) 10%, transparent);
                  border-color: var(--ok); }
.pick-new .proj-name { font-weight: 600; color: var(--ok); }

/* Rename and delete are NOT here. This dialog picks a project — to switch to, or to file an
   upload into — and managing them is a page with room for it. A hover action floating over
   the part count and the date was the symptom of asking one row to do both. */
.proj-manage { display: block; margin: 6px 0 0 10px; font-size: 12px; }

/* Our own prompt/confirm. */
#ask-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 20px; inset: 0; margin: auto;
  background: var(--card); color: var(--text); max-width: 380px; width: calc(100% - 32px);
}
#ask-dialog::backdrop { background: rgba(0, 0, 0, .55); }
#ask-form { display: flex; flex-direction: column; gap: 12px; }
/* `anywhere`, because a CAD filename is one unbreakable token —
   Casting_Crank_box_B_support_adding_low has no space to wrap at and simply ran off the
   edge of the dialog. */
#ask-form h2 { margin: 0; font-size: 17px; overflow-wrap: anywhere; }
#ask-body { overflow-wrap: anywhere; }
/* A destructive answer should not look like an OK. */
#ask-ok.danger { background: var(--err); border-color: var(--err); color: #fff; }

/* --- the header menu ----------------------------------------------------- */
.menu { position: relative; display: flex; }
.menu-btn {
  background: var(--card); color: var(--text); border: 1px solid var(--line);
  border-radius: 8px; width: 32px; height: 32px; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.menu-btn:hover { border-color: var(--accent); color: var(--accent); filter: none; }
.menu-btn[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }

.menu-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  min-width: 15.5rem; padding: 6px;
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 12px 32px var(--scrim);
  display: flex; flex-direction: column; gap: 1px;
  /* Grows from the button it belongs to, so it reads as having come from there rather than
     having been there all along. */
  transform-origin: top right;
  animation: menu-in .13s cubic-bezier(.2, .8, .3, 1);
}
@keyframes menu-in {
  from { opacity: 0; transform: scale(.96) translateY(-4px); }
}
@media (prefers-reduced-motion: reduce) { .menu-panel { animation: none; } }

/* A setting, not a destination: a name on the left and its control on the right. Dim,
   because unlike every other row in here it is not something you press. */
.menu-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 5px 10px; font-size: 13px;
}
.menu-row > span { color: var(--dim); }

/* Both options visible, the current one lit. Sized to sit inside a menu row rather than to
   fill a panel, which is what the quote panel's `.seg` does. */
.menu-seg {
  display: inline-flex; gap: 2px; padding: 2px; flex-shrink: 0;
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  /* Fixed, so the two controls are the same object at the same size rather than two widths
     the words happened to produce — a menu with a ragged inner edge looks like a mistake. */
  width: 8.6rem;
}
.menu-seg button {
  flex: 1; background: none; border: 0; border-radius: 6px; padding: 3px 4px; margin: 0;
  font: inherit; font-size: 11.5px; line-height: 1.45; color: var(--dim);
  cursor: pointer; white-space: nowrap; height: auto; width: auto;
}
.menu-seg button:hover { color: var(--text); filter: none; }
.menu-seg button[aria-pressed="true"] {
  background: var(--card); color: var(--text); font-weight: 600;
  box-shadow: 0 1px 2px var(--scrim);
}
/* Qualified with `button` so these beat `button.danger` further up, which is the outlined
   danger button used elsewhere and was drawing a red box around Sign out. */
button.menu-item {
  background: none; border: 0; border-radius: 7px; padding: 7px 10px;
  text-align: left; font: inherit; font-size: 13px; color: var(--text); cursor: pointer;
  white-space: nowrap; width: 100%; height: auto;
}
button.menu-item:hover { background: var(--bg); filter: none; }
/* Inset to the rows' own text, so the menu has one left edge instead of two. */
.menu-sep { height: 1px; background: var(--line); margin: 5px 10px; }
/* The one row you cannot undo by clicking again. Red the whole time rather than only on
   hover — a warning that appears once you are already pointing at it is late. */
button.menu-item.danger { color: var(--err); background: none; border: 0; }
button.menu-item.danger:hover {
  background: color-mix(in srgb, var(--err) 13%, transparent);
}

/* --- ops queue -----------------------------------------------------------
   The screen somebody opens this console to USE. Everything here is in service of one
   question asked several times a day: what is waiting on me, and what is it worth? */
.a-top {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin-bottom: 18px;
}
.a-stats { display: flex; gap: 22px; }
.a-stats div { display: flex; flex-direction: column; align-items: flex-end; }
.a-stats b { font-size: 20px; line-height: 1.1; }
.a-stats span { color: var(--dim); font-size: 11px; text-transform: uppercase;
                letter-spacing: .5px; }

.a-queue { margin-bottom: 26px; }
.a-queue h3 { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
/* Work that is merely IN PROGRESS is not work. It is shown because you sometimes want to
   look, and dimmed because you rarely do. */
.a-quiet h3, .a-quiet .hint { color: var(--dim); }
.a-count {
  font-size: 11px; font-weight: 600; color: var(--dim);
  border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px;
}
.a-back { margin-bottom: 12px; }

/* Age in days, not a date: the column is scanned for the oldest thing, never read. */
.a-age { font-variant-numeric: tabular-nums; }
.a-late { color: var(--err); font-weight: 600; }
.a-soon { color: var(--warn); }
/* Which number this is. The same column used to mean "quoted" on one row and "agreed" on
   the next, unlabelled, for the one person whose job is the margin. */
.a-money-tag {
  display: block; font-size: 10px; color: var(--dim); text-transform: uppercase;
  letter-spacing: .5px; font-weight: 500; line-height: 1.1; margin-top: 1px;
}


/* --- currency picker ----------------------------------------------------- */
/* A select, not a pair of buttons: one tap on a phone, it reads as a setting rather than an
   action, and it does not grow the header when a third currency arrives. */
.cur-pick {
  background: var(--card); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 4px 6px; font: inherit; font-size: 12px; cursor: pointer;
}
.cur-pick:hover { border-color: var(--accent); }

/* --- the cart ---------------------------------------------------------- */
/* The buy action sits with the price, under the quantity that produced it. */
.add-cart {
  margin-top: 12px; padding: 10px; width: 100%;
  background: var(--ok); color: #fff; border: none; border-radius: 7px;
  font-size: 13.5px; font-weight: 700; cursor: pointer;
}
.add-cart:hover { filter: brightness(1.08); }
.add-cart:disabled { opacity: .6; cursor: default; }
.add-done {
  margin-top: 12px; padding: 9px 10px; border-radius: 7px; font-size: 12.5px;
  background: color-mix(in srgb, var(--ok) 14%, transparent);
  color: var(--ok); text-align: center;
}
.add-done .link-btn { color: var(--ok); text-decoration: underline; font-size: 12.5px; }

/* Icon and word together: the icon is what the eye finds, the word is what removes any
   doubt. An inline SVG rather than an emoji — the objection to the trolley glyph was that it
   draws differently on every platform, which a path we ship does not. */
.cart-btn { display: inline-flex; align-items: center; gap: 6px; }
.cart-ico { flex-shrink: 0; }
/* It only stands out once it holds something — an empty cart has nothing to draw the eye to. */
.cart-btn.has { color: var(--ok); font-weight: 600; }
#cart-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 0; inset: 0; margin: auto;
  background: var(--card); color: var(--text); max-width: 440px; width: calc(100% - 32px);
}
#cart-dialog::backdrop { background: rgba(0, 0, 0, .55); }
.cart-dlg { display: flex; flex-direction: column; gap: 12px; padding: 20px; }
#cart-lines { display: flex; flex-direction: column; gap: 6px;
              max-height: 46vh; overflow-y: auto; }
.cart-empty { margin: 6px 0 2px; font-size: 12.5px; line-height: 1.5; }
.cart-line {
  display: grid; grid-template-columns: 1fr auto auto; gap: 4px 10px;
  align-items: center; padding: 10px;
  border: 1px solid var(--line); border-radius: 8px; background: var(--bg);
}
.cl-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; grid-row: 1; }
.cl-file { font-size: 13px; font-weight: 600;
           overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cl-meta, .cl-proj { color: var(--dim); font-size: 11px; }

/* Quantity and price sit on the second row, under the name — a part called something long
   should not squeeze the controls, and the controls should not truncate the name. */
.cl-qty { grid-row: 2; grid-column: 1; display: flex; align-items: center; gap: 0; }
.cl-step {
  width: 26px; height: 26px; padding: 0; cursor: pointer; font-size: 14px;
  border: 1px solid var(--line); background: var(--card); color: var(--text);
}
.cl-step:first-of-type { border-radius: 6px 0 0 6px; }
.cl-step:last-of-type { border-radius: 0 6px 6px 0; }
.cl-step:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.cl-step:disabled { opacity: .4; cursor: default; }
.cl-num {
  width: 46px; height: 26px; text-align: center; font: inherit; font-size: 12.5px;
  border: 1px solid var(--line); border-left: 0; border-right: 0;
  background: var(--card); color: var(--text); -moz-appearance: textfield;
}
.cl-num::-webkit-outer-spin-button, .cl-num::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0;
}
.cl-money { grid-row: 2; grid-column: 2; display: flex; flex-direction: column;
            align-items: flex-end; }
.cl-price { font-size: 13px; font-weight: 700; white-space: nowrap; }
.cl-each { color: var(--dim); font-size: 10.5px; white-space: nowrap; }
.cl-remove {
  grid-row: 2; grid-column: 3; align-self: center;
  background: none; border: none; padding: 4px 2px; cursor: pointer;
  color: var(--dim); font: inherit; font-size: 11.5px; text-decoration: underline;
}
.cl-remove:hover { color: var(--warn); }
.cart-foot { border-top: 1px solid var(--line); padding-top: 12px; }
.cart-total { display: flex; align-items: baseline; justify-content: space-between; }
.cart-total strong { font-size: 18px; }
.cart-fine { margin: 8px 0 0; color: var(--dim); font-size: 11px; line-height: 1.5; }
#cart-checkout {
  background: var(--ok); color: #fff; border: none; border-radius: 7px;
  padding: 9px 14px; font-size: 13px; font-weight: 700; cursor: pointer;
}
#order-dialog, #placed-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 0; inset: 0; margin: auto;
  background: var(--card); color: var(--text); max-width: 400px; width: calc(100% - 32px);
}
#order-dialog::backdrop, #placed-dialog::backdrop { background: rgba(0, 0, 0, .55); }
#order-form { display: flex; flex-direction: column; gap: 10px; padding: 20px; }
#order-form h2 { margin: 0; font-size: 17px; }
.order-as { margin: 0; color: var(--dim); font-size: 12px; }
.order-as b { color: var(--text); font-weight: 600; }
#order-form label { display: flex; flex-direction: column; gap: 4px; font-size: 12px;
                    color: var(--dim); }
#order-form input, #order-form textarea {
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 7px 9px; font-size: 13.5px; font-family: inherit;
  resize: vertical;
}
#order-form input:focus, #order-form textarea:focus { outline: none; border-color: var(--accent); }
/* Amber, not hidden. They were told the estimate is uncalibrated on the quote; suppressing
   it at the moment of commitment would be the dishonest half of being honest. */
.order-warn {
  margin: 0; font-size: 11.5px; line-height: 1.5; color: var(--warn);
  border: 1px solid var(--warn); border-radius: 7px; padding: 8px 10px;
}
.order-ref { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: .5px; }
.order-link {
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 7px 9px; font-size: 12px; font-family: inherit; width: 100%;
}
#placed-dialog .dlg { display: flex; flex-direction: column; gap: 12px; padding: 20px; }
#placed-dialog h2 { margin: 0; font-size: 17px; }

/* --- /reset ------------------------------------------------------------- */
#reset-page { padding: 48px 16px; display: flex; justify-content: center; }
.reset-card {
  display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 380px;
  background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 22px;
}
.reset-card h2 { margin: 0; font-size: 17px; }
.reset-card label { display: flex; flex-direction: column; gap: 4px; font-size: 12px;
                    color: var(--dim); }
.reset-card input {
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 7px 9px; font-size: 13.5px; font-family: inherit;
}

/* --- the order page ----------------------------------------------------- */
/* --- account pages: /projects and /orders -------------------------------- */
#account-page { padding: 26px 16px 60px; display: flex; justify-content: center; }
.acct { width: 100%; max-width: 780px; display: flex; flex-direction: column; gap: 14px; }
.acct-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.acct-head h1 { margin: 0; font-size: 22px; }

.acct-card {
  background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 16px;
  display: flex; flex-direction: column; gap: 10px;
}
.acct-card h2 { margin: 0; font-size: 16px; overflow: hidden; text-overflow: ellipsis; }
.acct-top { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; }
.acct-meta { color: var(--dim); font-size: 12px; white-space: nowrap; }
/* Their OWN row. In the dialog these sat on top of the part count and the date, because one
   line cannot hold a name, two figures and two actions. */
.acct-acts { display: flex; gap: 14px; align-items: center; }
.acct-acts .link-btn { font-size: 12.5px; }
.acct-open {
  background: var(--accent); color: var(--btn-text); border: 0; border-radius: 7px;
  padding: 6px 12px; font-size: 12.5px; font-weight: 600;
}

/* The strip of shapes. A name like Casting_Crank_box_B_support_adding_low tells you nothing
   a picture of the bracket does not tell you faster. */
/* The strip reserves its height BEFORE it has anything in it, and that is not cosmetic.
   AN EMPTY FLEX BOX IS ZERO PIXELS TALL, and IntersectionObserver never reports a
   zero-area element as intersecting — so a strip that waits to be scrolled into view was
   never visible enough to be scrolled into view, and its own loader never ran. It also
   stops the card jumping when the shapes land, which is worth having anyway. */
.acct-shots { display: flex; gap: 8px; flex-wrap: wrap; min-height: 72px; }
/* Nothing to draw after all — a project with no parts. Give the space back rather than
   leave a reserved hole. */
.acct-shots.empty-shots { min-height: 0; }
.acct-shot {
  width: 96px; height: 72px; border-radius: 8px; background: var(--bg);
  border: 1px solid var(--line); overflow: hidden; position: relative; flex: 0 0 auto;
}
/* `mountPart` calls `setSize(w, h, false)` — the `false` skips the CSS size on purpose, so
   the canvas would render at its DRAWING BUFFER size (188x140 at DPR 2) inside a 96x72 box
   and be cropped to a corner. The stylesheet owns the display size; the renderer owns the
   buffer. Same as `.pw-view canvas` on the workshop console. */
.acct-shot canvas { display: block; width: 100% !important; height: 100% !important; }
.acct-more { color: var(--dim); font-size: 12px; align-self: center; }

#order-page { padding: 28px 16px; display: flex; justify-content: center; }
.order-card {
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  padding: 22px; max-width: 620px; width: 100%;
  display: flex; flex-direction: column; gap: 16px;
}
.order-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.order-status {
  font-size: 12px; font-weight: 600; text-transform: capitalize;
  color: var(--ok); border: 1px solid var(--ok); border-radius: 999px; padding: 3px 10px;
}
.order-status.bad { color: var(--warn); border-color: var(--warn); }
/* A row of steps rather than a percentage: "in production" is a fact somebody reported,
   where a percentage would be a number we invented. */
.order-steps {
  display: flex; flex-wrap: wrap; gap: 6px 14px; list-style: none; padding: 0; margin: 0;
  font-size: 11.5px; color: var(--dim);
}
.order-steps li.past { color: var(--text); }
.order-steps li.now { color: var(--accent); font-weight: 700; }
.order-items { width: 100%; border-collapse: collapse; font-size: 13px; }
.order-items td { padding: 6px 0; border-bottom: 1px solid var(--line); }
.order-items td.num { text-align: right; font-variant-numeric: tabular-nums; }
.order-totals { display: flex; gap: 28px; font-size: 13px; flex-wrap: wrap; }

/* --- the 2D cut profile ------------------------------------------------- */
.prof-btn {
  width: 100%; margin: -4px 0 12px; padding: 8px 10px;
  background: var(--bg); color: var(--accent); border: 1px solid var(--accent);
  border-radius: 7px; font-size: 12.5px; font-weight: 600; cursor: pointer;
}
.prof-btn:hover { background: rgba(38, 160, 240, .12); filter: none; }

#profile-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 0; inset: 0; margin: auto;
  background: var(--card); color: var(--text);
  max-width: min(760px, calc(100vw - 32px)); width: 100%;
}
#profile-dialog::backdrop { background: rgba(0, 0, 0, .6); }
#profile-dialog .dlg { display: flex; flex-direction: column; gap: 12px; padding: 20px; }
#profile-dialog h2 { margin: 0; font-size: 17px; }
.prof-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
/* White regardless of theme: this is a drawing, and a drawing on a dark ground is not what
   anyone will compare against their CAD. */
.prof-canvas {
  background: #fff; border: 1px solid var(--line); border-radius: 8px;
  padding: 12px; height: min(58vh, 460px);
}
.prof-canvas svg { width: 100%; height: 100%; display: block; }
/* The same split as the DXF layers: boundary against cut-outs. */
.prof-outer { stroke: #0a3d62; fill: none; }
.prof-inner { stroke: #c2410c; fill: none; }
.prof-dl {
  background: var(--accent); color: #fff; border-radius: 7px; padding: 7px 14px;
  font-size: 13px; font-weight: 600; text-decoration: none;
}

/* --- the workshop console ------------------------------------------------ */
/* Everything below uses the shared theme variables, so light mode is the same page with a
   different palette rather than a second stylesheet to keep in step. */
/* See the note in partner.html. `overflow: auto` alone is not enough: `html, body` are
   height:100% for the quoting app's fixed shell, so the body becomes its OWN scroll
   container capped at the viewport — the wheel works but the window never scrolls, and
   anything past the fold is unreachable. Releasing the height puts the scroll back on the
   document, which is what an ordinary page should do. */
body.workshop {
  height: auto;
  min-height: 100%;
  overflow: visible;
}

.pw-nav {
  display: flex; gap: 4px; padding: 0 16px; max-width: 860px; margin: 0 auto; width: 100%;
  border-bottom: 1px solid var(--line);
}
.pw-nav a {
  padding: 10px 14px; font-size: 13px; font-weight: 600; color: var(--dim);
  text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.pw-nav a:hover { color: var(--text); }
.pw-nav a.on { color: var(--accent); border-bottom-color: var(--accent); }

.pw-main { display: flex; flex-direction: column; gap: 14px; padding: 18px 16px 40px;
           max-width: 860px; margin: 0 auto; width: 100%; }
.pw-page { display: flex; flex-direction: column; gap: 14px; }
.pw-card { background: var(--card); border: 1px solid var(--line); border-radius: 12px;
           padding: 18px; display: flex; flex-direction: column; gap: 12px; }
/* What the work is worth, above the lists: "how much am I owed" is asked before "what have
   I got". AFTER `.pw-card` on purpose — both are single-class selectors on the same element,
   so the later one wins, and written above it the card's `flex-direction: column` stacked
   three figures down the page. tests/test_css.py cannot catch this: the selectors differ,
   only the element they land on is shared. Order still matters. */
.pw-money { flex-direction: row; gap: 30px; flex-wrap: wrap;
            padding-top: 14px; padding-bottom: 14px; }
.pw-money div { display: flex; flex-direction: column; }
.pw-money b { font-size: 19px; line-height: 1.15; }
.pw-money span { color: var(--dim); font-size: 11px; text-transform: uppercase;
                 letter-spacing: .5px; }
/* Due within three days but not yet late. "Hurry" and "you have already missed it" are
   different messages to a shop floor. */
.pw-job-due.soon { color: var(--warn); }

.pw-card h2 { margin: 0; font-size: 16px; }
.pw-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.pw-back { color: var(--accent); font-size: 13px; text-decoration: none; font-weight: 600; }

.pw-signin { max-width: 380px; margin: 40px auto 0; }
.pw-signin label { display: flex; flex-direction: column; gap: 4px; font-size: 12px;
                   color: var(--dim); }
.pw-signin input, .pw-tg input, #pw-concern-text {
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  border-radius: 7px; padding: 8px 9px; font-size: 13.5px; font-family: inherit; width: 100%;
}

/* The toggle reads as a STATE first and a control second — what a workshop wants off this
   card is the answer to "am I open?", not a switch to hunt for. */
.pw-avail-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.pw-avail-state { font-size: 19px; font-weight: 700; }
.pw-avail-state.on { color: var(--ok); }
.pw-avail-state.off { color: var(--dim); }
.pw-toggle { border-radius: 999px; padding: 9px 22px; font-size: 13.5px; font-weight: 700;
             cursor: pointer; border: 1px solid; }
.pw-toggle.on { background: transparent; color: var(--dim); border-color: var(--line); }
.pw-toggle.off { background: var(--ok); color: #fff; border-color: var(--ok); }
.pw-avail-note { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--dim); }

.pw-jobs { display: flex; flex-direction: column; }
.pw-job {
  display: grid; grid-template-columns: 5.5rem 1fr auto 6.5rem 5rem; gap: 12px;
  align-items: baseline; text-decoration: none; color: var(--text); font-size: 13px;
  border-bottom: 1px solid var(--line); padding: 11px 8px;
}
.pw-job:last-child { border-bottom: none; }
.pw-job:hover { background: var(--bg); }

/* A job nobody has opened. The card carries the accent so the SECTION is what you notice
   from across a workshop, and the rows inside stay quiet enough to read. */
.pw-new-card { border-color: var(--accent); }
.pw-new-card h2 { color: var(--accent); }
.pw-job.fresh { padding-left: 12px; border-left: 3px solid var(--accent); }
.pw-job.fresh .pw-job-ref { color: var(--accent); }
/* Late still wins over new: a job can be both, and only one of them is urgent. */
.pw-job.fresh.late .pw-job-ref { color: var(--err); }
.pw-job-ref { font-weight: 700; }
.pw-job-parts, .pw-job-status { color: var(--dim); font-size: 12px;
                                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pw-job-due { font-size: 12px; color: var(--dim); }
/* Late is the one thing that has to be visible without reading. */
.pw-job-due.late, .pw-job.late .pw-job-ref { color: var(--err); font-weight: 700; }
.pw-job-price { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
.pw-job-title { margin: 0; font-size: 22px; }
.pw-job-price.big { font-size: 20px; white-space: nowrap; }
.pw-advance { align-self: flex-start; }
.pw-note { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--warn);
           border: 1px solid var(--warn); border-radius: 7px; padding: 9px 11px; }
.pw-job-foot { display: flex; justify-content: center; padding: 4px 0 8px; }
.pw-concern { color: var(--dim); font-weight: 500; }
.pw-concern:hover { color: var(--warn); }

.pw-item-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.pw-item-spec { font-size: 12.5px; color: var(--dim); }
.pw-dl { color: var(--accent); font-size: 12.5px; font-weight: 600; text-decoration: none;
         white-space: nowrap; }
/* Greyed rather than removed: the shop can see WHICH format is waiting for them, which is
   part of knowing what the job involves before they commit to starting it. */
.pw-dl-locked { color: var(--dim); font-size: 12.5px; font-weight: 600; white-space: nowrap;
                opacity: .6; }

/* The pack is the primary action once a job is under way — one download for the whole job
   beats three clicks and three files with names that say nothing. */
.pw-pack {
  display: inline-block; align-self: flex-start;
  background: var(--ok); color: #fff; border-radius: 7px; padding: 9px 15px;
  font-size: 13px; font-weight: 700; text-decoration: none;
}
.pw-locked { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--dim); }
/* The DFM findings matter more here than anywhere else in the product: they are what stops
   a shop starting a part that cannot be made as drawn. */
.pw-dfm-wrap { border-top: 1px solid var(--line); padding-top: 10px; }
.pw-dfm-title { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
                color: var(--dim); margin-bottom: 5px; }
.pw-dfm { margin: 0; padding-left: 18px; font-size: 12.5px; line-height: 1.6; color: var(--dim); }
.pw-dfm li.error { color: var(--err); }
.pw-dfm li.warning { color: var(--warn); }

/* The connect flow. Nothing to type, so nothing that looks like a form. */
#pw-tg p { margin: 0 0 8px; }
#pw-tg code {
  background: var(--bg); border: 1px solid var(--line); border-radius: 5px;
  padding: 2px 7px; font-size: 13px; user-select: all;
}
.pw-tg-on { color: var(--ok); font-size: 13.5px; }
.pw-tg-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.pw-tg-link {
  display: inline-block; background: var(--accent); color: #fff; text-decoration: none;
  border-radius: 7px; padding: 9px 15px; font-size: 13px; font-weight: 600;
}
.pw-tg-steps { margin: 0; padding-left: 20px; line-height: 1.9; }
.pw-tg-steps a { color: var(--accent); }

/* The part itself. Sized so a machinist can tell what it is at a glance and spin it if they
   want a closer look — the STEP download is still there for anything more than that. */
.pw-view {
  /* Enough to recognise a part at a glance, not so much that a four-part job becomes a
     scrolling exercise. They can spin it, and the STEP download is there for real study. */
  height: 190px; border-radius: 8px; background: var(--bg);
  border: 1px solid var(--line); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  touch-action: none;                 /* orbit on a tablet on the shop floor */
}
.pw-view canvas { display: block; width: 100%; height: 100%; }
.pw-machines { display: flex; flex-direction: column; gap: 6px; }
.pw-machine { display: flex; justify-content: space-between; align-items: baseline;
              font-size: 13px; border-bottom: 1px solid var(--line); padding-bottom: 6px; }
.pw-materials { padding-top: 4px; }

#pw-concern-dialog {
  border: 1px solid var(--line); border-radius: 12px; padding: 0; inset: 0; margin: auto;
  background: var(--card); color: var(--text);
  max-width: min(460px, calc(100vw - 32px)); width: 100%;
}
#pw-concern-dialog::backdrop { background: rgba(0,0,0,.6); }
#pw-concern-dialog .dlg { display: flex; flex-direction: column; gap: 12px; padding: 20px; }
#pw-concern-dialog label { display: flex; flex-direction: column; gap: 4px; font-size: 12px;
                           color: var(--dim); }

@media (max-width: 620px) {
  .pw-job { grid-template-columns: 1fr auto; row-gap: 2px; }
  .pw-job-parts { grid-column: 1 / -1; }
  .pw-job-status { grid-column: 1; }
}

/* --- ops: orders --------------------------------------------------------- */
.ord-table { width: 100%; border-collapse: collapse; font-size: 13px; margin: 8px 0 18px; }
/* 7px, not 8: a queue is SCANNED, and the two-line money cell added height back. Changed
   here rather than by adding a second `.ord-table td` further up — which is exactly what
   this file's naming note and tests/test_css.py exist to stop. */
.ord-table td { padding: 7px 6px; border-bottom: 1px solid var(--line); vertical-align: top; }
.ord-table td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ord-table tr[data-order] { cursor: pointer; }
.ord-table tr[data-order]:hover td { background: var(--bg); }
/* The queue leads with what wants a human. A list that shows everything equally is a list
   nobody works. */
.ord-table tr.todo .ord-ref { color: var(--accent); }
.ord-table tr.no td { opacity: .6; }
.ord-ref { font-weight: 700; }
.ord-status {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  border-radius: 999px; padding: 3px 9px; white-space: nowrap;
  border: 1px solid var(--line); color: var(--dim);
}
.ord-status.placed, .ord-status.needs_review { color: var(--warn); border-color: var(--warn); }
.ord-status.confirmed, .ord-status.assigned { color: var(--accent); border-color: var(--accent); }
.ord-status.shipped, .ord-status.delivered { color: var(--ok); border-color: var(--ok); }
.ord-status.cancelled { text-decoration: line-through; }

/* No rule above it any more: the detail used to be APPENDED under the queue and needed a
   divider. It replaces the queue now, so a line across the top of the page is a line under
   nothing. */
.ord-detail { padding-top: 2px; }
.ord-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.ord-head h2 { margin: 0 0 3px; font-size: 20px; }
.ord-note { margin: 10px 0; font-size: 12.5px; line-height: 1.55; color: var(--warn);
            border: 1px solid var(--warn); border-radius: 7px; padding: 9px 11px; }
/* The whole margin in one row — the customer's price, ours, and the difference. This is the
   view that must never exist on the partner side. */
.ord-money { display: flex; gap: 26px; flex-wrap: wrap; font-size: 13.5px;
             background: var(--bg); border-radius: 8px; padding: 11px 14px; margin: 4px 0 16px; }
.ord-spread { color: var(--ok); }
.ord-act { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 18px; }
.ord-act label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--dim); }
.ord-act input { width: 110px; background: var(--bg); color: var(--text);
                 border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px;
                 font-family: inherit; font-size: 13px; }
.ord-btn { background: var(--accent); color: #fff; border: none; border-radius: 7px;
           padding: 8px 14px; font-size: 12.5px; font-weight: 600; cursor: pointer; }
.ord-btn.small { padding: 5px 10px; font-size: 11.5px; }
.ord-here { font-size: 10.5px; font-weight: 700; color: var(--ok); text-transform: uppercase; }
