/* ME&QC rebuild mockup — "drawing sheet"
   ---------------------------------------------------------------------------
   Design language is taken from the subject's own materials: engineering
   drawing sheets. A ruled frame with corner ticks, a mono title block above
   every section, numbered plates, tabular spec tables.

   Two modes, one domain (see Rebuild-Presentation-Strategy.md §1):
     default        commercial — daylight workshop, gets the work
     [data-mode=archive]  the record — deep water, single committed look
   The mode flip IS the "hard seam". It is meant to be felt.
*/

/* ---------- tokens: light (bare :root defines the complete palette) ------- */
:root {
  --ground:      #F0F1EF;   /* machined aluminium, cool — deliberately not cream */
  --surface:     #FFFFFF;
  --surface-2:   #E5E8E5;
  --rule:        #C6CBC7;
  --rule-soft:   #DCE0DC;
  --ink:         #13181C;
  --ink-2:       #59636B;   /* steel grey, blue bias */
  --ink-3:       #838D94;
  --accent:      #1C5680;   /* engineer's blue — layout fluid, marking blue */
  --accent-ink:  #FFFFFF;
  --accent-soft: #DCE7F0;
  --flag:        #8A5A11;   /* editorial flag: needs Igor's answer */
  --flag-soft:   #F6EDDC;
  --ok:          #3D6142;   /* settled: Igor has answered and this is the answer */
  --ok-soft:     #EAF1EA;

  --display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", ui-sans-serif, system-ui, sans-serif;
  --body:    Charter, "Bitstream Charter", "Iowan Old Style", "Source Serif 4", Georgia, serif;
  --mono:    ui-monospace, "SF Mono", SFMono-Regular, Menlo, "Cascadia Mono", Consolas, monospace;

  --sheet: 74rem;
  --gut: clamp(1rem, 4vw, 2.75rem);
}

/* ---------- tokens: dark via system preference (no explicit choice) ------- */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground:      #0F1317;
    --surface:     #171C22;
    --surface-2:   #1E252C;
    --rule:        #333C45;
    --rule-soft:   #262E36;
    --ink:         #E4E8EB;
    --ink-2:       #9BA6AF;
    --ink-3:       #6F7A83;
    --accent:      #7DB2DC;
    --accent-ink:  #0F1317;
    --accent-soft: #1B2A38;
    --flag:        #D6A354;
    --flag-soft:   #2A2116;
    --ok:          #7FB489;
    --ok-soft:     #16211A;
  }
}

/* ---------- tokens: dark via explicit toggle ------------------------------ */
:root[data-theme="dark"] {
  --ground:      #0F1317;
  --surface:     #171C22;
  --surface-2:   #1E252C;
  --rule:        #333C45;
  --rule-soft:   #262E36;
  --ink:         #E4E8EB;
  --ink-2:       #9BA6AF;
  --ink-3:       #6F7A83;
  --accent:      #7DB2DC;
  --accent-ink:  #0F1317;
  --accent-soft: #1B2A38;
  --flag:        #D6A354;
  --flag-soft:   #2A2116;
  --ok:          #7FB489;
  --ok-soft:     #16211A;
}

/* ---------- base --------------------------------------------------------- */
* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font: 400 clamp(1rem, 0.96rem + 0.2vw, 1.0625rem)/1.62 var(--body);
  font-synthesis-weight: none;
}

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: 0.005em;
  line-height: 1.06;
  text-wrap: balance;
  margin: 0;
}
h1 { font-size: clamp(2.25rem, 1.5rem + 3.2vw, 3.75rem); letter-spacing: -0.01em; }
h2 { font-size: clamp(1.65rem, 1.25rem + 1.6vw, 2.4rem); }
h3 { font-size: clamp(1.2rem, 1.05rem + 0.6vw, 1.45rem); }
h4 { font-size: 1.0625rem; }

p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-underline-offset: 0.18em; text-decoration-thickness: 1px; }
a:hover { text-decoration-thickness: 2px; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 1px; }

img { max-width: 100%; height: auto; display: block; }

/* ---------- masthead ----------------------------------------------------- */
.masthead {
  position: sticky; top: 0; z-index: 40;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}

/* The contact strip is the single most important fix in the rebuild: the old
   boatworks address lived inside a Flash movie and has been invisible since
   2020. Here it is HTML, on every page, above everything else. */
.contactbar {
  border-bottom: 1px solid var(--rule-soft);
  background: var(--surface-2);
  font: 500 0.75rem/1 var(--mono);
  letter-spacing: 0.04em;
}
.contactbar > div {
  max-width: var(--sheet); margin-inline: auto; padding: 0.55rem var(--gut);
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; align-items: center;
}
.contactbar a { color: var(--ink-2); text-decoration: none; }
.contactbar a:hover { color: var(--accent); text-decoration: underline; }
.contactbar .spacer { margin-inline-start: auto; }

.masthead > nav {
  max-width: var(--sheet); margin-inline: auto; padding: 0.9rem var(--gut);
  display: flex; align-items: baseline; gap: clamp(1rem, 3vw, 2.5rem); flex-wrap: wrap;
}

.brand { text-decoration: none; color: inherit; display: block; line-height: 1; }
.brand b {
  display: block; font: 600 1.5rem/1 var(--display);
  letter-spacing: 0.01em;
}
.brand span {
  display: block; margin-top: 0.3rem;
  font: 500 0.6875rem/1 var(--mono); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-3);
}

.nav { display: flex; gap: clamp(0.9rem, 2.2vw, 1.9rem); flex-wrap: wrap; margin-inline-start: auto; }
.nav a {
  font: 500 0.8125rem/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-2); text-decoration: none; padding-bottom: 0.35rem;
  border-bottom: 2px solid transparent;
}
.nav a:hover { color: var(--ink); border-bottom-color: var(--rule); }
.nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }

/* ---------- the sheet ---------------------------------------------------- */
.sheet {
  max-width: var(--sheet); margin: clamp(1.5rem, 4vw, 3.25rem) auto;
  padding-inline: var(--gut);
}

/* corner ticks: the drawing-sheet register marks */
.field {
  position: relative;
  border: 1px solid var(--rule);
  background: var(--surface);
  padding: clamp(1.5rem, 4vw, 3rem);
}
.field::before, .field::after {
  content: ""; position: absolute; width: 10px; height: 10px; pointer-events: none;
  border: 1px solid var(--accent);
}
.field::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
.field::after  { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }

/* ---------- title block -------------------------------------------------- */
/* Mono strip carrying the fields a drawing sheet actually carries. The job
   number is not decoration: this is a job book, and the reference is how the
   entry is cited elsewhere on the site. */
.titleblock {
  display: flex; flex-wrap: wrap; gap: 0 1.25rem;
  font: 500 0.6875rem/1.7 var(--mono);
  letter-spacing: 0.11em; text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.6rem; margin-bottom: 1.4rem;
}
/* A field never splits from its own value. The strip wraps between fields. */
.titleblock .fld { white-space: nowrap; }
.titleblock b { color: var(--accent); font-weight: 600; }
.titleblock .sep { color: var(--rule); }

/* ---------- hero --------------------------------------------------------- */
.hero { display: grid; gap: clamp(1.75rem, 4vw, 3rem); }
@media (min-width: 62rem) {
  .hero { grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr); align-items: center; }
}
.hero .lede {
  font-size: clamp(1.125rem, 1rem + 0.55vw, 1.375rem);
  line-height: 1.5; color: var(--ink-2); margin-top: 1.1rem; max-width: 34ch;
}
.hero h1 em {
  font-style: normal; color: var(--accent);
}

.eyebrow {
  font: 500 0.6875rem/1 var(--mono); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: 0.9rem; display: block;
}

/* ---------- buttons ------------------------------------------------------ */
.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; }
.btn {
  font: 500 0.8125rem/1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
  padding: 0.85rem 1.35rem; text-decoration: none; border: 1px solid var(--accent);
  background: var(--accent); color: var(--accent-ink);
  transition: background 0.15s, color 0.15s;
}
.btn:hover { background: transparent; color: var(--accent); }
.btn.ghost { background: transparent; color: var(--accent); }
.btn.ghost:hover { background: var(--accent); color: var(--accent-ink); }

/* ---------- sections ----------------------------------------------------- */
.band { margin-top: clamp(2.5rem, 6vw, 4.5rem); }
.band > h2 + .lede,
.band > h2 + p { margin-top: 0.85rem; max-width: 62ch; color: var(--ink-2); }

.prose { max-width: 66ch; }
.prose h3 { margin-top: 2rem; margin-bottom: 0.6rem; }
.prose ul, .prose ol { margin: 0 0 1em; padding-inline-start: 1.2rem; }
.prose li { margin-bottom: 0.4em; }
.prose li::marker { color: var(--ink-3); }

/* ---------- plates ------------------------------------------------------- */
/* Source photographs top out at 800 px, so nothing is stretched full-bleed.
   Plates sit at honest size inside a hairline rule, the way drawings mount. */
.plates { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); margin-top: 1.75rem; }
.plates.wide { grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr)); }

figure.plate { margin: 0; }
figure.plate img {
  width: 100%; border: 1px solid var(--rule); background: var(--surface-2);
  cursor: zoom-in;
}
/* Every cell in a grid is the same box, so a row of plates reads as one
   register rather than a ragged edge.

   The box is SQUARE. Source photographs run from 0.51 (a step ladder shot on
   its end) to 1.49 (a workshop panorama), and a landscape box would slice the
   tall stainless instruments — which are most of the catalogue — down to a
   horizontal band. A square treats both orientations symmetrically and suits
   the sheet grid.

     cover    photographs — fill the box, crop the overflow. Anything markedly
              portrait is focused near the top by the build script, because on
              a splitter or a platform the identifying end is the top one.
     contain  drawings, CAD renders, FEA screenshots — anything where a crop
              removes the thing the plate exists to show. Letterboxed in the
              SAME box, so the grid stays even either way.

   The uncropped original is always one click away in the lightbox. */
.plates figure.plate img {
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: center;
}
.plates figure.plate img.fit {
  object-fit: contain;
  padding: 0.5rem;
}

/* A lead plate stands outside the grid and keeps its natural proportions —
   the hero is the one image that should not be cropped to match anything. */
figure.plate.lead img { aspect-ratio: auto; object-fit: fill; }

figure.plate figcaption {
  font: 400 0.8125rem/1.5 var(--mono); color: var(--ink-2);
  margin-top: 0.6rem; display: flex; gap: 0.6rem;
}
figure.plate figcaption .no {
  color: var(--accent); font-weight: 600; flex: none; letter-spacing: 0.06em;
}

/* ---------- spec tables -------------------------------------------------- */
.tablewrap { overflow-x: auto; margin-top: 1.5rem; border: 1px solid var(--rule); }
table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
th, td {
  text-align: left; padding: 0.7rem 1rem; border-bottom: 1px solid var(--rule-soft);
  font: 400 0.875rem/1.45 var(--mono); vertical-align: top;
}
thead th {
  font-weight: 600; font-size: 0.6875rem; letter-spacing: 0.11em; text-transform: uppercase;
  color: var(--ink-3); background: var(--surface-2); border-bottom: 1px solid var(--rule); white-space: nowrap;
}
tbody th { font-weight: 500; color: var(--ink-2); }
tbody tr:last-child td, tbody tr:last-child th { border-bottom: 0; }
td.num { text-align: right; }

/* ---------- cards -------------------------------------------------------- */
/* The hairlines are drawn by the cells, not by a coloured gap showing through
   between them. A gap-as-rule grid only looks right when the cells fill the
   last row exactly, and these never do: four cards into three columns, eleven
   numbers into six, and the empty tail comes out as a solid block of rule
   colour. Cells that carry their own borders simply stop, which is what a
   ruled table on a drawing sheet does anyway. */
.cards {
  display: grid; margin-top: 1.75rem;
  border: 1px solid var(--rule); border-width: 1px 0 0 1px;
}
@media (min-width: 46rem) { .cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 68rem) { .cards.three { grid-template-columns: repeat(3, 1fr); } }

.card {
  background: var(--surface); padding: 1.4rem; display: flex; flex-direction: column;
  gap: 0.7rem; text-decoration: none; color: inherit;
  border: 0 solid var(--rule); border-right-width: 1px; border-bottom-width: 1px;
  transition: background 0.15s;
}
a.card:hover { background: var(--accent-soft); }
.card .ref {
  font: 500 0.6875rem/1 var(--mono); letter-spacing: 0.11em; text-transform: uppercase;
  color: var(--ink-3);
}
a.card:hover .ref { color: var(--accent); }
.card h3 { font-size: 1.25rem; }
.card p { font-size: 0.9375rem; color: var(--ink-2); line-height: 1.5; }
.card img {
  border: 1px solid var(--rule); margin-bottom: 0.2rem;
  width: 100%; aspect-ratio: 3 / 2; object-fit: cover; object-position: center;
}
.card .more {
  margin-top: auto; font: 500 0.75rem/1 var(--mono); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--accent);
}

/* a card that is deliberately only a card — no URL of its own */
.card.stub { background: var(--surface-2); }
.card.stub .more { color: var(--ink-3); }

/* ---------- stat strip --------------------------------------------------- */
/* The praise on the profile page is carried by verifiable numbers rather than
   adjectives, so the numbers get the display face and the room. */
.stats {
  display: grid; margin-top: 1.75rem;
  border: 1px solid var(--rule); border-width: 1px 0 0 1px;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.stats > div, .stats > a {
  background: var(--surface); padding: 1.3rem 1.2rem;
  border: 0 solid var(--rule); border-right-width: 1px; border-bottom-width: 1px;
}
/* A number that leads somewhere says so. Same cell, same weight — only the
   source line turns into the invitation, so the row still reads as a row. */
.stats > a.statlink { display: block; text-decoration: none; color: inherit; transition: background .15s; }
.stats > a.statlink em { color: var(--accent); }
.stats > a.statlink:hover { background: var(--accent-soft); }
.stats b {
  display: block; font: 600 clamp(2rem, 1.4rem + 1.8vw, 2.9rem)/1 var(--display);
  letter-spacing: -0.015em; color: var(--accent); font-variant-numeric: tabular-nums;
}
.stats span {
  display: block; margin-top: 0.55rem;
  font: 400 0.8125rem/1.45 var(--body); color: var(--ink-2);
}
.stats em {
  display: block; margin-top: 0.3rem; font-style: normal;
  font: 500 0.625rem/1.4 var(--mono); letter-spacing: 0.11em;
  text-transform: uppercase; color: var(--ink-3);
}

/* ---------- timeline ---------------------------------------------------- */
/* A dated spine. The years are real and the order carries information, which
   is the only reason this is numbered at all. */
.timeline { margin-top: 1.75rem; border-top: 1px solid var(--rule); }
.timeline > div {
  display: grid; gap: 0.35rem 1.5rem; padding: 1.1rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
@media (min-width: 44rem) {
  .timeline > div { grid-template-columns: 8.5rem minmax(0, 1fr); align-items: baseline; }
}
.timeline dt, .timeline .yr {
  font: 600 0.8125rem/1.5 var(--mono); letter-spacing: 0.06em;
  color: var(--accent); font-variant-numeric: tabular-nums;
}
.timeline .ev { color: var(--ink); }
.timeline .ev b { font-weight: 600; }
.timeline .ev small {
  display: block; margin-top: 0.3rem; font-size: 0.875rem; color: var(--ink-2);
}

/* ---------- profile split ------------------------------------------------ */
.split { display: grid; gap: clamp(1.75rem, 4vw, 3rem); }
@media (min-width: 60rem) {
  .split { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: start; }
}

/* ---------- notes and flags ---------------------------------------------- */
.note {
  border-inline-start: 3px solid var(--rule); padding: 0.2rem 0 0.2rem 1rem;
  margin: 1.75rem 0; color: var(--ink-2); font-size: 0.9375rem;
}
.note.flag { border-inline-start-color: var(--flag); background: var(--flag-soft); padding: 0.85rem 1rem; }
.note.flag b { color: var(--flag); font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; display: block; margin-bottom: 0.3rem; }
.note.decided { border-inline-start-color: var(--ok); background: var(--ok-soft); padding: 0.85rem 1rem; }
.note.decided b { color: var(--ok); font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; display: block; margin-bottom: 0.3rem; }

/* ---------- video -------------------------------------------------------- */
video { width: 100%; border: 1px solid var(--rule); background: #000; }
.videonote { font: 400 0.75rem/1.5 var(--mono); color: var(--ink-3); margin-top: 0.55rem; }

/* ---------- footer ------------------------------------------------------- */
.foot {
  border-top: 1px solid var(--rule); margin-top: clamp(3rem, 7vw, 5rem);
  background: var(--surface);
}
.foot > div {
  max-width: var(--sheet); margin-inline: auto; padding: 2.5rem var(--gut);
  display: grid; gap: 2rem;
}
@media (min-width: 48rem) { .foot > div { grid-template-columns: 1.2fr 1fr 1fr; } }
.foot h4 {
  font: 600 0.6875rem/1 var(--mono); letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 0.9rem;
}
.foot ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.45rem; }
.foot a { color: var(--ink-2); text-decoration: none; font-size: 0.9375rem; }
.foot a:hover { color: var(--accent); text-decoration: underline; }
.foot .legal {
  border-top: 1px solid var(--rule-soft); grid-column: 1 / -1; padding-top: 1.4rem;
  font: 400 0.75rem/1.6 var(--mono); color: var(--ink-3);
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.5rem;
}

/* the provenance line — every page says where its copy came from */
.provenance {
  font: 400 0.75rem/1.6 var(--mono); color: var(--ink-3);
  border-top: 1px dashed var(--rule); margin-top: 2.5rem; padding-top: 0.9rem;
}
.provenance b { color: var(--ink-2); font-weight: 500; }

/* ---------- lightbox ----------------------------------------------------- */
.lightbox {
  position: fixed; inset: 0; z-index: 90; display: none;
  background: rgba(10, 14, 18, 0.93); padding: clamp(1rem, 4vw, 3rem);
  align-items: center; justify-content: center; flex-direction: column; gap: 1rem;
}
.lightbox[open], .lightbox.on { display: flex; }
.lightbox img { max-width: 100%; max-height: 82vh; border: 1px solid #3a444d; cursor: zoom-out; }
.lightbox p { font: 400 0.8125rem/1.5 var(--mono); color: #C6CED5; text-align: center; max-width: 60ch; }
.lightbox button {
  position: absolute; top: 1rem; right: 1rem; background: transparent; border: 1px solid #4a555f;
  color: #E4E8EB; font: 500 0.75rem/1 var(--mono); letter-spacing: 0.1em; padding: 0.6rem 0.9rem; cursor: pointer;
}
.lightbox button:hover { border-color: #8FBBDD; color: #8FBBDD; }

/* =========================================================================
   ARCHIVE MODE — the hard seam.
   Declared last so it wins over both theme blocks in all three theme states.
   This section commits to one look on purpose: it is a record, not a
   shopfront, and it should not feel like the pages that take orders.
   ========================================================================= */
:root[data-mode="archive"] {
  --ground:      #0B1016;
  --surface:     #111820;
  --surface-2:   #16202A;
  --rule:        #2A3742;
  --rule-soft:   #1D2831;
  --ink:         #DEE5EA;
  --ink-2:       #9DACB8;
  --ink-3:       #71818E;
  --accent:      #8FBBDD;
  --accent-ink:  #0B1016;
  --accent-soft: #16242F;
  --flag:        #D6A354;
  --flag-soft:   #241C10;
  --ok:          #7FB489;
  --ok-soft:     #131E17;
}

/* Archive prose reads as a book: wider measure, larger serif, real leading. */
[data-mode="archive"] .prose { max-width: 68ch; font-size: 1.0625rem; line-height: 1.72; }
[data-mode="archive"] .prose p + p { text-indent: 0; }
[data-mode="archive"] h1 { font-family: var(--body); font-weight: 600; letter-spacing: -0.015em; }
[data-mode="archive"] .brand span { color: var(--accent); }

/* Russian body text gets its own language and a face that carries Cyrillic. */
[lang="ru"] { font-family: Georgia, "Times New Roman", serif; }

/* Plate captions in the archive carry a date and place, so they run longer. */
[data-mode="archive"] figure.plate figcaption { font-family: var(--body); font-size: 0.875rem; }
[data-mode="archive"] figure.plate figcaption .no { font-family: var(--mono); font-size: 0.75rem; padding-top: 0.15rem; }

/* A download that costs megabytes says so next to the button, not after it. */
.dlnote {
  font: 500 0.6875rem/1 var(--mono); letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-3); align-self: center;
}


/* ===========================================================================
   ME&QC — production layer
   ---------------------------------------------------------------------------
   Appended to the mockup's site.css, which is the design and stays the single
   source of the drawing-sheet language. Nothing here restates a token or a
   component. What is here is only what the public site has and the review
   mockup did not:

     1. the animated page background
     2. a print sheet
     3. the small production affordances — skip link, focus, selection

   The rule the background is held to comes from 3d/USING-THESE.md and is not
   negotiable: it sits behind and stays there, it never carries a dimension,
   and `prefers-reduced-motion: reduce` stops it dead.
   =========================================================================== */

/* ---------- 1. the page background --------------------------------------- */

/* ONE background for the whole page: a single layer fixed to the viewport,
   behind the masthead, the sheet and the footer alike. It does not scroll —
   the drawing holds still and the page moves over it, which is what a
   background is. It used to be a per-section decoration, absolutely
   positioned inside each card; that was a workaround for the cards being
   opaque, and the workaround is gone.

   The drawing is always much larger than the viewport, so what you see is a
   crop — a hub, a blade root, the middle of a scissor. That is what a drawing
   sheet at full size looks like when you stand close to it. */

/* The symbol library. One <symbol> per distinct drawing per page; the layers
   reference it with <use>. It must be in the flow but occupy nothing, and it
   must not be `display:none`, which stops SMIL in some engines. */
.figdefs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* -- where it sits in the stack -------------------------------------------- */

/* Above the ground colour, below everything that carries meaning.

   Deliberately NOT `z-index: -1`. A negative-z fixed layer is the shorter
   spelling, but it only works while nothing between it and the canvas paints
   an opaque background, which means giving `body` up to `html` and hoping no
   engine treats the layer's own compositing — opacity, a mask, containment —
   as a reason to promote it out of order. WebKit has form there, and the
   failure is not a missing drawing, it is a blue wash over the whole page.

   At `z-index: 0` the layer is an ordinary positioned box that paints above
   in-flow content, and the three regions that carry the page are lifted over
   it explicitly. Nothing is implied and nothing depends on `body` being
   transparent — `body` keeps the ground colour it has in the mockup. */
.figfield {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
main.sheet, .foot { position: relative; z-index: 1; }
/* .masthead is already sticky at z-index 40, the lightbox at 90. */

/* Everything that composites is on the inner layer, not on the one whose
   place in the stack matters. */
.figstack {
  position: absolute;
  inset: 0;
  contain: paint;
  /* Fades at the edges rather than ending. A hard edge under a drawing reads
     as a second border, and this design already has enough borders. */
  -webkit-mask-image: radial-gradient(118% 108% at 62% 44%, #000 30%, transparent 100%);
  mask-image: radial-gradient(118% 108% at 62% 44%, #000 30%, transparent 100%);
}

/* -- the glaze -------------------------------------------------------------- */

/* The cards are the reason a page background is possible at all. Opaque white
   on the ground, they would have hidden the drawing everywhere except a
   hundred pixels of margin — which is why the figure used to live inside each
   section instead. Translucent, they glaze it: the line is at full strength in
   the gutters and between the bands, and a ghost of it carries on underneath
   the card, stepping down at the hairline border the way glass steps down over
   paper.

   Every surface that carries text takes the same glaze, and only ever one of
   them — the stat strip and the card grids sit on the bare ground rather than
   inside a `.field`, so they are glazed in their own right rather than stacked
   on top of one that already is.

   The opaque `background` on the line before each `color-mix` is the fallback
   and is not redundant. Safari below 16.2 drops the whole `color-mix`
   declaration; without a fallback in the same rule the card would fall back to
   whatever the mockup's stylesheet happens to still declare, which is a
   guarantee that lives in another file and could be edited away. */
main.sheet .field,
main.sheet .card,
main.sheet .stats > div,
main.sheet .stats > a,
.foot {
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
}
:root[data-mode="archive"] main.sheet .field,
:root[data-mode="archive"] main.sheet .card,
:root[data-mode="archive"] main.sheet .stats > div,
:root[data-mode="archive"] main.sheet .stats > a,
main.sheet .stats > a,
:root[data-mode="archive"] .foot {
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
}

/* The masthead stays opaque, and that is not an oversight. It is sticky: the
   whole page slides under it, so glazing it would put body copy behind the
   navigation rather than a drawing. */

/* -- the layers ------------------------------------------------------------- */

/* One drawing, and it stays. It used to be a stack that crossfaded as you
   scrolled from section to section; that is gone. A background that changes is
   not a background — the eye goes to it every time it moves, which is the
   opposite of the job of something sitting at 15% behind body copy. */
.figstack .fig {
  position: absolute;
  color: var(--accent);
  top: 50%;
  left: 58%;
  translate: -50% -50%;
  width: auto;
}
.figstack svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* Contrast, carried by the stack rather than by a layer — a layer's own
   opacity is the crossfade and the two must not fight. Low enough that a spec
   table always wins: at 0.15 behind a 70% card the line reaches the reader at
   about 0.045, a tint rather than a mark, and at full strength only where
   there is nothing to read. Lower again in the archive's dark register, where
   a light line on a dark ground reads hotter than the same line the other way
   round. */
.figstack { opacity: 0.15; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .figstack { opacity: 0.155; } }
:root[data-theme="dark"] .figstack { opacity: 0.155; }
:root[data-mode="archive"] .figstack { opacity: 0.09; }

/* -- per figure: aspect ratio, and how much bigger than the viewport ------- */

/* Sized against the viewport now, not against a section, so `vmax` is the
   unit: it is the long edge of the screen either way up, which keeps a
   landscape laptop and a portrait tablet getting the same crop of the same
   drawing rather than two unrelated ones.

   Two families, sized differently on purpose.

   The propeller is radially symmetric about the middle of its own viewBox, so
   a hard crop of it still reads as a propeller — a hub and three blade roots
   running off the edges. It can go very large, and Igor asked for exactly
   that.

   Everything else is an object with a top and a bottom. Crop a splitter to a
   third of itself and it stops being a splitter and becomes a set of lines,
   so those sit at about the long edge of the screen: filling it, edges
   running out, still legible as the thing they are. */
.fig-propeller         { aspect-ratio: 1; height: clamp(46rem, 168vmax, 150rem); }
.fig-propeller-hub     { aspect-ratio: 1; height: clamp(38rem, 132vmax, 118rem); }

.fig-ttprs            { aspect-ratio: 700 / 1000;  height: clamp(28rem, 104vmax, 76rem); }
.fig-splitter         { aspect-ratio: 1000 / 888;  height: clamp(26rem,  96vmax, 70rem); }
.fig-scissor          { aspect-ratio: 669 / 1000;  height: clamp(28rem, 104vmax, 76rem); }
.fig-gold-jig         { aspect-ratio: 932 / 1000;  height: clamp(26rem,  98vmax, 72rem); }
.fig-spinnaker        { aspect-ratio: 1000 / 633;  height: clamp(24rem,  92vmax, 66rem); }
.fig-sheet            { aspect-ratio: 1000 / 700;  height: clamp(24rem,  90vmax, 68rem); }
.fig-toolpath         { aspect-ratio: 1000 / 620;  height: clamp(24rem,  92vmax, 68rem); }

/* -- narrow screens -------------------------------------------------------- */

/* A phone inverts the problem. The viewport there is narrow and tall, so
   sizing against the long edge gives a drawing two screens tall and two
   fingers wide — a texture, not a drawing. Below this width every figure is
   sized against the viewport's WIDTH instead and centred, so the whole object
   is in frame, and the contrast comes down because on a small screen the copy
   and the drawing are never far apart. */
@media (max-width: 46rem) {
  .figstack .fig {
    height: auto;
    width: 128%;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
  }
  /* `.figstack` prefix again, and for the same reason as the animation table:
     the rule above is `.figstack .fig` at (0,2,0) and a bare `.fig-propeller`
     is (0,1,0), so without it every one of these loses and the phone gets the
     generic 128% for everything — including the propeller crop Igor asked for.
     The desktop block gets away without it only because the two rules happen
     to set different properties. */
  .figstack .fig-propeller     { width: 210%; }
  .figstack .fig-propeller-hub { width: 176%; }
  .figstack .fig-sheet,
  .figstack .fig-toolpath      { width: 116%; }

  .figstack { opacity: 0.085; }
  :root[data-mode="archive"] .figstack { opacity: 0.07; }

  /* Nothing to glaze: at this width the sheet is the screen and the gutters
     are a thumbnail wide, so a translucent card only costs contrast. */
  main.sheet .field, main.sheet .card, main.sheet .stats > div,
main.sheet .stats > a, .foot,
  :root[data-mode="archive"] main.sheet .field,
  :root[data-mode="archive"] main.sheet .card,
  :root[data-mode="archive"] main.sheet .stats > div,
:root[data-mode="archive"] main.sheet .stats > a,
main.sheet .stats > a,
  :root[data-mode="archive"] .foot { background: var(--surface); }

  /* The radial mask is tuned for a landscape viewport; on a portrait one it
     eats the middle of the drawing. Straight vertical falloff instead. */
  .figstack {
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 20%, #000 80%, transparent);
    mask-image: linear-gradient(to bottom, transparent, #000 20%, #000 80%, transparent);
  }
}

/* -- motion --------------------------------------------------------------- */

/* Everything moves, and everything moves slowly. The slowest thing here takes
   six minutes to come back to where it started; the fastest full cycle is
   fourteen seconds and belongs to a crank that really does turn that fast.
   The figures are large enough now that a small angular rate is a long way at
   the edge of the drawing, so the numbers went up, not down.

   The base is a drift: the sheet itself easing under the viewport, a couple of
   per cent either way. It is the honest motion for a figure that cannot turn
   — an oblique projection is not a 2D rotation, so spinning one would be a
   drawing tumbling rather than a propeller turning. */
.figstack .fig svg {
  animation: meqc-drift 150s ease-in-out infinite alternate;
}
/* One drawing, one compositor layer. */
.figstack .fig svg { will-change: transform; }
@keyframes meqc-drift {
  from { transform: translate3d(-2.4%, 1.6%, 0) scale(1.00); }
  to   { transform: translate3d( 2.4%, -1.6%, 0) scale(1.06); }
}

/* Every one of these carries the `.figstack` prefix, and it is load-bearing.
   The base rule above is `.figstack .fig svg` — two classes and a type, so a
   bare `.fig-propeller svg` is one class short and loses to it. Written that
   way, the whole of this table was dead: every drawing drifted at the base
   150 s and the propeller never turned at all. */
.figstack .fig-ttprs svg     { animation-duration: 190s; }
.figstack .fig-spinnaker svg { animation-duration: 170s; animation-direction: alternate-reverse; }
.figstack .fig-splitter svg  { animation-duration: 200s; }
.figstack .fig-scissor svg   { animation-duration: 230s; }
.figstack .fig-gold-jig svg  { animation-duration: 200s; animation-direction: alternate-reverse; }
.figstack .fig-sheet svg     { animation-duration: 240s; }
.figstack .fig-toolpath svg  { animation-duration: 220s; animation-direction: alternate-reverse; }

/* The propeller. One rotation, forever, about its own axis — the bake squares
   the viewBox about the projected model origin, so 50%/50% is the axis and
   this is a true rotation rather than a wobble. 300 seconds is 1.2 degrees a
   second: at this size a blade tip still travels, and you notice it only if
   you stop and look, which is the whole brief. */
.figstack .fig-propeller svg,
.figstack .fig-propeller-hub svg {
  transform-origin: 50% 50%;
  animation: meqc-spin 300s linear infinite;
}
.figstack .fig-propeller-hub svg { animation-duration: 380s; animation-direction: reverse; }
@keyframes meqc-spin { to { transform: rotate(360deg); } }

/* The drawing sheet ruling itself in — once, on arrival — and then drifting
   with everything else. Per-path length and delay are inline custom properties
   on the SVG, because only the file that drew the path knows how long it is. */
.fig-sheet .drawn path {
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
  animation: meqc-draw 2.4s ease-out var(--wait, 0s) forwards;
}
@keyframes meqc-draw { to { stroke-dashoffset: 0; } }

/* The lathe pass. The tool traverses right to left and the cut profile is
   revealed behind it, so the two motions are one motion. */
.fig-toolpath .cut  { animation: meqc-cut 26s linear infinite; }
.fig-toolpath .tool { animation: meqc-tool 26s linear infinite; }
@keyframes meqc-cut  { 0%, 4% { stroke-dashoffset: 100; } 82%, 100% { stroke-dashoffset: 0; } }
@keyframes meqc-tool {
  0%, 4%    { transform: translate(880px, 0); }
  82%, 100% { transform: translate(150px, 0); }
}

/* The sample stream. Falls from the hopper mouth, reaches the riffle bank at
   just past half the cycle, and leaves to whichever side its lane is on. */
.fig-splitter .stream circle {
  animation: meqc-fall 7s linear infinite;
  animation-delay: var(--t, 0s);
  opacity: 0;
}
@keyframes meqc-fall {
  0%        { transform: translateY(0);     opacity: 0; }
  7%        { opacity: 1; }
  52%       { transform: translateY(330px); opacity: 1; }
  100%      { transform: translateY(560px) translateX(var(--s)); opacity: 0; }
}

/* A hidden tab and reduced motion stop it. site.js sets this class; it covers
   the CSS half, and pauseAnimations() covers the SMIL half. */
.figstack .fig.paused, .figstack .fig.paused * { animation-play-state: paused !important; }

/* The non-negotiable. A slow rotation behind body copy is exactly what this
   media query exists for. SMIL does not observe it, so site.js pauses those
   documents directly — this half covers everything driven by CSS. */
@media (prefers-reduced-motion: reduce) {
  .figstack .fig svg,
  .figstack .fig .cut,
  .figstack .fig .tool { animation: none !important; }
  .fig-sheet .drawn path { stroke-dashoffset: 0; animation: none !important; }
  .fig-splitter .stream { display: none; }
}

/* Asked for more contrast, the background is the first thing to go: it is
   decoration, and it is decoration that tints the paper the copy sits on. */
@media (prefers-contrast: more) {
  .figfield { display: none; }
  main.sheet .field, main.sheet .card, main.sheet .stats > div,
main.sheet .stats > a, .foot,
  :root[data-mode="archive"] main.sheet .field,
  :root[data-mode="archive"] main.sheet .card,
  :root[data-mode="archive"] main.sheet .stats > div,
:root[data-mode="archive"] main.sheet .stats > a,
main.sheet .stats > a,
  :root[data-mode="archive"] .foot { background: var(--surface); }
}

/* ---------- 1b. interactive model viewers --------------------------------- */

/* The demonstration blocks on the product and job pages. Dark, because the
   viewer inside them is dark and a light frame round a dark stage reads as a
   mistake. Press-to-run: nothing is fetched until the button is used. */
figure.demo {
  margin: 1.75rem 0 0;
  border: 1px solid var(--rule);
  background: #15181c;
}

.demo-stage {
  position: relative;
  /* 4:3 rather than 16:9. The viewer gives 300 px of its width to the control
     panel, so a wide stage is a short one, and these are tall objects — a
     scissor at full lift, a three-tiered splitter. */
  aspect-ratio: 4 / 3;
  display: grid;
  place-content: center;
  gap: .9rem;
  justify-items: center;
  background:
    radial-gradient(120% 100% at 50% 0%, #1e242b 0%, #15181c 62%);
}
/* A phone is portrait and a 16:10 stage on one is a letterbox slit. The
   viewer stacks its control panel under the stage below 820 px, so the frame
   needs to be taller there, not wider. */
@media (max-width: 51rem) { .demo-stage { aspect-ratio: 3 / 4; } }

.demo-stage iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0; display: block;
}
figure.demo.running .demo-stage { background: #15181c; }

.demo-run {
  font-family: var(--mono);
  font-size: .8125rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #d7dde4;
  background: #232931;
  border: 1px solid #2c333c;
  border-radius: 2px;
  padding: .75rem 1.4rem;
  cursor: pointer;
}
.demo-run:hover { background: #2c333c; border-color: #3c454f; }
.demo-run:focus-visible { outline: 2px solid #7fb2e5; outline-offset: 3px; }

.demo-hint {
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: #8a949f;
}

figure.demo figcaption {
  padding: .9rem 1.1rem 1rem;
  border-top: 1px solid #2c333c;
  color: #a8b2bc;
  font-size: .9375rem;
  line-height: 1.55;
}
figure.demo figcaption b { color: #e2e8ee; }
/* Only the title is a heading. The notes use <b> inline — "SF 600 is a model
   designation" — and a blanket rule turned every one of those into a second
   heading mid-sentence. */
figure.demo figcaption b.demo-title {
  display: block; margin-bottom: .25rem;
  font-family: var(--display); font-size: 1.0625rem; letter-spacing: .01em;
}
figure.demo figcaption a { color: #7fb2e5; white-space: nowrap; }

/* The line under the lede that says there is a model to run further down. It
   is set as a control rather than as prose because that is what it is, and
   because on a page this long a sentence in the body text is not a signpost. */
.democue {
  margin: 1.25rem 0 0;
  display: flex; align-items: center; gap: .85rem;
  font-size: .9375rem;
  color: var(--ink-2);
  max-width: 54ch;
}
/* Its own column, so a sentence that wraps wraps under itself and not back
   under the control. */
.democue span { flex: 1 1 auto; line-height: 1.45; }
.democue a {
  display: inline-flex; align-items: center; gap: .45rem;
  font: 500 .75rem/1 var(--mono); letter-spacing: .09em; text-transform: uppercase;
  color: #0d1116; background: #7fb2e5;
  border: 1px solid #7fb2e5; border-radius: 2px;
  padding: .5rem .8rem .47rem;
  text-decoration: none;
  flex: 0 0 auto;
  white-space: nowrap;
}
.democue a::before { content: "\25B6"; font-size: .8em; }
.democue a:hover { background: #9cc6ef; border-color: #9cc6ef; }
.democue a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Two columns need a page wide enough for two columns. On a phone the control
   goes above the sentence rather than beside it. */
@media (max-width: 34rem) {
  .democue { flex-direction: column; align-items: flex-start; gap: .6rem; }
}

/* The stage is the button. The <button> stays for the keyboard and for what
   the control announces itself as; the click that actually happens is usually
   somewhere on the dark rectangle, and it used to do nothing. */
.demo-stage { cursor: pointer; }

/* The run control reads as the primary action on the page it sits on, because
   for a visitor it is: everything else here is a photograph and a number. */
.demo-run {
  color: #0d1116;
  background: #7fb2e5;
  border-color: #7fb2e5;
  font-weight: 600;
}
.demo-run::before { content: "\25B6"; margin-right: .55rem; font-size: .8em; }
.demo-run:hover { background: #9cc6ef; border-color: #9cc6ef; }

/* And the badge that says a model is behind this card at all. The viewers sit
   three-quarters of the way down a product page, and nothing on the index said
   they existed — which is the whole reason a visitor never found the one thing
   on this site that is worth playing with. */
.card { position: relative; }
.badge-model {
  position: absolute; top: 1.9rem; right: 1.9rem; z-index: 1;
  display: inline-flex; align-items: center; gap: .42rem;
  font: 500 .625rem/1 var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: #e6ecf2; background: #1b2027;
  border: 1px solid #39424c; border-radius: 2px;
  padding: .45rem .6rem .42rem;
  box-shadow: 0 1px 6px rgba(13, 17, 22, .35);
}
.badge-model::before { content: "\25B6"; font-size: .62em; color: #7fb2e5; }
a.card:hover .badge-model { background: #262d36; border-color: #5b6672; }
@media (max-width: 34rem) {
  .badge-model { top: 1.7rem; right: 1.7rem; font-size: .5625rem; }
}

/* The standing caveat. It is the same sentence the viewer itself carries, and
   it is not small print — 3d/USING-THESE.md is explicit that a model looks
   authoritative whether or not it is. */
.demo-caveat {
  margin-top: 1.4rem;
  padding: .85rem 1.1rem;
  border-left: 3px solid var(--flag);
  background: var(--flag-soft);
  color: var(--ink-2);
  font-size: .9375rem;
  line-height: 1.55;
  max-width: 62ch;
}
.demo-caveat b { color: var(--ink); }

/* ---------- 2. print ------------------------------------------------------ */

@media print {
  .figfield, .masthead .contactbar, .lightbox, .actions,
  figure.demo .demo-stage { display: none; }
  .masthead { position: static; border-bottom: 1px solid #000; }
  :root { --ground: #fff; --surface: #fff; --ink: #000; --ink-2: #333; --accent: #000; }
  main.sheet { max-width: none; }
  a { text-decoration: none; color: #000; }
  /* A printed page loses every href, so put the important ones back. */
  .prose a[href^="http"]::after,
  .prose a[href^="mailto"]::after { content: " (" attr(href) ")"; font-size: .8em; }
  figure.plate, section { break-inside: avoid; }
}

/* ---------- 3. production affordances ------------------------------------ */

/* Clipped to nothing rather than parked off-canvas at -9999px, which is a
   real box a long way outside the page and a real source of horizontal
   overflow. */
.skip {
  position: absolute; left: 0; top: 0; z-index: 100;
  width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
  background: var(--accent); color: var(--accent-ink);
  padding: .6rem 1rem; font-family: var(--mono); font-size: .8125rem;
}
.skip:focus {
  width: auto; height: auto; overflow: visible; clip-path: none;
}

::selection { background: var(--accent-soft); color: var(--ink); }
