/* =========================================================
   Section redesign — QUALITY.  See REDESIGN-BRIEF.md.

   Loads after pages.css so it can override the .tile/.g3 rules
   it replaces. Scoped to this page only; nothing here ships to
   the other six.

   Archetypes, in page order:
     policy         2 · editorial statement + register
     qc             3 · scroll-drawn spine   <- the one moment
     systems        7 · expanding rows (native <details>)
     certifications     the one real table, on paper
     assurance          horizontal ribbon, against the spine's axis

   Tokens only. No new colour, no new radius. GREEN IS BANNED ON
   THIS PAGE — pages.css paints `.pill--done` in `--green-lt`,
   which is the single green thing outside Sustainability. It is
   overridden at the bottom of this file.

   THE `--live` CONVENTION
   Every rule defaults to the FINISHED state; the hidden
   pre-states are scoped under `.qspine--live`, which
   js/sec-quality.js adds only once it has decided to animate. No
   JS, a thrown error, or reduced motion all leave a complete,
   readable page — the same contract js/transitions.js uses for
   the product cast.
   ========================================================= */

/* =========================================================
   0 · HERO — "under inspection"

   `.phero` and every `.phero__*` live in the shared pages.css,
   which this page may not edit. Everything below overrides them
   from the outside, scoped to `.qhero`, so the other six pages
   keep the hero they have.

   The composition, stated once so it is not lost:
   the PHOTOGRAPH IS THE GROUND. It is not a picture beside the
   copy — it is behind all of it, full bleed, with a scrim that
   pools darkness into the lower left so the copy has a black
   half to sit in and the specimen keeps the lit half. On top of
   that sits a drawn instrument: two edge rulers, one scan sweep
   on load, and a datum line that follows the pointer.

   GREEN IS BANNED ON THIS PAGE and nothing here reaches for it.
   Molten does the marking, white does the CTA.
   ========================================================= */

.qhero {
  min-height: clamp(500px, 70vh, 690px);
  /* Tighter at the top than the shared hero: `.phero`'s padding was set
     for a band with nothing in it but type, and this one has a whole
     photograph carrying the upper half. */
  padding-top: calc(var(--nav-h) + clamp(2.2rem, 5.5vh, 3.8rem));
  /* Two rows: the copy takes the space that is going spare and sits at
     the bottom of it; the badge rail is the plinth the hero stands on.
     The absolutely positioned layers below are out of flow and so do not
     become grid items. */
  display: grid;
  grid-template-rows: 1fr auto;
  padding-bottom: 0;
  background: var(--navy-900);
  /* One stacking context for the whole hero, so the caption's z-index is
     resolved against the scrim rather than against the document. */
  isolation: isolate;
}

/* No z-index on the figure ON PURPOSE. Giving it one opens a stacking
   context and traps the caption underneath the scrim that follows it in
   source order; leaving it `auto` lets the caption's z-index compete in
   .qhero's context, which is where it needs to win. */
.qhero__media { position: absolute; inset: 0; margin: 0; }

.qhero__img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: 66% 44%;
  /* The same grade `.ph--live` carries in main.css, so the hero belongs
     to the same world as every other photograph on the site. Static —
     a filter is never animated here. */
  filter: saturate(1) brightness(.96) contrast(1.06);
  transform-origin: 66% 44%;
}

/* The specimen label. Top right, under the ruler: the bottom of the
   frame belongs to the badge rail, and a plate that has to dodge a rail
   whose height changes with the wrap is a plate that will one day sit on
   top of it. */
.qhero__cap {
  position: absolute; z-index: 3;
  top: calc(var(--nav-h) + clamp(1.2rem, 3.6vh, 2.4rem));
  right: var(--gutter);
  max-width: 20ch;
  padding: .42rem .72rem;
  font: 500 var(--fs-label)/1.45 var(--f-mono);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--steel);
  background: rgba(6,21,41,.88);
  border: 1px solid var(--rule-d);
  border-left: 1px solid var(--molten);
}

/* The scrim. Five layers, in this order and for these reasons:
     1 · a pool anchored off the bottom-left corner — this is the one
         that buys the lede its contrast, and it is a radial rather than
         a linear ramp so the darkness follows the text block instead of
         darkening the whole left edge including the empty part of it;
     2 · a second pool at the TOP left. Added after measurement: the
         blurred lab window behind the probe is the brightest thing in
         the frame and it sits exactly where the eyebrow and the first
         line of the h1 land. With four layers the eyebrow measured 4.14
         (needs 4.5) and the h1 2.86 (needs 3.0) at real glyph pixels.
         Both were caused by this one corner, so this is the fix for
         this one corner rather than a heavier scrim everywhere;
     3 · a top-and-bottom vignette, which seats the frame under the nav
         and hands it down to the seam;
     4 · a gentle left-to-right ramp holding the far side up;
     5 · one warm bloom at the top right — the site's heat, at 13%.
   Measured at glyph pixels by _qc/herocontrast3.js, not by eye. */
.qhero__scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(134% 116% at -8% 108%,
      rgba(6,21,41,.99) 0%, rgba(6,21,41,.96) 36%, rgba(6,21,41,.66) 62%, rgba(6,21,41,0) 86%),
    radial-gradient(98% 80% at 2% 4%,
      rgba(6,21,41,.92) 0%, rgba(6,21,41,.64) 40%, rgba(6,21,41,0) 76%),
    linear-gradient(180deg,
      rgba(6,21,41,.82) 0%, rgba(6,21,41,.22) 26%, rgba(6,21,41,.26) 58%, rgba(6,21,41,.86) 100%),
    linear-gradient(90deg,
      rgba(6,21,41,.66) 0%, rgba(6,21,41,.20) 46%, rgba(6,21,41,.10) 76%, rgba(6,21,41,.32) 100%),
    radial-gradient(62% 54% at 80% 26%, rgba(255,107,26,.13) 0%, rgba(255,107,26,0) 64%);
}

/* ---- the instrument -------------------------------------
   Purely drawn, purely decorative, and aria-hidden in the
   markup: everything it means is already in the h1 and the
   image's alt text.
   --------------------------------------------------------- */
.qhero__grat { position: absolute; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }

.qhero__ruler--top {
  position: absolute; left: 0; right: 0;
  top: calc(var(--nav-h) + .55rem); height: 9px;
  background: repeating-linear-gradient(90deg, rgba(255,145,68,.5) 0 1px, transparent 1px 15px);
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 32%, #000 88%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0%, #000 32%, #000 88%, transparent 100%);
}
.qhero__ruler--side {
  position: absolute; right: 0; top: 24%; bottom: 16%; width: 11px;
  background: repeating-linear-gradient(180deg, rgba(255,145,68,.5) 0 1px, transparent 1px 13px);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 80%, transparent 100%);
          mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 80%, transparent 100%);
}

/* Invisible at rest. js/sec-quality.js sweeps it once and takes it back
   to zero, so no-JS and reduced-motion both land on "not there" rather
   than on a bright band stranded across the photograph. */
.qhero__scan {
  position: absolute; left: 0; right: 0; top: 0; height: 128px; opacity: 0;
  background: linear-gradient(180deg,
    rgba(255,145,68,0) 0%, rgba(255,145,68,.10) 62%,
    rgba(255,145,68,.26) 92%, rgba(255,214,170,.60) 100%);
  transform: translate3d(0, -150px, 0);
  will-change: transform, opacity;
}

/* The datum. Parked at 58% by CSS, which IS the finished state — the
   pointer tracking in JS only ever offsets it from here, so a page with
   no JS, no pointer or reduced motion still shows a measured frame.

   Transparent across the whole left half on purpose: the line's job is
   to measure the SPECIMEN, and a hairline ruled through the middle of a
   paragraph is just a hairline ruled through a paragraph. */
.qhero__datum {
  position: absolute; left: 0; right: 0; top: 58%; height: 1px;
  background: linear-gradient(90deg,
    rgba(255,107,26,0) 0%, rgba(255,107,26,0) 42%, rgba(255,107,26,.22) 54%,
    rgba(255,145,68,.6) 76%, rgba(255,145,68,.92) 100%);
  will-change: transform;
}
.qhero__datum::after {
  content: ''; position: absolute; right: 0; top: -4px;
  width: 0; height: 0;
  border: 4px solid transparent; border-right-width: 5px;
  border-right-color: var(--ember);
}

/* ---- the copy, and the plinth it stands on --------------- */
.qhero__inner {
  align-self: end;
  padding-bottom: clamp(1.6rem, 4.4vh, 2.8rem);
}
/* Narrower than the shared 58ch so the last line of the lede stays
   inside the dark pool rather than running out into the lit half. */
.qhero__inner .phero__lede { max-width: 50ch; }
.qhero__inner .phero__actions { margin-top: clamp(1.3rem, 3.2vh, 2rem); }

.qhero__rail {
  position: relative; z-index: 2;
  border-top: 1px solid var(--rule-d);
  background: linear-gradient(180deg, rgba(6,21,41,.70) 0%, rgba(6,21,41,.95) 100%);
  padding: clamp(.85rem, 2vh, 1.15rem) 0 clamp(1rem, 2.4vh, 1.5rem);
}
.qhero__rail .badges { margin-top: 0; gap: .45rem .55rem; }

/* A full-bleed photograph under a full-width column of copy is the one
   arrangement that cannot be scrimmed into contrast without erasing the
   photograph — measured at 375px the h1 came back at 1.27 against a
   backdrop of rgb(133,134,146).

   So the ground opens at the TOP instead. The image is still one
   continuous ground, not a band: the scrim simply holds off for the
   first fifth of the frame, where the probe and the block are, and
   closes to near-solid navy under the copy. The ruler stays with it,
   because the strip is only the specimen if the instrument is on it. */
@media (max-width: 900px) {
  .qhero { min-height: 0; padding-top: calc(var(--nav-h) + clamp(5rem, 17vh, 8rem)); }
  .qhero__img { object-position: 62% 34%; }
  .qhero__scrim {
    background:
      linear-gradient(180deg,
        rgba(6,21,41,.50) 0%, rgba(6,21,41,.32) 9%, rgba(6,21,41,.88) 19%,
        rgba(6,21,41,.965) 29%, rgba(6,21,41,.965) 74%, rgba(6,21,41,.98) 100%),
      radial-gradient(120% 40% at 64% 8%, rgba(255,107,26,.12) 0%, rgba(255,107,26,0) 74%);
  }
  .qhero__ruler--top { top: calc(var(--nav-h) + .4rem); }
  /* The side ruler and the datum both go: there is no pointer to drive the
     datum on a touch screen, and a hairline that stops in mid-air beside a
     button reads as a rendering fault rather than as an instrument. */
  .qhero__ruler--side, .qhero__datum { display: none; }
  .qhero__cap {
    top: auto; bottom: auto;
    /* parked at the foot of the open strip, where the scrim has closed */
    top: calc(var(--nav-h) + clamp(2.4rem, 9vh, 4.4rem));
    left: var(--gutter); right: var(--gutter);
    max-width: none; text-align: right;
  }
}
@media (max-width: 560px) {
  .qhero__cap { font-size: .66rem; letter-spacing: .1em; }
}

/* =========================================================
   1 · POLICY — the statement, and the register beside it
   ========================================================= */

/* Both of these sections park content translated to the RIGHT until it
   scrolls in — the register rows by 24px, the chain plates by 26px. In LTR
   a rightward overhang is real horizontal overflow (a leftward one is not),
   and at 375px it was pushing the document 5px wide. Clipping the band is
   the fix that survives every offset rather than one tuned per breakpoint. */
.qpol, .qspine { overflow: hidden; }

/* TYPOGRAPHY PASS — round 2, item 8. Layout and type only; not one word of
   this section's copy changed, and none may.

   What was wrong, precisely: the pillar name and its sentence were BOTH
   --fs-small (.885rem), both weight-differentiated only, and both painted
   --ink. Two lines of identical size and colour is not a hierarchy — it is
   a paragraph with a bold lead-in, and it made five undertakings read as
   one grey slab. The statement beside them topped out at 1.85rem, close
   enough to the section heading to compete with it rather than answer it.

   The fix is three separations, all of them typographic:
     1. the statement steps UP and its measure is capped, so it reads as
        the signed sentence it is;
     2. the pillar name steps UP and gains its own tracking;
     3. the sentence steps DOWN to --slate, so name and sentence occupy
        different planes instead of the same one.
   Row rhythm opens up to match, and the index sits on the name's cap line
   rather than on the row's baseline. */

/* RELAYOUT 2026-08-27, client's second note on this section. Was statement
   LEFT / pillars RIGHT. Now statement across the top running to the right
   edge, pillars as one horizontal band beneath it. Reason it works: the five
   pillars are peers, and a 5-across band says that far better than a stacked
   list, which implied a ranking that was never meant. It also gives the
   statement the full measure the client asked for without shrinking it
   further. */
.qpol__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(2.2rem, 5vh, 3.6rem);
  align-items: start;
}

.qpol__stmt {
  margin: 0;
  padding-left: clamp(1.1rem, 2.2vw, 1.9rem);
  border-left: 3px solid var(--molten-ink);
}
.qpol__stmt p {
  margin: 0;
  font-family: var(--f-display);
  font-weight: 300;
  /* 2.15rem -> 1.6rem at the top end, 2026-08-27: the client read the policy
     statement as oversized. Still a statement rather than body copy - it keeps
     the display face, the 300 weight, the short measure and the molten rule. */
  /* 1.6rem -> 1.3rem, 2026-08-27 (third pass). Now that it runs the full
     width it carries a long measure, and a long measure wants a smaller size. */
  font-size: clamp(1rem, .9rem + .5vw, 1.3rem);
  line-height: 1.24;
  letter-spacing: -.02em;
  /* A statement wants a short measure. Left uncapped it ran the full
     column and set at 1.85rem across ~46 characters, which is a paragraph
     shape, not a statement shape. */
  /* Cap lifted from 24ch: the statement no longer shares a line with the
     pillars, so a short measure just left a narrow column against dead
     space. It now runs the band. 62ch keeps it off the "one enormous line"
     end. Client asked for it to reach the right edge, and at the reduced
     size it now carries that measure without becoming a wall. */
  max-width: none;
  color: var(--ink);
  text-wrap: balance;
}

/* ROW LAYOUT, 2026-08-27 (Daniel's second pass on this section). The 5-across
   band gave each pillar about a fifth of the page, so every description wrapped
   to 4-5 short lines and the five columns ended up ragged. Each pillar is now
   one full-width ROW - index, then name, then sentence, reading left to right -
   and the five rows stack. Same "use the whole page" intent as the band, but
   the long axis is now the one the sentences actually need. */
/* TWO COLUMNS, 2026-08-27 - option B, the one Daniel actually meant. No
   border-top: the first rule in the band is the one UNDER row one, so the
   pillars read as a list that starts at the statement rather than as a table
   with a header edge. */
.qpol__list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(2rem, 5vw, 4.5rem);
}
.qpol__list li {
  display: grid;
  grid-template-columns: 2.6rem minmax(0, 1fr);
  gap: 0 .2rem;
  align-items: start;
  /* Still required: each li stretches to the tallest in its ROW, and
     align-content defaults to stretch, which would push the name down by
     whatever slack that cell has. */
  align-content: start;
  padding: clamp(1.2rem, 2.4vh, 1.6rem) 0;
  border-bottom: 1px solid var(--rule-l);
}
/* Only the final pillar loses its rule - it is the end of the list, not the
   end of a column. */
.qpol__list li:last-child { border-bottom: 0; }
/* TERM - DEFINITION, 2026-08-27 (option C of four rendered). The fixed
   15.5rem name column that preceded this left a river of dead space beside
   the short names - "Customer Focus" is half the width of "Product Conformity
   & Compliance", so the sentences all started miles from their own titles.
   Setting both inline means the sentence begins exactly where the name ends,
   and the dead column cannot exist by construction.

   h3 stays an h3 - inline is a rendering change, not a semantic one, so the
   heading is still a heading to assistive tech. The dash is decorative
   generated content and carries no meaning that the two type treatments do
   not already carry. */
/* Name over sentence, stacked. The inline term-definition pair that preceded
   this is gone with it - at half width the sentence has nowhere to run. */
.qpol__c {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: .34rem;
  align-items: start;
}
.qpol__no {
  font-family: var(--f-mono);
  font-size: .8rem;
  letter-spacing: .1em;
  color: var(--molten-ink);
  /* Optical: the mono digits' cap height sits lower than Poppins' at these
     two sizes, so `align-items: start` alone left the index floating a
     couple of pixels above the name it belongs to. */
  padding-top: .12em;
}
/* The client's revised policy (2026-08-17) gives each undertaking a name as
   well as a sentence. The li grid stays two columns — number, then content —
   so the pair is wrapped rather than added as a third grid child, which would
   otherwise wrap under the number. */
.qpol__c h3 {
  margin: 0;
  font-family: var(--f-display);
  font-size: clamp(1rem, .95rem + .2vw, 1.1rem);
  font-weight: 600;
  line-height: 1.28;
  letter-spacing: -.014em;
  color: var(--ink);
}
.qpol__list p {
  margin: 0;
  font-size: var(--fs-small);
  line-height: 1.58;
  letter-spacing: .001em;
  color: var(--slate);
}

/* Two columns need roughly 300px each before the sentences start breaking
   badly; below that it is one column. */
@media (max-width: 820px) {
  .qpol__list { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
  .qpol__list li { grid-template-columns: 2.3rem minmax(0, 1fr); }
}

/* =========================================================
   2 · QC — the chain.  This page's one moment.

   PHOTO-LED as of 2026-09-09. Checkpoints 02–05 carry a
   photograph; 01 and 06 do not, because there is no
   supplier-audit shot and no despatch shot to carry. The note
   in build/pages/quality.html says how the 4 + 2 split is made
   to read as a bracket, and lists the two edits that add a
   picture to either terminus later.

   THE RAIL IS MEASURED, NOT ASSUMED — and this file is what
   could break that. `.qchain__rail` starts at 24px, which is
   the first ring's CENTRE, and js/sec-quality.js re-derives
   every checkpoint threshold from its ring's live box on each
   refresh. So the nodes may become any height they like, but
   the ring must stay pinned to the top of its row: `.qnode__mark`
   takes no margin and no vertical offset, and the photograph
   sits UNDER it inside the plate, never above it. Move the ring
   and RAIL_TOP in the script is wrong at every breakpoint.

   The chain roughly doubled in height. That is the point of the
   rebuild, and the rail, the travelling head and the six rings
   all still resolve from the one scrub value.
   ========================================================= */

.qchain { position: relative; margin-top: clamp(2rem, 5vh, 3.4rem); }

/* No transform on the rail itself — its children need transforms of their
   own, and a translateX here would have to be restated in every one. */
.qchain__rail {
  position: absolute;
  left: 50%; margin-left: -1px;
  top: 24px; bottom: 0; width: 2px;
  z-index: 0;
  background: rgba(169, 187, 208, .24);
}
.qchain__rail::after {
  content: '';
  position: absolute;
  left: -6px; bottom: 0; width: 14px; height: 2px;
  background: rgba(169, 187, 208, .24);
}
.qchain__fill {
  position: absolute; inset: 0;
  display: block;
  background: linear-gradient(180deg, rgba(194, 65, 12, .8) 0%, var(--molten) 55%, var(--ember) 100%);
  box-shadow: 0 0 16px 1px rgba(255, 107, 26, .4);
  transform: scaleY(1);
  transform-origin: 50% 0;
}
.qchain__head {
  position: absolute;
  left: 50%; top: 0;
  width: 9px; height: 9px;
  margin: -4px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--ember);
  box-shadow: 0 0 10px 2px rgba(255, 107, 26, .85), 0 0 26px 6px rgba(255, 107, 26, .35);
  opacity: 0;                      /* JS owns this one entirely */
}

.qchain__list {
  position: relative; z-index: 1;
  display: grid;
  /* Wider than it was. The old clamp(1.8rem, 5vh, 3.2rem) was set between
     two-line plates; between a 270px photograph and a 405px one it reads
     as a crack, not as a gap. */
  gap: clamp(2.6rem, 7vh, 5rem);
}

.qnode {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 88px minmax(0, 1fr);
  align-items: start;
  /* The connector's exact reach, rather than a guessed 34px: half of the
     mark column that is not ring, plus the plate's own inner padding. It
     therefore LANDS on the plate's content edge — the photograph's frame,
     or the terminus hairline — instead of stopping in mid-air. */
  --qconn: calc(20px + clamp(1.2rem, 2.6vw, 2.4rem));
}

.qnode__mark {
  position: relative;
  grid-column: 2; grid-row: 1;
  justify-self: center;
  width: 48px; height: 48px;
  border-radius: 50%;
  background: var(--navy-900);   /* masks the rail behind the ring */
}
.qnode__mark svg { width: 100%; height: 100%; overflow: visible; }
.qnode__track { fill: none; stroke: rgba(255, 255, 255, .18); stroke-width: 1.5; }
/* 2πr for r=21. Parked at 0 so a no-JS visitor sees closed rings. */
.qnode__arc {
  fill: none;
  stroke: var(--molten);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 131.95;
  stroke-dashoffset: 0;
  transform: rotate(-90deg);
  transform-origin: center;
  transition: stroke-dashoffset var(--d-l) var(--e-out);
}
.qnode__mark i {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-family: var(--f-mono);
  font-style: normal;
  font-size: var(--fs-label);
  letter-spacing: .08em;
  color: var(--molten);
}
/* The connector reaches OUT from the ring toward its plate, so it grows
   from the chain rather than from the copy. */
.qnode__mark::before {
  content: '';
  position: absolute;
  top: calc(50% - 1px); height: 2px; width: var(--qconn);
  background: var(--molten);
  transform: scaleX(1);
  transition: transform var(--d-m) var(--e-out);
}

/* ---- The plate ------------------------------------------- */

/* A DEFINITE width, not the `max-width` this used to carry. `justify-self:
   end` plus `max-width` makes the plate shrink-to-fit, and a shrink-to-fit
   box sizes to its TEXT's max-content — so every plate came out as wide as
   its own longest sentence, which was invisible while the plates were type
   and would have given the four photographs four different widths. It is
   also the exact shape that measures 0×0 before its picture lands and then
   charges CLS for the snap. `min()` closes both. */
.qnode__plate {
  grid-row: 1;
  /* The plate takes its WHOLE half of the chain, so a photograph's outer
     edge lands on the same line as the h2 above it and the section's own
     margin — 143px more picture per node at 1440 than the 30rem the plate
     used to be capped at, and a grid the eye can actually find. The
     reading measure is put back on the copy instead, below. */
  width: 100%;
  /* Zero, so the photograph's top edge lines up with the top of the ring.
     `.qnode--edge` re-states it; nothing else may. */
  padding-top: 0;
  transition: opacity var(--d-m) var(--e-out), transform var(--d-m) var(--e-out);
}
/* The measure lives here now, not on the plate. The picture is allowed to
   be wider than its words — 586px of photograph over a 480px column of
   type at 1440 — and both are pinned to the RAIL side, so the chain keeps
   one hard inner edge and everything ragged falls outward. */
.qnode__plate h3 {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: clamp(1.02rem, .95rem + .35vw, 1.25rem);
  line-height: 1.2;
  letter-spacing: -.015em;
  color: var(--white);
  margin: 0 0 .45rem;
  max-width: 30rem;
}
.qnode__plate p {
  margin: 0;
  max-width: 30rem;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--steel);
}

/* ---- The photographed checkpoints ------------------------ */

/* The frame is a fixed ratio box and the `.ph` fills it absolutely, so the
   height is known before a single byte of the photograph arrives — no
   reserved-space guessing, no shift when it lands, and the rail's measured
   thresholds are right on the first frame rather than after the last image.
   The ratio is per photograph, not per grid: `--qar` defaults to the two
   floor shots and the two modifiers below carry the other two. */
.qnode__fig {
  position: relative;
  /* `auto`, not `100%`. A block with an explicit width ignores a negative
     margin, which would have made the phone bleed at the bottom of this
     file silently do nothing — the frame would have measured the same
     265px with the rule present and absent. */
  width: auto;
  aspect-ratio: var(--qar, 16 / 9);
  margin: 0 0 clamp(.95rem, 2.2vh, 1.3rem);
  border: 1px solid var(--rule-d);
  border-radius: var(--rad);
  overflow: hidden;
  /* Behind the photo, and what the frame reads as while a picture is still
     being probed. Never over it. */
  background: var(--navy-800);
}
/* 3:4. The testing machine is a two-metre column and a letterbox crop
   throws the subject away; it is given a narrower frame instead, so the
   chain gains a tall beat without gaining 640px of height. The cap is not
   decoration — at 600px wide, a full-width 3:4 frame measured 622px tall,
   which is most of a phone screen for one photograph. */
.qnode__fig--tall { --qar: 3 / 4; width: min(100%, 24rem); }
/* 4:3, the bench's native ratio. */
.qnode__fig--box  { --qar: 4 / 3; }

/* No scrim, no opacity, no extra darkening — see the note in the markup.
   The only thing on the photograph is the shared `.ph--live` grade this
   site applies everywhere, which css/main.css sets deliberately (and which
   was already re-graded once for being too dark). The scale settle is
   transform-only and lands at 1 with no JS and under reduced motion. */
.qnode__ph {
  position: absolute; inset: 0;
  transform: scale(1);
  transition: transform var(--d-xl) var(--e-out);
}

/* The frame's rail-side edge, drawn in the same molten as the connector
   that arrives at it — so the picture is wired into the chain rather than
   floating beside it. Inside the frame, so the radius clips it. */
.qnode__fig::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0; width: 2px;
  background: var(--molten);
  transform: scaleY(1);
  transform-origin: 50% 0;
  transition: transform var(--d-l) var(--e-out);
}

/* ---- The two termini: 01 and 06 -------------------------- */

/* The connector does not stop at these plates, it becomes them: the copy
   starts ON the line the ring sent out, which is 23px down — the
   connector's own centre — and the hairline runs the plate's full width
   with a molten lead-in at the rail end. Nothing here says "a photograph
   is missing"; it says these two are the ends of the chain. Delete
   `qnode--edge` from the markup and the whole treatment goes with it. */
.qnode--edge .qnode__plate { padding-top: 23px; }
.qnode--edge .qnode__plate::before {
  content: '';
  display: block;
  height: 1px;
  margin-bottom: clamp(.95rem, 2.2vh, 1.3rem);
  transform: scaleX(1);
  transition: transform var(--d-l) var(--e-out);
}
/* A statement, so it is allowed one step more voice than a caption under a
   picture. It is the only size difference in the chain and it is on the
   two nodes that have to hold their own beside a 480px photograph. */
.qnode--edge .qnode__plate h3 {
  font-size: clamp(1.12rem, 1rem + .5vw, 1.45rem);
  margin-bottom: .5rem;
}

/* Alternating sides — the whole reason this is not the shared `.proc`. */
.qnode:nth-child(odd) .qnode__plate {
  grid-column: 1;
  justify-self: end;
  text-align: right;
  padding-right: clamp(1.2rem, 2.6vw, 2.4rem);
  --qdir: -30px;
}
.qnode:nth-child(even) .qnode__plate {
  grid-column: 3;
  justify-self: start;
  padding-left: clamp(1.2rem, 2.6vw, 2.4rem);
  --qdir: 30px;
}
.qnode:nth-child(odd) .qnode__mark::before { right: 100%; transform-origin: 100% 50%; }
.qnode:nth-child(even) .qnode__mark::before { left: 100%; transform-origin: 0 50%; }

/* A narrow frame — and a capped line of copy — hugs the rail, not the
   outside edge, so the chain keeps a hard inner line and everything ragged
   falls outward. */
.qnode:nth-child(odd)  .qnode__fig,
.qnode:nth-child(odd)  .qnode__plate h3,
.qnode:nth-child(odd)  .qnode__plate p { margin-left: auto; margin-right: 0; }
.qnode:nth-child(even) .qnode__fig,
.qnode:nth-child(even) .qnode__plate h3,
.qnode:nth-child(even) .qnode__plate p { margin-left: 0; margin-right: auto; }
.qnode:nth-child(odd)  .qnode__fig::after { right: 0; left: auto; }
.qnode:nth-child(even) .qnode__fig::after { left: 0; right: auto; }
/* The lead-in sits at the rail end of the hairline, which is the right end
   on the left-hand plates. 270deg puts the first stop against that edge. */
.qnode--edge:nth-child(odd) .qnode__plate::before {
  background: linear-gradient(270deg, var(--molten) 0 2.2rem, var(--rule-d2) 2.2rem);
  transform-origin: 100% 50%;
}
.qnode--edge:nth-child(even) .qnode__plate::before {
  background: linear-gradient(90deg, var(--molten) 0 2.2rem, var(--rule-d2) 2.2rem);
  transform-origin: 0 50%;
}

@media (max-width: 900px) {
  .qchain__rail { left: 24px; }
  .qnode {
    grid-template-columns: 48px minmax(0, 1fr);
    /* The ring now sits flush left in its own 48px column, so the gap the
       connector has to cross is the plate's padding and nothing else. */
    --qconn: clamp(1.3rem, 5vw, 2rem);
  }
  .qnode__mark { grid-column: 1; justify-self: start; }
  .qnode:nth-child(odd) .qnode__plate,
  .qnode:nth-child(even) .qnode__plate {
    grid-column: 2;
    justify-self: stretch;
    /* Back to auto so `stretch` can do its job — the column is a definite
       width, so the ratio boxes stay definite too. */
    width: auto;
    text-align: left;
    padding-right: 0;
    padding-left: clamp(1.3rem, 5vw, 2rem);
    --qdir: 26px;
  }
  .qnode:nth-child(odd) .qnode__mark::before,
  .qnode:nth-child(even) .qnode__mark::before {
    left: 100%; right: auto; transform-origin: 0 50%;
  }
  /* Every plate is now on the right of the rail, so every frame, every
     capped line and every molten edge hugs the left instead. */
  .qnode:nth-child(odd)  .qnode__fig,
  .qnode:nth-child(even) .qnode__fig,
  .qnode:nth-child(odd)  .qnode__plate h3,
  .qnode:nth-child(even) .qnode__plate h3,
  .qnode:nth-child(odd)  .qnode__plate p,
  .qnode:nth-child(even) .qnode__plate p { margin-left: 0; margin-right: auto; }
  .qnode:nth-child(odd)  .qnode__fig::after,
  .qnode:nth-child(even) .qnode__fig::after { left: 0; right: auto; }
  .qnode--edge:nth-child(odd) .qnode__plate::before,
  .qnode--edge:nth-child(even) .qnode__plate::before {
    background: linear-gradient(90deg, var(--molten) 0 2.2rem, var(--rule-d2) 2.2rem);
    transform-origin: 0 50%;
  }
  /* `width: auto` + `max-width`, not `min()`, for the same reason as the
     base frame: an explicit width would kill the phone bleed below. The
     cap is what stops the 3:4 frame reaching 622px tall at 600px wide, and
     it also stops it reading as a stamp beside a 623px landscape at 768. */
  .qnode__fig--tall { width: auto; max-width: 24rem; }
}

/* On a phone the ring column and the plate's indent together eat a fifth of
   a 375px screen, and a photograph in what is left is a stamp. The frame
   runs off the right edge instead — exactly `--gutter`, which is the wrap's
   own padding, so it reaches the viewport edge and not one pixel past it.
   `.qspine { overflow: hidden }` at the top of this file is the backstop. */
@media (max-width: 700px) {
  /* Carries the nth-child weight on purpose. The 900px block above sets
     `margin-right: auto` on the same element with (0,3,0), so a plain
     `.qnode__fig` bleed here loses the cascade and measures identical to
     no rule at all — which is exactly what it did on the first pass. */
  .qnode:nth-child(odd)  .qnode__fig,
  .qnode:nth-child(even) .qnode__fig {
    margin-right: calc(var(--gutter) * -1);
    border-right: 0;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
  }
}

/* ---- Armed: the pre-states, only once JS is driving ------- */

.qspine--live .qchain__fill { transform: scaleY(var(--qp, 0)); }
.qspine--live .qnode__arc { stroke-dashoffset: 131.95; }
.qspine--live .qnode__mark::before { transform: scaleX(0); }
.qspine--live .qnode__plate { opacity: 0; transform: translateX(var(--qdir, 0)); }
.qspine--live .qnode__ph { transform: scale(1.06); }
.qspine--live .qnode__fig::after { transform: scaleY(0); }
.qspine--live .qnode--edge .qnode__plate::before { transform: scaleX(0); }

.qspine--live .qnode.is-on .qnode__arc { stroke-dashoffset: 0; }
.qspine--live .qnode.is-on .qnode__mark::before { transform: scaleX(1); }
.qspine--live .qnode.is-on .qnode__plate { opacity: 1; transform: none; }
/* Seven things off one scrub value now, not five. The photograph settling
   out of a 1.06 scale is what makes the checkpoint read as arriving rather
   than as having been there all along, and it is the same `is-on` the ring
   and the connector answer to, so it cannot drift out of step with them. */
.qspine--live .qnode.is-on .qnode__ph { transform: scale(1); }
.qspine--live .qnode.is-on .qnode__fig::after { transform: scaleY(1); }
.qspine--live .qnode--edge.is-on .qnode__plate::before { transform: scaleX(1); }

/* =========================================================
   3 · SYSTEMS — three records that open
   ========================================================= */

.qsys__rows { margin-top: clamp(1.2rem, 3vh, 2rem); }

.qrow { border-top: 1px solid var(--rule-d); }
.qrow:last-child { border-bottom: 1px solid var(--rule-d); }

.qrow__head {
  display: grid;
  grid-template-columns: 3.4rem minmax(0, 1fr) 2rem;
  gap: .5rem clamp(.8rem, 2vw, 1.6rem);
  align-items: start;
  padding: clamp(1.3rem, 3vh, 2rem) 0;
  cursor: pointer;
  list-style: none;
}
.qrow__head::-webkit-details-marker { display: none; }
.qrow__head::marker { content: ''; }

.qrow__no {
  grid-column: 1; grid-row: 1;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: .14em;
  /* Navy-700 is the lightest band; molten measures 4.31:1 there, under the
     floor. Ember clears it at 5.5:1 without leaving the accent. */
  color: var(--ember);
  padding-top: .35em;
}
.qrow__t {
  grid-column: 2; grid-row: 1;
  font-family: var(--f-display);
  font-weight: 600;
  font-size: clamp(1.15rem, 1rem + .55vw, 1.55rem);
  line-height: 1.16;
  letter-spacing: -.018em;
  color: var(--white);
  margin: 0;
}
.qrow__lede {
  grid-column: 2; grid-row: 2;
  display: block;
  max-width: 62ch;
  font-size: var(--fs-small);
  line-height: 1.55;
  color: var(--steel);
}
.qrow__mark {
  grid-column: 3; grid-row: 1;
  position: relative;
  justify-self: end;
  width: 20px; height: 20px;
  margin-top: .3rem;
}
.qrow__mark::before, .qrow__mark::after {
  content: '';
  position: absolute;
  background: var(--ember);
}
.qrow__mark::before { left: 0; right: 0; top: 50%; height: 1.5px; margin-top: -.75px; }
.qrow__mark::after {
  top: 0; bottom: 0; left: 50%; width: 1.5px; margin-left: -.75px;
  transform: scaleY(1);
  transition: transform var(--d-s) var(--e-out);
}
.qrow[open] .qrow__mark::after { transform: scaleY(0); }

.qrow__items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 clamp(1rem, 2.4vw, 2.4rem);
  padding: 0 0 clamp(1.2rem, 3vh, 1.8rem) calc(3.4rem + clamp(.8rem, 2vw, 1.6rem));
}

/* A photograph beside the open row (2026-08-09), on Laboratory & Testing and
   Compliance & Documentation. The wrapper takes over the indent that aligns
   the list with the row heading, so the list itself drops its own padding and
   keeps its two columns — the picture is a third column, not a replacement
   for one.

   Traceability deliberately has no photograph and keeps the bare <ul>. Its
   mockup image works only because you can read "HEAT NO: 24MAY-00122", and a
   picture whose entire subject is text is the one thing this generator
   reliably gets wrong. Because these are <details> rows, only one is usually
   open, so the difference never presents as an inconsistent grid. */
.qrow__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 30%);
  gap: clamp(1rem, 2.4vw, 2.4rem);
  align-items: start;
  padding: 0 0 clamp(1.2rem, 3vh, 1.8rem) calc(3.4rem + clamp(.8rem, 2vw, 1.6rem));
}
.qrow__body .qrow__items { padding-left: 0; padding-bottom: 0; }
.qrow__ph {
  aspect-ratio: 16 / 9;
  border-radius: var(--rad);
  border: 1px solid var(--rule-d);
}
.qrow__ph.ph--live { filter: saturate(.94) brightness(.94) contrast(1.04); }

@media (max-width: 860px) {
  /* Below this the two-column list already collapses; a 30% picture beside a
     single column of items would be a stamp. It goes above them instead. */
  .qrow__body { grid-template-columns: minmax(0, 1fr); }
  .qrow__ph { order: -1; }
}
.qrow__items li {
  position: relative;
  padding: .55rem 0 .55rem 1.5rem;
  font-size: var(--fs-small);
  line-height: 1.45;
  color: var(--steel);
  border-bottom: 1px solid var(--rule-d);
}
/* 2px, not 1px: these ticks sit at fractional device-pixel offsets down a
   list, so a hairline anti-aliases to a washed grey on half the rows and
   renders solid on the other half. */
.qrow__items li::before {
  content: '';
  position: absolute;
  left: 0; top: 1.05em;
  width: 10px; height: 2px;
  background: var(--ember);
}

@media (max-width: 760px) {
  .qrow__head { grid-template-columns: 2.6rem minmax(0, 1fr) 1.6rem; }
  .qrow__items { grid-template-columns: 1fr; padding-left: calc(2.6rem + .8rem); }
}

/* =========================================================
   4 · CERTIFICATIONS — the register
   ========================================================= */

/* This section is linked from the site nav as quality.html#certifications
   and from this page's own hero. The fixed-nav offset is NOT restated here:
   tokens.css already sets `scroll-padding-top` on <html>, and a
   `scroll-margin-top` on the target would ADD to it, parking the heading a
   full extra nav-height down the screen. */

/* TYPOGRAPHY PASS — round 2, item 10. Type, colour and column rhythm only;
   every word in this table, its intro and its footnote is the client's and
   stays exactly as it is.

   What was wrong, precisely: `.dtable tbody th` sets the row name in
   DISPLAY 600 at --fs-h3 in molten-ink. Six rows of that is six shouted
   orange headings, and the one column a reader actually scans — the
   status — was the quietest thing in the row at .66rem. The accent was
   spent on the labels and there was none left for the answer.

   So the name comes down to a calm ink subhead, the status keeps the
   accent alone, and the three columns get fixed proportions instead of
   auto widths that redistributed on every row. */

.qcert .dtable { margin-top: clamp(1.6rem, 4vh, 2.4rem); table-layout: fixed; }
.qcert .dtable th, .qcert .dtable td { padding-top: clamp(1.05rem, 2vw, 1.45rem); padding-bottom: clamp(1.05rem, 2vw, 1.45rem); }
.qcert .dtable th:first-child, .qcert .dtable td:first-child { padding-left: 0; }
.qcert .dtable td:last-child { padding-right: 0; text-align: right; }
.qcert .dtable thead th:last-child { text-align: right; }

/* 31 / 46 / 23. The names are the longest fixed strings, the scopes the
   longest wrapping ones, and the status column needs exactly one pill. */
.qcert .dtable thead th:nth-child(1) { width: 31%; }
.qcert .dtable thead th:nth-child(2) { width: 46%; }
.qcert .dtable thead th:nth-child(3) { width: 23%; }

.qcert .dtable thead th {
  letter-spacing: .17em;
  padding-bottom: .8rem;
}

/* Ink, not accent, and one step down from --fs-h3. `white-space: nowrap`
   is dropped with it: at this size "IATF 16949:2016" fits its column
   anyway, and nowrap on a fixed-layout column is how a table starts
   overflowing on a phone. */
.qcert .dtable tbody th {
  font-size: clamp(1rem, .96rem + .2vw, 1.12rem);
  line-height: 1.3;
  letter-spacing: -.012em;
  color: var(--ink);
  white-space: normal;
}
.qcert .dtable td { line-height: 1.55; color: var(--slate); }

/* GREEN BAN. pages.css paints a certified pill in `--green-lt`, and green
   is Sustainability's alone. Navy carries the same "settled" weight on
   paper and measures far above the floor. */
.qcert .pill--done { color: var(--navy-800); }
.qcert .pill--wip { color: var(--molten-ink); }
.qcert .pill--plan { color: var(--slate); }
/* Sits on the name's first line rather than on the cell's top edge. */
.qcert .pill { margin-top: .12rem; font-size: .68rem; letter-spacing: .13em; padding: .32rem .68rem; }

/* The intro paragraph is two full sentences of client text. It was running
   the whole 46ch the shared `.sechead` rule allows but at --fs-small, which
   is a long grey ribbon under a display heading. Same words, a proper
   reading measure and a line-height that suits it. */
.qcert .sechead > p:not(.label):not(.sechead__sub) {
  font-size: .92rem;
  line-height: 1.62;
  max-width: 54ch;
}

/* Block, not inline-flex. `.arrowlink` is inline-flex by default, so inside
   the blockquote it ran on from the last sentence AND inflated both line
   boxes with its arrow — the quote read as three-line-spaced prose with a
   link glued to the end of it. */
.qcert .quotebar .arrowlink { display: flex; width: max-content; margin-top: 1.1rem; }

/* =========================================================
   5 · ASSURANCE — the ribbon
   ========================================================= */

.qflow {
  display: flex;
  margin-top: clamp(1.4rem, 3vh, 2.2rem);
}
.qflow__seg {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  padding: clamp(1rem, 2vw, 1.5rem) 1rem clamp(1.1rem, 2.2vw, 1.7rem) 2.5rem;
  background: linear-gradient(180deg, rgba(6, 21, 41, .78), rgba(6, 21, 41, .34));
  /* Interlocking segments. clip-path is on the compositor's allowed list,
     which is also what lets the entry animation stay cheap. */
  clip-path: polygon(0 0, calc(100% - 22px) 0, 100% 50%, calc(100% - 22px) 100%, 0 100%, 22px 50%);
}
.qflow__seg + .qflow__seg { margin-left: -18px; }
/* Deliberately unequal: the two ends carry the most words. */
.qflow__seg:first-child { flex-grow: 1.14; }
.qflow__seg:last-child { flex-grow: 1.1; }

.qflow__no {
  display: block;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: .16em;
  color: var(--molten);
  margin-bottom: .5rem;
}
.qflow__seg h3 {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: .95rem;
  line-height: 1.22;
  letter-spacing: -.012em;
  color: var(--white);
  margin: 0 0 .35rem;
}
.qflow__seg p { margin: 0; font-size: .78rem; line-height: 1.45; color: var(--steel); }

@media (max-width: 1180px) {
  .qflow { flex-wrap: wrap; }
  .qflow__seg { flex: 0 0 calc(33.333% + 6px); }
  .qflow__seg:nth-child(4) { margin-left: 0; }
  .qflow__seg:first-child, .qflow__seg:last-child { flex-grow: 0; }
}
@media (max-width: 740px) {
  .qflow { display: block; }
  .qflow__seg {
    clip-path: none;
    margin-left: 0;
    background: none;
    border-top: 1px solid var(--rule-d);
    padding: 1rem 0 1.1rem 3.4rem;
  }
  .qflow__seg:last-child { border-bottom: 1px solid var(--rule-d); }
  .qflow__no { position: absolute; left: 0; top: 1.15rem; margin: 0; }
}

/* =========================================================
   REDUCED MOTION
   js/sec-quality.js never arms the spine, so nothing above is
   in a pre-state. The head is the one element the script owns
   outright, so it is stated here as well.
   ========================================================= */

@media (prefers-reduced-motion: reduce) {
  .qchain__head { display: none; }
}
