/* Fabulous — the whole look of the product.
 *
 * TWO RULES DECIDE EVERYTHING HERE.
 *
 * 1. THE VIEWER IS THE HERO. The type sits directly on the geometry, at the
 *    edges, and gets out of the way the moment there is a model to look at.
 *
 * 2. THIS IS THE FABER SYSTEM, NOT A THIRD ONE. The palette, the type stack
 *    and the motion curves below are `crazyshit/src/crazyshit/ui/static/app.css`
 *    and `fab/apps/desktop/ui/static/styles.css`, token for token. Fabulous
 *    shipped its own — warm graphite, a brass accent, Instrument Serif at
 *    13vw — and it read as a different company's product beside the other two.
 *
 *    Both of the things it invented had already been decided against upstream.
 *    Fab's own `fonts.css` says it in one line: "Bundled DM Sans / Instrument
 *    Serif / JetBrains faces are no longer loaded." And its `styles.css` says
 *    the accent is "ink-with-alpha by default (CrazyShit)". The one warm tone
 *    in the whole system is CREAM (`248 240 222`), and it is spent on the
 *    thing you press and on nothing else.
 *
 *    That is also why the loader is the kit's `void` variant unrecoloured —
 *    its own notes define `void` as "l'app depuis la PR #33 : fond #0b0c0e,
 *    encre #e8eaee, etincelle creme #f8f0de".
 */

:root {
  /* SPACE-separated, and that is not cosmetic: `rgb(var(--ink-rgb) / 0.22)` is
     the modern syntax and it requires space-separated channels. With commas
     the whole declaration is REJECTED — measured in crazyshit, where a border
     asked for at 22% had silently been rendering fully opaque. */
  --ink-rgb:     232 234 238;
  --cream-rgb:   248 240 222;   /* the only warm tone in the system */

  --void-0:      #0b0c0e;
  --void-1:      #15171b;

  --ink:         #e8eaee;
  --ink-soft:    #b2b7bd;
  --ink-mute:    #737981;

  --line:        rgb(var(--ink-rgb) / 0.10);

  /* The accent is ink-with-alpha; cream is what a DECISION looks like. */
  --accent-deep: rgb(var(--cream-rgb) / 0.92);
  --accent-soft: rgb(var(--cream-rgb) / 0.38);

  --radius:      6px;

  /* System faces only. The serif is the product's own VOICE — the sentences it
     says about itself — and never a label; mono is every label; sans is
     everything else. Nothing is vendored, so nothing is downloaded. */
  --sans:  ui-sans-serif, -apple-system, "Segoe UI Variable Text", "Segoe UI",
           system-ui, sans-serif;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  --mono:  ui-monospace, "Cascadia Mono", "Cascadia Code", Consolas,
           "DejaVu Sans Mono", monospace;

  --wake: 180ms cubic-bezier(0.2, 0.7, 0.3, 1);
  --dim:  280ms cubic-bezier(0.2, 0.7, 0.3, 1);
  --enter: 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--void-0);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* The viewer draws its own colours; nothing here may be re-tinted by the
     browser's own dark-mode heuristics. */
  color-scheme: dark;
}

::selection { background: rgb(var(--cream-rgb) / 0.28); color: var(--ink); }

/* ------------------------------------------------------------------- the stage */

#stage {
  position: fixed;
  inset: 0;
  /* ATMOSPHERE, NOT DECORATION — and specifically ONE LIGHT, from above and
     slightly forward, which is the whole reason this reads as a room rather
     than a swatch. The cool pool is fab's own field, verbatim; the second,
     warmer and tighter, is the lamp over the drawing board. A flat black field
     makes a dark grey part look like a hole. Under the canvas the viewer
     paints, so it only shows where there is no model. */
  background:
    radial-gradient(52% 44% at 50% 34%, rgb(var(--cream-rgb) / 0.026) 0%,
                    transparent 70%),
    radial-gradient(120% 90% at 52% 26%, var(--void-1) 0%, transparent 62%),
    var(--void-0);
}

/* A DRAWING OFFICE RATHER THAN A VOID. The empty stage is where somebody
   decides whether this is a serious tool, and a flat black field with three
   lines of type on it is a splash screen. This is the paper the drawings this
   thing eats are drawn on — a 44 px minor grid with a major line every fifth,
   faded out radially so it is a ground and never a texture.

   It goes at the first build. The geometry is the interface and a grid behind
   a solid is a rectangle taken away from it; it is also under the canvas, so
   it could only ever show where there is nothing anyway. */
#stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgb(var(--ink-rgb) / 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgb(var(--ink-rgb) / 0.05) 1px, transparent 1px),
    linear-gradient(rgb(var(--ink-rgb) / 0.03) 1px, transparent 1px),
    linear-gradient(90deg, rgb(var(--ink-rgb) / 0.03) 1px, transparent 1px);
  background-size: 220px 220px, 220px 220px, 44px 44px, 44px 44px;
  -webkit-mask-image: radial-gradient(58% 52% at 50% 34%, #000 0%, transparent 72%);
  mask-image: radial-gradient(58% 52% at 50% 34%, #000 0%, transparent 72%);
  opacity: 0;
  transition: opacity 900ms ease;
  pointer-events: none;
}

body.empty #stage::before { opacity: 1; }

/* The viewer's own canvas fills the stage; its wrapper insists on a width. */
#stage .tcv_cad_view,
#stage canvas { max-width: none !important; }

/* THE VIEWER PAINTS ITS OWN BACKGROUND, and under `theme: "dark"` that is a
   flat #444 — which is not this product's black, and which covers the field
   above with a grey rectangle the exact size of the canvas. Its renderer is
   built with `alpha: true`, so handing it a transparent colour is enough: the
   WebGL clear goes through to the stage underneath. Declared after the vendor
   sheet, on its own selector, so a version bump keeps overriding cleanly. */
[data-theme="dark"] { --tcv-bg-color: transparent; }
.tcv_cad_viewer, .tcv_cad_view { background: transparent !important; }

/* --------------------------------------------------------------- the veil
 *
 * The first viewport when there is nothing built, and the drag frame at every
 * other moment.
 *
 * THIS USED TO BE THREE LINES OF TYPE AND NOTHING ELSE, and the whole window
 * was the only drop target. On mesh2cad that reads fine — the mesh appears the
 * instant it is opened, so the drop confirms itself. On draw2cad it does not:
 * a drawing goes to `/api/file`, the stage has nothing to put on it, and the
 * screen for the minutes before the first build is bit-for-bit the screen from
 * before the drop. There was no zone, no upload confirmation, and no moment
 * the user decided anything.
 *
 * WHAT MAKES IT A COMPOSITION RATHER THAN A COLUMN, since two attempts at this
 * were "a logo, a title, a subtitle and a dashed box" and both looked like a
 * template:
 *
 *   - DEPTH. The mark is not an item in the stack, it is a watermark BEHIND
 *     the stack at 4% — so the screen has a foreground and a background.
 *   - ONE LIGHT. A single warm pool from above-centre, on the stage, on the
 *     grid's mask, on the top edge of the card and along the top of the
 *     letterforms. Everything is lit by the same lamp, which is what makes a
 *     dark interface look built instead of unfinished.
 *   - GRAIN. 2.8% fractal noise over the veil. It is the single cheapest thing
 *     that separates an expensive-looking dark surface from a flat one, and it
 *     is on the PAPER only — the geometry stays clean, which is also the
 *     honest split.
 *   - A REAL CARD instead of a dashed rectangle. A dashed border is the
 *     universal mark of an unstyled file input. This one has a hairline, a
 *     specular top edge, an elevation shadow and a footer caption below it.
 *   - TYPE THAT COMMITS. 68 px, weight 600, -0.045em, in a top-lit silver —
 *     against a 16 px humanist italic. Two faces doing two jobs, with a real
 *     jump between them, rather than one face at four sizes.
 */

#veil {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  z-index: 5;
  transition: opacity 700ms ease;
}

/* GRAIN. Generated in the URL — no asset, no request, and it tiles. Kept low
   enough to be felt and not seen: at 0.05 it is visibly dirty, at 0.015 it
   does nothing. It rides the veil, so it is over the paper and never over
   somebody's part. */
#veil::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.028;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

#veil-inner {
  position: relative;
  z-index: 2;
  text-align: center;
  padding: 0 6vw;
  animation: fade-up 600ms cubic-bezier(.2, .8, .2, 1);
  transition: opacity 500ms ease, transform 700ms cubic-bezier(.2, .8, .2, 1);
}

@keyframes fade-up { from { opacity: 0; transform: translateY(6px); } }

/* With a model on screen the composition is spent; while the agent is working
   the compass takes its place. `pointer-events` goes with the opacity or the
   zone stays clickable through an invisible layer — a click that reopens a
   file picker over a model somebody is orbiting. */
body:not(.empty) #veil-inner,
body.working #veil-inner {
  opacity: 0;
  transform: translateY(-14px);
  pointer-events: none;
}

/* AND THE CHILD THAT OPTED BACK IN HAS TO OPT BACK OUT. `pointer-events: none`
   on a parent is not inherited the way opacity is: a descendant declaring
   `auto` — which `#drop` does, because it has to be clickable while the stage
   IS empty — re-enables hit-testing on itself and goes on being clicked
   through the invisible layer above. The rule above says `pointer-events` goes
   with the opacity, and it did, on the wrong element.
   Reported from production 2026-08-19: with a mesh on screen, double-clicking
   anywhere near the middle of the viewer opened a file browser. The card is
   `min(452px, 86vw)` centred, so "near the middle" is most of where somebody
   orbits. */
body:not(.empty) #drop,
body.working #drop {
  pointer-events: none;
}

/* THE NAME, LIT FROM ABOVE. The gradient is not a colour — it is the same lamp
   that lights the stage and the card, landing on the cap line and falling off
   to the baseline. The previous two versions were a brass gradient and then a
   40 px book serif; this is the sans the whole system is set in, pushed to a
   weight and a tracking that make it a mark rather than a heading. */
#mark {
  margin: 0;
  font-family: var(--sans);
  font-size: clamp(38px, 4.2vw, 52px);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.032em;
  color: rgb(var(--ink-rgb) / 0.94);
}

#line {
  margin: 13px auto 0;
  max-width: 40ch;
  font-family: var(--serif);
  font-style: italic;
  font-size: 15px;
  line-height: 1.45;
  color: rgb(var(--ink-rgb) / 0.44);
}

/* The one warm word on the screen, and it is the word the product is about. */
#line .accent { color: rgb(var(--ink-rgb) / 0.72); font-style: normal; }

/* ------------------------------------------------------------------ the card
 *
 * One rectangle with two faces — the invitation, and then what landed in it —
 * so confirming an upload does not move the page under the cursor that is
 * about to press the button.
 *
 * NOT DASHED. A dashed border is what an unstyled `<input type=file>` looks
 * like, and this is the object the whole screen is built around. It is a
 * surface: a hairline, a 1 px specular highlight along the top edge (the lamp
 * again), a long soft shadow underneath, and a gradient that is fractionally
 * brighter at the top than at the bottom.
 */
#drop {
  position: relative;
  width: min(452px, 86vw);
  margin: 38px auto 0;
  padding: 23px 24px;
  border: 1px solid rgb(var(--ink-rgb) / 0.09);
  border-radius: 10px;
  background: rgb(var(--ink-rgb) / 0.035);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.045),
    0 14px 34px -24px rgb(0 0 0 / 0.85);
  backdrop-filter: blur(8px);
  /* The uploading hairline below sweeps +/-42% of this width; without a clip
     it runs out past both edges of the rectangle it belongs to. */
  overflow: hidden;
  pointer-events: auto;
  cursor: pointer;
  transition: border-color var(--wake), background var(--wake),
              box-shadow var(--wake), transform var(--dim);
}

#drop:hover,
#drop:focus-visible {
  border-color: rgb(var(--ink-rgb) / 0.16);
  background: rgb(var(--ink-rgb) / 0.055);
  outline: none;
}

/* Under a dragged file the card is the target, and says so loudly enough to be
   read out of the corner of an eye that is on the file, not on the page: the
   hairline goes cream and the lamp above it brightens. */
body.dragging #drop {
  border-color: rgb(var(--cream-rgb) / 0.62);
  background: rgb(var(--cream-rgb) / 0.055);
  box-shadow:
    inset 0 1px 0 rgb(var(--cream-rgb) / 0.22),
    0 14px 34px -24px rgb(0 0 0 / 0.85);
}

body.dragging #drop-verb { color: rgb(var(--cream-rgb) / 0.9); }

/* Holding a file: no longer an invitation, so it stops behaving like one. */
body.staged #drop { cursor: default; }

/* Nobody is signed in, so a drop would be refused before it left the browser.
   Saying that with the card costs no words. */
body.need-auth #drop { opacity: 0.3; pointer-events: none; }

#drop-face {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 11px;
}

#drop-glyph {
  color: rgb(var(--ink-rgb) / 0.42);
  transition: color var(--wake), transform var(--dim);
}

#drop:hover #drop-glyph { color: rgb(var(--ink-rgb) / 0.62); }
body.dragging #drop-glyph {
  color: rgb(var(--cream-rgb) / 0.9);
}

#drop-verb {
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--ink-soft);
}

#drop-verb .or {
  display: block;
  margin-top: 7px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* The caption under the card, not a fourth block inside it. */
#hint {
  margin: 22px 0 0;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgb(var(--ink-rgb) / 0.26);
  line-height: 1.8;
}

/* The bytes are in flight. A 40 MB drawing over a slow line is several seconds
   of a card that looks exactly like a card nothing was dropped on, and the
   receipt below cannot be drawn until a 200 comes back — so the wait gets its
   own hairline, running along the bottom edge of the rectangle the file was
   let go of on. No words: the file's name is about to be the words. */
#drop::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: linear-gradient(90deg, rgb(var(--cream-rgb) / 0) 0%,
              var(--accent-deep) 50%, rgb(var(--cream-rgb) / 0) 100%);
  opacity: 0;
  transition: opacity 160ms ease;
}

body.uploading #drop::after { opacity: 1; animation: uploading 1.1s ease-in-out infinite; }

@keyframes uploading {
  0%   { transform: translateX(-42%) scaleX(0.4); }
  50%  { transform: translateX(42%) scaleX(0.4); }
  100% { transform: translateX(-42%) scaleX(0.4); }
}

/* A SECOND FILE DROPPED ONTO A FINISHED MODEL HAS TO BE CONFIRMABLE TOO, and
   the receipt lives inside the composition that went away when the model
   arrived — so without this the drop would upload, stage, and show nothing at
   all, which is the exact failure this whole change exists to remove. The name
   and the line stay gone: those are a title page, not a control. */
body.staged #veil-inner {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

body.staged:not(.empty) #mark,
body.staged:not(.empty) #line,
body.staged:not(.empty) #hint { display: none; }

body.staged:not(.empty) #drop { margin-top: 0; }

/* ...and it needs a ground of its own, because a hairline card over a lit
   solid is a card nobody can read. */
body.staged:not(.empty) #veil {
  background: rgb(9 10 12 / 0.66);
  backdrop-filter: blur(4px);
}

/* ------------------------------------------------------- what actually landed
 *
 * THE ANSWER TO "DID MY UPLOAD WORK", and it is a real answer rather than a
 * hopeful one: the name is the one the SERVER wrote to disk (so a collision
 * shows up as `plate-2.pdf` here, where it is readable, instead of surprising
 * the agent), the byte count is what the server counted, and the thumbnail is
 * the file's own pixels. Nothing here is drawn until a 200 comes back.
 */
#staged[hidden] { display: none !important; }
#drop-face[hidden] { display: none !important; }

#staged { animation: staged-in var(--enter); }

/* FROM the invisible state, never TO it. A visible state that lives only in a
   keyframe is `opacity: 0` until the animation completes, and an animation
   outranks a plain declaration — which is how `#refused` spent a session
   invisible. This way a keyframe that never runs costs a fade, not the card. */
@keyframes staged-in {
  from { opacity: 0; transform: translateY(7px); }
}

#staged-row {
  display: flex;
  align-items: center;
  gap: 14px;
  text-align: left;
}

#staged-thumb {
  flex: none;
  width: 52px;
  height: 52px;
  border: 1px solid rgb(var(--ink-rgb) / 0.12);
  border-radius: 6px;
  background-color: rgb(0 0 0 / 0.35);
  background-size: cover;
  background-position: center;
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--ink-mute);
  overflow: hidden;
}

#staged-text { min-width: 0; flex: 1; }

#staged-name {
  display: block;
  font-size: 15.5px;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--ink);
  /* A long CAD filename must not push the tick off the card. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#staged-meta {
  display: block;
  margin-top: 5px;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

#staged-tick {
  flex: none;
  position: relative;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgb(var(--cream-rgb) / 0.42);
}

/* Drawn rather than typed: a check mark character is whatever the user's
   fallback font decides, and on a machine without one it is a box. */
#staged-tick::after {
  content: "";
  position: absolute;
  left: 7.5px;
  top: 5px;
  width: 5px;
  height: 10px;
  border: solid var(--accent-deep);
  border-width: 0 1.6px 1.6px 0;
  transform: rotate(45deg);
}

/* ----------------------------------------------------------- the confirmation
 *
 * The only filled control in the product, and the only thing on the page that
 * starts work. CREAM because cream is the one warm tone in this system and it
 * is spent on decisions — the same paint fab puts on its send button. The
 * gloss along its top edge and the light it throws underneath are the same
 * lamp as everything else on this screen.
 */
#go {
  display: block;
  margin: 22px auto 0;
  padding: 13px 30px;
  border: none;
  border-radius: 999px;
  background: rgb(var(--cream-rgb) / 0.94);
  color: #14161a;
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  cursor: pointer;
  pointer-events: auto;
  transition: background var(--wake), transform 120ms ease;
}

#go[hidden] { display: none !important; }

#go:hover { background: rgb(var(--ink-rgb) / 0.96); }
#go:active { transform: translateY(1px); }
#go:focus-visible { outline: none; box-shadow: 0 0 0 3px rgb(var(--cream-rgb) / 0.3); }

#auth {
  margin: 28px auto 0;
  max-width: 320px;
  pointer-events: auto;
  text-align: left;
}

#auth[hidden], #quota[hidden], #auth-err[hidden] { display: none !important; }

#google-btn { display: flex; justify-content: center; min-height: 40px; }

/* UNDER the button, not over it, and set as a sentence rather than as a
   control: pressing the button is the acceptance, so a second thing to press
   first would be a second decision where there is one. */
#auth-consent {
  margin: 14px 0 0;
  text-align: center;   /* `#auth` is left-aligned for the error line */
  font-family: var(--sans);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-mute);
}

#auth-consent a {
  color: rgb(var(--ink-rgb) / 0.62);
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color var(--wake);
}

#auth-consent a:hover { color: rgb(var(--cream-rgb) / 0.92); }

#auth-err {
  margin: 10px 0 0;
  color: var(--accent-deep);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.06em;
}

/* The window at large is still a drop target, and under a dragged file it says
   so with one inset hairline rather than a modal. Quieter than the card: the
   card is the loud one, and two things shouting is a flicker. */
#veil::after {
  content: "";
  position: absolute;
  inset: 16px;
  border: 1px solid rgb(var(--cream-rgb) / 0);
  border-radius: 12px;
  transition: border-color var(--wake), background var(--wake);
}

body.dragging #veil::after {
  border-color: rgb(var(--cream-rgb) / 0.13);
}

/* --------------------------------------------------------------- the compass
 *
 * WHAT THE AGENT WORKING LOOKS LIKE. The two-pixel hairline at the very top of
 * a full-bleed viewer was the whole answer before this, and on a draw2cad job
 * — where the stage stays empty until the first build lands, which is minutes
 * — the honest reading of that screen is that nothing is happening.
 *
 * It is the watermark from the veil, drawn by a compass and withdrawn again:
 * axis line, envelope arc, inner arcs, cote. The backdrop coming alive. Still
 * no agent prose — a shape and a clock, nothing said.
 */
#thinking {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 11;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 420ms ease,
              top 620ms cubic-bezier(.2, .8, .2, 1),
              transform 620ms cubic-bezier(.2, .8, .2, 1);
}

body.working #thinking { opacity: 1; }

#thinking-mark {
  width: 168px;
  height: 168px;
  transition: width 620ms cubic-bezier(.2, .8, .2, 1),
              height 620ms cubic-bezier(.2, .8, .2, 1);
}

#elapsed {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.26em;
  color: rgb(var(--ink-rgb) / 0.34);
  font-variant-numeric: tabular-nums;
}

/* The one line under the compass. Sized and coloured to be READ WITHOUT BEING
   LOOKED AT: a line that competes with the geometry is a chat panel by another
   name. It fades between lines rather than snapping, because a hard swap every
   few seconds reads as a flicker; and it is width-capped with an ellipsis so a
   sentence the server failed to cut still cannot reflow the layout. */
#thought {
  max-width: min(30ch, 72vw);
  font-size: 11.5px;
  letter-spacing: 0.01em;
  color: rgb(var(--ink-rgb) / 0.30);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  transition: opacity 260ms ease;
}

#thought.in { opacity: 1; }

/* THE WAY OUT — one corner, in both layouts, and the corner is the whole
   reason it is not inside `#thinking`. That cluster is centred with a
   `transform`, and a transformed ancestor becomes the containing block for a
   `position: fixed` child: `right: 26px` in there is 26 px from the CLUSTER's
   right edge, which put the button in the middle of the screen while the
   computed style read exactly what was asked for. Measured in a browser,
   x = 612 on a 1280 px page.

   Held still on purpose, too. The cluster's width is the sum of its children
   and one of them is a sentence replaced every few seconds, so a Stop inside
   it slides ~70 px sideways while somebody is reaching for it — the last
   thing the one control that ends a turn may do. It sits at the project
   pastille's height on the opposite side, under `#quota` (`top: 22px`).

   Quiet enough to be ignored for the whole of a turn that is going fine, and
   findable in the one second somebody wants it: no fill, no border until the
   pointer is on it, and the cream reserved for that moment. It is not styled
   as a danger — nothing is lost by pressing it, the model on screen is kept. */
#stop {
  position: fixed;
  top: 44px;
  right: 26px;
  z-index: 11;
  /* `visibility` and not `hidden`, so the fade still runs BOTH ways — and it
     is what takes the button out of the tab order and the accessibility tree
     between turns. `opacity: 0` alone leaves a focusable control on a page
     that has nothing to stop. The delay is on the way OUT only, so the
     element survives its own fade. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 420ms ease, visibility 0s linear 420ms,
              color var(--wake), border-color var(--wake),
              background var(--wake);
  padding: 5px 13px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: none;
  color: rgb(var(--ink-rgb) / 0.30);
  font-family: inherit;
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}

body.working #stop {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 420ms ease, visibility 0s, color var(--wake),
              border-color var(--wake), background var(--wake);
}

#stop:not(:disabled):hover {
  border-color: rgb(var(--cream-rgb) / 0.34);
  background: rgb(var(--ink-rgb) / 0.07);
  color: var(--accent-deep);
}

/* Pressed, and the turn is unwinding: the request is in, the thread stops at
   its next frame. Dead rather than gone — a control that vanishes on click
   reads as a misfire. */
#stop:disabled { opacity: 0.4; cursor: default; }

/* A stop can be refused: the turn ended in the moment between the button being
   there and the click landing. Same idiom as the layer switches — the border
   says no and the reason is in the console — but its own keyframes, because
   this button has no resting border to take AWAY. The visible state is still
   the plain declaration above; only the flash lives in here. */
#stop.refused { animation: refuse-stop 700ms ease; }

@keyframes refuse-stop {
  0%, 100% { border-color: transparent; }
  30%, 70% { border-color: rgb(var(--cream-rgb) / 0.34); }
}

/* ...and once there is geometry, it gets out of the way of it. Top centre is
   the only edge of this page not already spoken for — wordmark, quota, layers
   and the STEP hold the four corners. */
body.working:not(.empty) #thinking {
  top: 14px;
  transform: translate(-50%, 0);
  flex-direction: row;
  gap: 12px;
}

body.working:not(.empty) #thinking-mark { width: 46px; height: 46px; }
body.working:not(.empty) #thought { max-width: min(26ch, 40vw); font-size: 10.5px; }
body.working:not(.empty) #elapsed { font-size: 10px; letter-spacing: 0.18em; }

/* ------------------------------------------------------------- the live mark */

#mark-live {
  position: fixed;
  top: 20px;
  left: 26px;
  z-index: 8;
  font-family: var(--serif);
  font-size: 15px;
  letter-spacing: 0.01em;
  color: rgb(var(--ink-rgb) / 0.5);
  opacity: 0;
  transition: opacity 500ms ease;
  pointer-events: none;
}

body:not(.empty) #mark-live { opacity: 1; }

/* The quota and the way out share one row, and the row holds the corner. Two
   separately-positioned elements here would have had to agree about the
   quota's width — which is `3 / 3 RUNS` today and a different length the
   moment a limit is not one digit. */
#account {
  position: fixed;
  top: 22px;
  right: 26px;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 14px;
  /* The quota is a mark and takes no pointer; the button turns it back on
     for itself alone. */
  pointer-events: none;
}

#quota {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* Set as the quota is, one shade quieter, and with no border and no fill: it
   is not a danger — nothing is lost by pressing it — and it must be ignorable
   for the whole of a session and findable in the one second somebody wants it.
   Same argument as `#stop`, which sits directly under it. */
#signout {
  pointer-events: auto;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgb(var(--ink-rgb) / 0.34);
  cursor: pointer;
  transition: color var(--wake);
}

#signout:hover { color: var(--ink-soft); }
#signout[hidden] { display: none !important; }

/* ---------------------------------------------------------------- the refusal
 *
 * One sentence, low and centred, in the serif this system reserves for what
 * the product says about ITSELF — never the mono the quota and the format list
 * are set in. That is deliberate: mono here is the voice of the chrome
 * (`3 / 3 RUNS`, `PDF · PNG · JPG`), and this is the product speaking to a
 * person about the file they just dropped. Same rule crazyshit applies to its
 * own refusal line.
 *
 * It sits ABOVE the veil's z-index so it survives both states — a drop refused
 * on an empty page (veil up) and a drop refused over a model (veil gone) — and
 * it is `pointer-events: none` because there is nothing to click: the way out
 * of a refusal is to drop a different file, and the whole window is already
 * the drop zone. */
#refused {
  position: fixed;
  left: 50%;
  bottom: 13%;
  transform: translateX(-50%) translateY(6px);
  transition: opacity 420ms ease, transform 420ms ease;
  z-index: 12;
  max-width: min(560px, 78vw);
  text-align: center;
  font-family: var(--serif);
  font-style: italic;
  font-size: 17px;
  line-height: 1.45;
  color: rgb(var(--ink-rgb) / 0.86);
  opacity: 0;
  pointer-events: none;
}

#refused::before {
  content: "";
  display: block;
  width: 34px;
  height: 1px;
  margin: 0 auto 14px;
  background: var(--accent-deep);
  opacity: 0.6;
}

/* A TRANSITION AND NOT AN ANIMATION, and the difference is the difference
 * between a refusal being seen and not.
 *
 * This was `animation: refused-in 420ms ease forwards`, with the visible state
 * living only in the keyframes' `to`. That makes `opacity: 0` the element's
 * real value until an animation COMPLETES — so any reason it does not
 * complete leaves the product's one and only refusal channel invisible while
 * every other part of the product reports success. Worse, an animation
 * outranks a plain declaration in the cascade, so a second rule saying
 * `opacity: 0.92` does not rescue it: the frozen `from` keyframe still wins.
 *
 * Caught 2026-08-19 driving a real server. The element measured 560 x 70, in
 * the right place, with the right text and the right colour — and `opacity`
 * read 0 seven hundred milliseconds later. That particular instance was an
 * instrument artifact (a browser tab that is not composited freezes its
 * animation clock: `currentTime` stuck at 0 with `playState: "running"`), but
 * a reduced-motion setting, a user stylesheet or an extension reaches the same
 * place for real, and the failure is silent from every other angle.
 *
 * A transition's end state is an ORDINARY declaration on `.in`. If it runs,
 * it fades; if it never runs, the sentence is simply there. The replay trick
 * is unchanged — `app.js` removes `in`, forces a reflow and adds it back — so
 * a second refusal in a row still fades on text that barely changed. */
#refused.in { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Somebody who asked for less motion still gets the sentence, at once. */
@media (prefers-reduced-motion: reduce) {
  #refused { transition: none; transform: translateX(-50%); }
  #veil-inner, #staged { animation: none; }
}

/* --------------------------------------------------------------- the one control */

#step {
  position: fixed;
  right: 26px;
  bottom: 26px;
  z-index: 9;
  padding: 10px 20px;
  border: 1px solid var(--accent-soft);
  border-radius: 999px;
  background: rgb(11 12 14 / 0.6);
  backdrop-filter: blur(6px);
  color: var(--accent-deep);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  transition: background var(--wake), color var(--wake), border-color var(--wake);
}

#step:hover {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
  color: var(--void-0);
}

/* ------------------------------------------------- the two solids, and the gap
 *
 * The second and last control cluster on this page, and it earns its rectangle
 * the way `#step` does: in mesh2cad there are TWO solids in the frame, in the
 * same millimetres, and taking either away is the only way to read the other.
 * Same idiom as Download STEP and the opposite corner — no panel, no labels
 * above it, and nothing at all on a drawing job.
 */

#layers {
  position: fixed;
  left: 26px;
  bottom: 26px;
  z-index: 9;
  display: flex;
  gap: 6px;
}

#layers[hidden] { display: none !important; }

#layers button {
  padding: 8px 15px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgb(11 12 14 / 0.6);
  backdrop-filter: blur(6px);
  color: var(--ink-mute);
  font-family: inherit;
  font-size: 12.5px;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background var(--wake), color var(--wake), border-color var(--wake);
}

/* ON is the layer being ON SCREEN, which is the state a user reads off the
   geometry anyway — so the switch agrees with what they are looking at. */
#layers button[aria-pressed="true"] {
  border-color: var(--accent-soft);
  color: var(--accent-deep);
}

#layers button:disabled { opacity: 0.28; cursor: default; }

/* HOVER MUST NOT LOOK LIKE ON. Borrowing the accent here — which is what this
   rule did first — makes a switch that is OFF read as ON for as long as the
   pointer rests on it, and the pointer rests on the one just clicked. Caught
   on `dev/try_layers.py`'s own screenshots. So hover is a warmth behind it, and
   the cream stays the property of the state. */
#layers button:not(:disabled):hover { background: rgb(var(--ink-rgb) / 0.10); }

/* A comparison can be refused — mid-turn, or before anything has been built.
   Nothing on this page paints words, so the refusal is the border going out
   and coming back; the reason is in the console. */
#layers button.refused { animation: refuse 700ms ease; }

@keyframes refuse {
  0%, 100% { border-color: var(--line); }
  30%, 70% { border-color: rgb(var(--ink-rgb) / 0); }
}

/* ---------------------------------------------------------------- the heartbeat */

#pulse {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 10;
  opacity: 0;
  background: linear-gradient(90deg,
              rgb(var(--cream-rgb) / 0) 0%, var(--accent-deep) 50%,
              rgb(var(--cream-rgb) / 0) 100%);
  transition: opacity 300ms ease;
}

#pulse.on { opacity: 1; animation: sweep 2.4s ease-in-out infinite; }

@keyframes sweep {
  0%   { transform: translateX(-55%); }
  50%  { transform: translateX(55%); }
  100% { transform: translateX(-55%); }
}

/* The vendored viewer ships a tree pane, a toolbar and a set of light chrome
   defaults. This product shows none of them — `treeWidth: 0` and `tools: false`
   ask for that, and these hold it when a version bump forgets. */
.tcv_cad_tree, .tcv_cad_tools, .tcv_cad_toolbar, .tcv_cad_inset { display: none !important; }
.tcv_cad_body { border: none !important; }

/* ------------------------------------------------------ the deviation scale
 *
 * The only numbers this product puts on screen, and they earn it: on a cloud
 * the Diff switch colours the SCANNED POINTS by their distance to the model,
 * and a colour map whose colours mean nothing is a decoration. Sits above the
 * layer cluster, same corner, same type — it belongs to that switch.
 *
 * Green and red are NOT swapped for the metrology convention on purpose. In
 * this product green has meant "material to add" and red "material to remove"
 * since the first comparison, and a user who sees both pictures in one turn is
 * better served by one vocabulary than by two correct ones.
 */
#scale {
  position: fixed;
  left: 26px;
  bottom: 72px;
  z-index: 9;
  width: 208px;
  padding: 10px 12px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgb(11 12 14 / 0.6);
  backdrop-filter: blur(6px);
  pointer-events: none;
}

#scale[hidden] { display: none !important; }

#scale-bar {
  height: 6px;
  border-radius: 1px;
  background: linear-gradient(to right,
    #ef4444 0%, #9aa3ad 50%, #22c55e 100%);
}

#scale-marks {
  display: flex;
  justify-content: space-between;
  margin-top: 6px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}

#scale-mid { color: var(--accent-deep); }

/* ------------------------------------------------------------- the projects
 *
 * The third and last control cluster on this page, and it earns its corner the
 * way `#step` and `#layers` earn theirs: it is the only thing here that says
 * this process holds MORE than the model in front of you. Same idiom as both —
 * a pill, a corner, no panel, no labels — and it says the one word it has to,
 * which is the name of the project you are in.
 *
 * It sits under the wordmark rather than opposite it because the top right is
 * the quota's, and two pills in one corner read as a toolbar starting.
 */

#place {
  position: fixed;
  top: 44px;
  left: 26px;
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 7px;
  max-width: 240px;
  padding: 5px 12px 5px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgb(11 12 14 / 0.5);
  backdrop-filter: blur(6px);
  color: var(--ink-mute);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--wake), border-color var(--wake), background var(--wake);
}

#place[hidden] { display: none !important; }
#place:hover { color: var(--ink-soft); border-color: rgb(var(--ink-rgb) / 0.22); }
#place[aria-expanded="true"] { color: var(--accent-deep); border-color: var(--accent-soft); }

#place-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#place-dot {
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: 50%;
  background: var(--accent-soft);
}

/* The overlay. Full bleed and opaque enough to read type over geometry — the
   model is still there under it, and a picker you cannot read is worse than a
   list you have to leave the model for. */
#places {
  position: fixed;
  inset: 0;
  z-index: 14;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgb(11 12 14 / 0.82);
  backdrop-filter: blur(10px);
  animation: places-in var(--enter) both;
}

#places[hidden] { display: none !important; }

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

#places-inner {
  width: min(720px, 86vw);
  max-height: 76vh;
  overflow-y: auto;
}

#places-title {
  margin: 0 0 16px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

#places-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
  gap: 10px;
}

.place-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgb(var(--ink-rgb) / 0.03);
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
  transition: background var(--wake), border-color var(--wake), color var(--wake);
}

.place-card:hover { background: rgb(var(--ink-rgb) / 0.09); color: var(--ink); }

/* The one DECISION on this screen, so it is the one thing wearing the cream —
   the same rule that makes Build the CAD and Download STEP the only warm
   things on their own screens. */
.place-card.new { border-color: var(--accent-soft); color: var(--accent-deep); }
.place-card.new:hover { background: var(--accent-deep); color: var(--void-0); }

/* The project you are already in is not a destination. It stays in the list
   because taking it out would make the grid re-flow under the pointer every
   time you open it, and because "you are here" is half of what a picker is
   for. */
.place-card.here { cursor: default; border-color: var(--accent-soft); }
.place-card.here:hover { background: rgb(var(--ink-rgb) / 0.03); color: var(--ink-soft); }

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

.place-meta {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* A turn outlives the tab that started it and can be running in another
   project. Switching into one of those is switching into a model that is
   moving, and this is the only place that fact is visible. */
.place-card.busy .place-meta { color: var(--accent-deep); }

/* ------------------------------------------------------- what the user SAYS
 *
 * The third and last control cluster, and the only one that is not about
 * looking at the model — it is about answering it. Same idiom as the other
 * two: pills, no panel, no labels above them, nothing at all until there is
 * geometry to write on.
 *
 * Bottom CENTRE on purpose. `#layers` (left) and `#step` (right) are already
 * spoken for, and this belongs between them in more than position: it is what
 * you do when the part is NEARLY right, which is the state between reading it
 * and downloading it. It is also the only control on this page a user has to
 * FIND rather than recognise, and the middle is where a person looks.
 */

#marks {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  z-index: 9;
  display: flex;
  gap: 6px;
}

#marks[hidden] { display: none !important; }
#marks button[hidden] { display: none !important; }

#marks button {
  padding: 8px 15px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgb(11 12 14 / 0.6);
  backdrop-filter: blur(6px);
  color: var(--ink-mute);
  font-family: inherit;
  font-size: 12.5px;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background var(--wake), color var(--wake), border-color var(--wake);
}

/* ARMED is a MODE, and a mode nobody can see is a mode that eats clicks: while
   this is on, a click on the part plants a note instead of doing nothing. Same
   rule as `#layers` — the cream belongs to the state, never to hover. */
#marks button[aria-pressed="true"] {
  border-color: var(--accent-soft);
  color: var(--accent-deep);
}

#marks button:not(:disabled):hover { background: rgb(var(--ink-rgb) / 0.10); }
#marks button:disabled { opacity: 0.28; cursor: default; }

/* The decision, in cream, exactly like Download STEP. The two never compete:
   this one is "the part is not right yet" and that one is "it is". */
#mark-send {
  border-color: var(--accent-soft) !important;
  color: var(--accent-deep) !important;
}

#mark-send:not(:disabled):hover {
  background: var(--accent-deep) !important;
  border-color: var(--accent-deep) !important;
  color: var(--void-0) !important;
}

/* A send can be refused — mid-turn, or with three runs spent. Nothing here
   paints words, so it is the border going out and coming back, borrowing
   `#layers`'s own `refuse` keyframes. */
#marks button.refused { animation: refuse 700ms ease; }
