/* Fabulous — the whole look of the product.
 *
 * THREE 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 *SITE*'S SYSTEM, and that is a change. Until
 *    2026-08-26 this file was `crazyshit/src/crazyshit/ui/static/app.css`
 *    and `fab/apps/desktop/ui/static/styles.css`, token for token — a cool
 *    graphite void with a CREAM accent, on the written grounds that the two
 *    internal apps had already settled it. What that argument never covered
 *    is the one surface a customer actually meets: `faber-website`, whose
 *    own stylesheet carries the accent in a comment naming it —
 *
 *        --accent is Faber's accent colour: acid lime #d8ef62. It is the
 *        colour of every action (primary buttons, nav hover, active link,
 *        live state) across the whole site.
 *
 *    So the product now wears the site's palette rather than the two tools'.
 *    Jules's call, 2026-08-26, off the design handoff; the values below are
 *    that handoff's "Palette du chrome sombre" and the site's `globals.css`,
 *    value for value, and `dev/gate.py` holds them there.
 *
 *    THE CHROME STAYS DARK and that was re-decided, not inherited: the
 *    viewer needs a black room, and three light alternatives were tried and
 *    dropped. The six dark tones are the site's own ink `#17332c` walked
 *    down, so the room is in the same green family as the paper rather than
 *    in a second neutral one.
 *
 * 3. NO ROUNDING, ANYWHERE. The site has not a single `border-radius`, and
 *    its shadows are hard offsets with no blur. It is the most recognisable
 *    thing about the system, so `--radius` is 0 and stays 0.
 */

: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:     255 255 255;
  --accent-rgb:  216 239 98;    /* #d8ef62 — the site's acid, and the only accent */

  /* The room. `--ground` is the product's field, `--lift` the one pool of
     light in it, `--panel` every rectangle that holds something (receipt,
     scale, mark bar), `--rule` their contour.

     THE GROUND IS THE CONSTELLATION'S, AND THAT IS THE THIRD ANSWER TO THE
     SAME QUESTION. It went #08130f (the handoff's derivation of the site's
     ink) → #080b0a (`.workflow-demo`, the ground the site's own product video
     plays on) → here. Jules, 2026-08-26, on seeing the second: *"c'est le
     fond noir/galaxie en dégradé"*. That is not a colour on the marketing
     pages at all — it is `app/constellation.css`, the HOMEPAGE's own visual
     world: `--void: #060608` under a field of stars and four nebulae, with
     the acid unchanged on top of it (`constellationData.ts` carries the same
     #d8ef62, and says so).

     So the room is the site's SKY. Every value below is read out of that file
     rather than derived, and the composition is in `#stage`.

     THE PANELS COME FROM THE SAME WORLD, for the same reason one level down:
     #102d26 is the MARKETING pages' panel, and a green rectangle on a violet
     and blue galaxy is two houses in one frame. `--void-panel` (#0d0d11) and
     `--hair` are the constellation's own. */
  --ground:      #060608;
  --ground-rgb:  6 6 8;         /* the same, for the scrims that sit over geometry */
  --lift:        rgb(240 220 180 / 0.20);   /* the warm glow the site throws from below */
  --panel:       rgb(13 13 17 / 0.86);      /* --void-panel #0d0d11, ground showing through */
  --rule:        rgb(226 216 192 / 0.16);   /* --hair, one step up so a panel has an edge */

  --ink:         #ffffff;   /* titles and live labels */
  --ink-soft:    #a9c0b6;   /* inactive labels, second rank */
  --ink-mute:    #809b8f;   /* mono labels — quota, formats, graduations */

  /* Controls sit ON the ground rather than on a panel, so their contour is
     white-with-alpha and not the panel filet. Two weights: the resting one,
     and the one an ON switch is drawn at. */
  --line:        rgb(var(--ink-rgb) / 0.22);
  --line-live:   rgb(var(--ink-rgb) / 0.40);

  /* THE ACCENT IS SPENT ON WHAT YOU PRESS and on three or four state marks —
     never decoratively. `--on-accent` is the site's ink, and it is the only
     colour type is ever set in on top of the acid: white on #d8ef62 is
     unreadable, and this pair is the site's own button. */
  --accent-deep: rgb(var(--accent-rgb) / 1);
  --accent-soft: rgb(var(--accent-rgb) / 0.38);
  --on-accent:   #17332c;

  /* ZERO. See rule 3 above; this is the system's signature. */
  --radius:      0px;

  /* THE SITE'S FACES, and nothing is vendored so nothing is downloaded.
     Arial for everything, Georgia ITALIC for the one emphasised word in a
     title (the site sets `h1 em` that way and nowhere else), mono for every
     label — a byte count, a quota, a graduation, a format list. */
  --sans:  Arial, Helvetica, sans-serif;
  --serif: Georgia, "Times New Roman", 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(--ground);
  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(--accent-rgb) / 0.28); color: var(--ink); }

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

/* THE SKY, AND IT IS THE SITE'S OWN — `app/constellation.css`, the homepage.
   Four nebulae and a vignette, at that file's own stops and its own alphas;
   nothing here is invented and nothing is a hex nobody can point at:

     blue    rgba(146,174,222,.12)   `#agent`
     violet  rgba(183,154,216,.13)   `#stack`
     gold    rgba(226,193,132,.11)   `#stack`
     warm    rgba(240,220,180,.20)   `.cv-waitlist`, thrown from below
     veil    rgba(6,6,8,.62)         `.cv-hero-veil`, verbatim

   THE VEIL IS ON TOP AND IT IS LOAD-BEARING. Every cluster of chrome on this
   page lives at an EDGE — the wordmark, the quota, the layer switches, the
   scale, Download STEP — and a nebula running out to a corner puts coloured
   light under 11 px of mono. The site's own vignette darkens exactly the ring
   they sit in, which is why it is copied rather than approximated.

   A flat black field makes a dark grey part look like a hole; this is the
   opposite problem answered the same way. Under the canvas the viewer paints,
   so it only ever shows where there is no model. */
#stage {
  position: fixed;
  inset: 0;
  background:
    radial-gradient(ellipse at 50% 52%, rgb(var(--ground-rgb) / 0) 48%,
                    rgb(var(--ground-rgb) / 0.62) 100%),
    radial-gradient(ellipse 900px 500px at 50% 40%, rgb(146 174 222 / 0.12), transparent 68%),
    radial-gradient(ellipse 820px 420px at 82% 14%, rgb(183 154 216 / 0.13), transparent 70%),
    radial-gradient(ellipse 700px 420px at 8% 88%, rgb(226 193 132 / 0.11), transparent 70%),
    radial-gradient(ellipse 1100px 520px at 50% 118%, var(--lift), transparent 66%),
    var(--ground);
}

/* THE STARS. This layer used to be a drawing-office grid — 44 px minor, 220 px
   major — on the argument that the paper this product's own inputs are drawn
   on is a better ground than a void. The sky replaced it rather than joining
   it: two ground textures is one idea too many, and the grid was OURS where
   this is the site's.

   IT IS A SEEDED FIELD AND NOT A RANDOM ONE. The site draws 320 stars on a
   canvas with a per-load `Math.random()` and twinkles them; here the sky is
   generated once, deterministically, so every screenshot and every check sees
   the same one. The density is the site's — 320 over 1440x900 is one star per
   4 050 px², and this tile is 760² carrying 143 — and the radii (.2–1.3), the
   alphas (.08–.53) and the cream (#f2ede2, the constellation's own CREAM) are
   its numbers too.

   NO TWINKLE, AND THAT IS THIS PRODUCT'S RULE RATHER THAN A SHORTCUT. On the
   site the stars are the hero of an empty page. Here motion means something is
   HAPPENING, and a field that breathes behind a compass is a second thing
   moving during the one moment this product asks somebody to wait.

   UNLIKE THE GRID IT DOES NOT GO AT THE FIRST BUILD. The sky is the room, and
   a room that empties when the work arrives is a splash screen. It is under
   the canvas the viewer paints, so it only ever shows around the part. */
#stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22760%22 height=%22760%22%3E%3Cg fill=%22%23f2ede2%22%3E%3Ccircle cx=%22360.6%22 cy=%22459.1%22 r=%221.11%22 opacity=%220.36%22/%3E%3Ccircle cx=%22117.1%22 cy=%22156.1%22 r=%220.33%22 opacity=%220.11%22/%3E%3Ccircle cx=%2282.4%22 cy=%22664.4%22 r=%220.43%22 opacity=%220.12%22/%3E%3Ccircle cx=%2224.3%22 cy=%22303.1%22 r=%220.46%22 opacity=%220.13%22/%3E%3Ccircle cx=%22661.7%22 cy=%22496.2%22 r=%221.05%22 opacity=%220.30%22/%3E%3Ccircle cx=%2293.1%22 cy=%22627.0%22 r=%221.05%22 opacity=%220.11%22/%3E%3Ccircle cx=%22590.0%22 cy=%22552.5%22 r=%220.84%22 opacity=%220.20%22/%3E%3Ccircle cx=%22752.0%22 cy=%22575.2%22 r=%221.06%22 opacity=%220.21%22/%3E%3Ccircle cx=%22408.6%22 cy=%22724.3%22 r=%220.52%22 opacity=%220.24%22/%3E%3Ccircle cx=%22398.0%22 cy=%22352.7%22 r=%221.11%22 opacity=%220.44%22/%3E%3Ccircle cx=%22702.2%22 cy=%22324.1%22 r=%221.04%22 opacity=%220.19%22/%3E%3Ccircle cx=%22637.0%22 cy=%22268.9%22 r=%221.29%22 opacity=%220.13%22/%3E%3Ccircle cx=%22297.1%22 cy=%22437.7%22 r=%220.90%22 opacity=%220.12%22/%3E%3Ccircle cx=%2295.4%22 cy=%22525.2%22 r=%220.67%22 opacity=%220.18%22/%3E%3Ccircle cx=%22542.0%22 cy=%22366.2%22 r=%220.22%22 opacity=%220.46%22/%3E%3Ccircle cx=%22357.5%22 cy=%22226.3%22 r=%221.05%22 opacity=%220.45%22/%3E%3Ccircle cx=%22130.2%22 cy=%22424.4%22 r=%220.99%22 opacity=%220.29%22/%3E%3Ccircle cx=%22127.1%22 cy=%22434.6%22 r=%220.53%22 opacity=%220.49%22/%3E%3Ccircle cx=%2217.5%22 cy=%22659.6%22 r=%220.65%22 opacity=%220.46%22/%3E%3Ccircle cx=%22192.4%22 cy=%2294.4%22 r=%220.49%22 opacity=%220.48%22/%3E%3Ccircle cx=%22391.3%22 cy=%2246.9%22 r=%220.37%22 opacity=%220.22%22/%3E%3Ccircle cx=%22698.5%22 cy=%22353.0%22 r=%220.88%22 opacity=%220.33%22/%3E%3Ccircle cx=%22217.4%22 cy=%22715.0%22 r=%221.20%22 opacity=%220.46%22/%3E%3Ccircle cx=%22332.9%22 cy=%22418.5%22 r=%220.23%22 opacity=%220.37%22/%3E%3Ccircle cx=%2292.0%22 cy=%22391.6%22 r=%220.58%22 opacity=%220.43%22/%3E%3Ccircle cx=%2276.3%22 cy=%22546.4%22 r=%220.95%22 opacity=%220.26%22/%3E%3Ccircle cx=%22396.5%22 cy=%22608.3%22 r=%220.95%22 opacity=%220.47%22/%3E%3Ccircle cx=%22539.4%22 cy=%22716.0%22 r=%220.47%22 opacity=%220.32%22/%3E%3Ccircle cx=%22517.6%22 cy=%22129.4%22 r=%220.54%22 opacity=%220.20%22/%3E%3Ccircle cx=%22334.2%22 cy=%2277.0%22 r=%220.69%22 opacity=%220.09%22/%3E%3Ccircle cx=%22329.6%22 cy=%22390.8%22 r=%220.45%22 opacity=%220.39%22/%3E%3Ccircle cx=%229.0%22 cy=%22204.2%22 r=%220.75%22 opacity=%220.48%22/%3E%3Ccircle cx=%22152.9%22 cy=%22370.3%22 r=%220.86%22 opacity=%220.31%22/%3E%3Ccircle cx=%22405.9%22 cy=%22561.0%22 r=%220.24%22 opacity=%220.17%22/%3E%3Ccircle cx=%22197.3%22 cy=%22737.4%22 r=%221.13%22 opacity=%220.14%22/%3E%3Ccircle cx=%22607.2%22 cy=%22374.0%22 r=%220.55%22 opacity=%220.39%22/%3E%3Ccircle cx=%22370.1%22 cy=%22143.1%22 r=%221.29%22 opacity=%220.25%22/%3E%3Ccircle cx=%22203.0%22 cy=%22233.9%22 r=%221.20%22 opacity=%220.43%22/%3E%3Ccircle cx=%22109.4%22 cy=%22332.3%22 r=%220.32%22 opacity=%220.38%22/%3E%3Ccircle cx=%22654.5%22 cy=%22608.9%22 r=%220.42%22 opacity=%220.29%22/%3E%3Ccircle cx=%22451.1%22 cy=%22118.9%22 r=%221.05%22 opacity=%220.23%22/%3E%3Ccircle cx=%22172.3%22 cy=%22173.0%22 r=%220.38%22 opacity=%220.49%22/%3E%3Ccircle cx=%22457.9%22 cy=%22675.5%22 r=%220.67%22 opacity=%220.28%22/%3E%3Ccircle cx=%22758.3%22 cy=%2247.6%22 r=%220.42%22 opacity=%220.39%22/%3E%3Ccircle cx=%22669.3%22 cy=%22535.1%22 r=%221.21%22 opacity=%220.10%22/%3E%3Ccircle cx=%2264.7%22 cy=%22134.7%22 r=%220.37%22 opacity=%220.36%22/%3E%3Ccircle cx=%22104.4%22 cy=%22141.3%22 r=%220.84%22 opacity=%220.11%22/%3E%3Ccircle cx=%22661.3%22 cy=%22190.8%22 r=%221.12%22 opacity=%220.31%22/%3E%3Ccircle cx=%22304.3%22 cy=%2233.8%22 r=%221.06%22 opacity=%220.46%22/%3E%3Ccircle cx=%22150.2%22 cy=%22391.8%22 r=%220.25%22 opacity=%220.43%22/%3E%3Ccircle cx=%22658.4%22 cy=%22388.8%22 r=%221.11%22 opacity=%220.53%22/%3E%3Ccircle cx=%228.7%22 cy=%22388.5%22 r=%220.24%22 opacity=%220.13%22/%3E%3Ccircle cx=%22610.4%22 cy=%22325.1%22 r=%221.01%22 opacity=%220.37%22/%3E%3Ccircle cx=%22446.2%22 cy=%22420.7%22 r=%221.18%22 opacity=%220.37%22/%3E%3Ccircle cx=%22663.3%22 cy=%22687.6%22 r=%220.54%22 opacity=%220.35%22/%3E%3Ccircle cx=%22104.3%22 cy=%22544.9%22 r=%221.13%22 opacity=%220.41%22/%3E%3Ccircle cx=%2255.5%22 cy=%22488.7%22 r=%220.45%22 opacity=%220.31%22/%3E%3Ccircle cx=%22437.0%22 cy=%22150.5%22 r=%220.27%22 opacity=%220.29%22/%3E%3Ccircle cx=%22456.7%22 cy=%22299.2%22 r=%221.09%22 opacity=%220.49%22/%3E%3Ccircle cx=%22484.5%22 cy=%22671.7%22 r=%220.26%22 opacity=%220.20%22/%3E%3Ccircle cx=%22647.0%22 cy=%22113.3%22 r=%220.82%22 opacity=%220.26%22/%3E%3Ccircle cx=%22109.1%22 cy=%22629.0%22 r=%220.91%22 opacity=%220.16%22/%3E%3Ccircle cx=%22516.1%22 cy=%22467.6%22 r=%220.70%22 opacity=%220.22%22/%3E%3Ccircle cx=%22243.4%22 cy=%2248.5%22 r=%221.09%22 opacity=%220.51%22/%3E%3Ccircle cx=%22526.6%22 cy=%2291.2%22 r=%221.09%22 opacity=%220.35%22/%3E%3Ccircle cx=%227.4%22 cy=%22521.1%22 r=%220.82%22 opacity=%220.23%22/%3E%3Ccircle cx=%22197.7%22 cy=%22441.9%22 r=%220.46%22 opacity=%220.35%22/%3E%3Ccircle cx=%22386.5%22 cy=%22142.8%22 r=%221.05%22 opacity=%220.26%22/%3E%3Ccircle cx=%22664.0%22 cy=%22753.5%22 r=%221.06%22 opacity=%220.34%22/%3E%3Ccircle cx=%22250.9%22 cy=%22338.0%22 r=%220.61%22 opacity=%220.28%22/%3E%3Ccircle cx=%22676.6%22 cy=%22546.1%22 r=%220.68%22 opacity=%220.29%22/%3E%3Ccircle cx=%22479.5%22 cy=%22552.6%22 r=%220.41%22 opacity=%220.09%22/%3E%3Ccircle cx=%22394.5%22 cy=%22704.6%22 r=%220.85%22 opacity=%220.27%22/%3E%3Ccircle cx=%22681.6%22 cy=%22316.3%22 r=%220.77%22 opacity=%220.35%22/%3E%3Ccircle cx=%2298.6%22 cy=%22701.1%22 r=%220.59%22 opacity=%220.53%22/%3E%3Ccircle cx=%22107.7%22 cy=%22522.5%22 r=%221.05%22 opacity=%220.20%22/%3E%3Ccircle cx=%22502.7%22 cy=%22625.8%22 r=%221.16%22 opacity=%220.11%22/%3E%3Ccircle cx=%22259.8%22 cy=%22269.4%22 r=%220.65%22 opacity=%220.23%22/%3E%3Ccircle cx=%22437.4%22 cy=%22231.9%22 r=%221.17%22 opacity=%220.27%22/%3E%3Ccircle cx=%22357.8%22 cy=%22663.8%22 r=%220.84%22 opacity=%220.29%22/%3E%3Ccircle cx=%22298.3%22 cy=%22141.5%22 r=%220.67%22 opacity=%220.14%22/%3E%3Ccircle cx=%22732.4%22 cy=%224.1%22 r=%220.26%22 opacity=%220.20%22/%3E%3Ccircle cx=%22213.1%22 cy=%22372.5%22 r=%220.43%22 opacity=%220.42%22/%3E%3Ccircle cx=%22733.2%22 cy=%22165.7%22 r=%220.32%22 opacity=%220.38%22/%3E%3Ccircle cx=%22476.3%22 cy=%22257.9%22 r=%220.51%22 opacity=%220.16%22/%3E%3Ccircle cx=%22207.1%22 cy=%2232.6%22 r=%220.56%22 opacity=%220.41%22/%3E%3Ccircle cx=%22362.8%22 cy=%22302.4%22 r=%220.85%22 opacity=%220.29%22/%3E%3Ccircle cx=%22706.9%22 cy=%22593.5%22 r=%220.63%22 opacity=%220.15%22/%3E%3Ccircle cx=%22592.0%22 cy=%22594.2%22 r=%221.19%22 opacity=%220.32%22/%3E%3Ccircle cx=%22404.2%22 cy=%22605.1%22 r=%220.58%22 opacity=%220.16%22/%3E%3Ccircle cx=%22619.6%22 cy=%22758.2%22 r=%220.21%22 opacity=%220.42%22/%3E%3Ccircle cx=%2279.4%22 cy=%22248.6%22 r=%220.45%22 opacity=%220.18%22/%3E%3Ccircle cx=%22734.0%22 cy=%22423.3%22 r=%220.53%22 opacity=%220.25%22/%3E%3Ccircle cx=%22670.9%22 cy=%22704.5%22 r=%221.23%22 opacity=%220.45%22/%3E%3Ccircle cx=%22570.5%22 cy=%22449.8%22 r=%221.05%22 opacity=%220.23%22/%3E%3Ccircle cx=%22403.7%22 cy=%22372.7%22 r=%221.02%22 opacity=%220.52%22/%3E%3Ccircle cx=%22340.0%22 cy=%22531.9%22 r=%220.43%22 opacity=%220.10%22/%3E%3Ccircle cx=%22700.4%22 cy=%22410.3%22 r=%221.04%22 opacity=%220.33%22/%3E%3Ccircle cx=%22660.5%22 cy=%22615.0%22 r=%220.38%22 opacity=%220.31%22/%3E%3Ccircle cx=%22413.1%22 cy=%22174.2%22 r=%220.45%22 opacity=%220.43%22/%3E%3Ccircle cx=%22364.5%22 cy=%22254.3%22 r=%220.29%22 opacity=%220.49%22/%3E%3Ccircle cx=%22171.8%22 cy=%22614.2%22 r=%220.95%22 opacity=%220.33%22/%3E%3Ccircle cx=%22405.6%22 cy=%22367.2%22 r=%220.60%22 opacity=%220.41%22/%3E%3Ccircle cx=%22150.8%22 cy=%2256.5%22 r=%220.69%22 opacity=%220.09%22/%3E%3Ccircle cx=%22514.8%22 cy=%22352.8%22 r=%220.65%22 opacity=%220.13%22/%3E%3Ccircle cx=%22367.1%22 cy=%2246.1%22 r=%221.04%22 opacity=%220.46%22/%3E%3Ccircle cx=%22536.9%22 cy=%22550.5%22 r=%220.22%22 opacity=%220.46%22/%3E%3Ccircle cx=%22579.2%22 cy=%22273.4%22 r=%220.73%22 opacity=%220.51%22/%3E%3Ccircle cx=%22374.2%22 cy=%22241.6%22 r=%220.44%22 opacity=%220.37%22/%3E%3Ccircle cx=%22620.0%22 cy=%22711.9%22 r=%221.25%22 opacity=%220.35%22/%3E%3Ccircle cx=%2279.3%22 cy=%22324.0%22 r=%220.44%22 opacity=%220.40%22/%3E%3Ccircle cx=%22580.4%22 cy=%22511.4%22 r=%220.26%22 opacity=%220.47%22/%3E%3Ccircle cx=%2265.1%22 cy=%22369.2%22 r=%220.95%22 opacity=%220.37%22/%3E%3Ccircle cx=%2274.4%22 cy=%22699.6%22 r=%220.49%22 opacity=%220.49%22/%3E%3Ccircle cx=%22748.4%22 cy=%2225.6%22 r=%220.20%22 opacity=%220.48%22/%3E%3Ccircle cx=%228.3%22 cy=%22135.4%22 r=%220.77%22 opacity=%220.46%22/%3E%3Ccircle cx=%22114.9%22 cy=%22285.5%22 r=%220.56%22 opacity=%220.41%22/%3E%3Ccircle cx=%22177.2%22 cy=%22572.4%22 r=%220.26%22 opacity=%220.11%22/%3E%3Ccircle cx=%22233.7%22 cy=%22479.8%22 r=%220.68%22 opacity=%220.52%22/%3E%3Ccircle cx=%22330.8%22 cy=%22657.5%22 r=%221.09%22 opacity=%220.37%22/%3E%3Ccircle cx=%22156.6%22 cy=%22471.6%22 r=%220.36%22 opacity=%220.32%22/%3E%3Ccircle cx=%22611.6%22 cy=%22752.6%22 r=%220.66%22 opacity=%220.41%22/%3E%3Ccircle cx=%22295.7%22 cy=%22537.5%22 r=%220.35%22 opacity=%220.12%22/%3E%3Ccircle cx=%22526.6%22 cy=%22558.5%22 r=%221.12%22 opacity=%220.13%22/%3E%3Ccircle cx=%2272.1%22 cy=%22545.5%22 r=%220.28%22 opacity=%220.09%22/%3E%3Ccircle cx=%22475.0%22 cy=%2225.1%22 r=%220.84%22 opacity=%220.18%22/%3E%3Ccircle cx=%22547.7%22 cy=%22476.1%22 r=%220.27%22 opacity=%220.15%22/%3E%3Ccircle cx=%22756.0%22 cy=%22539.0%22 r=%220.43%22 opacity=%220.21%22/%3E%3Ccircle cx=%22183.9%22 cy=%2253.0%22 r=%220.72%22 opacity=%220.27%22/%3E%3Ccircle cx=%22139.1%22 cy=%22518.7%22 r=%221.16%22 opacity=%220.40%22/%3E%3Ccircle cx=%2240.0%22 cy=%22527.0%22 r=%220.89%22 opacity=%220.33%22/%3E%3Ccircle cx=%22166.4%22 cy=%22214.0%22 r=%220.68%22 opacity=%220.47%22/%3E%3Ccircle cx=%22129.2%22 cy=%22127.9%22 r=%220.76%22 opacity=%220.35%22/%3E%3Ccircle cx=%22665.0%22 cy=%22706.4%22 r=%220.58%22 opacity=%220.53%22/%3E%3Ccircle cx=%22111.9%22 cy=%22675.5%22 r=%221.11%22 opacity=%220.11%22/%3E%3Ccircle cx=%22509.5%22 cy=%22267.6%22 r=%221.26%22 opacity=%220.11%22/%3E%3Ccircle cx=%22144.9%22 cy=%22587.7%22 r=%221.10%22 opacity=%220.27%22/%3E%3Ccircle cx=%2257.2%22 cy=%22241.3%22 r=%220.41%22 opacity=%220.29%22/%3E%3Ccircle cx=%22609.1%22 cy=%22664.8%22 r=%220.39%22 opacity=%220.12%22/%3E%3Ccircle cx=%22677.6%22 cy=%2255.5%22 r=%221.16%22 opacity=%220.39%22/%3E%3Ccircle cx=%22157.3%22 cy=%22692.2%22 r=%220.99%22 opacity=%220.20%22/%3E%3Ccircle cx=%22219.9%22 cy=%2292.3%22 r=%220.33%22 opacity=%220.12%22/%3E%3Ccircle cx=%2258.6%22 cy=%22670.9%22 r=%220.23%22 opacity=%220.13%22/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: repeat;
  /* The same ring the veil darkens: no star sits under a corner cluster. */
  -webkit-mask-image: radial-gradient(ellipse 74% 68% at 50% 46%, #000 0%, transparent 86%);
  mask-image: radial-gradient(ellipse 74% 68% at 50% 46%, #000 0%, transparent 86%);
  pointer-events: none;
}

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

/* NOTHING ON THE STAGE IS TEXT TO BE SELECTED, and saying so is what removes a
   panel this product does not draw. Reported by Jules 2026-08-29 with a
   screenshot: a plain left click on the viewer's background made Firefox put
   its own little rounded card on the page, over the part. That card is the
   shortcut a browser offers for a SELECTION, and a click in a page whose text
   is selectable starts one. The vendored sheet already says `user-select:
   none` on `body`; this says it where it actually matters and where a later
   rule cannot quietly take it back. `comets.js` is what happens instead. */
#stage,
#stage * { user-select: none; -webkit-user-select: none; }

/* THE COMETS. Above the stage and under everything that can be pressed: the
   pins layer is 4, the veil 5, and the chrome above that. It takes no pointer
   — it is drawn BY the click, it never receives one — and it is created on the
   first comet rather than at boot, so a session where nobody clicks the sky
   pays nothing for it. */
#comets {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

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

/* LEFT-ALIGNED, AND THAT IS THE SITE'S DOING. Every composition on
   faber-website is a column against a left margin with the measure running
   out to the right — a centred stack of five things is a template, and two
   attempts at this screen were exactly that. The width is the handoff's 700,
   which is also about the measure the title wants at 64 px. */
#veil-inner {
  position: relative;
  z-index: 2;
  width: min(700px, 88vw);
  text-align: left;
  padding: 0;
  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 BEFORE ANY OF THAT IS KNOWN. `boot()` has to ask the server what this
   page is before it can say, and for the second that takes, a refresh of a
   project in progress painted the home screen over the part that was about to
   arrive — reported 2026-08-29 and reproduced in a browser on the first try.
   `animation: none` rather than `opacity: 0` alone: the composition arrives on
   `fade-up`, and killing the animation is what makes it play when the class
   comes off instead of having already played behind it. */
body.booting #veil-inner {
  opacity: 0;
  animation: none;
  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;
}

/* ...AND ONE 26 PX CHILD OPTS BACK IN, WHICH IS THE RULE ABOVE RUN IN REVERSE
   AND HAS TO EARN IT. The rule above is the fix for a production bug — a
   double-click near the middle of a mesh opened a file browser — and it works
   by killing the WHOLE card, receipt included. That is right for the card and
   wrong for the cross on it: a mesh staged over a finished model puts the
   receipt back on screen (`body.staged #veil-inner`), and with the card
   pointer-dead the only way off that receipt was pointer-dead with it. Found
   by driving it, 2026-08-31: Playwright reported `#veil-inner` swallowing the
   click, which is what a `pointer-events: none` descendant looks like from the
   outside.
   IT IS SAFE BECAUSE IT IS NOT THE CARD. Only while a file is staged, only the
   cross, and it opens no picker — `drop.js` stops the click before it reaches
   `#drop`'s own handler. `body.working` is deliberately not covered: a turn is
   running, and the file that started it is not being changed. */
body.staged:not(.working) #staged-change { pointer-events: auto; }

/* THE TITLE IS THE PRODUCT'S SENTENCE, NOT ITS NAME — and that is the one
   structural thing the redesign moved. The word "Fabulous" used to be the h1,
   with the sentence set under it as a subtitle; the site does the opposite,
   and it is right: the name says nothing to somebody who has just arrived
   with a drawing in their hand, and the sentence says the whole product. The
   wordmark went to the corner, where the site keeps it (`#mark-live`).

   One word is emphasised and it is the word the product is about. Georgia
   ITALIC, which is the site's `h1 em` and the only place a serif appears at
   all — nothing else in the chrome is ever set in it. */
#mark {
  margin: 0 0 34px;
  font-family: var(--sans);
  font-size: clamp(38px, 4.6vw, 64px);
  font-weight: 500;
  line-height: 0.91;
  letter-spacing: -0.08em;
  color: var(--ink);
  text-wrap: pretty;
}

#mark .accent {
  font-family: var(--serif);
  font-weight: 400;
  font-style: italic;
  letter-spacing: -0.03em;
}

/* THE EYEBROW, and it is one of the three or four state marks the accent is
   allowed outside a pressable: it names what this thing eats, which is the
   first question anybody standing on this screen has. */
#line {
  margin: 0 0 19px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-deep);
}

/* ------------------------------------------------------------------ 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: 100%;
  margin: 0;
  padding: 17px 18px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  /* A PANEL, NOT A FLOATING SLAB. The specular top edge and the long soft
     shadow that used to be here are both blurs, and this system has none:
     its shadows are hard offsets. What separates a rectangle from the ground
     is its filet and its fill, and nothing else. */
  background: var(--panel);
  /* 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: var(--line-live);
  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 acid and the lamp above it brightens. */
body.dragging #drop {
  border-color: var(--accent-deep);
  box-shadow: inset 3px 0 0 var(--accent-deep);
}

body.dragging #drop-verb { color: rgb(var(--accent-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(--accent-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: 0;
  text-align: right;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: #6f8a7e;
  line-height: 1.7;
}

/* 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(--accent-rgb) / 0) 0%,
              var(--accent-deep) 50%, rgb(var(--accent-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(var(--ground-rgb) / 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: 54px;
  height: 54px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  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: 15px;
  font-weight: 700;
  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: 10px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: #bed1c8;
}

/* A FILLED SQUARE, not a ring. The tick is the whole answer to "did my upload
   work", and on this system the thing that says yes is the accent itself —
   the same paint as the button under it. */
#staged-tick {
  flex: none;
  position: relative;
  width: 26px;
  height: 26px;
  border-radius: var(--radius);
  background: var(--accent-deep);
}

/* 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: 9.5px;
  top: 6px;
  width: 5px;
  height: 11px;
  border: solid var(--on-accent);
  border-width: 0 2.4px 2.4px 0;
  transform: rotate(45deg);
}

/* THE WAY OFF THE RECEIPT. Quiet on purpose: it is beside a tick that says the
   upload worked, and the mistake it undoes is "wrong file", not "something
   went wrong". The tick's own 26 px height so the row keeps its rhythm, and no
   ground until the pointer is on it — this card is a receipt, not a toolbar.
   IT IS THE LAST CONTROL IN THE ROW AND IT IS NOT THE CARD. A click on it must
   not also be a click on `#drop`, whose handler would ask for the picker a
   second time, so `drop.js` stops the event here. */
#staged-change {
  flex: none;
  height: 26px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-mute);
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background var(--wake), color var(--wake), border-color var(--wake);
}

#staged-change:hover {
  background: rgb(var(--ink-rgb) / 0.10);
  border-color: var(--line-live);
  color: var(--ink);
}

/* ----------------------------------------------------------- the confirmation
 *
 * The only filled control in the product, and the only thing on the page that
 * starts work. ACID because the acid is the site's one accent and it is spent
 * on what you press — this is the site's own primary button, brought over
 * whole: the fill, the ink on it, the 27 px gap and the arrow after the
 * label.
 */
/* ONE RULE FOR THE TWO BUTTONS THAT MATTER, and it is the site's `.button`
   verbatim: 15/17 of padding, a 27 px gap, 12 px at 750, and a 16 px glyph
   after the label. The gap is what makes it the site's button rather than a
   generic filled rectangle — the label and the arrow are pushed apart, not
   set beside each other. Build the CAD and Download STEP never appear on the
   same screen, so wearing the same paint costs nothing and saying the same
   thing twice in two idioms would. */
#go, #step {
  display: inline-flex;
  align-items: center;
  gap: 27px;
  padding: 15px 17px;
  border: none;
  border-radius: var(--radius);
  background: var(--accent-deep);
  color: var(--on-accent);
  font-family: inherit;
  font-size: 12px;
  font-weight: 750;
  letter-spacing: 0;
  text-decoration: none;
  cursor: pointer;
  pointer-events: auto;
  transition: background var(--wake), transform 120ms ease;
}

/* THE ARROW IS A PSEUDO-ELEMENT AND NOT A SPAN, and that is not a tidiness
   preference: `app.js` writes `#mark-send`'s label with `textContent` on
   every change to what is pinned ("Send 2 pins + a note"), which would wipe
   a child element on the first pin — and `dev/try/try_send.py` reads that label
   back and asserts it is exactly `Send`. Drawn in CSS, the glyph survives
   both. */
#go::after, #step::after {
  content: "\2192";
  font-size: 16px;
  line-height: 1;
}
#step::after { content: "\2193"; }

/* THE BUTTON AND THE FORMAT LIST SHARE ONE ROW, at the two ends of the
   measure. They answer the same question from opposite sides — "press this"
   and "with what" — and stacking them made the second read as a footnote to
   the first rather than as its condition. */
#commit {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  margin-top: 28px;
}

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

/* Nothing brighter than the acid exists in this palette, so hover cannot go
   up. It goes to the site's own `--accent-bright`, which is the acid lifted
   towards paper. */
#go:hover, #step:hover { background: #e5f5a8; }
#go:active, #step:active { transform: translateY(1px); }
#go:focus-visible, #step:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.34);
}

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

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

/* THE TWO DOORS, AND WE PAINT BOTH OF THEM NOW.
 *
 * Google's widget used to be the thing on the page: a white card, its own
 * radius, its own font, and — for anybody with a live Google session — the
 * PERSONALIZED variant, which is taller than the button it sits above, says
 * "Se connecter en tant que Jules" in the browser's language on an English
 * page, and prints the address back at the person who came to type nothing.
 * Reported by Jules, 2026-08-31. None of it is configurable: `renderButton`
 * takes a theme and a size and decides the rest.
 *
 * So the widget is still the CLICK and no longer the PICTURE. `#google-slot`
 * holds Google's real button at exactly the face's size, transparent, on top
 * — the press is theirs, on their element, with the user gesture intact and
 * not one line of click-forwarding in the normal path. `#google-face` is what
 * a person sees: the same 320x40 as the Microsoft door, the house font, the
 * house zero radius, and the official G. Google's branding guidance is what
 * makes this legitimate rather than a trick — it publishes the rules for a
 * custom button precisely so a product can use its own; the mark and the
 * words are the part that must not change, and neither does.
 *
 * WHY THE WRAPPER CLIPS: the personalized variant renders TALLER than the
 * 40px it was asked for. Un-clipped, its invisible overflow lies across the
 * Microsoft button underneath and swallows presses meant for it — one door
 * silently eating the other, which is the exact failure this page cannot
 * have. `overflow: hidden` clips hit-testing as well as paint.
 */
#google-btn { position: relative; width: 320px; height: 40px; margin: 0 auto; overflow: hidden; }
#google-slot { position: absolute; inset: 0; opacity: 0; }
/* Google sizes its own button from `renderButton`'s `width`, so this only has
   to hold the wrapper it puts around it. */
#google-slot > * { width: 100% !important; }
#google-face { position: absolute; inset: 0; }
/* THE KEYBOARD STILL PRESSES GOOGLE'S BUTTON, not ours: the face is
   `aria-hidden` and out of the tab order, the real control is focusable where
   it always was, and the ring is painted on the thing that is visible. */
#google-btn:focus-within #google-face {
  box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.34);
}
/* ONE BUTTON, TWO DOORS. They were 4px-rounded white slabs matching Google's
   widget — the one exemption the zero-radius rule carried. Nothing needs
   matching now, so both join the system. Each keeps its own mark and its own
   words, which is the half of a brand's guidance that is not negotiable, and
   both marks are inline SVG: a veil that waits on a third-party image is a
   veil with a hole in it. */
.door-btn {
  display: none; align-items: center; justify-content: center; gap: 12px;
  width: 320px; height: 40px;
  font: 500 14px/1 var(--sans); color: var(--ink);
  background: rgb(var(--ink-rgb) / 0.06);
  border: 1px solid var(--rule); border-radius: var(--radius);
  cursor: pointer; transition: background var(--wake);
}
.door-btn.on { display: flex; }
.door-btn:hover { background: rgb(var(--ink-rgb) / 0.12); }
.door-btn:disabled { opacity: .55; cursor: default; }
.door-btn svg { width: 18px; height: 18px; flex: none; }
#ms-btn { margin: 10px auto 0; }

/* 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(--accent-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(--accent-rgb) / 0);
  border-radius: var(--radius);
  transition: border-color var(--wake), background var(--wake);
}

body.dragging #veil::after {
  border-color: rgb(var(--accent-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. The
 * model's own line sits under it; under that, one product sentence about
 * how long a turn can take. Not agent prose — ours, and it stays.
 */
#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 cluster becomes a row once `empty` comes off (first build). A long
     thought must shrink, not shove the clock off the edge of the page. */
  flex-shrink: 0;
}

/* 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 WAS `white-space: nowrap` IN A 30ch BOX, and that was the bug. The server
   trims a thought to `loop._THINKING_MAX`; this box then trimmed THAT to about
   thirty characters with an ellipsis of its own, so a reader got two clauses'
   worth of set-up and none of the verb — every line on screen ended mid
   sentence. It now wraps to two lines, which is what the server's cap is
   sized for, and `line-clamp` is the backstop rather than the rule.

   The height is RESERVED, both lines of it, whether or not there is anything
   on them. The cluster is a centred flex column and the compass sits on top
   of this element: a line that grew from one row to two would shove a 168 px
   mark up and down the screen every few seconds. */
#thought {
  max-width: min(64ch, 78vw);
  height: calc(2 * 1.55em);
  font-size: 12px;
  line-height: 1.55;
  letter-spacing: 0.01em;
  color: rgb(var(--ink-rgb) / 0.34);
  text-align: center;
  text-wrap: balance;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  opacity: 0;
  transition: opacity 260ms ease;
}

#thought.in { opacity: 1; }

/* ...and the words arrive one at a time. THIS IS WHERE THE STREAMING IS
   VISIBLE. The frames come off the wire in bursts on the model's schedule —
   three sentences can close in the same millisecond — so the rhythm a person
   reads is not the wire's, it is this: `app.js::paintThought` gives every word
   its own delay and the cascade is short enough (520 ms, capped) that the line
   is never still arriving when it is being read. A swap with no reveal at all
   was the other half of the flicker. */
#thought .w {
  animation: thought-word 300ms cubic-bezier(.2, .8, .2, 1) both;
}

@keyframes thought-word {
  from { opacity: 0; filter: blur(1.5px); }
  to   { opacity: 1; filter: blur(0); }
}

/* Somebody who asked not to be moved gets the line and not the cascade. */
@media (prefers-reduced-motion: reduce) {
  #thought .w { animation: none; }
}

/* Under the thought, and it is not a thought. The line above is replaced
   every few seconds and emptied when the turn ends; this one has to stay
   for the whole of it, including the minutes before any thought arrives.
   Opacity rides `#thinking` itself (`body.working`), so there is no second
   clock and no script to forget to call. Quieter than the thought, same
   ink, so it cannot compete with a sentence that is about THIS step. */
#wait {
  max-width: min(64ch, 78vw);
  font-size: 11px;
  line-height: 1.45;
  letter-spacing: 0.01em;
  color: rgb(var(--ink-rgb) / 0.28);
  text-align: center;
  text-wrap: balance;
}

/* ...AND IT GOES AT THE FIRST BUILD. Jules's call, 2026-08-29. The whole of
   what it answers is "ten minutes of a turning compass and nothing on screen —
   has this died?", and that question only exists while the stage is empty. Once
   there is geometry the screen answers it every time a version lands, and a
   paragraph about how long a turn CAN take, printed along the top edge over a
   part that is visibly being worked on, is the product talking about itself.
   Same line the compass moves on (`body.empty`), so the two change together.

   WHICH ALSO MEANS IT NEVER APPEARS ON MESH2CAD OR A POINT CLOUD, and Jules
   asked for that by name. It falls out rather than needing a second rule: a
   geometry drop opens in the viewer at once (`drop.openGeometry` →
   `refreshModel` → `loadModel`), so `empty` is off before the turn is even
   started — measured in a browser, `body.className` is `staged` with the mesh
   ghosted on screen. Only draw2cad keeps `empty` through a first turn, because
   there really is nothing to look at. If a wedge ever staged a mesh WITHOUT
   drawing it, this rule would stop covering that and the note would be back. */
body:not(.empty) #wait { display: none; }

/* 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 lives in `#session-actions`
   beside Sign out — one flex row, no fixed corner of its own.

   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 acid reserved for that moment. It is not styled
   as a danger — nothing is lost by pressing it, the model on screen is kept. */
#session-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: auto;
}

/* ONE SIZE FOR THE ROW. The padding and the type below make the two the same
   HEIGHT; this is what makes them the same rectangle, because their labels are
   not the same length and a 55 px Stop beside a 71 px Sign out is still two
   different buttons. 72 is Sign out's own natural width — 71.1 measured in
   Chromium 2026-08-29, rounded up to the pixel — so this pushes nothing out and
   only pulls Stop up to it. If that label ever changes, this changes with it.

   NOT on `#stop` in general: between turns that button collapses to a zero
   width so Sign out keeps the corner alone, and a floor under it would hold
   the row open around nothing. `body.working` is exactly when it is there. */
#signout,
body.working #stop { min-width: 72px; }

#stop {
  /* Not `display: none` — the fade runs both ways. Out of the flex row while
     hidden so Sign out keeps its corner alone between turns. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  width: 0;
  padding: 0;
  margin: 0;
  border-width: 0;
  overflow: hidden;
  white-space: nowrap;
  transition: opacity 420ms ease, visibility 0s linear 420ms,
              color var(--wake), border-color var(--wake),
              background var(--wake), width 420ms ease, padding 420ms ease,
              border-width 420ms ease;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: rgb(var(--ink-rgb) / 0.52);
  font-family: inherit;
  /* SET AS SIGN OUT IS, and that is the point rather than a coincidence: the
     two share one row, and a Stop at 63x28 beside a Sign out at 71x32 read as
     a smaller, lesser control — which is the opposite of what the one button
     that ends a turn should look like. Same size, same case, same tracking;
     only the paint differs, and the acid is still reserved for the hover. */
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  cursor: pointer;
}

body.working #stop {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  width: auto;
  padding: 9px 13px;
  border-width: 1px;
  border-color: rgb(var(--accent-rgb) / 0.46);
  background: rgb(var(--ink-rgb) / 0.10);
  color: var(--ink);
  transition: opacity 420ms ease, visibility 0s, color var(--wake),
              border-color var(--wake), background var(--wake),
              width 420ms ease, padding 420ms ease, border-width 420ms ease;
}

#stop:not(:disabled):hover {
  border-color: rgb(var(--accent-rgb) / 0.62);
  background: rgb(var(--accent-rgb) / 0.12);
  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(--accent-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, and `#nav` left that band on 2026-08-29
   because this row is what it kept colliding with. GRID, not a flex row, for
   the third column: the thought is the only thing here that may grow, and
   `minmax(0, …)` is what lets it shrink instead of shoving the clock off the
   edge. It carried a second row for the wait note until the note stopped
   appearing on this layout at all. */
body.working:not(.empty) #thinking {
  top: 14px;
  transform: translate(-50%, 0);
  display: grid;
  grid-template-columns: auto auto minmax(0, min(52ch, 44vw));
  grid-template-areas: "mark elapsed thought";
  column-gap: 12px;
  align-items: center;
}
body.working:not(.empty) #thinking-mark {
  grid-area: mark;
  width: 46px;
  height: 46px;
}
body.working:not(.empty) #elapsed { grid-area: elapsed; }
body.working:not(.empty) #thought { grid-area: thought; }
/* With geometry on screen the cluster is a ROW along the top edge, so the
   line has the compass and the clock beside it rather than above: narrower,
   and quieter still. Two lines, same as the centred layout — one line here
   would put the cut back, just at 52 characters instead of thirty. */
body.working:not(.empty) #thought {
  max-width: min(52ch, 44vw);
  font-size: 11px;
  line-height: 1.5;
  text-align: left;
  /* NO reserved height here, and that is not an oversight: this is the ROW,
     where the 46 px mark sets the cluster's height and `align-items: center`
     puts whatever the line is against the middle of it. Reserving two lines
     instead makes a one-line thought sit at the TOP of an empty two-line box,
     a few pixels above the clock beside it — visible, and wrong the moment a
     tool verb replaces a sentence. The column layout below the compass is the
     one that needs the reservation, because there the line is what the mark
     is standing on. */
  height: auto;
  min-width: 0;
}
body.working:not(.empty) #elapsed {
  font-size: 10px;
  letter-spacing: 0.18em;
  flex-shrink: 0;
}

/* ----------------------------------------------------------- coming back
 *
 * THE ONE WAIT IN THIS PRODUCT THAT IS NOT A TURN, and it had no screen at
 * all. Opening a saved project pulls its files out of the store, puts the
 * process on them and RELOADS this page — so what a person saw between
 * clicking a project and seeing it was the project they were leaving, then a
 * flash of empty page, then the drop zone, for as long as the whole thing
 * took. Every one of those screens says the same wrong thing: nothing is
 * happening.
 *
 * OPAQUE, and that is the point rather than a style: it is the only element
 * here that has to survive a navigation, so it is armed before the POST, held
 * across the reload out of `sessionStorage`, and taken down when the geometry
 * is actually on screen. The vocabulary is the turn's — the same compass, the
 * same clock, the same quiet mono — because a person waiting on this product
 * should not have to learn a second thing that means "wait".
 */
#opening {
  position: fixed;
  inset: 0;
  z-index: 20;              /* over the veil (5), the compass (11) and the
                               projects overlay (14) — and UNDER the refusal,
                               which went to 22 for exactly that reason */
  display: grid;
  place-items: center;
  background:
    radial-gradient(56% 46% at 50% 42%, rgb(var(--accent-rgb) / 0.030) 0%,
                    transparent 70%),
    var(--ground);
  opacity: 1;
  transition: opacity 340ms ease;
}

/* Faded rather than cut, because the geometry arriving underneath is the whole
   reward and a hard swap steals it. `hidden` follows on the transition's end
   (`app.js::endOpening`), so nothing is left taking the pointer. */
#opening.out { opacity: 0; }
#opening[hidden] { display: none; }

#opening-inner {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  padding: 0 6vw;
  animation: fade-up 500ms cubic-bezier(.2, .8, .2, 1);
}

#opening-mark { width: 132px; height: 132px; }

/* THE NAME IS THE MESSAGE. It is the one thing a person wants confirmed —
   that the thing coming back is the thing they clicked — so it is the biggest
   type on the screen. It was in the SERIF until 2026-08-26, on the argument
   that the serif is the product's own voice; that argument went with the
   system. Georgia is the site's `h1 em` and nothing else, so a sentence set
   in it reads as a pull quote rather than as the product speaking. */
#opening-name {
  font-family: var(--sans);
  font-size: 21px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: rgb(var(--ink-rgb) / 0.82);
  max-width: min(38ch, 80vw);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ...and under it, what is being done and for how long. Mono, at the chrome's
   own weight: a verb and a clock, never a progress bar — there is no honest
   percentage to draw, and a bar that guesses is worse than a number that is
   true. */
#opening-note {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(var(--ink-rgb) / 0.30);
  font-variant-numeric: tabular-nums;
  min-height: 1.4em;
}

#opening-clock:not(:empty)::before {
  content: " · ";
  color: rgb(var(--ink-rgb) / 0.22);
}

@media (prefers-reduced-motion: reduce) {
  #opening, #opening-inner { transition: none; animation: none; }
}

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

/* THE WORDMARK, and it is in the corner on EVERY screen now — the title is
   the product's sentence and no longer its name, so this is the only place
   the name appears at all. It was ink-at-50% and hidden until there was a
   model; it is the site's own wordmark and always up. It is a control: the
   same New project action as `#places-new`, quiet, no new chrome. */
/* ONE ROW, and the row is what is positioned. Two fixed elements stacking
   put the pastille across the bottom half of the wordmark on the empty
   screen, because each was placed against the viewport and neither could
   know how tall the other was. */
#brand {
  position: fixed;
  top: 26px;
  left: 26px;
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 18px;
  pointer-events: none;
}

body.empty:not(.booting) #brand { top: 33px; left: 40px; }

#mark-live {
  /* A <button> ships a border, padding and a grey chip. Kill that chrome
     so the wordmark still reads as the name, not a control that grew a
     box — `#brand` is `pointer-events: none` so the row does not eat
     orbits, and this turns them back on for itself alone, like `#place`. */
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
  pointer-events: auto;
  transition: opacity var(--wake);
}

#mark-live:hover,
#mark-live:focus-visible { opacity: 0.7; }
#mark-live:focus { outline: none; }

/* On the empty screen it is the mark on a masthead rather than a corner
   label, so it takes the site's own 20 px and sits in the 86 px bar. */
body.empty:not(.booting) #mark-live { font-size: 20px; }

/* THE FILET UNDER THE MASTHEAD, and only while the composition is up: on the
   part screen there is no bar, because a rule across the top of a full-bleed
   viewer is a frame around somebody's geometry. */
#topline {
  position: fixed;
  left: 0;
  right: 0;
  top: 86px;
  z-index: 7;
  border-top: 1px solid rgb(var(--ink-rgb) / 0.14);
  opacity: 0;
  transition: opacity 500ms ease;
  pointer-events: none;
}

body.empty:not(.booting) #topline { opacity: 1; }

/* The masthead's own metrics, on the one screen that has a masthead. */
body.empty:not(.booting) #account { top: 28px; right: 40px; gap: 22px; }

/* 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: 18px;
  /* 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;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-mute);
  /* IT MOVES NOW, and it did not before. A run count changed once a turn;
     credits fall every few seconds while the agent works (`auth.js`
     `refreshQuota`), so the number is a live one and the eye is drawn to it
     with the smallest thing that can be: a colour, eased, and nothing else.
     No pulse, no count-up animation — the corner of a silent product is not
     where a widget goes. */
  transition: color 400ms ease;
}

/* NEARLY GONE, AND GONE. Two states and no sentence: the whole argument for a
   live number is that nobody has to be told twice.

   THERE IS NO ALARM COLOUR IN THIS PALETTE and one was not invented. An amber
   or a red would be the only warm hue on a page whose whole room is the site's
   own sky, and this product does not shout — even the one sentence it ever
   says (`#refused`) is plain white type. So `low` is the mute lifted one step
   to the soft ink, which is the same move a live label already makes here, and
   `out` is the acid — the site's single accent, spent exactly where the page
   already spends it: on the one thing that wants a look. */
#quota.low { color: var(--ink-soft); }
#quota.out { color: var(--accent-deep); }

/* HOW THE MOUSE MOVES THE VIEW. Same voice as the quota — mono, ten
   pixels, uppercase, mute — because it is a preference, not a tool.
   No panel, no border, no fill, no acid. The live word is just a shade
   brighter. Hidden with the empty stage, same as every other control
   that only exists once there is a part.

   UNDER THE WORDMARK, AND IT USED TO BE TOP CENTRE. That band is not free:
   the moment a turn starts, `body.working:not(.empty) #thinking` moves the
   compass, the clock and the model's own line up there and centres them on
   the same axis, so the two sat ON each other for the whole of every turn.
   Measured in Chromium 2026-08-29 on a 1280 px page: `#thinking` spans
   x 383→897, `#nav` x 571→709, y 28→39 — inside it on both axes, with three
   mono words printed across a sentence.

   Left, under `#brand` (26/26, 28 px tall), rather than west with the layer
   switches: those are pills you press and this is still a preference you set
   once, and it keeps the wordmark's own margin so the top-left column reads as
   one column. Jules's call, 2026-08-29. */
#nav {
  /* A CHILD OF `#brand` SINCE 2026-08-29, so it has no corner of its own and
     cannot collide with one: the row is positioned, this rides in it. Third
     address in two days — top centre collided with the turn's own cluster,
     under the wordmark read as a second row of chrome. `pointer-events` back
     on, because `#brand` turns them off for the row and each control turns
     them on for itself. */
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-left: 4px;
  pointer-events: auto;
}

#nav[hidden],
body.empty #nav,
/* ...AND IT STANDS DOWN FOR THE TURN. Jules, 2026-08-29, after the move into
   the brand row made the collision come back from the other side: that row
   now runs to x=406 with a short project name and up to ~562 with a long one
   (`#place` caps at 240), while `body.working:not(.empty) #thinking` centres
   the compass, the clock and the model's line on the page and starts at
   x=383 on a 1280 px window. Both measured. There is no width the centred
   cluster can take that clears a left corner that wide — the arithmetic gives
   it 156 px, and the compass alone is 46 — so one of the two has to yield for
   the duration, and it is the one that is a PREFERENCE rather than the state
   of the work. Nothing is lost: the part still orbits, and the setting is
   there again the moment the turn ends. */
body.working #nav { display: none !important; }

#nav button {
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  color: var(--ink-mute);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--wake);
}

#nav button[aria-pressed="true"] { color: var(--ink); }

#nav button:hover { color: var(--ink-soft); }

#nav button[aria-pressed="true"]:hover { color: var(--ink); }

/* 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.
   Pointer events come from `#session-actions`. */
#signout {
  padding: 9px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: none;
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--ink);
  cursor: pointer;
  transition: border-color var(--wake), background var(--wake);
}

/* ONE SIZE, EVERYWHERE. This rule used to carry `padding: 11px 15px` and
   `font-size: 12px` as well — the site's own ghost-button weight for the
   masthead, the quieter contour over a part — and the cost was a button that
   changed shape under the pointer: 79x34 on the empty screen, 71x32 the moment
   the first build landed, in a corner nobody is looking at while it happens.
   Jules, 2026-08-29: fix its size. What is left is the one thing that was never
   about size — the masthead's brighter contour, because there it is a button on
   an empty page rather than one of twelve over somebody's geometry. */
body.empty #signout { border-color: rgb(var(--ink-rgb) / 0.42); }

#signout:hover { background: rgb(var(--ink-rgb) / 0.09); border-color: var(--line-live); }
#signout[hidden] { display: none !important; }

/* The shop, and it is set EXACTLY as Sign out is — same mono, same size, same
   shade — because it is the same kind of thing: a control in the chrome that
   must be ignorable for the whole of a session. It is deliberately not
   brighter than the quota it sits beside. Nothing on this page is a call to
   action; the geometry is. */
#buy {
  pointer-events: auto;
  text-decoration: 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);
}

#buy:hover { color: var(--ink-soft); }
#buy[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
 * above the PICKER's, which is the one screen that can produce a refusal
 * without closing first: `switchTo` closes the overlay before it says anything
 * and a rename does not, because the card being renamed is the context for
 * what went wrong. At z-index 12 that sentence was painted underneath it. 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;
  /* ABOVE THE OPENING SCREEN (20), and that is the same argument that put it
     above the veil. A file dropped while a project is coming back IS refused
     — it would otherwise land in whichever workspace the process reached
     first — and at 15 the sentence saying so was painted UNDERNEATH the
     cover: present, correct, and invisible. Found by looking at
     `dev/_scratch/opening/6-refused.png`, on a check that read `textContent`
     and was perfectly happy. */
  z-index: 22;
  max-width: min(560px, 78vw);
  text-align: center;
  /* THE CHAPÔ, which is the system's own name for a sentence set under a
     title: sans, 17 px, 1.55. It was the serif in italic, and on the old
     system that read as prose; Georgia is reserved for the one emphasised
     word in a title now, so the same declaration made the one sentence this
     product says look like a quotation from somewhere else. */
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.55;
  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 */

/* WHAT LEAVES, and there are two of them now. This says WHERE; the paint and
   the metrics of the STEP button are `#go`'s, above — the two never appear on
   the same screen, so wearing the same paint costs nothing. */
#exports {
  position: fixed;
  right: 26px;
  bottom: 26px;
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* THE PAIR STACKS BEFORE IT PUSHES THE MARK BAR OVER. Two 48 px buttons side
   by side are 366 px wide, and this box is centred against a bar that is
   centred: below ~1120 the two meet, and at 1024 they overlapped by 27 px —
   measured, not guessed, on the day Import took Download STEP's metrics.
   Stacked, the corner is one button wide and Download STEP keeps the actual
   corner, which is where a hand goes for it. */
@media (max-width: 1120px) {
  #exports {
    /* `column`, not `column-reverse`: `#code` is first in the DOM, so this is
       what leaves Download STEP on the bottom row — the actual corner. */
    flex-direction: column;
    align-items: flex-end;
  }
}

/* ------------------------------------------------- the west corner
 *
 * Three clusters, one column, bottom left. `#west` stacks the gap key,
 * the layer switches and inspect so hidden children take no gap — the scale that
 * used to be hardcoded at `bottom: 72px` just rides above whatever is
 * showing.
 *
 * The wrapper eats no pointer: empty space in the column is the viewer,
 * and the scale card already opts out so a drag across the legend still
 * orbits. The pills opt back in.
 */

#west {
  position: fixed;
  left: 26px;
  bottom: 26px;
  z-index: 9;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  pointer-events: none;
}

/* The pills take the pointer back. `#scale` stays `none` so a drag across
   the legend still orbits — `#west > * { auto }` would have beaten that
   rule on specificity and made the key a dead patch of the viewer. */
#layers { pointer-events: auto; }
#inspect { pointer-events: auto; }

/* ONE ROW, BOTH CLUSTERS. `#west` is a column and this is the one rung of it
   that is a row: the layer menu's trigger and the three knives on one line,
   which is what draw2cad always looked like and what mesh2cad now looks like
   too (Jules, 2026-08-31 — "je voudrais qu'il n'y ait qu'une ligne de boutons
   comme en draw2cad pas 2").
   `flex-start` and not `center`: `#inspect` grows a slider UNDER its pills, so
   centring the row would walk the View pill downward the moment a plane came
   on. Aligned at the top, the pills stay on one line and the slider hangs off
   the bottom of the row, which is where it already was.
   It takes no pointer of its own — the gap between the two clusters is the
   viewer, and a drag across it still orbits. Its children opt back in. */
#tools {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 6px;
  pointer-events: none;
}
/* THE DRAWING. It wore the west column's clothes — a pill with its own ground
   and a blur, because it sat directly on the scene — until 2026-08-29 put it
   in `#mark-row`. Inside the bar's panel it is one of the two buttons there,
   so its whole look now comes from `#marks button` and the only thing left
   here is the pressed state, which is that cluster's idiom rather than the
   column's. It is still the one control in the bar that is live DURING a
   turn: reading your own sheet while the agent reads it changes nothing and
   starts nothing. */
#sheet-open[hidden] { display: none !important; }

#sheet-open[aria-pressed="true"] {
  border-color: var(--line-live);
  background: rgb(var(--ink-rgb) / 0.09);
  box-shadow: inset 3px 0 0 var(--accent-deep);
  color: var(--ink);
}

/* Same idiom as Download STEP — no panel, no labels above them. */

#layers {
  display: flex;
  gap: 6px;
}

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

/* THE PANEL. `#fai`'s idiom, because it is the same thing — a card of chrome
   over somebody's part, in the same column — at the size of four switches.
   IT IS A ROW OF `#west`, IN THE FLOW, and `position: absolute` is what it is
   not. Absolute was the first try and one screenshot killed it: this column
   also holds the gap key, the dimensions report and the run's own question,
   and the floating panel drew straight through the *Happy with this build?*
   card. In the flow it rides above the pills the way everything else in this
   column rides above what is under it, and the trigger it belongs to does not
   move when it opens. */
#layers-panel {
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  min-width: 132px;
  padding: 6px;
  background: rgb(var(--ground-rgb) / 0.72);
  border: 1px solid rgb(var(--accent-rgb) / 0.42);
  border-radius: var(--radius);
  pointer-events: auto;
}

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

/* Left-aligned in the panel and not centred like a pill: it is a list of
   states being read down, and four labels of four different lengths centred in
   a column have no edge for the eye to run along. */
#layers-panel button {
  width: 100%;
  text-align: left;
}

/* THE PILL AND THE PANEL ARE ONE CONTROL, so they are one width: the panel is
   as wide as its longest label and the trigger under it stops looking like a
   66 px tab stuck to a 140 px card. */
#layers-panel { min-width: 132px; }

/* The grammar of the panel, drawn: two switches over what is on screen, then
   two measurements of the gap that are exclusive of each other. */
.layers-rule {
  height: 1px;
  margin: 2px 1px;
  background: rgb(214 228 220 / 0.16);
}

/* WHAT THE CLOSED PILL HAS TO SAY. Four borders used to answer "what is on
   screen" without being asked; behind a menu that is one border, so the
   trigger carries the acid whenever the view is NOT the plain one — a solid
   hidden, or a gap view up. `paintLayers` owns which. */
/* THE CARET POINTS WHERE THE PANEL GOES, which on a cluster 26 px off the
   bottom of the window means UP while it is shut. It looks inverted next to a
   web dropdown and it is not: the panel opens upward, so shut is "it will come
   up from here" and open is "press to fold it back down". A caret pointing
   down at a menu that cannot open downward would be the only thing in this
   corner that lies. */
#layers-menu::after {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  margin-left: 7px;
  vertical-align: 1px;
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
  border-bottom: 4px solid currentColor;
  opacity: 0.7;
}

#layers-menu[aria-expanded="true"]::after {
  border-bottom: 0;
  border-top: 4px solid currentColor;
}

/* ONE IDIOM, TWO BOXES. The trigger lives in `#layers` and the four switches
   live in `#layers-panel`, which is its SIBLING in the column rather than its
   child — so every rule below has to name both, or the panel's buttons come up
   as bare browser buttons. Caught the moment the panel moved into the flow. */
#layers button,
#layers-panel button {
  padding: 9px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  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"],
#layers-panel button[aria-pressed="true"] {
  border-color: var(--line-live);
  background: rgb(var(--ink-rgb) / 0.09);
  box-shadow: inset 3px 0 0 var(--accent-deep);
  color: var(--ink);
}

#layers button:disabled,
#layers-panel 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/try_layers.py`'s own screenshots. So hover is a warmth behind it, and
   the acid stays the property of the state. */
#layers button:not(:disabled):hover,
#layers-panel 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,
#layers-panel button.refused { animation: refuse 700ms ease; }

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

/* ------------------------------------------------ the dimensions report
 *
 * THE SURFACE THE 3D OVERLAY COULD NOT BE. A missing figure has nowhere to
 * hang a witness line, and two numbers that are centimetres apart in model
 * space still land on each other from the default camera. A row can hold
 * both. It is a report, not a first-article inspection: the same columns
 * (kind, view, nominal, measured, verdict) without claiming a FAI process.
 *
 * THIS BOX SITS IN `#west`. `#stage` stays full-bleed and painted; the
 * table is chrome in the same column as the pills, so inspect cannot land
 * under a `bottom: 72px` slab. Hidden children take no gap. Pointer-events
 * only on this box, so the rest of the window is still the part.
 */

#fai {
  display: flex;
  flex-direction: column;
  /* WIDE ENOUGH FOR ITS OWN TABLE, and never a question anybody is asked.
     Jules, 2026-08-29: the report resizes UP and only up, and its width is set
     to the right one so nothing ever scrolls sideways.
     THE NUMBER COMES OFF THE COLUMNS. Seven of them, all sized below in the
     table's own 10 px mono — 2.4 + 14 + 6.2 + 7.2 + 6.4 + 6.4 + 5.6 = 48.2em =
     482 px — plus the scrollbar gutter, which is reserved rather than
     conditional so the panel does not change width the moment the rows
     outgrow it. It was 480 with the CHAR column unsized, which is where the
     sideways scrollbar came from.
     NOT `max-content`, which was the first try: with a fixed table layout the
     max-content width is still pushed out by the longest word in a cell, so
     one long note from the model made the panel 671 px and then 656 px when
     the scrollbar came and went. Measured, both. */
  width: min(500px, calc(100vw - 80px));
  max-height: min(26vh, 200px);
  background: rgb(var(--ground-rgb) / 0.58);
  border: 1px solid rgb(var(--accent-rgb) / 0.50);
  pointer-events: auto;
}

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

/* THE GRIP, and there is one. `position: relative` on the panel so it can hang
   off the top edge; `#west` is the fixed column and this is a block inside it,
   so no new stacking problem.
   8 px and not 4: this is a hairline panel on a dark ground, and a 4 px target
   on a 1 px contour is a control found by accident. It sits half outside the
   box for the same reason — the pointer meets it just before it leaves the
   panel, which is where a hand aims.
   THE TOP EDGE AND NO OTHER. The bottom and left are the column's anchor, and
   the width is the table's own; the only thing left to choose is how many rows
   are on screen at once. */
#fai { position: relative; }

.fai-grip {
  position: absolute;
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  z-index: 2;
}

.fai-grip-n { left: 8px; top: -4px; height: 8px; right: 8px; cursor: ns-resize; }

/* NOTHING UNTIL THE POINTER IS ON IT. The report is chrome over somebody's
   part; three permanent handles round it would be three more things on a page
   whose whole argument is that there is almost nothing on it. */
.fai-grip:hover,
#fai.gripping .fai-grip {
  background: rgb(var(--accent-rgb) / 0.28);
}

/* A drag that leaves the panel must not select the table under it. */
#fai.gripping { user-select: none; }

#fai-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  flex: 0 0 auto;
  padding: 12px 14px 10px;
  border-bottom: 1px solid rgb(214 228 220 / 0.20);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgb(var(--accent-rgb) / 0.90);
}

#fai-score {
  letter-spacing: 0.08em;
  color: rgb(214 228 220 / 0.40);
}

#fai-scroll {
  flex: 1 1 auto;
  min-height: 0;
  /* UP AND DOWN ONLY. The panel is sized to the table above, so there is
     nothing to the left or right to reach; `hidden` says so rather than
     leaving a scrollbar to appear over a rounding error.
     `scrollbar-gutter` so the columns do not shift 15 px sideways the first
     time the rows outgrow the box — the report is a table of numbers somebody
     is comparing, and a column that moves while they read it is worse than a
     strip of unused space. */
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-gutter: stable;
}

#fai table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

#fai th {
  padding: 8px 8px 7px;
  border-bottom: 1px solid rgb(214 228 220 / 0.20);
  color: rgb(214 228 220 / 0.40);
  font-weight: 500;
  text-align: left;
  letter-spacing: 0.08em;
}

#fai td {
  padding: 5px 8px;
  color: rgb(214 228 220 / 0.80);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#fai th:nth-child(1), #fai td:nth-child(1) { width: 2.4em; }
/* CHAR IS THE ONE THAT USED TO HAVE NO WIDTH, and it held the table together
   only as long as the panel had a fixed one of its own. It carries the
   figure — `Ø10`, `4× R6` — or the model's own note, which has no bound, so it
   is the column that WRAPS: `table-layout: fixed` wraps rather than widens,
   and the report scrolls up and down. 14em puts the seven columns at 48.2em,
   which is 482 px at this 10 px mono — the width the panel was designed at
   before any of this, arrived at from the columns instead of guessed. */
#fai th:nth-child(2), #fai td:nth-child(2) { width: 14em; }
#fai th:nth-child(3), #fai td:nth-child(3) { width: 6.2em; }
#fai th:nth-child(4), #fai td:nth-child(4) { width: 7.2em; }
#fai th:nth-child(5), #fai td:nth-child(5),
#fai th:nth-child(6), #fai td:nth-child(6) { width: 6.4em; }
#fai th:nth-child(7), #fai td:nth-child(7) { width: 5.6em; }

#fai tbody tr:nth-child(even) { background: rgb(255 255 255 / 0.028); }

#fai .fai-n { color: rgb(var(--accent-rgb) / 0.85); }
#fai .fai-ok { color: rgb(var(--accent-rgb) / 0.95); }
#fai .fai-bad { color: rgb(232 116 84 / 0.95); }
#fai .fai-wait { color: rgb(214 228 220 / 0.35); }

/* ---------------------------------------------------------------- inspect
 *
 * Same pills as `#layers`, and the same west column. The slider is the
 * Fusion offset: it only exists while a plane is cutting, and the millimetre
 * readout is the only number this cluster paints. The accent stays on the
 * ON state of a plane, never on the thumb — a slider is a preference, a
 * plane being ON is the thing you pressed. */

#inspect {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

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

#inspect-planes {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

#inspect-planes button {
  padding: 9px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  cursor: pointer;
  transition: background var(--wake), color var(--wake), border-color var(--wake);
}

#inspect-planes button[aria-pressed="true"] {
  border-color: var(--line-live);
  background: rgb(var(--ink-rgb) / 0.09);
  box-shadow: inset 3px 0 0 var(--accent-deep);
  color: var(--ink);
}

#inspect-planes button:disabled { opacity: 0.28; cursor: default; }

#inspect-planes button:not(:disabled):hover { background: rgb(var(--ink-rgb) / 0.10); }

#inspect-planes button[hidden] { display: none !important; }

#inspect-slide {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 2px 0;
}

#inspect-slide[hidden] { display: none !important; }

#inspect-range {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  height: 2px;
  margin: 0;
  background: var(--rule);
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}

#inspect-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 10px;
  height: 10px;
  background: var(--ink);
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}

#inspect-range::-moz-range-thumb {
  width: 10px;
  height: 10px;
  background: var(--ink);
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}

#inspect-mm {
  min-width: 3.2em;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink-mute);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

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

#pulse {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 10;
  opacity: 0;
  background: linear-gradient(90deg,
              rgb(var(--accent-rgb) / 0) 0%, var(--accent-deep) 50%,
              rgb(var(--accent-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; }

/* ...AND ITS WRAPPER PAINTS A FLAT GREY OVER THE SKY. The canvas is genuinely
   transparent — `alpha: true`, `setClearColor(0xffffff, 0)` — so `#stage`'s
   nebulae and stars were always meant to show around the part. What hid them
   is one CSS line in the vendored sheet: `.tcv_cad_viewer` takes
   `--tcv-bg-color`, and the viewer stamps `data-theme="dark"` on `#stage` and
   on `body`, which resolves it to `#444`. Measured in a browser, 2026-08-29:
   the stage computed `rgb(6 6 8)` and the wrapper on top of it `rgb(68 68 68)`.
   Every "the viewer background went grey" report is this rule. */
.tcv_cad_viewer { background: none !important; }

/* -------------------------------------------------------------- the gap key
 *
 * The only numbers this product puts on screen, and they earn it: a colour map
 * whose colours mean nothing is a decoration, and two lobes with no cubic
 * millimetres beside them are a disagreement nobody can size. Sits above the
 * layer cluster, same corner, same type — it belongs to those switches, and
 * there is one box because only one of the two views is ever up.
 *
 * GREEN AND RED WERE NOT SWAPPED FOR THE METROLOGY CONVENTION, and that
 * decision now applies to one of the two views rather than to both. It was
 * written here when Diff was the only gap: 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. The Volume switch still keeps exactly
 * that vocabulary — its lobes are `mesh_view.ADD` and `mesh_view.REMOVE`, the
 * same two hexes the agent's own contact sheet is painted in. The DIFF map is
 * the metrology rainbow now (`DEVIATION_RAMP` in `app.js`), where green is not
 * a direction at all but the nominal band at zero, so there is nothing left
 * for it to clash with. The stops are NOT repeated in this file: the bar's
 * gradient is set from that one array, because the version of this rule that
 * kept its own named `#9aa3ad` at the middle while the painter used `#5c6269`.
 */
#scale {
  width: 224px;
  padding: 12px 14px 11px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--panel);
  pointer-events: none;
}

#scale[hidden] { display: none !important; }
#scale-map[hidden], #scale-gap[hidden] { display: none !important; }

/* No `background` here on purpose — `paintScale` sets it from the same seven
   stops the pixels are interpolated from. See the note above. */
#scale-bar {
  height: 6px;
  border-radius: var(--radius);
}

/* NOWRAP, AND THE WIDTH ABOVE IS SIZED FOR THE WIDEST THIS EVER READS. The
   scale is auto-fitted, so the graduations are not a fixed length: on a model
   that is far off the limit reached ±11.0 mm and the three of them wrapped —
   `-11.0` over `mm`, `2.68 mm` over `mean` — a legend in four lines under a
   picture whose whole claim is that it is readable at a glance. Seen on
   `dev/_scratch/layers/12-diff-far.png` and nowhere else, because every
   earlier shot of this box was of a model close enough to read ±0.4 mm. */
#scale-marks {
  display: flex;
  justify-content: space-between;
  gap: 6px;
  margin-top: 6px;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: #bed1c8;
  font-variant-numeric: tabular-nums;
}

/* THE MEAN IS EMPHASIS INSIDE A MONO ROW, AND EMPHASIS HERE IS WHITE. It wore
   the accent, which was harmless while the accent was cream and is a clash now
   that it is a yellow-green: this legend sits directly under the metrology
   ramp, whose own stops run through yellow and green, and the handoff's
   `Mesures` page says in as many words that the ramp is what an accent has to
   be chosen against. The ramp is the one thing in the product that may not be
   recoloured, so the accent is what moves. */
#scale-mid { color: var(--ink); }

/* THE VOLUME VIEW'S KEY: one row per lobe the server actually wrote, and — when
   it wrote none — the one sentence saying which precondition refused it. Rows
   are built as nodes by `paintGapKey`; the swatch colour is the server's own,
   so nothing here may name a colour. */
.gap-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 5px;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.05em;
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}

.gap-row:last-child { margin-bottom: 0; }

.gap-dot {
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-radius: var(--radius);
}

/* The sentence, and it is the one thing in this cluster that is prose. It
   earns it: a view that cannot be computed has to say so where the view would
   have been, or the switch reads as broken. Set in the page's serif, small and
   dim, so it is a caption and never a status line — and it is only ever the
   SERVER's own words about a precondition, never anything an agent wrote. */
.gap-why {
  margin: 8px 0 0;
  padding-top: 8px;
  border-top: 1px solid var(--line);
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.42;
  color: rgb(var(--ink-rgb) / 0.52);
}

/* ------------------------------------------------------------- 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 {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 7px;
  max-width: 240px;
  gap: 9px;
  padding: 7px 12px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  /* THE PANEL, like every other rectangle on this page. It was the site's ink
     #17332c — the handoff draws it that way, and on a green ground that is a
     panel one step up from the room. On the constellation's sky it was the
     ONLY green object on the screen and read as a leftover from another
     palette; seen in `dev/_scratch/home/6-done.png`. The acid dot is what
     carries it, and that has not moved. */
  background: var(--panel);
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  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;
}

/* 6 px, square, at full strength: it is one of the three or four state marks
   the accent is allowed outside a pressable. */
#place-dot {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: var(--radius);
  background: var(--accent-deep);
}

/* 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(var(--ground-rgb) / 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;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* THE TWO GESTURES ARE TWO CONTROLS. New project used to be a card in the
   same grid as every saved project — first among equals, acid-outlined —
   so starting work and resuming it read as the same click. It sits in the
   header now, wearing the same decision-colour as Build the CAD, and the
   list under it is only things that already exist. */
#places-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin: 0 0 16px;
}

#places-new {
  flex: none;
  padding: 8px 14px;
  border: 0;
  border-radius: var(--radius);
  background: var(--accent-deep);
  color: var(--on-accent);
  font-family: inherit;
  font-size: 12px;
  font-weight: 750;
  letter-spacing: 0;
  cursor: pointer;
  transition: background var(--wake), transform 120ms ease;
}

#places-new:hover { background: #e5f5a8; }
#places-new:active { transform: translateY(1px); }

#places-empty {
  margin: 8px 0 0;
  font-family: inherit;
  font-size: 13px;
  color: var(--ink-mute);
}

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

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

/* A CELL, NOT A CARD, is what the grid lays out now. The card is still the
   button that opens the project and fills the whole of it; the pencil is a
   sibling in the corner, because a button inside a button is invalid HTML and
   the browser hoists the inner one clean out of the outer. */
.place-cell {
  position: relative;
  display: flex;
}

.place-card {
  flex: 1;
  min-width: 0;
  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 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;
  /* ROOM FOR THE PENCIL, which is drawn over this line's right end. Without
     it a long folder name runs underneath the control and the ellipsis never
     fires — `IMG_4127-bracket-2` reads as complete with a glyph sitting on
     its last character. The meta line under it keeps the full width. */
  padding-right: 16px;
}

.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); }

/* The rename, and it is quiet without being hidden. A picker is a list of
   names to click, not a list of names to edit — but a control nobody can see
   is a control nobody uses, and nothing else on this screen says a project can
   be renamed at all. So it is drawn at rest, faintly, and comes up to full
   under the pointer. `:focus` is on the same rule: a keyboard reaching a
   control at `opacity: 0` would tab to nothing. */
.place-rename {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-mute);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.4;
  transition: opacity var(--enter);
}

.place-cell:hover .place-rename,
.place-rename:focus { opacity: 1; }
.place-rename:hover { color: var(--ink); background: rgb(var(--ink-rgb) / 0.10); }

/* ...AND THE BIN, beside it, on the same terms — and in the same INK.
   This palette has exactly one colour, the acid, and it means "a decision worth
   making": New project wears it (the header button), Build the CAD wears it, Download STEP wears
   it. A red would be a second meaning invented for one control, on the only
   screen where nothing else is coloured at all. So the destructive thing is
   distinguished by what it ASKS rather than by what it looks like — the
   confirmation below is the whole of the safety, and it is unmissable in a
   way a tint never is.

   It sits LEFT of the pencil rather than in the opposite corner: the two are
   one pair of corner controls, and a delete on the far side of a card is a
   control somebody meets by travelling across the name they were reading. */
.place-delete { right: 32px; }

/* THE CONFIRMATION IS THE CARD, for the same reason the rename field is: one
   short question does not deserve a second overlay over the overlay. It
   covers the card rather than sitting under it, so the click that deletes is
   never within a few pixels of the click that OPENS — and the name stays
   readable through it, because deleting the wrong project is exactly the
   mistake a dialog that hides the name invites. */
.place-confirm {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  padding: 0 10px;
  border-radius: var(--radius);
  /* LIGHT ENOUGH TO READ THE NAME THROUGH. Measured, not chosen: at 0.82 the
     card under it went to ~11 grey levels against the page and the project
     being deleted was legible only if you knew what it said — which defeats
     the one thing this strip is for. 0.55 keeps the two buttons crisp and the
     name at ~34. */
  background: rgb(var(--ground-rgb) / 0.55);
  backdrop-filter: blur(1px);
}

.place-confirm button {
  padding: 4px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-mute);
  font-family: inherit;
  font-size: 11.5px;
  cursor: pointer;
  transition: color var(--wake), border-color var(--wake),
              background var(--wake);
}

.place-confirm button:hover { color: var(--ink); border-color: var(--ink-mute); }

/* The one that acts is the one that is already lit — a confirmation whose
   default reading is "cancel" is a confirmation people click through. */
.place-confirm button.yes {
  color: var(--ink);
  border-color: rgb(var(--ink-rgb) / 0.34);
}

.place-confirm button.yes:hover {
  background: rgb(var(--ink-rgb) / 0.92);
  color: var(--on-accent);
}

.place-cell.confirming .place-rename,
.place-cell.confirming .place-delete { display: none; }

/* THE FIELD IS THE CARD, laid over it rather than beside it: renaming a
   project is editing the name that is already on screen, and a dialog for one
   short string would be a second overlay over the overlay. The card stays
   underneath and stays legible — the meta line still says when you were last
   there, which is half of what tells two projects apart. */
.place-field {
  position: absolute;
  left: 14px;
  right: 14px;
  top: 12px;
  padding: 2px 6px;
  border: 1px solid var(--accent-soft);
  border-radius: var(--radius);
  background: var(--ground);
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
}

.place-field:focus { outline: none; border-color: var(--accent-deep); }
.place-cell.renaming .place-name { visibility: hidden; }
.place-cell.renaming .place-rename { display: none; }

/* ------------------------------------------------------- what the user SAYS
 *
 * The marks cluster, and the only one that is not about looking at the
 * model — it is about answering it. Same idiom as the west-corner pills:
 * no panel of its own until the field arrived, no labels above them,
 * nothing at all until there is geometry to write on.
 *
 * Bottom CENTRE on purpose. `#west` (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;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  /* SIZED BY WHAT IS IN IT, since the field came off the page on 2026-08-29.
     There used to be four paragraphs of arithmetic here and every line of it
     was about one thing: giving a SENTENCE a readable measure, and then
     keeping that measure clear of `#layers` on the left and `#exports` on the
     right, on a box that is centred and therefore pays for the wider side
     twice. The width was `max(280px, min(520px, calc(100vw - 832px)))` and the
     832 was measured, twice, on the merged tree.

     None of it survives the field. What is left in the bar is two buttons and
     Send — at its widest, Pin a comment beside Annotate the drawing with a
     long Send after them, under 480 px — so it is narrower than the measure it
     used to reserve and the clearance stops being a thing to compute. If a
     field ever comes back, so does that paragraph and so does the number. */
  width: auto;
  max-width: calc(100vw - 60px);
  /* AND NO PANEL ROUND THEM. It carried one from the day the field arrived —
     the sentence, the switches and Send were one thing a person addressed, and
     without a contour they were three objects floating on the ground with only
     the field to hold them together. The field is what justified it and the
     field is gone: what is left is two or three buttons that already have
     their own contours, and a second rectangle behind them reads as the prompt
     bar this product does not have. Jules, 2026-08-29, on the screenshot. */
  padding: 0;
  border: none;
  background: none;
}

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

#mark-row {
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}

/* THE TWO WAYS OF POINTING AT SOMETHING ARE ONE SIZE, and Jules asked for that
   by name: they are the same kind of gesture — here on the part, here on the
   sheet — and two buttons of different widths read as a primary and a
   secondary. `flex: 1 1 0` rather than a `min-width` fitted to the longer
   label: the basis is zero for both, so they split whatever the row has and
   stay equal if either label is ever reworded or translated. Send is not in
   that division — it is not a way of pointing, it is the decision after one,
   and its label grows with what is pinned ("Send 2 comments + 1 region"). */
/* THE TWO WAYS OF POINTING AT SOMETHING ARE ONE SIZE, and Jules asked for that
   by name. A GRID OF EQUAL COLUMNS and not `flex: 1 1 0`, which was the first
   try and does not work here: this bar is shrink-to-fit, so there is no free
   space for flex to distribute and each button came out at its own max-content
   — 105 px beside 140 px, measured. `grid-auto-columns: 1fr` sizes every
   column to the widest item instead of to its own, which is the property being
   asked for, and it degrades correctly: hidden children leave grid flow, so
   mid-turn there is ONE column and Drawing is its own width rather than half a
   row with a hole in it.
   `nowrap` because the equal column is the WIDER label's, and without it the
   shorter button keeps one line while the longer takes two — 122x44 beside
   122x32, also measured. */
#mark-points {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 6px;
}

#mark-pin,
#sheet-open { white-space: nowrap; }

#mark-send { flex: 0 0 auto; }

/* WHAT THE PART IS WRONG ABOUT WHEN NO FACE IS WRONG. A rounded rectangle and
   not a pill: a pill is a control you press, and this is a place you write.

   ONE LINE, AND IT STAYS ONE LINE. It used to grow to four as the sentence
   did, and Jules's call on 2026-08-24 is that a box which changes height is a
   panel arriving in instalments — this page has no panel, and the one place a
   person writes should not be the largest thing on screen. The scrollbar goes
   with it: with the height fixed, a long sentence scrolls under the caret, and
   a scroll TRACK drawn down the inside of a 42 px field is a piece of chrome
   wider than the ink it is measuring. Hidden in both engines, never
   `overflow: hidden` — that would put the end of the sentence somewhere the
   caret can reach and the eye cannot. */
#mark-said {
  display: block;
  box-sizing: border-box;
  width: 100%;
  height: 20px;
  min-height: 20px;
  max-height: 20px;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
  line-height: 1.45;
  resize: none;
  /* ONE LINE, ENFORCED BY THE WRAPPING AND NOT BY THE HEIGHT. Bounding the
     height alone was not enough and the screenshot is what said so: the box
     measured 42 px and rendered two lines of type inside it. `nowrap` makes a
     second line impossible rather than clipped, and the sentence scrolls
     sideways under the caret. */
  white-space: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;              /* firefox */
  transition: border-color var(--wake);
}

#mark-said::-webkit-scrollbar { width: 0; height: 0; }

#mark-said[hidden] { display: none !important; }
#mark-said:focus { outline: none; }

/* THE ONLY INSTRUCTION ON THIS PAGE, and it has to be readable to be one —
   the muted ink the rest of the chrome uses is for labels beside a control,
   not for a sentence somebody is meant to act on. */
#mark-said::placeholder {
  color: rgb(var(--ink-rgb) / 0.42);
  opacity: 1;
}

#marks button {
  padding: 9px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  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 acid belongs to the state, never to hover. */
#marks button[aria-pressed="true"] {
  border-color: var(--line-live);
  background: rgb(var(--ink-rgb) / 0.09);
  box-shadow: inset 3px 0 0 var(--accent-deep);
  color: var(--ink);
}

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

/* The decision, in acid, 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 {
  display: inline-flex !important;
  align-items: center;
  gap: 16px;
  border-color: transparent !important;
  background: var(--accent-deep) !important;
  color: var(--on-accent) !important;
  font-weight: 750;
}

#mark-send::after { content: "\2192"; font-size: 14px; line-height: 1; }

#mark-send:not(:disabled):hover { background: #e5f5a8 !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; }

/* HOW CAREFULLY TO BUILD IT — two words beside the receipt.
   It shares the receipt's lifetime exactly: shown with the staged file, gone
   the moment the turn starts, so nothing of it stands on screen while the
   agent works. `aria-checked` carries the state, so the selected look and what
   a screen reader announces cannot drift apart. */
#mode {
  /* EQUAL COLUMNS, on the drop zone's own width. Sized by content they read
     as unrelated buttons — "Max" is 92 px against "Fast"'s 160 — and the eye
     has to measure them before it can compare them. A grid of `1fr` on
     `#drop`'s `min(452px, 86vw)` makes the row a base under the card rather
     than a toolbar beside it, and the choice becomes one control with two
     faces. `grid-auto-columns` rather than `repeat(3, 1fr)`, so the row is as
     many equal columns as there are buttons and cannot be left a column
     short of its own width the next time a mode is added or retired — which
     is exactly what a hard 3 would have done on 2026-08-25, when `Auto` went
     and the grid would have gone on reserving a third of the row for it. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  width: 100%;
  gap: 8px;
  margin: 8px 0 0;
  /* IN FLOW EVEN WHEN IT IS NOT OFFERED. `display: none` here moved the drop
     zone ~50 px under the cursor between letting go of the file and pressing
     the button — `try_home.py` calls that "one footprint, two faces" and goes
     red on it. Reserving the row costs an empty strip on the idle screen and
     buys a composition that does not jump. */
  visibility: hidden;
}
body.staged #mode { visibility: visible; }
#mode[hidden] { display: flex; }
#mode button {
  appearance: none;
  /* The label and its gloss are centred on a column the content does not get
     to size, so a longer sentence wraps inside its own box instead of pushing
     its neighbours out of alignment. */
  min-width: 0;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink-soft);
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.3;
  /* TALLER SINCE THERE ARE TWO OF THEM. The row is the drop zone's width
     whatever it holds, so losing `Auto` did not leave a gap beside the
     buttons — it made each one HALF AS WIDE AGAIN for the same height, and a
     223 px box reads as a stretched strip rather than a control. The height
     follows the COUNT so the proportion does not: measured on this page, the
     old box is 4.65 wide for 1 tall and this is 3.48, which is about where
     three of them sat. Reported 2026-08-25, in those words — fill the space
     the third button left. `dev/try/try_fault.py` holds the ratio at 3.8, a bound
     the redproof had to tighten: reverting the padding alone lands at 4.29
     and passed the first threshold. */
  padding: 13px 15px 14px;
  text-align: left;
  transition: color .14s, border-color .14s, background .14s;
}
/* The gloss is a mono LABEL, not a second sentence — same rank as the quota
   and the format list, and it turns accent on the chosen one. That is the
   fourth and last state mark the accent is spent on. */
#mode button span {
  display: block;
  margin-top: 6px;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
#mode button:hover { border-color: var(--line-live); }
#mode button[aria-checked="true"] {
  background: rgb(var(--ink-rgb) / 0.06);
  box-shadow: inset 3px 0 0 var(--accent-deep);
  color: var(--ink);
}
#mode button[aria-checked="true"] span { color: var(--accent-deep); }
#mode button:focus-visible {
  outline: 2px solid rgb(var(--ink-rgb) / 0.55);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) { #mode button { transition: none } }

/* ------------------------------------------------------- was that any good
 *
 * THE ONLY THING ON THIS PAGE THAT ASKS THE USER ANYTHING, and the only
 * cluster that is about neither the model nor the agent: it is about the RUN.
 * A trace in obs says what the agent did and never what the person made of
 * it, so a build that came back wrong and one somebody downloaded and
 * machined read exactly alike. Two thumbs answer that in one bit.
 *
 * A NOTIFICATION, WHICH IS WHAT IT WAS NOT BEFORE. Three earlier shapes were
 * furniture standing in a corner whether or not anybody had an opinion — the
 * Download STEP corner (2026-08-24), the end of the mark-up bar, then the
 * middle of it as two bare glyphs — and Jules took the last of them off the
 * page on 2026-08-28. What is back on 2026-08-30 arrives when a build ends,
 * carries its own cross, and is gone the moment it is answered or dismissed.
 *
 * NOTHING HERE IS ADDRESSED TO THE AGENT. `#marks` is the channel that talks
 * to the model and every word in it rides the next turn; this one talks to
 * us, reaches no model and starts no turn (`server.record_feedback`). It
 * carries no field at all, which is the cheapest possible way to stay on the
 * right side of that fence — `dev/try/try_marks.py` and
 * `dev/try/try_feedback.py` hold it from both sides.
 *
 * IT IS A ROW OF `#west`, at the TOP of that column, and both halves of that
 * matter. A row, because the alternative is a fourth `position: fixed` box in
 * the bottom-left corner computing where `#layers` ends — the `bottom: 72px`
 * shape this column was built to stop repeating. At the top, because a card
 * added at the FOOT of the column would shove the layer switches and the
 * inspect row up when it appears and drop them back when it is dismissed:
 * a control moving under a cursor that is reaching for it, at exactly the
 * moment a turn ends and a hand goes for Deviation.
 */

#feedback {
  display: flex;
  align-items: center;
  /* The cross hangs off the end of the row rather than sitting a gap away
     from the thumbs: it is about the CARD and they are about the run. */
  gap: 10px;
  max-width: min(320px, 66vw);
  /* Less on the right, because `#fb-close` carries its own padding and a
     symmetric box would put the glyph 20 px from the edge; more on the left,
     because the acid edge below is drawn inside it. */
  padding: 8px 8px 8px 15px;
  /* `--line` and not `--rule`: the panel filet is for a rectangle that HOLDS
     something and is meant to recede. This one has to be found. */
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  /* THE ACID EDGE, and it is the page's own vocabulary rather than a
     decoration invented here: a 3 px inset bar down the left is what `#layers`
     and `#sheet-open` wear when they are LIVE. Jules, 2026-08-30 — *"on la
     voit à peine"* — and the first version was right about the doctrine and
     wrong about the product: a notification nobody notices collects nothing,
     which is the whole reason it exists. An inset shadow paints a hairline and
     occupies nothing, so it cannot push the row the way a fourth border
     would. */
  box-shadow: inset 3px 0 0 var(--accent-deep);
  backdrop-filter: blur(6px);
  /* IT ARRIVES AND IT LEAVES, and both are a TRANSITION rather than an
     animation — `#refused`'s lesson, and it is the same trap: the visible
     state of an animation lives in its `to` keyframe, so anything that stops
     the animation completing (a reduced-motion setting, a user stylesheet, a
     tab that is not composited and freezes its clock) leaves the element at
     its `from` and the card is simply never seen. On a transition the visible
     state is an ordinary declaration on `.in`: if it runs, it fades; if it
     never runs, the card is just there.
     THE CLASS IS WHAT MOVES, and `hidden` follows it a beat later
     (`paintFeedback`) — a transition only runs while the element is still
     RENDERED, and `display: none` in the same tick as the class skips it
     entirely. Measured on `#refused` over 181 frames: opacity 1 to 0 in ONE
     frame, with nothing in between. */
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--enter), transform var(--enter);
  /* `#west` eats no pointer so a drag across the empty column still orbits.
     Off until the card is really in: 340 ms of an invisible rectangle that
     swallows a click on the part is exactly the "control that looks live and
     does nothing" `paintMarks` already pays for. */
  pointer-events: none;
}

#feedback.in {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

#feedback[hidden] { display: none !important; }
#fb-ask[hidden], #fb-thanks[hidden] { display: none !important; }
#fb-up[hidden], #fb-down[hidden] { display: none !important; }

/* The question, and it is a question rather than a label. It was `--ink-soft`
   — the voice of a caption — and at 12.5 px on a dark panel that is what it
   read as. Full ink: this is the one sentence on the page addressed to the
   person rather than about the model. */
#fb-ask, #fb-thanks {
  margin: 0;
  flex: 1 1 auto;
  color: var(--ink);
  font-size: 12.5px;
  line-height: 1.35;
}

/* ...and the acknowledgement in the acid, because by then it is not a
   question any more: it is the product confirming, next to the thumb that is
   lit in the same colour. */
#fb-thanks { color: var(--accent-deep); }

#fb-thumbs {
  display: flex;
  flex: none;
  gap: 2px;
}

/* THE TWO THINGS TO PRESS, IN THE COLOUR OF EVERY ACTION ON THIS SITE. They
   were bare `--ink-soft` marks, on the argument that the acid is spent on what
   you press and never decoratively — which was the right rule read the wrong
   way round: these ARE what you press, and they are the only thing in this
   card that does anything. Outlined rather than filled at rest, so the filled
   state below still has somewhere to go. */
#fb-thumbs button {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 28 px of TARGET round a 16 px glyph: the mark is small, the thing a
     finger has to hit is not. */
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--accent-soft);
  border-radius: var(--radius);
  background: transparent;
  color: var(--accent-deep);
  cursor: pointer;
  transition: color var(--wake), background var(--wake),
              border-color var(--wake);
}

/* Filled, exactly like Download STEP's hover — and `--on-accent` because
   white on #d8ef62 is unreadable and that pair is the site's own button. */
#fb-thumbs button:hover {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
  color: var(--on-accent);
}

/* PRESSED IS A STATE AND NOT A HOVER — the same rule the layer switches live
   under: the acid belongs to what is true, never to where the pointer is. It
   is a state anybody can SEE since 2026-08-30: the thumb that was pressed
   stays on screen beside the acknowledgement, lit, and the other one goes.
   Before that both were hidden the instant one was pressed, which made this
   rule the shape the root `CLAUDE.md` calls the compare lobes — correct, live
   and unreachable. */
#fb-thumbs button[aria-pressed="true"] {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
  color: var(--on-accent);
}

#fb-thumbs button:focus-visible {
  outline: 1px solid var(--accent-soft);
  outline-offset: 1px;
}

/* The way out, in `#scripts-close`'s own clothes — this page has one cross
   and it should look like itself wherever it appears. */
#fb-close {
  flex: none;
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: var(--ink-mute);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--wake);
}

#fb-close:hover { color: var(--ink); }

/* The label a screen reader needs and a 28 px square has no room for. */
#feedback .vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Somebody who asked for less motion still gets the card, at once — the same
   shape as `#refused`'s own reduced-motion rule. `.in` is still added and
   still removed; it simply arrives and goes without the travel. */
@media (prefers-reduced-motion: reduce) {
  #feedback { transition: none; transform: none; }
  #feedback.in { transform: none; }
  #fb-thumbs button, #fb-close { transition: none; }
}

/* ------------------------------------------------------- the OTHER export
 *
 * Import in your CAD software. The STEP is the only file that leaves. This
 * button opens the Faber desktop waitlist — same cream/ink card as the rest
 * of the page, no second brand colour, no Python.
 *
 * DOWNLOAD STEP'S OWN SIZE, and the numbers are arithmetic rather than taste.
 * `#step` is an `<a>` with 15/17 of padding on a 12 px label and no border,
 * and it INHERITS the 1.5 line-height the page sets on `html, body` — 18 + 30
 * = 48 px. This one is a `<button>`, and a button does not inherit that: the
 * UA's `font` shorthand resets line-height to `normal`, which is ~1.2 and made
 * the first spelling of this rule 44 px against 48. Measured, not read — a
 * button and an anchor styled from the same numbers are two different heights.
 * So: the line-height said out loud, and 14/16 of padding, because the 1 px
 * border has to come out of the 15/17. 18 + 28 + 2 = 48.
 * It was 9/13 on an 11 px label, 33 px against 48 in the same row, and a
 * button two thirds the height of the one beside it reads as its caption.
 * NOT the same paint: the acid is spent on the file that actually leaves.
 */

/* DOWNLOAD STEP'S OWN HEIGHT, to the pixel, and the contour is why that took
   a second try. The two are the pair a part leaves this page by and they share
   one row, so they have to be the same box — but `#step` has no border and
   this one does, and a 1 px border is not 1 px: on the machine this was
   reported from it rasterises at 0.571 px, which put the pair at 47.1 and 48.0
   with padding that already said 14/16 against 15/17 to compensate. Two
   numbers fitted to one device ratio.
   So the contour comes off the BOX and goes into an inset shadow, which paints
   a hairline and occupies nothing. Same padding as `#step` now, same line
   height, same font — one number each, and they match at any ratio. */
#code {
  display: inline-flex;
  align-items: center;
  padding: 15px 17px;
  border: none;
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 1px var(--line);
  background: transparent;
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 12px;
  line-height: 1.5;
  font-weight: 750;
  cursor: pointer;
  transition: background var(--wake), color var(--wake), box-shadow var(--wake);
}

/* Hover is a warmth behind the type and never the accent — the accent belongs
   to a state, and this button has none (`#layers` records what borrowing it
   for `:hover` cost). */
#code:hover {
  background: rgb(var(--ink-rgb) / 0.09);
  box-shadow: inset 0 0 0 1px var(--line-live);
}
#code[hidden], #scripts[hidden] { display: none !important; }

#scripts {
  position: fixed;
  inset: 0;
  /* 23 and not 40: OVER everything this page already draws — the veil (5), the
     compass (11), the refusal (20), the opening cover (22) — and UNDER the
     walkthrough's overlay at 30, which `dev/gate.py` requires to outrank every
     layer the page has. The two can meet: the tour reopens on `#tour`, which
     is a fragment and fires on a page that already has a model on it. */
  z-index: 23;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 24px;
  background: rgb(var(--ground-rgb) / 0.72);
  backdrop-filter: blur(3px);
}

/* THE SITE'S OWN MODAL, which is `.contact-modal`: an opaque card and a 1 px
   filet. Opaque, and not the translucent `--panel`: 465 lines of code read
   over somebody's geometry are 465 lines nobody reads.

   AND NO OFFSET SHADOW, which the site's modal does have. It was written in
   and taken out the same hour, on the picture: the site's `14px 16px 0` is a
   block of ITS OWN INK thrown onto PAPER, and there is no dark equivalent —
   a near-black offset behind a near-black card over a near-black scrim draws
   nothing at all. `dev/_scratch/scripts/1-scripts.png` shows the card with it
   declared and no shadow visible anywhere. The site agrees where it faces the
   same problem: `.demo-cut-preview-overlay` and `.demo-review-player > div`
   are its two panels over dark media and neither carries one. A declaration
   that cannot be seen is the `compare`-frame shape — present, correct, inert
   — and this repo has paid for that twice. */
#scripts-card {
  display: flex;
  flex-direction: column;
  width: min(440px, 100%);
  max-height: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--ground);
  overflow: hidden;
}

#scripts-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px 0 16px;
}

#scripts-title {
  margin: 0;
  color: var(--ink);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.01em;
}

#scripts-close {
  border: 0;
  background: transparent;
  color: var(--ink-mute);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

#scripts-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 14px 16px 18px;
}

#scripts-lead {
  margin: 0;
  color: var(--ink-soft);
  font-size: 13px;
  line-height: 1.55;
}

#scripts-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

#scripts-email {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgb(var(--ink-rgb) / 0.04);
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
}

#scripts-email:focus {
  outline: none;
  border-color: var(--line-live);
}

#scripts-join {
  padding: 10px 14px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--accent-deep);
  color: var(--on-accent);
  font-family: inherit;
  font-size: 12px;
  font-weight: 750;
  cursor: pointer;
}

#scripts-join:hover { background: #e5f5a8; }
#scripts-join:disabled { opacity: 0.4; cursor: default; }

#scripts-note {
  margin: 0;
  color: var(--ink-mute);
  font-size: 12.5px;
  line-height: 1.45;
}

#scripts-note[hidden] { display: none !important; }

#scripts .vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
