/* =========================================================
   Direction 02 — FOUNDRY
   Depth instead of hairlines. Glass surfaces, real shadow,
   heat as the only accent. Where Precision is a printed
   page, this is a room with a furnace in it.
   ========================================================= */

/* ---------------------------------------------------------
   PRELOADER — "The Pour"

   The same clip as the hero, full-bleed, with the welcome
   set over it. On exit the hero video is synced to this
   one's currentTime and this fades out, so the pour never
   restarts — it reframes into the hero.
   --------------------------------------------------------- */

.pl { position: fixed; inset: 0; z-index: 9999; background: var(--navy-900); }

.pl__stage { position: absolute; inset: 0; overflow: hidden; }

.pl__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 58% center;
  transform: scale(1.08); transform-origin: 58% 50%;
  will-change: transform;
}

/* Legibility for centred copy over a moving pour — heavier than the
   hero's ramp because this text sits mid-frame, not off to one side. */
/* Lightened 2026-08-07 with the hero scrim — at .90 in the centre the pour
   was almost entirely hidden behind its own veil. The ring and counter sit
   dead centre, so some ground has to stay, but not that much. */
.pl__veil {
  position: absolute; inset: 0;
  background:
    radial-gradient(120% 82% at 50% 52%, rgba(6,21,41,.78) 0%, rgba(6,21,41,.58) 46%, rgba(6,21,41,.40) 100%),
    linear-gradient(180deg, rgba(6,21,41,.52) 0%, rgba(6,21,41,.20) 38%, rgba(6,21,41,.66) 100%);
}

.pl__copy {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 0 var(--gutter); text-align: center;
}

/* Every line is a masked wrapper with an <i> inside, so the reveal is a
   transform on the inner element and never an opacity fade on large text. */
.pl__kicker, .pl__name, .pl__tag { margin: 0; overflow: hidden; }
/* One mask per LINE, never one per block. A mask taller than the line it
   hides does not hide it. */
.pl__line { display: block; overflow: hidden; }
.pl__kicker i, .pl__name i, .pl__tag i { display: block; font-style: normal; will-change: transform; }

.pl__kicker {
  font-family: var(--f-mono); font-size: var(--fs-label); letter-spacing: .22em;
  text-transform: uppercase; color: var(--ember); margin-bottom: clamp(.8rem, 2vh, 1.3rem);
}

.pl__name {
  font-family: var(--f-display); font-weight: 600;
  font-size: clamp(1.7rem, 5.2vw, 4.1rem); line-height: 1.06; letter-spacing: -.022em;
  color: var(--white);
}
/* The spacing lives on the WRAPPER, never on the moving <i>. A margin on the
   animated element is not counted by yPercent — that percentage is of the
   element's own height — so the line under-travels by exactly the margin and
   leaves a sliver inside the clip box. */
.pl__name em { display: block; overflow: hidden; font-style: normal; margin-top: clamp(.5rem, 1.4vh, .9rem); }
.pl__name em i {
  font-size: clamp(.78rem, 1.7vw, 1.22rem); font-weight: 300; letter-spacing: .06em;
  color: var(--steel);
}

.pl__tag {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(92vw, 24ch);
  font-family: var(--f-display); font-weight: 300;
  font-size: clamp(1.4rem, 4vw, 3.2rem); line-height: 1.16; letter-spacing: -.016em;
  color: var(--white);
}

/* Progress hairline. The only element that admits this is a loader. */
/* ---------------------------------------------------------
   The cast ring — beat one.
   Replaced the bottom progress bar 2026-08-07. A 1px line at
   the foot of the screen is the same loader every site has;
   a mould filling with metal is this company's own.
   --------------------------------------------------------- */

.pl__load {
  position: absolute; inset: 0; z-index: 3;
  display: grid; place-items: center;
}
.pl__ring { width: clamp(168px, 25vh, 250px); height: auto; overflow: visible; }
.pl__ringtrack { stroke: rgba(169,187,208,.14); stroke-width: 1; }

/* Aluminium, not white — these are meant to read as metal segments
   dropping into a mould, and pure white reads as light instead. */
.pl__tick { fill: #cfdae6; }

/* 2πr for r=86 on both. Set here so the arc is empty before JS touches it. */
.pl__arc, .pl__arcglow {
  stroke-linecap: round;
  stroke-dasharray: 540.35; stroke-dashoffset: 540.35;
}
.pl__arc { stroke: var(--molten); stroke-width: 2.5; }
.pl__arcglow { stroke: var(--ember); stroke-width: 9; opacity: .22; }

.pl__pct {
  position: absolute; margin: 0;
  display: flex; align-items: baseline; gap: .1em;
  font-family: var(--f-mono); font-weight: 500;
  font-size: clamp(1.5rem, 3.6vh, 2.3rem); letter-spacing: .03em;
  color: var(--chrome);
  /* Tabular figures — without this the counter's width jitters on every
     tick and the whole ring looks like it is breathing. */
  font-variant-numeric: tabular-nums;
}
.pl__sym { font-size: .48em; color: var(--molten); transform: translateY(-.15em); }

/* ---------------------------------------------------------
   Skip intro — round 2, item 7.

   Bottom-right, clear of the ring and of both lines of copy at
   every width. Quiet by default and unmistakable on hover or
   focus: this is an escape hatch, not a call to action, and a
   loud button here would compete with the sequence it exists
   to interrupt.

   The `[hidden]` rule is stated explicitly. `el.hidden = true`
   loses to ANY author `display` rule at equal specificity, and
   this element has one — see PLAYBOOK, "the hidden attribute
   loses to any author display rule".
   --------------------------------------------------------- */
.pl__skip {
  position: absolute; z-index: 4;
  right: clamp(1rem, 4vw, 2.6rem); bottom: clamp(1rem, 4vh, 2.4rem);
  display: inline-flex; align-items: center; gap: .55rem;
  padding: .62rem 1.05rem;
  font-family: var(--f-mono); font-size: var(--fs-label);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--chrome);
  background: rgba(6, 21, 41, .58);
  border: 1px solid rgba(207, 218, 230, .34);
  border-radius: 999px;
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  transition: color var(--d-s) var(--e-out), border-color var(--d-s) var(--e-out),
              background-color var(--d-s) var(--e-out);
}
.pl__skip[hidden] { display: none; }
.pl__skip svg { transition: transform var(--d-s) var(--e-out); }
.pl__skip:hover, .pl__skip:focus-visible {
  color: var(--white);
  border-color: var(--molten);
  background: rgba(6, 21, 41, .82);
}
.pl__skip:hover svg, .pl__skip:focus-visible svg { transform: translateX(3px); }
.pl__skip:focus-visible { outline: 2px solid var(--molten); outline-offset: 3px; }


/* ---------------------------------------------------------
   CUSTOM CURSOR
   A molten dot with a ring chasing it. `.has-cursor` is added
   by js/cursor.js only once it has decided to run, so the
   native cursor is never hidden on a device that will not get
   a replacement.
   --------------------------------------------------------- */

.has-cursor, .has-cursor a, .has-cursor button, .has-cursor [role="button"],
.has-cursor .btn, .has-cursor .arrowlink, .has-cursor input, .has-cursor label {
  cursor: none;
}

.cur {
  position: fixed; top: 0; left: 0; z-index: 10000;
  pointer-events: none;                 /* must never eat a click */
  will-change: transform;
  transition: opacity .2s var(--e-out);
}

.cur__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--molten);
  box-shadow: 0 0 10px 1px rgba(255,107,26,.6);
  transition: width .22s var(--e-out), height .22s var(--e-out),
              opacity .2s var(--e-out), background-color .22s var(--e-out);
}

.cur__ring {
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid rgba(255,107,26,.55);
  transition: width .28s var(--e-out), height .28s var(--e-out),
              border-color .28s var(--e-out), opacity .2s var(--e-out),
              background-color .28s var(--e-out);
}

/* Over something clickable the ring opens up and fills faintly — the
   affordance is the size change, not a colour nobody will notice. */
.cur__ring.is-link { width: 56px; height: 56px; border-color: var(--ember); background: rgba(255,107,26,.10); }
.cur__dot.is-link  { width: 5px; height: 5px; }

/* Over running text the ring shrinks to a caret-ish mark so it stops
   sitting on the words like a lens. */
.cur__ring.is-text { width: 20px; height: 20px; border-color: rgba(169,187,208,.5); }

/* Over the X-ray figure and the montage stages — the two places where
   the page rewards moving the pointer around. */
.cur__ring.is-view { width: 72px; height: 72px; border-color: rgba(255,145,68,.7); }
.cur__dot.is-view  { width: 4px; height: 4px; }

.cur__ring.is-down { width: 26px; height: 26px; border-color: var(--molten); }

/* The whole thing is desktop-and-motion only, but state it here too: a
   stylesheet loaded without the script must not leave these visible. */
@media (max-width: 900px), (prefers-reduced-motion: reduce), (hover: none) {
  .cur { display: none; }
  .has-cursor, .has-cursor a, .has-cursor button { cursor: auto; }
}

/* ---------------------------------------------------------
   SKIP LINK
   Every page carries an eight-item nav ahead of its content,
   which is exactly the case a skip link exists for. Off-screen
   until focused — never display:none, because a hidden link is
   not reachable by the keyboard at all, which defeats it.

   This MUST live in main.css, not pages.css. index.html loads
   base + tokens + main only; when this rule sat in pages.css
   the homepage shipped "Skip to content" as raw unstyled text
   in the top-left corner. Every page's markup has it, so every
   page's stylesheet needs the rule.
   --------------------------------------------------------- */

.u-skip {
  position: absolute; left: 1rem; top: -3.5rem; z-index: 10001;
  padding: .7rem 1.1rem; border-radius: var(--rad);
  background: var(--molten); color: #1a0a02;
  font-family: var(--f-mono); font-size: var(--fs-label);
  letter-spacing: .1em; text-transform: uppercase; text-decoration: none;
  transition: top var(--d-s) var(--e-out);
}
.u-skip:focus { top: 1rem; }

/* ---------------------------------------------------------
   NAV
   --------------------------------------------------------- */

.nav {
  position: fixed; top: 0; left: 0; z-index: 100;
  width: 100%; height: var(--nav-h);
  display: flex; align-items: center; gap: 2rem;
  /* SAME BUG AS THE MONTAGE, one element over — see the note above
     `.imon--live`. The bar is full-bleed, so a bare `--gutter` measures from
     the VIEWPORT edge, while every section's copy sits inside the centred
     --maxw column. Below 1440px they are the same number, which is why this
     never showed on a 1440 desktop. Above it they diverge by (100vw-maxw)/2:
     measured 80px at 1600 and 240px at 1920, with the brand stranded at the
     screen edge while the page stepped right. Reported by the client, who is
     on a wide screen. Same max() idiom as `--imon-left` so both stay in step. */
  padding: 0 max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter)));
  background: rgba(6,21,41,0);
  border-bottom: 1px solid transparent;
  transition: background var(--d-s) var(--e-quad), border-color var(--d-s) var(--e-quad);
}
.nav.is-stuck {
  background: rgba(6,21,41,.86);
  backdrop-filter: blur(16px) saturate(140%); -webkit-backdrop-filter: blur(16px) saturate(140%);
  border-bottom-color: var(--rule-d);
}
.nav__brand { font-family: var(--f-display); font-weight: 600; font-size: 1.1rem; letter-spacing: .05em; white-space: nowrap; color: var(--white); }
/* Both words are ONE weight, 2026-08-27. Daniel: "one bold one word not bold".
   Cause was a bug, not taste: <b> inherits font-weight 600 from .nav__brand,
   and the UA's `bolder` maps 600 -> 900. Only Poppins 300 and 600 are
   self-hosted, so the browser SYNTHESISED the 900 by smearing the 600
   outline. Proof: canvas advance widths for 600/700/900 were all identical
   (34.21px at 17.6px) - a real weight change moves the advance.
   So `b` is pinned to 600 explicitly, and ALLOYS moves 300 -> 600 to match.
   The two words are separated by TRACK, not weight. No new font file - a 700
   woff2 would cost bytes on a mobile Lighthouse score that is already a
   launch blocker.
   Track and gap tightened again 2026-08-27 (option 5 of 7) after Daniel read
   .16em as too airy on both counts: track .16 -> .09em, margin-left 0 ->
   -.03em. The margin has to go NEGATIVE to move at all - the parent's .05em
   letter-spacing leaves a trailing space after the "D", so 0 was never
   touching. Lockup 176.1 -> 168.2px.
   Floor to respect if this is tightened again: the track is the ONLY thing
   distinguishing ALLOYS now that both words are 600. Below ~.07em the two
   words stop reading as a lockup and start reading as one word - take it out
   of the track, not out of the gap, and stop there. */
.nav__brand b    { font-weight: 600; }
.nav__brand span { font-weight: 600; letter-spacing: .09em; margin-left: -.03em; }

/* The client's emblem, added 2026-08-20 at their request.
   Supplied as a 532x364 SCREENSHOT on solid white with no alpha at all. The
   background was removed by flood-filling inward from the edges, NOT by a
   global near-white removal — the chrome highlights inside the mark are also
   near-white and a global key punched holes straight through them.
   Sized by HEIGHT, never width: the mark is wider than it is tall and the nav
   is the height-constrained axis. `flex: none` stops it being squeezed when
   the brand sits next to the burger at narrow widths. */
/* Sized against the WORDMARK'S CAP HEIGHT, not by eye and not against the
   line box. Measured 2026-08-26 after the client called the lockup weird:
   cap height here is 13px and the mark was 30px — 2.31x — so the badge was
   partnering nothing, it was dominating. 22px puts it at ~1.7x, inside the
   1.4-1.8x range a symbol normally sits at beside its own wordmark.
   Optical centring was checked at the same time and was already correct
   (0.5px out), so alignment is deliberately untouched.
   Settled at 26px (~2.0x cap) after 22px read as timid — still well inside
   the range, and the wordmark STAYS here. The nav is the only place the
   company name appears above the fold, and the badge cannot replace it:
   the "MYGLD" lettering is 17% of the emblem's height, so it needs the
   mark at ~60px to be readable, in a 76px bar. Emblem-only belongs in the
   footer, where there is no height ceiling — see .foot__mark. */
.nav__brand { display: inline-flex; align-items: center; gap: .7rem; }
/* HAIRLINE DIVIDER, 2026-08-27. The lockup carries TWO brand names - "MYGLD"
   embossed in the badge and "GLD ALLOYS" in the wordmark - so without a break
   they can read as one string, "MYGLD GLD ALLOYS". This separates them into
   logo, then company name, without touching the client's artwork. It is point
   2 of the four things diagnosed when the client called the lockup weird;
   point 3 (emblem oversized) was fixed in 0.18.

   Drawn on the IMG, not on the <b>, deliberately: a border on the wordmark is
   the text line-box height, a flat 28.5px at every width, which is TALLER than
   the 21px emblem on mobile. On the img the rule is the emblem's own height,
   so it tracks 26px -> 21px across the breakpoint for free.

   Known consequence, not a regression: this grows the mark's measured box, so
   shotlogo.js now prints 49x26 / 42x21 where it printed 37x26 / 30x21. That
   line is a console.log, not an assertion - no gate depends on it. */
/* ENLARGED 2026-09-04. Wee: 「logo 要大点吗?」, then 「你自己看一下，你是设计师，
   我相信你」; Daniel: biggest possible, WORDMARK UNCHANGED. So the wordmark stays
   at 1.1rem and only the emblem grows: 26px -> 52px, exactly double.

   This deliberately breaks the 1.4-1.8x emblem-to-cap-height norm — it is now
   4.0x — and the reason is specific to THIS mark. The embossed MYGLD sits at
   17% of the emblem's height, so at the old 26px it rendered ~4.4px and read
   as a dark smudge rather than a word. That, not the overall scale, is what
   made it look wrong. At 52px it is ~8.8px and legible. The norm assumes a
   symbol with no type inside it; this one has type inside it.

   52px is the measured ceiling, not a guess. The nav is a constant 76px — at
   the top, when `is-stuck`, and at every breakpoint — so 52px leaves 12/13px
   above and below. Verified at 390/420/768/1024/1440: no document overflow,
   and 440px+ of clearance to the burger on tablet. Past ~56px the gaps fall
   under 10px and the bar starts to feel packed. 52px was tried first and
   read as too heavy, so this sits one step down at 44px: 3.4x cap, 58% of
   the bar, 16/17px of air, and the embossed MYGLD still resolves at ~7.5px. */
.nav__mark  { flex: none; height: 44px; width: auto; display: block;
              border-right: 1px solid rgba(255,255,255,.28); padding-right: .7rem; }

@media (max-width: 420px) {
  /* Below this the wordmark and the burger start competing for the row.
     Raised 21 -> 36px with the desktop change: measured at 390px the brand
     block still clears the burger by 107px and nothing overflows. Kept below
     the desktop 52 so the mark does not dominate a phone-width bar. */
  .nav__mark { height: 32px; }
}

/* The links and the CTA travel together: on desktop they are a row pinned
   right, below 1080px they are the contents of the drawer. Keeping the CTA
   inside <nav> is what lets the drawer hold it without a second copy in the
   markup. */
.nav__panel { display: flex; align-items: center; gap: 2rem; margin-left: auto; }

.nav__links { display: flex; gap: clamp(.9rem, 1.5vw, 1.9rem); font-size: .84rem; }
.nav__links a { position: relative; color: var(--steel); transition: color var(--d-xs) var(--e-quad); }
.nav__links a::after {
  content: ''; position: absolute; left: 0; bottom: -6px; width: 100%; height: 1.5px;
  background: var(--molten); transform: scaleX(0); transform-origin: 0 50%;
  transition: transform var(--d-s) var(--e-out);
}
.nav__links a:hover { color: var(--white); }
.nav__links a:hover::after { transform: scaleX(1); }

.nav__cta {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .62rem 1.15rem; border: 1px solid rgba(255,255,255,.36); border-radius: var(--rad);
  font-size: .82rem; white-space: nowrap; color: var(--white);
  transition: background var(--d-xs) var(--e-quad), color var(--d-xs) var(--e-quad), border-color var(--d-xs) var(--e-quad);
}
.nav__cta:hover { background: var(--white); color: var(--navy-900); border-color: var(--white); }

/* ---------------------------------------------------------
   The burger and the drawer.

   This used to be `@media (max-width:1080px){ .nav__links{display:none} }`
   with nothing to replace it — no button, no drawer, no markup. Below 1080px
   the header was a logo and a Quote button, and the only way from Quality to
   Products was via the homepage.

   The drawer is `.nav__panel` itself, restyled. Closed it is
   `visibility:hidden`, which is load-bearing rather than decorative: opacity
   or transform alone would leave eight links and a button in the tab order,
   off screen, so a keyboard user tabs into a menu they cannot see. The
   `visibility` transition is delayed on close so the panel stays painted
   while it slides out, and instant on open so it is focusable immediately.
   --------------------------------------------------------- */

.nav__burger {
  display: none;
  /* Above the drawer. The panel comes later in the DOM and is positioned, so
     it painted over the button — the bars turned into an X that nobody could
     see, and the only ways out were Escape and the scrim. Neither is
     discoverable on a phone. The drawer's top padding leaves this corner
     empty precisely so the X can sit in it. */
  position: relative; z-index: 2;
  width: 44px; height: 44px;            /* 44px: the pointer-target floor */
  margin-left: auto; padding: 0;
  align-items: center; justify-content: center;
  background: none; border: 0; border-radius: var(--rad);
  color: var(--white); cursor: pointer;
}
.nav__burgerbox { position: relative; display: block; width: 22px; height: 14px; }
.nav__burgerbox i {
  position: absolute; left: 0; width: 100%; height: 1.5px;
  background: currentColor; border-radius: 1px;
  transition: transform var(--d-s) var(--e-out), opacity var(--d-xs) linear;
}
.nav__burgerbox i:nth-child(1) { top: 0; }
.nav__burgerbox i:nth-child(2) { top: 6.25px; }
.nav__burgerbox i:nth-child(3) { bottom: 0; }
.nav.is-open .nav__burgerbox i:nth-child(1) { transform: translateY(6.25px) rotate(45deg); }
.nav.is-open .nav__burgerbox i:nth-child(2) { opacity: 0; }
.nav.is-open .nav__burgerbox i:nth-child(3) { transform: translateY(-6.25px) rotate(-45deg); }

.nav__scrim { display: none; }

@media (max-width: 1080px) {
  .nav__burger { display: inline-flex; }

  /* The bar cannot stay transparent while the drawer is out, or the drawer
     reads as floating loose over the hero. */
  .nav.is-open {
    background: rgba(6,21,41,.92);
    backdrop-filter: blur(16px) saturate(140%); -webkit-backdrop-filter: blur(16px) saturate(140%);
    border-bottom-color: var(--rule-d);
  }

  /* SIZED IN VIEWPORT UNITS, NOT `inset: 0` — and the panel below likewise.
     `.nav` carries `backdrop-filter` when it is stuck or open, and a
     backdrop-filter makes an element the containing block for its
     position:fixed descendants exactly the way a transform does. `inset: 0`
     therefore resolved against the 76px nav bar, not the screen: the scrim
     measured 375x75 and the drawer was a 76px-tall scrollable sliver.

     Worse, it depended on STATE — at the top of an unscrolled page there is no
     backdrop-filter and no containing block, so the same rule resolved against
     the viewport and looked perfectly correct. Explicit vw/vh is right in both
     cases, because the header is fixed at the viewport's top edge and full
     width, so its padding box and the viewport share an origin.
     `dvh` second so mobile browsers discount their own URL bar. */
  .nav__scrim {
    display: block; position: fixed; top: 0; left: 0; z-index: -1;
    width: 100vw; height: 100vh; height: 100dvh;
    background: rgba(3,11,22,.62);
    opacity: 0; visibility: hidden;
    transition: opacity var(--d-s) var(--e-quad), visibility 0s linear var(--d-s);
  }
  .nav.is-open .nav__scrim {
    opacity: 1; visibility: visible;
    transition: opacity var(--d-s) var(--e-quad), visibility 0s;
  }

  .nav__panel {
    /* height in vh, not `bottom: 0` — see the note on .nav__scrim above. */
    position: fixed; top: 0; right: 0; z-index: 1;   /* below .nav__burger */
    width: min(86vw, 340px); height: 100vh; height: 100dvh;
    margin: 0; gap: 0;
    flex-direction: column; align-items: stretch; justify-content: flex-start;
    padding: calc(var(--nav-h) + 1.4rem) var(--gutter) 2.2rem;
    background: var(--navy-900);
    border-left: 1px solid var(--rule-d);
    overflow-y: auto; overscroll-behavior: contain;
    transform: translateX(100%); visibility: hidden;
    transition: transform var(--d-m) var(--e-out), visibility 0s linear var(--d-m);
  }
  .nav.is-open .nav__panel {
    transform: none; visibility: visible;
    transition: transform var(--d-m) var(--e-out), visibility 0s;
  }

  .nav__links { flex-direction: column; align-items: stretch; gap: 0; font-size: 1rem; }
  .nav__links a {
    display: block; padding: .95rem .1rem;
    border-bottom: 1px solid var(--rule-d);
    color: var(--white);
  }
  /* The underline sweep is a pointer affordance and there is no pointer here;
     at full width it also reads as a second, brighter rule under every row. */
  .nav__links a::after { display: none; }
  .nav__links a[aria-current="page"] { color: var(--molten); }

  .nav__cta { margin-top: 1.6rem; justify-content: center; }
}

/* Locking the page behind the drawer. */
html.is-navlocked, html.is-navlocked body { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .nav__panel, .nav__scrim, .nav__burgerbox i { transition: none; }
}

/* ---------------------------------------------------------
   BANDS + SHARED PIECES
   --------------------------------------------------------- */

.sec { position: relative; padding: var(--pad-normal) 0; }
.sec--loose { padding: var(--pad-loose) 0; }
.sec--tight { padding: var(--pad-tight) 0; }

.sec--deep  { background: var(--navy-900); }
.sec--mid   { background: var(--navy-800); }
.sec--rise  { background: var(--navy-700); }
.sec--light { background: var(--paper); color: var(--ink); --focus: var(--molten-ink); }

/* Metal grain — 3.5% over dark bands only. Kills the flat-hex-fill look
   that makes the mockups read as vector art. */
.grain::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: .038;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}
.grain > * { position: relative; z-index: 1; }

.label {
  display: flex; align-items: center; gap: .75rem;
  font-family: var(--f-mono); font-size: var(--fs-label);
  letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--molten);
}
.label::before {
  content: ''; width: clamp(24px, 3.4vw, 46px); height: 1px; background: currentColor;
  transform: scaleX(var(--eb, 0)); transform-origin: 0 50%;
}
.sec--light .label { color: var(--molten-ink); }
/* The raised band is the lightest navy, and a glass card lifts it further:
   molten measures 4.31:1 there, under the floor. Ember clears it at 5.5:1
   without leaving the accent. */
.sec--rise .label { color: var(--ember); }
/* Doubled up deliberately: green is a semantic, not a tint, so it has to
   out-specify the per-band accent rules above rather than sit under them. */
.label.label--green { color: var(--green-lt); }
/* ...but --green-lt is tuned for navy (6.6:1 there) and washes out to 1.91:1
   on the light band, which is what happened the moment round 2 put a green
   eyebrow on `.sec--light` (#responsible). The accent needs both halves
   written, same as molten/ember above: the dark token on dark, the dark-green
   token on light, where it measures 4.87:1. */
.sec--light .label.label--green { color: var(--green); }

/* THE CTA EYEBROW IS WHITE, and this is the same class of bug as the green
   one above. .cta stacks an orange radial glow (.cta__glow, rgba(255,145,68,.42)
   at its centre) at left:-6% top:-40% - which is exactly where the eyebrow
   sits. Molten text on a molten halo. Client-reported 2026-09-09: "the word
   and the picture collides the colour, can't watch it at all".

   Measured on industries.html at 2x, sampling the real rendered background
   behind the glyphs (label is 11.52px/400, so AA wants 4.5:1):

       molten            4.10 median   2.39 in the photo's hot 5%   FAIL
       ember             5.22          3.05                        still fails
       ember + glow .55  5.47          4.01                        still fails
       white            11.68          6.81                        PASSES

   Ember is not enough: the failure is worst over the bright patches of
   cta-metal.jpg, and no warm hue survives there at this size. Dimming the
   glow to rescue a warm eyebrow costs the CTA its signature and STILL does
   not reach 4.5, so the glow is left alone.

   The rule is scoped to .cta on purpose. The other seven .label instances
   sit on flat navy where molten measures fine - this is not a global
   retreat from the accent colour.

   ::before is restated because it paints from currentColor, so whitening
   the text would whiten the dash too and throw away the brand mark for no
   contrast gain - the dash is a 1px rule, not text. It stays molten. */
.cta .label { color: var(--white); }
.cta .label::before { background: var(--molten); }

.sechead { display: grid; grid-template-columns: 1fr auto; gap: 1.5rem 3rem; align-items: end; margin-bottom: clamp(2.2rem, 5vh, 3.6rem); }

/* ---------------------------------------------------------
   Inverted hierarchy, 2026-08-07 (client's call).
   The section's SUBJECT — "Our Products", "Industries We
   Serve" — is now the display type, and the descriptive line
   that used to be the h2 sits under it as support. It is
   also the more honest structure: the section IS "Our
   Products", so that is what the h2 should say.
   --------------------------------------------------------- */
/* --page-accent is molten everywhere except Sustainability, which sets it
   to green on <body>. Section titles, rules and page furniture read the
   token; the nav, footer and CTA buttons keep molten directly, because
   those are site chrome rather than page voice. One override, whole page. */
.sechead__title {
  font-family: var(--f-display); font-weight: 600; font-size: var(--fs-h2);
  line-height: 1.06; letter-spacing: -.022em; max-width: 16ch;
  color: var(--page-accent, var(--molten)); margin: 0;
}
/* The accent rule moves ABOVE the words rather than sitting inline. At
   label size it read as a bullet; at display size an inline rule would
   push the first word half a column to the right. It stays a pseudo, so
   the runtime .lift wrapper (which only moves real child nodes) leaves it
   in place — the rule holds while the words lift under it. */
.sechead__title::before {
  content: ''; display: block; width: clamp(24px, 3.4vw, 46px); height: 1px;
  background: currentColor; margin-bottom: clamp(.7rem, 1.6vh, 1.1rem);
  transform: scaleX(var(--eb, 1)); transform-origin: 0 50%;
}
.sec--light .sechead__title { color: var(--page-accent-ink, var(--molten-ink)); }
.sec--rise .sechead__title { color: var(--page-accent-lt, var(--ember)); }

.sechead p.sechead__sub, p.sechead__sub {
  font-family: var(--f-display); font-weight: 300;
  font-size: clamp(1.05rem, .9rem + .7vw, 1.5rem);
  line-height: 1.3; letter-spacing: -.01em;
  max-width: 30ch; margin-top: clamp(.7rem, 1.6vh, 1rem); color: var(--white);
}
/* Must out-specify `.sechead p.sechead__sub` (0,2,1) above — a bare
   `.sec--light .sechead__sub` is 0,2,0 and loses, which left this line
   painting white on the paper band at about 1:1. */
.sec--light .sechead p.sechead__sub, .sec--light p.sechead__sub { color: var(--ink); }

.sechead h2:not(.sechead__title) {
  font-family: var(--f-display); font-weight: 600; font-size: var(--fs-h2);
  line-height: 1.12; letter-spacing: -.02em; max-width: 18ch; margin-top: .9rem; color: var(--white);
}
.sec--light .sechead h2:not(.sechead__title) { color: var(--ink); }
.sechead p:not(.label):not(.sechead__sub) { max-width: 46ch; color: var(--steel); font-size: var(--fs-small); }
.sec--light .sechead p:not(.label):not(.sechead__sub) { color: var(--slate); }
@media (max-width: 800px) { .sechead { grid-template-columns: 1fr; } }

.line { display: block; overflow: hidden; padding-bottom: .08em; }
.line i { display: block; font-style: normal; transform: translateY(110%); will-change: transform; }

.btn {
  display: inline-flex; align-items: center; gap: .6rem;
  padding: .92rem 1.55rem; border-radius: var(--rad); border: 1px solid transparent;
  font-size: .9rem; font-weight: 500; cursor: pointer;
  transition: background var(--d-xs) var(--e-quad), border-color var(--d-xs) var(--e-quad),
              color var(--d-xs) var(--e-quad), transform var(--d-xs) var(--e-quad);
}
.btn svg { transition: transform var(--d-s) var(--e-out); }
.btn:hover svg { transform: translateX(4px); }
/* Primary is WHITE, never molten. */
.btn--fill { background: var(--white); color: var(--navy-900); box-shadow: 0 18px 40px -20px rgba(0,0,0,.7); }
.btn--fill:hover { background: #e9eff6; }
.btn--line { border-color: rgba(255,255,255,.34); color: var(--white); }
.btn--line:hover { border-color: var(--white); background: rgba(255,255,255,.08); }
.sec--light .btn--fill { background: var(--navy-800); color: var(--white); }
.sec--light .btn--fill:hover { background: var(--navy-700); }
.sec--light .btn--line { border-color: var(--rule-l2); color: var(--ink); }
.sec--light .btn--line:hover { border-color: var(--navy-800); background: transparent; }

.arrowlink {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: .82rem; font-weight: 500; color: var(--white);
}
.arrowlink svg { transition: transform var(--d-s) var(--e-out); }
.arrowlink:hover svg { transform: translateX(4px); }
.sec--light .arrowlink { color: var(--navy-800); }

/* ---------------------------------------------------------
   HERO — full-bleed billet field, copy in the lower left
   --------------------------------------------------------- */

.hero {
  position: relative; overflow: hidden;
  min-height: 100svh; display: flex; flex-direction: column; justify-content: flex-end;
  padding-top: calc(var(--nav-h) + 2rem);
}

.hero__field { position: absolute; inset: 0; z-index: 0; background: var(--navy-900); }

/* The pour sits BESIDE the headline, not behind it — it occupies the right
   panel and dissolves into the navy rather than butting against it.

   object-position alone could not do this. The clip is 16:9 and a 1440x900
   hero only overflows it by 160px, so panning tops out with the pour still
   near the middle. Cropping to a 70%-wide panel gives ~594px of horizontal
   overflow instead, which is what puts the stream out at ~63%.

   Scaled slightly past 100% so the scroll parallax never exposes an edge. */
.hero__video {
  position: absolute; inset: 0 0 0 30%;
  width: auto; height: 100%;
  object-fit: cover; object-position: 58% center;
  transform: scale(1.06); transform-origin: 70% 50%;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
          mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
  will-change: transform;
}

/* Sized and placed so its left edge lands at ~61% of the viewport, clear
   of the copy column's 56% cap. Widening it past that starts lifting the
   background behind the headline. */
.hero__glow {
  position: absolute; right: -10%; bottom: -24%; width: min(700px, 56vw); aspect-ratio: 1;
  border-radius: 50%; filter: blur(16px); pointer-events: none;
  background: radial-gradient(circle,
      rgba(255,150,72,.52) 0%, rgba(255,107,26,.30) 28%,
      rgba(226,78,0,.12) 50%, transparent 70%);
}

/* Heat haze. Transform-only, so it costs nothing; killed under
   reduced motion along with everything else. */
.hero__haze {
  position: absolute; left: 0; right: 0; bottom: 0; height: 62%;
  pointer-events: none; mix-blend-mode: screen; opacity: .3;
  background: radial-gradient(120% 60% at 68% 100%, rgba(255,145,68,.30), transparent 62%);
  animation: haze 9s var(--e-quad) infinite alternate;
}
@keyframes haze {
  from { transform: translate3d(0, 0, 0) scaleY(1); }
  to   { transform: translate3d(-2%, -3%, 0) scaleY(1.1); }
}

/* Legibility scrim.
   Retuned 2026-08-07 when the video replaced the billet field. The old
   ramp was modelled against a still, and measured fine at 1440x900 — but
   at 1512x982 the eyebrow landed on the pour's bright shoulder and fell
   to 3.48:1. Measured at glyph pixels across six frames of the loop, not
   computed from an assumed backdrop.

   Only the HORIZONTAL ramp was strengthened. It covers the copy column
   and reaches zero by 74%, so the pour — sitting at 62% and right — is
   untouched. Raising the vertical ramp instead would have dulled the
   heat across the whole frame, which is the one thing this direction
   cannot afford to lose. */
.hero__scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    /* Horizontal: unchanged. This one is load-bearing — it is the only
       thing standing between the headline and a moving pour. */
    linear-gradient(90deg, rgba(6,21,41,.95) 0%, rgba(6,21,41,.78) 30%,
                    rgba(6,21,41,.34) 52%, rgba(6,21,41,0) 74%),
    /* Vertical: lightened 2026-08-07 (Daniel — "too dark"). This one covers
       the full width, so it was darkening the pour itself as well as the
       copy, and .70 across the middle of the frame is where the clip was
       being lost. Text contrast re-verified after the change. */
    linear-gradient(180deg, rgba(6,21,41,.62) 0%, rgba(6,21,41,.22) 26%,
                    rgba(6,21,41,.46) 52%, rgba(6,21,41,.76) 100%);
}

.hero__inner { position: relative; z-index: 2; max-width: min(var(--maxw), 100%); padding-bottom: clamp(2.5rem, 7vh, 5rem); }
.hero__copy { max-width: 56%; }

.hero__eye { margin-bottom: clamp(1.2rem, 3vh, 1.9rem); }

.hero__title {
  font-family: var(--f-display); font-weight: 600; font-size: var(--fs-h1);
  line-height: 1.02; letter-spacing: -.028em; max-width: 12ch; color: var(--white);
}
/* Aluminium, not chrome: the ramp only spans the LIGHT half of the metal
   range. A full light→dark→light chrome ramp looks great in isolation and
   drops under 2:1 on real type. */
.hero__title em {
  font-style: normal;
  background-image: linear-gradient(96deg, #f4f8fc 0%, #cdd8e4 46%, #93a7bb 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

.hero__sub { max-width: 46ch; margin-top: clamp(1.3rem, 3vh, 2.1rem); color: var(--steel); opacity: 0; }

.hero__actions { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: clamp(1.7rem, 3.6vh, 2.6rem); }
.hero__actions .btn { opacity: 0; }

@media (max-width: 960px) {
  .hero { min-height: auto; padding-top: calc(var(--nav-h) + clamp(4rem, 14vh, 8rem)); }
  .hero__copy { max-width: 100%; }
  /* No room for a side-by-side split — the poster goes full-bleed behind
     the copy and the vertical scrim below carries legibility. */
  .hero__video {
    inset: 0; width: 100%; object-position: 56% center;
    -webkit-mask-image: none; mask-image: none;
  }
  .hero__scrim {
    background:
      linear-gradient(180deg, rgba(6,21,41,.66) 0%, rgba(6,21,41,.52) 22%,
                      rgba(6,21,41,.93) 46%, rgba(6,21,41,.97) 100%);
  }
}

/* ---------------------------------------------------------
   STAT BAND — dark, rules in white-11%
   --------------------------------------------------------- */

.stats { position: relative; z-index: 2; border-top: 1px solid var(--rule-d); background: rgba(4,16,31,.72); backdrop-filter: blur(6px); }
.stats__grid { display: grid; grid-template-columns: repeat(5, 1fr); }
.stat { padding: clamp(1.4rem, 3vw, 2.1rem) clamp(.9rem, 1.6vw, 1.6rem); }
.stat + .stat { border-left: 1px solid var(--rule-d); }
.stat b {
  display: block; font-family: var(--f-display); font-weight: 600;
  font-size: clamp(1.3rem, .9rem + 1.25vw, 2.1rem); letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; line-height: 1.1; white-space: nowrap; color: var(--white);
}
.stat span {
  display: block; margin-top: .35rem; font-family: var(--f-mono);
  font-size: var(--fs-label); letter-spacing: .1em; text-transform: uppercase; color: var(--steel-dim);
}
@media (max-width: 900px) {
  .stats__grid { grid-template-columns: repeat(2, 1fr); }
  .stat + .stat { border-left: 0; }
  .stat { border-top: 1px solid var(--rule-d); }
  .stat:nth-child(-n+2) { border-top: 0; }
  .stat:nth-child(2n) { border-left: 1px solid var(--rule-d); }
  .stat:last-child { grid-column: 1 / -1; }
}

/* ---------------------------------------------------------
   ABOUT — the arrival.
   Full-bleed warehouse instead of a boxed figure: the hero
   pours, this is where the metal ends up, and a framed photo
   inside a card broke that continuity. Copy sits in a left
   column over a scrim built the same way as the hero's.
   --------------------------------------------------------- */

.about { position: relative; overflow: hidden; }

.about__bg { position: absolute; inset: 0; z-index: 0; background: var(--navy-900); }
.about__img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 58% center;
  /* Same grade as .ph--live so the warehouse belongs to Foundry rather
     than sitting in it as a bright rectangle. Held one stop under the
     photo slots (.86 vs .96) because this one carries the About copy
     directly on top of it — .about__scrim does the rest. */
  filter: saturate(1) brightness(.86) contrast(1.06);
  transform-origin: 58% 50%;
  will-change: transform;
}
.about__scrim {
  position: absolute; inset: 0;
  background:
    linear-gradient(90deg, rgba(6,21,41,.95) 0%, rgba(6,21,41,.86) 34%,
                    rgba(6,21,41,.46) 60%, rgba(6,21,41,.24) 100%),
    linear-gradient(180deg, rgba(6,21,41,.62) 0%, rgba(6,21,41,.28) 34%,
                    rgba(6,21,41,.42) 70%, rgba(6,21,41,.72) 100%);
}

/* Single column now. With the photo full-bleed there is no second column to
   fill — the copy holds the left, the warehouse holds the right, and the
   scrim ramp is what divides them.

   The width cap goes on the INNER block, not on .about__grid: that element
   is also .wrap, which is margin:0 auto, so capping it there centres the
   copy instead of holding it left. */
.about__grid { position: relative; z-index: 2; }
.about__grid > div { max-width: 52%; }
/* `:not(.sechead__title)` because About's heading became the inverted
   display line — an element selector plus a class out-specifies the bare
   `.sechead__title`, so without this the section title silently paints
   white while every other one is molten. */
.about h2:not(.sechead__title) { font-family: var(--f-display); font-weight: 600; font-size: var(--fs-h2); line-height: 1.12; letter-spacing: -.02em; margin: .9rem 0 1.2rem; max-width: 17ch; color: var(--white); }
.about .sechead__title { margin-bottom: .2rem; }
/* In a .sechead the grid gap separates these; About lays out in normal
   flow, so the sub needs its own room or it collides with the body copy. */
.about p.sechead__sub { margin-bottom: clamp(.9rem, 2vh, 1.4rem); }
.about p:not(.label):not(.about__chip) { color: var(--steel); max-width: 54ch; }
.about .arrowlink { margin-top: 1.8rem; }

/* The facility figure now floats in the copy column rather than clipping
   the corner of a photo that no longer exists. */
.about__chip {
  display: block; width: max-content; margin-top: 2.4rem; padding: 1rem 1.3rem;
  background: rgba(6,21,41,.6); border: 1px solid var(--rule-d2); border-radius: var(--rad);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
.about__chip b { display: block; font-family: var(--f-display); font-weight: 600; font-size: 1.35rem; letter-spacing: -.02em; color: var(--white); }
.about__chip span { display: block; margin-top: .2rem; font-family: var(--f-mono); font-size: var(--fs-label); letter-spacing: .1em; text-transform: uppercase; color: var(--steel-dim); }

@media (max-width: 900px) {
  .about__grid > div { max-width: 100%; }
  .about__img { object-position: 62% center; }
  .about__scrim {
    background: linear-gradient(180deg, rgba(6,21,41,.86) 0%, rgba(6,21,41,.74) 40%, rgba(6,21,41,.9) 100%);
  }
}

/* ---------------------------------------------------------
   PRODUCTS — the light relief band.
   The one place in the page where product photography gets
   clean white ground instead of competing with navy.
   --------------------------------------------------------- */

/* Six columns, not three (round 2, item 1). The range grew from three
   products to five, and five children in a repeat(3, 1fr) grid is a full
   row plus two orphans. A 6-track field lets the two cast forms lead at
   3 tracks each and the three derived forms run at 2 tracks each — an
   intentional 2-up over 3-up, with the hierarchy carried by size rather
   than by order alone. */
.prods { display: grid; grid-template-columns: repeat(6, 1fr); gap: clamp(1.2rem, 2.4vw, 2rem); }
.prods > .prod { grid-column: span 2; }
.prods > .prod--lead { grid-column: span 3; }
/* The lead pair earns a wider plate and a step up in type. Everything else
   about the card — the cast reveal, the number, the hover — is unchanged,
   so there is one card component here, not two. */
.prods > .prod--lead .prod__fig { aspect-ratio: 16 / 9; }
.prods > .prod--lead .prod__body { padding: clamp(1.5rem, 2.6vw, 2.1rem); }
.prods > .prod--lead h3 { font-size: clamp(1.28rem, 1.02rem + .7vw, 1.62rem); }
.prods > .prod--lead p:not(.label) { font-size: var(--fs-body); max-width: 46ch; }
.prod {
  display: flex; flex-direction: column; background: var(--white);
  border: 1px solid var(--rule-l); border-radius: var(--rad); overflow: hidden;
  transition: transform var(--d-s) var(--e-out), box-shadow var(--d-s) var(--e-out), border-color var(--d-s) var(--e-out);
}
.prod:hover { transform: translateY(-4px); border-color: var(--molten-ink); box-shadow: 0 26px 54px -26px rgba(10,30,60,.34); }
/* ---------------------------------------------------------
   The cast — Products' signature moment.
   Each figure fills like a mould: the frame pours in from the
   top, glowing at the leading edge, then cools into the
   graded photograph while its number counts up.

   The heat is a SIBLING overlay, never a filter on .ph.
   `.ph--live` already carries the grade and `.prod:hover`
   replaces it — putting the cooling on the same property
   would have meant hovering a card cancelled its own cast.
   --------------------------------------------------------- */
.prod__fig {
  position: relative; aspect-ratio: 3 / 2; overflow: hidden;
  /* Default 0% so a visitor whose JS never runs sees a finished card,
     not an empty box. transitions.js sets it to 100% before arming. */
  clip-path: inset(0 0 var(--cast, 0%) 0);
}
.prod__cast {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  opacity: var(--heat, 0);
  background:
    linear-gradient(180deg,
      rgba(255,246,230,.95) 0%, rgba(255,145,68,.85) 18%,
      rgba(255,107,26,.55) 46%, rgba(194,65,12,.18) 74%, rgba(194,65,12,0) 100%);
}
/* Hover warms the photo instead of zooming it. transitions.js scrubs a
   settle-scale on this same element, so a CSS transform here would be
   overwritten by GSAP's inline transform and the hover would silently stop
   working. Filter is free of that conflict — and the full grade has to be
   restated, because `filter` replaces rather than composes. */
.prod__fig .ph { position: absolute; inset: 0; transition: filter var(--d-s) var(--e-out); }
.prod:hover .prod__fig .ph--live { filter: saturate(1.06) brightness(1.06) contrast(1.04); }
.prod__no {
  position: absolute; left: 0; top: 0; z-index: 2; padding: .45rem .7rem;
  background: var(--white); font-family: var(--f-mono); font-size: var(--fs-label);
  letter-spacing: .16em; color: var(--molten-ink);
}
.prod__body { padding: clamp(1.2rem, 2.2vw, 1.7rem); display: flex; flex-direction: column; flex: 1; }
.prod h3 { font-family: var(--f-display); font-weight: 600; font-size: var(--fs-h3); letter-spacing: -.015em; margin-bottom: .55rem; color: var(--ink); }
.prod p:not(.label) { font-size: var(--fs-small); color: var(--slate); flex: 1; }
.prod .arrowlink { margin-top: 1.3rem; }
/* Below the 3-up threshold the field halves: two per row, and the fifth
   card — which would otherwise sit alone against a gap — spans the full
   width and turns on its side. A deliberate closing row rather than an
   orphan. */
@media (max-width: 1020px) {
  .prods { grid-template-columns: repeat(2, 1fr); }
  .prods > .prod, .prods > .prod--lead { grid-column: span 1; }
  .prods > .prod--lead .prod__fig { aspect-ratio: 3 / 2; }
  .prods > .prod:last-child {
    grid-column: span 2;
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    align-items: stretch;
  }
  .prods > .prod:last-child .prod__fig { aspect-ratio: auto; min-height: 15rem; height: 100%; }
  .prods > .prod:last-child .prod__body { justify-content: center; }
}
@media (max-width: 700px) {
  .prods { grid-template-columns: 1fr; }
  .prods > .prod:last-child {
    grid-column: span 1;
    display: flex; flex-direction: column;
  }
  .prods > .prod:last-child .prod__fig { aspect-ratio: 3 / 2; min-height: 0; height: auto; }
}

/* ---------------------------------------------------------
   INDUSTRIES — bento, not a flat 4-up row.
   One tall lead card, two standard, one wide.
   --------------------------------------------------------- */

/* ---------------------------------------------------------
   BASE = the static grid. This is what a phone, a
   reduced-motion visitor and a no-JS visitor get, and it has
   to be a real composition on its own — not a degraded one.
   js/industries.js adds `.imon--live` to opt into the montage.
   --------------------------------------------------------- */

.imon { margin-top: clamp(1.6rem, 3vw, 2.6rem); padding: 0 var(--gutter); }

.imon__frame {
  display: grid; gap: clamp(.9rem, 1.7vw, 1.4rem);
  grid-template-columns: 1fr 1fr;
}

.imon__stage {
  position: relative; overflow: hidden;
  border-radius: var(--rad); border: 1px solid var(--rule-d);
  aspect-ratio: 16 / 10;
}
.imon__media { position: absolute; inset: 0; }
.imon__vid {
  width: 100%; height: 100%; object-fit: cover; display: block;
  /* Same grade the photo slots carry, so stills and clips sit in one world.
     Saturation no longer pulled down to .72 — that was reading as washed-out
     grey next to the brightened stills. */
  filter: saturate(.94) brightness(.88) contrast(1.06);
}
.imon__stage::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(6,21,41,.12) 30%, rgba(6,21,41,.92) 100%);
}
.imon__cap { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; padding: 1.2rem 1.3rem; }
.imon__cap h3 {
  font-family: var(--f-display); font-weight: 600; font-size: 1.05rem;
  letter-spacing: -.012em; color: var(--white);
}
.imon__cap > p:not(.imon__no) { display: none; }  /* the long line is montage-only */
.imon__no { display: none; }
.imon__go {
  display: block; margin-top: .3rem; font-family: var(--f-mono);
  font-size: var(--fs-label); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ember); opacity: 0; transform: translateY(6px);
  transition: opacity var(--d-s) var(--e-out), transform var(--d-s) var(--e-out);
}
.imon__stage:hover .imon__go, .imon__stage:focus-within .imon__go { opacity: 1; transform: none; }
.imon__stage:hover .imon__vid { filter: saturate(1.04) brightness(1.02) contrast(1.04); }
.imon__edge, .imon__rail { display: none; }

/* ---------------------------------------------------------
   LIVE = the montage. Four beats on one stage, pinned.
   Stages stack; each wipes over the last, so once a stage is
   fully drawn it simply covers the one below and no opacity
   bookkeeping is needed.
   --------------------------------------------------------- */

.imon--live { padding: 0; margin-top: clamp(2rem, 4vw, 3.4rem); }

/* WHERE THE MONTAGE'S COPY STARTS.
   This is the only full-bleed section on the page, so a bare `left: gutter`
   measures from the VIEWPORT edge while every other section's copy sits inside
   the centred --maxw column. Below 1440px those are the same number, which is
   why this never showed on a 1440 or 1512 desktop. Above it they diverge by
   (100vw - maxw)/2 — 130px at 1700, 240px at 1920 — and the montage copy is
   left behind at the screen edge while the whole page steps right.

   The visible symptom was the molten thread, which follows the content column
   past 1500px (see `.thread`), running straight through the headline: measured
   thread x=104 against copy x=61 at 1700, and 214 against 67 at 1920. Fixing
   the copy rather than moving the thread keeps the thread doing its one job —
   tracking the content column — and fixes the alignment at the same time.

   Both the caption and the beat rail read this, or they would part company on
   a wide screen. */
.imon--live {
  --imon-left: max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter)));
}

.imon--live .imon__viewport { height: 100vh; overflow: hidden; }

.imon--live .imon__frame {
  display: block; position: relative; height: 100%;
  border-radius: 0; overflow: hidden;
}

.imon--live .imon__stage {
  position: absolute; inset: 0;
  border: 0; border-radius: 0; aspect-ratio: auto; overflow: visible;
}
.imon--live .imon__stage::after { display: none; }

/* The wipe lives on the MEDIA, never on the stage — the caption is a
   sibling and must stay unclipped so text never gets sliced mid-transition. */
/* Inset from the BOTTOM, so the incoming clip grows downward. Metal
   pours down on this site; a wipe travelling up would fight it. It also
   puts the seam at (1 - wipe) of the frame, which is exactly where
   industries.js parks the molten edge — inset from the top would leave
   the glow half a viewport away from the cut it is meant to be making. */
.imon--live .imon__media {
  overflow: hidden;
  clip-path: inset(0 0 var(--wipe, 0%) 0);
  will-change: clip-path;
}
.imon--live .imon__vid { transform: scale(var(--vs, 1)); transform-origin: 50% 50%; }

/* Full-bleed footage takes a lighter grade than a thumbnail does. The
   card grade (brightness .60) plus this scrim stacked into mud — at
   thumbnail size that reads as moody, at 1440×900 it reads as broken. */
.imon--live .imon__vid { filter: saturate(1.02) brightness(1.02) contrast(1.10); }

/* Legibility floor. The generator ignored "keep the left third calm" in
   three of four clips, so the copy carries its own ground rather than
   trusting the footage underneath it. It has to fall away fast, though —
   past ~55% the frame must be clean footage or there was no point
   shooting it. */
.imon--live .imon__stage::before {
  content: ''; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background:
    linear-gradient(90deg, rgba(6,21,41,.94) 0%, rgba(6,21,41,.82) 24%, rgba(6,21,41,.30) 46%, rgba(6,21,41,0) 66%),
    linear-gradient(180deg, rgba(6,21,41,.6) 0%, transparent 15%, transparent 80%, rgba(6,21,41,.5) 100%);
}

.imon--live .imon__cap {
  position: absolute; left: var(--imon-left); right: auto; bottom: auto;
  top: 50%; transform: translateY(-50%);
  z-index: 4; padding: 0; max-width: min(38ch, 42vw);
  opacity: 0; pointer-events: none;
}
.imon--live .imon__cap.is-on { pointer-events: auto; }

.imon--live .imon__no {
  display: flex; align-items: center; gap: .55rem;
  font-family: var(--f-mono); font-size: var(--fs-label); letter-spacing: .12em;
  color: var(--ember); margin-bottom: .9rem;
}
.imon--live .imon__no i { display: block; width: 34px; height: 1px; background: currentColor; opacity: .5; }
.imon--live .imon__no span:last-child { color: var(--steel-dim); }

.imon--live .imon__cap h3 {
  font-family: var(--f-display); font-weight: 600;
  font-size: clamp(2rem, 3.6vw, 3.4rem); line-height: 1.06;
  letter-spacing: -.022em; color: var(--white); margin-bottom: .8rem;
}
.imon--live .imon__cap > p:not(.imon__no) {
  display: block; color: var(--steel); font-size: var(--fs-body); max-width: 34ch;
}
.imon--live .imon__go { opacity: 1; transform: none; margin-top: 1.4rem; }

/* The wipe's leading edge. Molten, and only visible while a wipe is
   actually running — a bar parked at the top or bottom reads as a bug,
   the same rule the thread's head follows. */
.imon--live .imon__edge {
  display: block; position: absolute; left: 0; right: 0; top: 0; height: 2px;
  z-index: 3; pointer-events: none; opacity: 0;
  background: linear-gradient(90deg, transparent 0%, var(--molten) 18%, #ffd9a8 50%, var(--molten) 82%, transparent 100%);
  box-shadow: 0 0 18px 3px rgba(255,107,26,.55), 0 0 46px 12px rgba(255,107,26,.25);
}

/* Beat rail — where you are in the four, and how far through. */
.imon--live .imon__rail {
  display: flex; gap: 6px; position: absolute; z-index: 4;
  left: var(--imon-left); bottom: clamp(1.6rem, 4vh, 2.6rem);
  list-style: none; margin: 0; padding: 0;
}
.imon--live .imon__rail li {
  width: 46px; height: 2px; background: rgba(169,187,208,.28); position: relative; overflow: hidden;
}
.imon--live .imon__rail li::after {
  content: ''; position: absolute; inset: 0; background: var(--molten);
  transform: scaleX(var(--p, 0)); transform-origin: 0 50%;
}

@media (max-width: 900px) {
  .imon__frame { grid-template-columns: 1fr; }
  .imon__stage { aspect-ratio: 4 / 3; }
}
@media (max-width: 520px) {
  .inds { grid-template-columns: 1fr; }
  .ind:nth-child(n) { grid-column: 1; grid-row: auto; aspect-ratio: 16 / 10; }
}

/* ---------------------------------------------------------
   PROCESS — the dark statement band.
   Direction 01 draws this chain horizontally. Foundry pours
   it DOWNWARD: the molten rail runs top-to-bottom and each
   node ignites as the heat reaches it. It also means the
   mobile layout is the desktop layout, not a fallback.
   --------------------------------------------------------- */

.proc__grid { display: grid; grid-template-columns: 5fr 7fr; gap: clamp(2rem, 5vw, 5.5rem); align-items: start; }
.proc__intro { position: sticky; top: calc(var(--nav-h) + clamp(2rem, 7vh, 4.5rem)); }
.sechead--stack { grid-template-columns: 1fr; margin-bottom: 0; }
.sechead--stack p:not(.label) { margin-top: 1.4rem; }

.proc__track { position: relative; }
.proc__rail { position: absolute; left: 26px; top: 12px; bottom: 12px; width: 2px; background: rgba(255,255,255,.13); }
.proc__fill {
  position: absolute; inset: 0; background: linear-gradient(180deg, var(--ember), var(--molten) 40%, #d8460a);
  transform: scaleY(0); transform-origin: 50% 0;
  box-shadow: 0 0 18px 1px rgba(255,107,26,.5);
}
.proc__steps { display: grid; gap: clamp(1.4rem, 3vh, 2.4rem); }
.proc__step { position: relative; display: grid; grid-template-columns: 54px 1fr; gap: clamp(1.1rem, 2.4vw, 2.2rem); align-items: start; }
.proc__dot {
  position: relative; z-index: 2; width: 54px; height: 54px; border-radius: 50%;
  display: grid; place-items: center; transform: scale(.86);
  border: 1px solid rgba(255,255,255,.2); background: var(--navy-900);
  font-family: var(--f-mono); font-size: .78rem; color: var(--steel-dim);
  transition: border-color var(--d-s) var(--e-quad), color var(--d-s) var(--e-quad), box-shadow var(--d-s) var(--e-quad);
}
.proc__step.is-on .proc__dot {
  border-color: var(--molten); color: var(--ember);
  box-shadow: 0 0 0 5px rgba(255,107,26,.1), 0 0 26px 2px rgba(255,107,26,.34);
}
.proc__body { padding-top: .55rem; max-width: 62ch; }
.proc__step h3 { font-family: var(--f-display); font-weight: 600; font-size: 1.05rem; letter-spacing: -.012em; margin-bottom: .3rem; color: var(--white); }
.proc__step p:not(.label) { font-size: var(--fs-small); color: var(--steel); }

@media (max-width: 900px) {
  .proc__grid { grid-template-columns: 1fr; }
  .proc__intro { position: static; }
  .proc__track { margin-top: clamp(2rem, 5vh, 3.5rem); }
}
@media (max-width: 560px) {
  .proc__rail { left: 21px; }
  .proc__step { grid-template-columns: 44px 1fr; gap: 1rem; }
  .proc__dot { width: 44px; height: 44px; font-size: .7rem; }
}

/* ---------------------------------------------------------
   FEATURE PAIR — glass panels on the raised band
   --------------------------------------------------------- */

.feats { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.2rem, 2.4vw, 2rem); }
.feat {
  position: relative; overflow: hidden; border-radius: var(--rad);
  background: rgba(255,255,255,.045); border: 1px solid var(--rule-d);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  transition: border-color var(--d-s) var(--e-quad), background var(--d-s) var(--e-quad), transform var(--d-s) var(--e-out);
}
.feat:hover { transform: translateY(-4px); background: rgba(255,255,255,.07); border-color: var(--rule-d2); }
.feat__fig { position: relative; aspect-ratio: 16 / 9; }
.feat__fig .ph { position: absolute; inset: 0; }
.feat__body { padding: clamp(1.4rem, 2.6vw, 2.1rem); }
.feat__body h3 { font-family: var(--f-display); font-weight: 600; font-size: var(--fs-h3); letter-spacing: -.015em; margin: .8rem 0 .6rem; color: var(--white); }
.feat__body p:not(.label) { font-size: var(--fs-small); color: var(--steel); max-width: 46ch; }
.feat .arrowlink { margin-top: 1.3rem; }
.feat--green .arrowlink { color: var(--green-lt); }
@media (max-width: 800px) { .feats { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------
   CERTIFICATIONS — the second light band
   --------------------------------------------------------- */

/* Five badges, not six. The 2026-08-17 cert-tier edit dropped the sixth
   badge ("Future Certification") but left the grid at six tracks, so the row
   ended in an empty white cell — the blank box the client reported. Every
   breakpoint below is sized so five cards fill their rows exactly: where five
   does not divide the track count, the trailing cards take the leftover
   tracks instead of leaving a hole. */
.certs { display: grid; grid-template-columns: repeat(5, 1fr); background: var(--white); border: 1px solid var(--rule-l); border-radius: var(--rad); overflow: hidden; }
.cert { padding: clamp(1.2rem, 2.2vw, 1.9rem) 1rem; text-align: center; transition: background var(--d-xs) var(--e-quad); }
.cert:hover { background: var(--paper); }
.cert + .cert { border-left: 1px solid var(--rule-l); }
.cert svg { margin: 0 auto .8rem; color: var(--navy-800); }
.cert b { display: block; font-family: var(--f-display); font-weight: 600; font-size: .92rem; letter-spacing: -.01em; color: var(--ink); }
.cert span { display: block; margin-top: .25rem; font-size: .74rem; color: var(--slate-lt); line-height: 1.4; }
.certs__note { margin-top: 1rem; font-size: .78rem; color: var(--slate-lt); text-align: center; }
/* Six tracks, not three: three cards of two on the first row, two cards of
   three on the second. A plain 3-up leaves the fifth card beside a hole. */
@media (max-width: 1000px) {
  .certs { grid-template-columns: repeat(6, 1fr); }
  .cert { grid-column: span 2; }
  .cert:nth-child(n+4) { grid-column: span 3; border-top: 1px solid var(--rule-l); }
  .cert:nth-child(n) { border-left: 1px solid var(--rule-l); }
  .cert:nth-child(1), .cert:nth-child(4) { border-left: 0; }
}
/* Two up, with the fifth card taking the full width rather than half a row. */
@media (max-width: 560px) {
  .certs { grid-template-columns: repeat(2, 1fr); }
  .cert, .cert:nth-child(n+4) { grid-column: span 1; }
  .cert:nth-child(5) { grid-column: span 2; }
  .cert:nth-child(n) { border-left: 1px solid var(--rule-l); }
  .cert:nth-child(odd) { border-left: 0; }
  .cert:nth-child(n+3) { border-top: 1px solid var(--rule-l); }
}

/* ---------------------------------------------------------
   CTA + FOOTER
   --------------------------------------------------------- */

.cta { overflow: hidden; }
.cta__bg { position: absolute; inset: 0; z-index: 0; opacity: .16; }
.cta__glow {
  position: absolute; left: -6%; top: -40%; z-index: 0; width: min(680px, 62vw); aspect-ratio: 1;
  border-radius: 50%; filter: blur(16px); pointer-events: none;
  background: radial-gradient(circle, rgba(255,145,68,.42) 0%, rgba(255,107,26,.2) 32%, transparent 68%);
}
.cta__inner { position: relative; z-index: 2; display: grid; grid-template-columns: 1fr auto; gap: 2rem 3rem; align-items: end; }
.cta h2 { font-family: var(--f-display); font-weight: 600; font-size: var(--fs-h2); line-height: 1.12; letter-spacing: -.02em; max-width: 18ch; margin-top: .9rem; color: var(--white); }
.cta p:not(.label) { margin-top: 1rem; color: var(--steel); max-width: 48ch; font-size: var(--fs-small); }
.cta__actions { display: flex; gap: .7rem; flex-wrap: wrap; }
@media (max-width: 860px) { .cta__inner { grid-template-columns: 1fr; } }

.foot { background: var(--void); color: var(--steel); padding: clamp(2.6rem, 6vh, 4rem) 0 0; font-size: .82rem; }
.foot__grid { display: grid; grid-template-columns: 2fr 1.4fr 1.4fr 1fr; gap: 2rem; padding-bottom: clamp(2rem, 5vh, 3rem); }
.foot__brand { font-family: var(--f-display); font-weight: 600; font-size: 1.15rem; letter-spacing: .05em; color: var(--white); }
.foot__brand span { font-weight: 300; margin-left: .12em; }
/* EMBLEM ALONE here since 2026-08-26 — the wordmark is gone from the markup,
   not hidden with CSS. This is the one place on the site the badge can be
   large: the footer has no height ceiling, and the copyright line below
   already carries "GLD Alloys (Malaysia) Sdn. Bhd." in full, so nothing is
   lost by dropping the wordmark. 44px is where the embossed MYGLD lettering
   stops being a smear (it is 17% of the emblem height, so 44px puts it at
   ~7.5px — marginal but readable; 60px would be comfortable if the client
   ever wants it bigger still).
   The b/span rules above are kept deliberately: they cost nothing and they
   are what the lockup needs if the wordmark is ever restored. */
.foot__brand { display: inline-flex; align-items: center; gap: .7rem; }
.foot__mark  { flex: none; height: 44px; width: auto; display: block; }
.foot__tag { margin-top: .5rem; font-family: var(--f-mono); font-size: var(--fs-label); letter-spacing: .1em; text-transform: uppercase; color: var(--steel-dim); }
.foot h4 { font-family: var(--f-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--steel-dim); margin-bottom: .9rem; font-weight: 400; }
.foot li + li { margin-top: .45rem; }
.foot a:hover { color: var(--white); }
.foot__bar { border-top: 1px solid var(--rule-d); padding: 1.2rem 0; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; font-size: .76rem; color: var(--steel-dim); }

/* Legal row, 2026-09-03. The client (Ms Wee, WhatsApp 2026-09-02) asked for
   exactly this arrangement and these labels:
       Privacy Notice | Terms of Use | Cookie Policy | Sitemap
   .foot__bar was already flex/wrap/space-between, so this drops in as a
   second child with no layout rework — copyright left, links right, wrapping
   under the copyright on narrow viewports.

   The pipes are CSS-generated, never markup. A literal "|" between two <a>
   elements is read aloud by a screen reader and gets copied with the link
   text; ::before on the adjacent sibling gives the same picture and is
   invisible to the accessibility tree.

   Cookie Policy is an ANCHOR, not a page — privacy-notice.html#cookies. The
   client supplied no separate cookie document, and the Privacy Notice already
   carries a full "Cookies and Other Tracking Technologies" section covering
   essential, functional and analytics cookies plus third-party content, so
   nothing is invented and her four links all resolve. That id is now a live
   target on all eleven pages: it is guarded by _qc/anchors.js. */
.foot__legal { display: flex; flex-wrap: wrap; align-items: center; gap: .55rem; }
.foot__legal a { color: var(--steel-dim); }
.foot__legal a:hover { color: var(--white); }
.foot__legal a + a::before { content: "|"; margin-right: .55rem; color: var(--rule-d); }

@media (max-width: 860px) { .foot__grid { grid-template-columns: 1fr 1fr; } }

/* ---------------------------------------------------------
   Cursor interactions — DESIGN-SPEC.md §6.4.
   Exactly three, all desktop-and-motion only. Every block in
   this section is inside the hover/pointer query below, so a
   touch device never pays for any of it.
   --------------------------------------------------------- */

/* The X-ray label is hidden until js/interactions.js proves the lens is
   actually wired (it adds `.xray-on`). It MUST be stated here, outside the
   pointer media query below, for the same reason the skip link had to move out
   of pages.css: the markup is served to every device, so a device the
   media query excludes gets NO rules at all — and an unstyled
   `<span class="xray__hint">` is not invisible, it is the words "X-ray view"
   rendered as inline text at the top-left of the About section. That is what
   Daniel's tablet was showing.

   `.grain > *` already sets `position: relative` on it, which is why it looked
   positioned while sitting in normal flow — do not test this with
   `position !== 'static'`. */
.xray__hint { display: none; }

@media (hover: hover) and (pointer: fine) {

  /* --- 1 · Heat halo -------------------------------------
     The billet field used to carry a real Three.js point light
     that tracked the pointer. With the video hero there is no
     light to move, so the halo is what survives of it: a warm
     bloom that follows the cursor across the pour. It sits
     inside .hero__field, so it is always behind the copy and
     can never lift the text background. */
  .hero__halo {
    position: absolute; top: 0; left: 0;
    width: 520px; aspect-ratio: 1; margin: -260px 0 0 -260px;
    border-radius: 50%; pointer-events: none;
    mix-blend-mode: screen; opacity: 0;
    background: radial-gradient(circle,
        rgba(255,150,72,.34) 0%, rgba(255,107,26,.16) 34%, transparent 68%);
    transition: opacity var(--d-m) var(--e-out);
    will-change: transform;
  }
  .hero:hover .hero__halo { opacity: 1; }

  /* --- 2 · X-ray lens ------------------------------------
     The brand's one uncopyable claim, made literal: first
     plant in Malaysia to X-ray aluminium in recycling. One
     per page — its power is entirely in being rare.

     The lens is a full-size layer masked to a circle that
     tracks the pointer. Masking a whole layer rather than
     moving a small element means the revealed image stays
     registered with the one underneath. */
  .xray { position: relative; }

  .xray__lens {
    position: absolute; inset: 0; opacity: 0; pointer-events: none;
    transition: opacity var(--d-m) var(--e-out);
    -webkit-mask-image: radial-gradient(circle var(--lens-r, 116px) at var(--lens-x, 50%) var(--lens-y, 50%),
                        #000 62%, rgba(0,0,0,.35) 82%, transparent 100%);
            mask-image: radial-gradient(circle var(--lens-r, 116px) at var(--lens-x, 50%) var(--lens-y, 50%),
                        #000 62%, rgba(0,0,0,.35) 82%, transparent 100%);
  }
  .xray:hover .xray__lens { opacity: 1; }

  /* Cold, high-contrast, inverted — a radiograph, not a filter
     preset. Kept off .ph deliberately so the direction's photo
     grading never stacks on top of it. */
  /* background-image comes from the element's own inline style, not from
     var(--photo): a relative url() in a custom property is resolved against
     the stylesheet that substitutes it, so consuming --photo from THIS file
     would resolve ../assets/ against 02-foundry/css/ and 404. */
  .xray__img {
    position: absolute; inset: 0;
    background-size: cover; background-position: center;
    filter: grayscale(1) invert(1) contrast(1.42) brightness(1.04);
  }
  .xray__img::after {
    content: ''; position: absolute; inset: 0;
    background:
      linear-gradient(rgba(10,30,60,.34), rgba(10,30,60,.34)),
      repeating-linear-gradient(0deg, rgba(255,255,255,.055) 0 1px, transparent 1px 3px);
  }

  /* The instrument ring. Separate from the mask so the edge
     stays crisp while the reveal itself stays soft. */
  .xray__ring {
    position: absolute; top: 0; left: 0;
    width: calc(var(--lens-r, 116px) * 2); aspect-ratio: 1;
    margin: calc(var(--lens-r, 116px) * -1) 0 0 calc(var(--lens-r, 116px) * -1);
    border-radius: 50%; pointer-events: none; opacity: 0;
    border: 1px solid rgba(255,145,68,.55);
    box-shadow: 0 0 0 1px rgba(6,21,41,.5), 0 0 26px rgba(255,107,26,.25);
    transition: opacity var(--d-m) var(--e-out);
    will-change: transform;
  }
  .xray:hover .xray__ring { opacity: 1; }

  /* Nobody hovers a photo they have no reason to hover, so the
     affordance is stated rather than hoped for.
     Top-right, not bottom-left: the facility chip already owns
     the bottom-left corner and was covering this entirely. */
  /* Bottom-right, not top-right: the section's top edge passes under the
     fixed nav, which swallowed this entirely at y=45. */
  /* `display` here is deliberately NOT enough on its own — the base rule above
     hides it and only `.xray-on` (written by interactions.js once the lens is
     listening) brings it back. */
  .xray-on .xray__hint {
    position: absolute; right: var(--gutter); bottom: clamp(1.6rem, 6vh, 3.4rem); z-index: 3;
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .42rem .7rem;
    font: 500 var(--fs-label)/1 var(--f-mono); letter-spacing: .12em; text-transform: uppercase;
    color: var(--ember); background: rgba(6,21,41,.82);
    border: 1px solid rgba(255,145,68,.28);
    transition: opacity var(--d-m) var(--e-out);
  }
  .xray__hint::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: var(--molten); box-shadow: 0 0 8px var(--molten);
  }
  .xray:hover .xray__hint { opacity: 0; }

  /* Written by interactions.js while the pointer is over (or within the lens
     core of) the text column. Higher specificity than `.xray:hover .xray__lens`
     above, so it wins without !important. Both layers go, not just the lens -
     a bare orange ring floating over the copy reads as broken. */
  .xray.xray-mute:hover .xray__lens,
  .xray.xray-mute:hover .xray__ring { opacity: 0; }
}

/* ---------------------------------------------------------
   THE THREAD — the pour continues.

   One molten line runs from the seam under About down to the
   CTA. It is not decoration: its head is pinned to 62% of the
   viewport, which is the exact line every card ignites on, so
   the thread reads as the cause of what happens beside it.

   Deliberately thin and mostly dark. The failure mode for
   this device is looking like a gimmick, and the only defence
   is restraint — the bright part is a short head, never the
   whole line.
   --------------------------------------------------------- */

.flow { position: relative; }

.thread {
  position: absolute; z-index: 5; pointer-events: none;
  left: calc(var(--gutter) - 1.15rem); top: 0; bottom: 0; width: 2px;
}
@media (min-width: 1500px) {
  /* Past the content max-width the gutter balloons; keep the thread near
     the copy rather than stranded against the viewport edge. */
  .thread { left: calc((100vw - var(--maxw)) / 2 - 1.6rem); }
}

/* Mid-grey, not white-on-transparent: this line crosses BOTH the navy
   bands and the paper ones, and a single element cannot recolour itself
   per band. Anything tuned for one ground disappears on the other. */
.thread__track { position: absolute; inset: 0; background: rgba(126,140,158,.26); }

/* Height is written by js/transitions.js. Cool at the top, hot at the head —
   but the cool end is a deep ORANGE, not a near-transparent brown, for the
   same reason: it has to survive a white background. */
.thread__fill {
  position: absolute; top: 0; left: 0; width: 100%; height: 0;
  background: linear-gradient(180deg,
      rgba(194,65,12,.55) 0%, rgba(194,65,12,.85) 46%,
      var(--molten) 90%, var(--ember) 100%);
}

.thread__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;
}

/* Ignition. The ring is its own overlay rather than a box-shadow on the card:
   .prod:hover already sets box-shadow, and a shared property means hovering
   an ignited card would silently cancel its ring. */
.prod, .feat, .cert { --ignite: 0; position: relative; }
.prod::before, .feat::before, .cert::before {
  content: ''; position: absolute; inset: 0; z-index: 3;
  border-radius: inherit; pointer-events: none;
  opacity: var(--ignite);
  box-shadow: inset 0 0 0 1px rgba(255,145,68,.85), inset 0 0 22px -6px rgba(255,107,26,.65);
}
.sec--light .prod::before, .sec--light .cert::before {
  box-shadow: inset 0 0 0 1px rgba(194,65,12,.75), inset 0 0 22px -8px rgba(194,65,12,.45);
}

@media (max-width: 900px) {
  .thread { display: none; }
}

/* ---------------------------------------------------------
   Molten seam — the band divider.
   Where a dark band meets a light one, the edge pours rather
   than simply ending. Background-position only, so it costs a
   compositor layer and nothing else.
   --------------------------------------------------------- */

.seam {
  position: relative; height: 3px; z-index: 3;
  background: linear-gradient(90deg,
      transparent 0%, var(--molten) 12%, #ffd9a8 28%, var(--molten) 44%,
      #ffb066 62%, var(--molten) 78%, transparent 100%);
  background-size: 220% 100%;
  animation: seamflow 14s linear infinite;
}
.seam::after {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 26px;
  background: linear-gradient(180deg, rgba(255,107,26,.34), transparent 72%);
  filter: blur(7px); pointer-events: none;
}
@keyframes seamflow {
  from { background-position: 0% 50%; }
  to   { background-position: 220% 50%; }
}

/* ---------------------------------------------------------
   Photo grading — Direction 02, see IMAGE-BRIEF.md §10.
   Dark and cinematic. saturate() stays high (.95 vs Precision's
   .32) — that is what keeps molten orange warm while the navy
   wash unifies everything else.

   Gated on .ph--live, set by photos.js only when the file
   really loaded. The fallback gradients are tuned ungraded and
   must stay that way, so this block is inert until Tier A
   lands. An attribute selector cannot do this — every .ph
   carries its --photo url whether or not the file exists.

   JUDGED against real photographs 2026-08-08, and re-graded.
   The brief's brightness(.60) + wash .22 was compounding into
   roughly 0.47x on an already mid-key source: prod-billets
   masters at YAVG 160 and was painting at ~69. Daniel read the
   product shots as "too dark" and he was right — the source
   photos were never the problem, this filter was.

   Now .96 / wash .08. Product captions sit BELOW the figure,
   not on it, so these slots carry no text-contrast debt and can
   take the full lift. The two surfaces that do carry overlaid
   copy — .about__img and .imon__vid — are held a stop under
   this on purpose; see the notes at each.
   --------------------------------------------------------- */

.ph--live {
  --grade-wash: .08;
  filter: saturate(1) brightness(.96) contrast(1.06);
}

.ph--live::after {
  display: block;
  background: var(--navy-900);
  background-image: none;
  opacity: var(--grade-wash);
  mix-blend-mode: multiply;
}

/* ---------------------------------------------------------
   Reduced motion — land everything in its end state
   --------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .line i { transform: none; }
  .hero__sub, .hero__actions .btn { opacity: 1; }
  .hero__haze { animation: none; }
  .label::before { transform: scaleX(1); }
  .proc__fill { transform: scaleY(1); }
  .proc__dot { transform: none; }
  .pl__video { transform: none; }
  .pl__kicker i, .pl__name i, .pl__tag i { transform: none; }

  /* The video never loads here (js/hero-video.js gates it), so the
     poster is the hero — drop the parallax scale that assumed motion. */
  .hero__video { transform: none; }
  .seam { animation: none; }
  .hero__halo { display: none; }
  .thread { display: none; }
  .prod::before, .ind::before, .feat::before, .cert::before { opacity: 0; }
}
