*{margin:0;padding:0;box-sizing:border-box}
:root{--ink0:#05070B;--ink:#080B11;--ink2:#0E141F;--navy:#1C293C;--paper:#F6F5F2;--paper2:#ECEAE4;--red:#FF3A42;--red2:#D42832;--red3:#B31E27;
 /* Contrast-safe red roles (WCAG AA, measured with alpha compositing):
    --red      #FF3A42  brand red — hairlines, ticks, large display type only (3:1 rule)
    --redtx            small red TEXT: #FF3A42 on ink = 5.2:1 ✅ ; flipped to --red2 inside .sec-paper (4.6:1)
    --redfill  #D42832  red FILL under white text (white on #FF3A42 is only 3.5:1) → 5.1:1 */
 --redtx:var(--red);--redfill:var(--red2);
 --tx:#141A24;--tx2:#5B6472;--muted:rgba(246,245,242,.6);--line:rgba(246,245,242,.12);--lineD:#E4E6EA;
 --maxw:1280px;--pad:clamp(20px,5vw,64px)}

/* ===========================================================================
   THEME — LIGHT IS THE DEFAULT, DARK IS OPT-IN.

   The handoff ships it the other way round (dark = the absence of an
   attribute) because its single-file build emits a fragment with no <html> tag
   to write data-theme="dark" on. WordPress renders the whole document, so the
   polarity is inverted here: the light values are the plain :root block and
   dark is an attribute/class the switch adds. Client decision — the site is
   light on first paint on every device, so there is deliberately NO
   prefers-color-scheme query anywhere in this theme.

   Either hook turns dark on, so the future switch can pick whichever fits:
     <html data-theme="dark">  — preferred; settable in <head> before paint
     <body class="trg-dark">   — everything themed lives inside <body>

   ROLE TOKENS, NOT COLOURS. Components read --onChrome / --chromeRGB / --sRGB
   and never a palette constant, so a theme is a token flip rather than a
   second copy of every rule. --sRGB is the surface channel triple (white on
   ink, ink on paper) and --aScale is its multiplier — white at 14% on ink is a
   hairline, ink at 14% on paper is a rule — so glass alphas are written
   rgba(var(--sRGB),calc(.14 * var(--aScale))) and flip with one number.

   MIGRATION: --muted, --line and --redtx above still hold their DARK values.
   The sections that consume them (hero, products, applications, process,
   quote form, footer) have not been converted yet, and flipping them early
   would bleach light text onto surfaces that are still dark. Move each one
   into the light block below as its part is converted; the dark block already
   declares them, so dark stays correct the moment they move.
   =========================================================================== */
:root{
 --bg-base:var(--paper);--bg-alt:var(--paper2);
 --fg:var(--tx);--fg2:var(--tx2);
 --sRGB:20,26,36;--fgRGB:20,26,36;--aScale:.62;
 /* chrome = the floating glass surfaces: nav pill, mobile-menu overlay */
 --chromeRGB:246,245,242;--chromeA:.58;--chromeAStuck:.74;
 --onChrome:var(--tx);--onChrome2:var(--tx2);
 /* the pill's control chips — menu toggle and theme switch. Both hold ONE look in
    every state (no hover, focus or active change), and the pair is stated outright
    rather than derived from --sRGB and --aScale, whose rounding would drift the
    plate off the specified alpha */
 --chipBg:rgba(20,26,36,.05);--chipFg:#141A24;
 --chromeBlur:22px;--chromeSat:180%;--chromeLift:2.2;
 /* the rim is a SPECULAR — light catching an edge, so it stays white in both
    themes and only its strength changes. Written in --sRGB it would become a
    dark line along the top of every light surface: 'border', not 'glass'. */
 --rimHi:rgba(255,255,255,.9);--rimLo:rgba(20,26,36,.07);--chromeDrop:rgba(20,26,36,.16);
 /* screen-blending a spectral wash onto paper is a no-op; multiply keeps the
    hues and reads as tinted light instead of washed light */
 --blend:multiply;--meshA:.42;
 color-scheme:light}
:root[data-theme=dark],
body.trg-dark{
 --bg-base:var(--ink);--bg-alt:var(--ink2);
 --fg:var(--paper);--fg2:rgba(246,245,242,.72);
 --sRGB:255,255,255;--fgRGB:246,245,242;--aScale:1;
 --muted:rgba(246,245,242,.6);--line:rgba(246,245,242,.12);
 --chromeRGB:10,14,21;--chromeA:.46;--chromeAStuck:.7;
 --onChrome:var(--paper);--onChrome2:rgba(246,245,242,.8);
 --chipBg:rgba(255,255,255,.08);--chipFg:#fff;
 --chromeBlur:16px;--chromeSat:150%;--chromeLift:1;
 --rimHi:rgba(255,255,255,.28);--rimLo:rgba(0,0,0,.16);--chromeDrop:rgba(0,0,0,.3);
 --redtx:var(--red);
 --blend:screen;--meshA:.55;
 color-scheme:dark}

/* Dormant until the switch is built: the toggle adds .theme-anim for the
   length of the cross-fade and removes it again. A permanent transition on
   every element would slow every ordinary hover on the site. Colour only —
   photographs, transforms and the WebGL canvases are untouched, so nothing
   re-lays-out mid-switch. */
:root.theme-anim,:root.theme-anim *{
 transition:background-color .32s ease,border-color .32s ease,color .32s ease,box-shadow .32s ease,fill .32s ease !important}
@media(prefers-reduced-motion:reduce){:root.theme-anim,:root.theme-anim *{transition:none !important}}

html{scroll-behavior:smooth}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
/* SCROLL: `clip`, never `hidden`, on the page box.
   `overflow-x:hidden` forces the other axis from `visible` to `auto`, which turns BOTH html and body
   into scroll containers. The wheel then latches onto body — which had only 26px of overflow of its
   own — and Chrome makes you exhaust that before it chains to the real scroller, so the page felt
   stuck for several wheel turns at a time. `clip` is exempt from that rule: it blocks horizontal
   scrolling without creating a scroll container, leaving exactly one scroller (the viewport).
   `hidden` is declared first as the fallback for Safari < 16, which ignores `clip`. */
html{overflow-x:hidden;overflow-x:clip}
body{background:var(--ink);color:var(--paper);font-family:'Geist',system-ui,sans-serif;-webkit-font-smoothing:antialiased;overflow-x:hidden;overflow-x:clip;max-width:100%}
.mono{font-family:'Geist Mono',ui-monospace,monospace;letter-spacing:.14em;text-transform:uppercase}
.ser{font-family:'Zodiak',Georgia,serif;font-weight:700;letter-spacing:-.02em}
h1,h2,h3{font-family:'Zodiak',Georgia,serif;font-weight:700;letter-spacing:-.02em;text-wrap:balance;line-height:1.02}
a{color:inherit;text-decoration:none}
.wrap{max-width:var(--maxw);margin:0 auto;padding-left:var(--pad);padding-right:var(--pad)}
.eyebrow{display:inline-flex;align-items:flex-start;gap:12px;font-size:12px;font-family:'Geist Mono',monospace;letter-spacing:.14em;text-transform:uppercase;line-height:1.5}
.eyebrow .tick{width:28px;height:2px;background:var(--red);flex:0 0 auto;margin-top:.55em}
.btn{font-family:'Geist';font-weight:600;font-size:16px;border-radius:32px;padding:15px 28px;cursor:pointer;border:none;transition:transform .3s cubic-bezier(.22,1,.36,1),box-shadow .3s}
.btn.p{background:var(--redfill);color:#fff;box-shadow:0 14px 34px rgba(255,58,66,.32)}
.btn.p:hover{transform:translateY(-2px);box-shadow:0 18px 42px rgba(255,58,66,.42)}
.btn.g{background:rgba(255,255,255,.08);color:var(--paper);border:1px solid rgba(255,255,255,.2)}
.btn.g:hover{background:rgba(255,255,255,.14)}
/* reveal — animation-based (robust; never sticks at 0) */
.rv{opacity:0}
.rv.in{animation:rvIn .85s cubic-bezier(.22,1,.36,1) both}
@keyframes rvIn{from{opacity:0;transform:translateY(26px)}to{opacity:1;transform:none}}
.rv.d1.in{animation-delay:.08s}.rv.d2.in{animation-delay:.16s}.rv.d3.in{animation-delay:.24s}.rv.d4.in{animation-delay:.32s}
@media(prefers-reduced-motion:reduce){.rv,.rv.in{opacity:1;animation:none}}

/* NAV pill effects → trglass/chrome.css (#tr_nav / #tr_navpill) */
/* multi-page: page-hero sections (compact, not full-viewport) */
.page-hero{padding-top:clamp(140px,16vh,200px)}
/* Inner pages have no full-viewport hero for the fixed nav to float over, so the page's
   first section needs top clearance. Put `.page-top` on that first section by hand on every
   inner page — nothing adds it for you, and without it the nav pill overlaps the heading. */
.page-top{padding-top:clamp(150px,17vh,210px) !important}
/* inner-page hero typography: h1 scale + breathing room + a readable measure */
.page-top h1{font-size:clamp(40px,6vw,86px);line-height:1.0;letter-spacing:-.02em}
.page-top h1+p,.page-top .sec-head p{margin-top:22px}
.page-top p{max-width:62ch;line-height:1.6}
.page-top .eyebrow{margin-bottom:20px}

/* HERO — cinematic architectural */
#hero-sec{position:relative;height:100svh;min-height:640px;overflow:hidden;background:var(--ink0)}
#hero-bg{position:absolute;inset:0;z-index:0;overflow:hidden}
#hero-bg-inner{position:absolute;inset:-6% -3%;background-size:cover;background-position:60% 50%;
 transform:translate3d(0,var(--hp,0),0) scale(1.06);will-change:transform;filter:saturate(1.04) contrast(1.02)}
/* spectral light sweep — a diagonal glass glint that crosses the facade */
#hero-sweep{position:absolute;inset:0;z-index:1;pointer-events:none;mix-blend-mode:screen;opacity:.0;
 background:linear-gradient(115deg,transparent 34%,rgba(94,200,255,.10) 44%,rgba(255,255,255,.16) 50%,rgba(180,120,255,.10) 56%,transparent 66%);
 background-size:260% 100%;background-position:120% 0;animation:heroSweep 7.2s cubic-bezier(.4,0,.2,1) 1.1s infinite}
@keyframes heroSweep{0%{background-position:135% 0;opacity:0}12%{opacity:.9}30%{background-position:-35% 0;opacity:0}100%{background-position:-35% 0;opacity:0}}
#hero-scrim{position:absolute;inset:0;z-index:2;pointer-events:none;
 background:linear-gradient(95deg,rgba(5,7,11,.92) 0%,rgba(5,7,11,.66) 26%,rgba(5,7,11,.2) 48%,rgba(5,7,11,0) 66%),
            linear-gradient(0deg,rgba(5,7,11,.78) 0%,transparent 34%)}
#hero-col{position:absolute;z-index:4;left:var(--pad);top:50%;transform:translateY(-50%);max-width:min(52vw,720px)}
#hero-col .eyebrow{color:var(--muted);margin-bottom:24px;line-height:1.5;align-items:flex-start}
#hero-col .eyebrow .tick{margin-top:.55em;flex:0 0 auto}
#hero-col h1{font-size:clamp(48px,8vw,120px);line-height:.98;color:var(--paper);text-shadow:0 4px 44px rgba(0,0,0,.55);letter-spacing:-.02em}
#hero-col h1 em{font-style:normal;color:var(--red)}
#hero-col p{margin-top:26px;font-size:clamp(16px,1.35vw,19px);line-height:1.55;color:rgba(246,245,242,.86);max-width:52ch;text-shadow:0 1px 16px rgba(0,0,0,.5)}
#hero-cta{margin-top:34px;display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.hglass{border-radius:28px}
/* Centred by BOX, not by transform. It carries `.rv`, whose reveal keyframes animate `transform`
   and so overwrote the `translateX(-50%)` this used to rely on — measured 26px off centre, exactly
   half its own width, on every viewport. Anything with a .rv class must not encode position in a
   transform. Full-width box + the flex centring it already had does the same job untouchably. */
#scrollcue{position:absolute;z-index:4;left:0;right:0;bottom:26px;font-size:11px;color:var(--muted);display:flex;flex-direction:column;align-items:center;gap:8px}
#scrollcue .mono{letter-spacing:.2em}
#scrollcue .bar{width:1px;height:34px;background:linear-gradient(var(--red),transparent);animation:drop 1.8s infinite}
@keyframes drop{0%{transform:scaleY(0);transform-origin:top}50%{transform:scaleY(1);transform-origin:top}51%{transform-origin:bottom}100%{transform:scaleY(0);transform-origin:bottom}}
@media(prefers-reduced-motion:reduce){#hero-sweep{animation:none;opacity:0}#hero-bg-inner{transform:scale(1.04)}#scrollcue .bar{animation:none}}
@media(max-width:820px){
 /* Was .94 -> .5 -> .15 bottom-up, i.e. almost nothing where the headline actually is. This
    facade photo is high-contrast (bright sky, dark mullions) and the display face is thin at
    display sizes, so the title has to sit on a floor of its own rather than on luck. */
 #hero-scrim{background:linear-gradient(0deg,rgba(5,7,11,.95) 0%,rgba(5,7,11,.86) 30%,rgba(5,7,11,.72) 58%,rgba(5,7,11,.55) 82%,rgba(5,7,11,.42) 100%)}
 /* The hero column is bottom-anchored and its content is tall, so on a short viewport its TOP ran
    up under the nav pill and swallowed the first line of the eyebrow. A hard `top` stops that from
    being possible at all; the content sits at the bottom of whatever space is left. */
 #hero-col{max-width:none;right:var(--pad);top:98px;bottom:clamp(120px,20vh,190px);transform:none;
  display:flex;flex-direction:column;justify-content:flex-end}
 #hero-bg-inner{background-position:70% 50%}
}
/* SHORT VIEWPORTS — Safari's real viewport is ~660-700 CSS px, not the 844 of the screen. At 660
   the hero's own content (eyebrow + 3 display lines + 4 lines of body + two stacked buttons) is
   taller than the space between the nav pill and the scroll cue, which is what pushed it upward
   in the first place. Same treatment as the configurator panel: make it fit rather than move it. */
@media(max-width:820px) and (max-height:760px){
 #hero-col{bottom:clamp(96px,14vh,130px)}
 #hero-col .eyebrow{margin-bottom:14px}
 #hero-col h1{font-size:clamp(34px,9vw,48px)}
 #hero-col p{margin-top:16px;font-size:15px;line-height:1.5}
 #hero-cta{margin-top:22px;gap:12px}
}
@media(max-width:520px){#hero-cta .btn{flex:1 1 auto;text-align:center}}

/* SECTIONS */
section{position:relative}
.sec-pad{padding-block:clamp(84px,12vw,150px)}
.sec-dark{background:var(--ink)}
.sec-navy{background:var(--ink2)}
/* light sections deepen every red TEXT role (brand red on paper is only 3.2:1) */
.sec-paper{background:var(--paper);color:var(--tx);--redtx:var(--red2)}
.sec-head{max-width:760px;margin-bottom:clamp(40px,6vw,72px)}
.sec-head .kick{color:var(--redtx);margin-bottom:16px}
.sec-head h2{font-size:clamp(34px,4.8vw,62px);line-height:1.03;letter-spacing:-.02em}
.sec-paper .sec-head h2{color:var(--tx)}
.sec-head p{margin-top:18px;font-size:clamp(15px,1.3vw,18px);line-height:1.6;color:var(--muted);max-width:560px}
.sec-paper .sec-head p{color:var(--tx2)}
.idx{font-family:'Geist Mono',monospace;font-size:13px;letter-spacing:.1em;color:var(--redtx);margin-bottom:20px}
/* SMALL RED TEXT CANNOT SURVIVE A MESH SECTION. In the dark theme the mesh screens the section
   toward its brightest blue, and 13px #FF3A42 on that is unreadable, exactly as the contrast notes
   warned. Same context-flip trick as .sec-paper: small red TEXT gives up its colour on any section
   carrying a mesh, while the red tick stays red (a graphic, judged at 3:1, not body text).
   The escape hatch inverts with the blend: on paper the mesh multiplies DOWN rather than lifting
   toward cyan, so the text goes to ink there and to paper in the dark. */
:is(section,.e-con):has(> .meshbg),
.trg-meshbg-yes{--redtx:var(--tx)}
:root[data-theme=dark] :is(section,.e-con):has(> .meshbg),
:root[data-theme=dark] .trg-meshbg-yes,
body.trg-dark :is(section,.e-con):has(> .meshbg),
body.trg-dark .trg-meshbg-yes{--redtx:var(--paper)}

/* trust marquee */
#trust{background:var(--ink);border-block:1px solid var(--line);padding-block:30px;overflow:hidden}
#trust .track{display:flex;gap:64px;align-items:center;white-space:nowrap;width:max-content;animation:marq 32s linear infinite}
#trust .track span{font-family:'Geist Mono',monospace;font-size:13px;letter-spacing:.12em;color:rgba(246,245,242,.55);text-transform:uppercase}
#trust .track b{color:var(--red)}
@keyframes marq{to{transform:translateX(-50%)}}
@media(prefers-reduced-motion:reduce){#trust .track{animation:none}}

/* PRODUCTS — live finish configurator styles live in trglass/widget-tr-products.css */

/* products — the eight finishes gallery */
.pgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
.pcard{position:relative;border-radius:22px;overflow:hidden;min-height:340px;display:flex;flex-direction:column;justify-content:flex-end;
 border:1px solid rgba(255,255,255,.1);isolation:isolate;cursor:pointer}
.pcard .pb,.tr_product_item .pb{position:absolute;inset:0;z-index:3;pointer-events:none;width:100%;height:100%;opacity:0;transition:opacity .5s ease}
/* gated to real hover: without this, a tap on iOS reveals a canvas that _tail.html deliberately
   never mounted there, i.e. an empty or garbage layer over the card image. */
@media(hover:hover) and (pointer:fine){.pcard:hover .pb,.tr_product_item:hover .pb{opacity:1}}
/* Keep the border canvas inside the card when the parent section is position:relative
   for Mesh Background (.trg-meshbg-yes). Absolute .pb would otherwise fill the section. */
.tr_product_item{position:relative;isolation:isolate}
.pcard .ph{position:absolute;inset:0;z-index:0;background-size:cover;background-position:center;
 transform:scale(1.08) translateY(var(--par,0px));transition:transform .8s cubic-bezier(.22,1,.36,1)}
/* The 1.08 base is the parallax headroom (motion.js only supplies --par now — it must never write
   `transform` inline, or it outranks every rule below).
   NO hover zoom here (user, 2026-07-25): the inspection zoom belongs to the four Process macros
   only, where the material detail IS the content. These cards are renders of a finish seen whole;
   enlarging one shows nothing more. They answer the pointer with the border glow and lift instead.
   (The old `:hover{scale(1.06)}` had never actually run — it was outranked by the inline parallax
   transform, and 1.06 was smaller than the 1.08 already applied, so it would have zoomed OUT.) */
/* The scrim was tuned for the old dim product photos. The cards now carry a bright, busy render of
   a sunlit city, and the copy sat straight on top of it. The text zone is the bottom ~60%, so the
   scrim reaches near-full opacity by then and only the upper part stays open for the material. */
.pcard .gl{position:absolute;inset:0;z-index:1;background:linear-gradient(0deg,
 rgba(8,11,17,.96) 0%,rgba(8,11,17,.93) 32%,rgba(8,11,17,.74) 52%,rgba(8,11,17,.34) 74%,rgba(8,11,17,.06) 100%)}
.pcard .body{padding:24px;position:relative;z-index:2}
.pcard .fin{display:inline-block;font-family:'Geist Mono',monospace;font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--paper);
 border:1px solid rgba(255,255,255,.28);border-radius:20px;padding:5px 12px;margin-bottom:14px;background:rgba(20,26,36,.42)}  /* no backdrop-filter: 8 cards x blur layer starves the compositor */
/* second line of defence over the render: a soft shadow, not a hard one — enough to hold an edge
   against a bright highlight without the copy looking embossed */
.pcard h3,.pcard p,.pcard .spec,.pcard .fin{text-shadow:0 1px 14px rgba(5,7,11,.75)}
.pcard h3{font-size:23px;color:var(--paper);margin-bottom:8px}
.pcard p{font-size:13.5px;line-height:1.5;color:rgba(246,245,242,.74)}
.pcard .spec{margin-top:14px;display:flex;gap:14px;flex-wrap:wrap;font-family:'Geist Mono',monospace;font-size:11px;color:rgba(246,245,242,.62)}
@media(max-width:1000px){.pgrid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.pgrid{grid-template-columns:1fr}}

/* applications */
.appgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
/* 6-item grids: 3×2 fills cleanly — 4 cols would leave a 2-card orphan row */
.appgrid.g3{grid-template-columns:repeat(3,1fr)}
/* Applications reuses .pcard but carries three to six lines of copy per card where Products carries
   two. On a 340px box the bottom-aligned body filled the whole card and the scrim swallowed the
   image, so eight cards of dense text read as clutter. A taller box keeps a real window of material
   above the text; the copy itself is real spec detail and stays as written. */
#applications .pcard{min-height:clamp(440px,33vw,520px)}
#applications .pcard p{max-width:38ch}
.acard{border-radius:20px;padding:26px 24px;background:rgba(255,255,255,.04);border:1px solid var(--line);transition:background .3s,transform .3s cubic-bezier(.22,1,.36,1),border-color .3s}
.acard:hover{background:rgba(255,255,255,.07);border-color:rgba(255,255,255,.22);transform:translateY(-4px)}
.acard .bar{width:26px;height:3px;border-radius:2px;background:rgba(246,245,242,.42);margin-bottom:16px;transition:background .3s}
.acard:hover .bar{background:var(--red)}
.acard h3{font-family:'Geist';font-weight:600;font-size:20px;color:var(--paper);margin-bottom:8px;letter-spacing:0}
.acard p{font-size:13.5px;line-height:1.5;color:rgba(246,245,242,.66)}
/* .acard is a dark-section card; on paper (process.html "six things") its off-white
   text composited to ~1:1 — invisible. Light variant, same geometry. */
.sec-paper .acard{background:#fff;border-color:var(--lineD);box-shadow:0 1px 2px rgba(20,26,36,.04)}
.sec-paper .acard:hover{background:#fff;border-color:#CFD3DA;box-shadow:0 10px 30px rgba(20,26,36,.08)}
.sec-paper .acard .bar{background:rgba(20,26,36,.28)}
.sec-paper .acard:hover .bar{background:var(--redtx)}
.sec-paper .acard h3{color:var(--tx)}
.sec-paper .acard p{color:var(--tx2)}
@media(max-width:900px){.appgrid,.appgrid.g3{grid-template-columns:1fr 1fr}}
@media(max-width:520px){.appgrid,.appgrid.g3{grid-template-columns:1fr}}

/* process */
.proc{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}
.step{position:relative}
.step .n{font-family:'Geist Mono',monospace;font-weight:500;font-size:13px;letter-spacing:.1em;color:var(--muted);margin-bottom:14px}
.step .im{aspect-ratio:4/3;border-radius:16px;overflow:hidden;margin-bottom:16px;border:1px solid var(--line)}
.step .im .ph{width:100%;height:100%;background-size:cover;background-position:center;
 transform:scale(1.08) translateY(var(--par,0px));transition:transform .9s cubic-bezier(.22,1,.36,1)}
/* A 6% image scale on its own read as nothing — the step cards felt dead next to .pcard and
   .acard, which both answer the pointer. One gesture, four coordinated parts: the frame lifts and
   brightens, the image pushes in, the step number takes the accent, and a hairline draws under the
   title. Transform/opacity/colour only, and the whole thing is off under reduced motion. */
.step .im{transition:transform .45s cubic-bezier(.22,1,.36,1),border-color .45s ease,box-shadow .45s ease}
.step .n{transition:color .35s ease}
.step h3{position:relative;display:inline-block}
.step h3::after{content:"";position:absolute;left:0;bottom:-3px;width:0;height:1.5px;background:var(--red);
 transition:width .45s cubic-bezier(.22,1,.36,1)}
.step:hover .im{transform:translateY(-6px);border-color:rgba(255,255,255,.28);box-shadow:0 22px 48px rgba(0,0,0,.42)}
/* 2x, not the usual 6-8% nudge (user, 2026-07-25). These four frames are small and the whole point
   of the set is the MATERIAL — a score line, a polished edge, the dice fracture — none of which is
   legible at 271x203. So the hover is a proper inspection zoom inside the frame (.im is
   overflow:hidden), which is also why the sources are now 1600x1200: at scale(2) the visible box is
   ~1084x812 device px on a 2x screen. Longest easing in the system (.9s) — a 2x move at 450ms
   snaps; and it stays transform-only, so it composites and is off under reduced motion. */
.step:hover .im .ph{transform:scale(2) translateY(var(--par,0px))}
.step:hover .n{color:var(--redtx)}
.step:hover h3::after{width:100%}
@media(prefers-reduced-motion:reduce){
 .step .im,.step .im .ph,.step h3::after{transition:none}
 .step:hover .im{transform:none}.step:hover .im .ph{transform:none}}
.step h3{font-family:'Geist';font-weight:600;font-size:19px;margin-bottom:6px;letter-spacing:0;color:var(--paper)}
.step p{font-size:13.5px;line-height:1.5;color:rgba(246,245,242,.64)}
@media(max-width:900px){.proc{grid-template-columns:1fr 1fr}}

/* glass showpiece band — stage styles also bind to .gb-photo.glass-stage so [trg_gb_photo]
   works without a #glassband ancestor (Elementor). */
#glassband{padding-block:clamp(48px,7vw,96px)}
#glassband .gb-photo,
.gb-photo.glass-stage{position:relative;border-radius:24px;overflow:hidden;aspect-ratio:21/9;border:1px solid var(--line)}
/* No hover transform on this one, ever (user, 2026-07-25). This .ph is the plate UNDER the live
   WebGL render and under .gb-frames, the mullion cut-out that is pixel-aligned to the identical
   2400x1030 crop. Scaling it moves the photograph out of register with both — the seam of the
   with/without compare stops lining up with the image it is comparing. */
#glassband .gb-photo .ph,
.gb-photo.glass-stage .ph{position:absolute;inset:0;background-size:cover;background-position:center}
/* The pane is now REAL refracting glass, drawn by the WebGL stage canvas above .ph — so the element
   itself is only a geometry marker (rect + radius) plus the float shadow the shader can't cast.
   Still NO backdrop-filter here: one GL surface costs less than a blur layer over a large photo,
   which is what starved the compositor before (see HANDOFF perf lesson). */
#glassband .gb-photo>canvas,
.gb-photo.glass-stage>canvas{position:absolute;inset:0;width:100%;height:100%;display:block;z-index:1}
/* the pane now fills the opening: the compare is "this whole view, through our glass", not a
   rectangle pasted onto a wall. Geometry marker only — the shader draws the material. */
#glassband .gb-glass,
.gb-photo.glass-stage .gb-glass{position:absolute;z-index:2;inset:0;border-radius:24px}
/* window mullions, composited back over the glass render (see the note in home.html).
   `cover` on purpose — it is the same fit the shader uses for the photo underneath, so the two
   stay aligned at any container size rather than drifting apart. */
#glassband .gb-frames,
.gb-photo.glass-stage .gb-frames{position:absolute;z-index:2;inset:0;pointer-events:none;
 background-size:cover;background-position:center;background-repeat:no-repeat}
/* the seam + its handle */
#glassband .gb-seam,
.gb-photo.glass-stage .gb-seam{position:absolute;z-index:3;width:1px;pointer-events:none;
 background:linear-gradient(transparent,rgba(255,255,255,.92) 8%,rgba(255,255,255,.92) 92%,transparent)}
#glassband .gb-seam i,
.gb-photo.glass-stage .gb-seam i{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
 width:38px;height:38px;border-radius:50%;background:rgba(10,14,21,.55);border:1px solid rgba(255,255,255,.7);
 box-shadow:0 6px 22px rgba(0,0,0,.45)}
#glassband .gb-seam i::before,#glassband .gb-seam i::after,
.gb-photo.glass-stage .gb-seam i::before,.gb-photo.glass-stage .gb-seam i::after{content:"";position:absolute;top:50%;width:0;height:0;
 border-top:5px solid transparent;border-bottom:5px solid transparent}
#glassband .gb-seam i::before,
.gb-photo.glass-stage .gb-seam i::before{left:9px;transform:translateY(-50%);border-right:6px solid var(--paper)}
#glassband .gb-seam i::after,
.gb-photo.glass-stage .gb-seam i::after{right:9px;transform:translateY(-50%);border-left:6px solid var(--paper)}
/* the control itself: a real range laid over the photo, invisible but fully operable */
#glassband .gb-range,
.gb-photo.glass-stage .gb-range{position:absolute;z-index:4;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:ew-resize;-webkit-appearance:none;appearance:none;background:transparent}
#glassband .gb-range:focus-visible,
.gb-photo.glass-stage .gb-range:focus-visible{outline:2px solid var(--paper);outline-offset:4px;border-radius:24px}
#glassband .gb-labels,
.gb-photo.glass-stage .gb-labels{position:absolute;z-index:3;left:0;right:0;bottom:0;display:flex;justify-content:space-between;
 padding:0 clamp(14px,2.5vw,26px) clamp(12px,2vw,20px);pointer-events:none;
 font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--paper);text-shadow:0 1px 12px rgba(0,0,0,.8)}
/* reduced transparency: glass.js skips the decorative stage entirely, so .ph shows the photo and
   the pane becomes an opaque panel rather than a refraction of what's behind it. */
@media(prefers-reduced-transparency:reduce){
 #glassband .gb-photo>canvas,
 .gb-photo.glass-stage>canvas{display:none}
 #glassband .gb-glass,
 .gb-photo.glass-stage .gb-glass{background:rgba(14,20,31,.94);border:1px solid rgba(255,255,255,.20);
  backdrop-filter:none;-webkit-backdrop-filter:none}}
/* NO mobile inset on this pane. `left:6%;width:52%` lived here from when the band was a
   decorative showpiece — a small floating pane over a photo, which on a phone wanted insetting.
   The band is a with/without COMPARE now: the pane must fill the opening (inset:0) and the seam
   decides how much of it is glass. Left in place, it made the pane 173px inside a 333px band, so
   on a phone the frosted half was a narrow strip in the middle and the right-hand side — labelled
   "THROUGH FROSTED GLASS" — was showing the bare photograph. The label was telling the truth about
   the intent and lying about the pixels. When a section changes job, re-read its breakpoints. */

/* quote */
#quote{text-align:center}
#quote .mark{font-family:'Zodiak';font-weight:800;font-size:90px;color:var(--red);line-height:.5;height:56px}
#quote blockquote{font-family:'Zodiak',Georgia,serif;font-weight:700;font-size:clamp(28px,3.6vw,48px);line-height:1.24;letter-spacing:-.02em;max-width:900px;margin:0 auto;color:var(--tx)}
#quote .by{margin-top:28px;font-weight:600;font-size:16px;color:var(--tx)}
#quote .role{font-size:14px;color:var(--tx2)}

/* CTA */
/* band deepened from #FF3A42 → red2/red3: white small text needs 4.5:1 and brand red gives 3.5:1.
   Reads as the same red at a glance, but every label on it is now legible. */
#cta{position:relative;overflow:hidden;text-align:center;color:#fff;background:linear-gradient(120deg,var(--red2),var(--red3))}
#cta .glow{position:absolute;width:900px;height:900px;border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,.16),transparent 62%);top:-360px;right:-200px;pointer-events:none}
#cta .kick{color:#fff}
#cta h2{color:#fff;font-size:clamp(32px,4.8vw,60px);max-width:900px;margin:0 auto;line-height:1.05}
#cta p{margin:18px auto 0;max-width:600px;color:rgba(255,255,255,.94);font-size:17px;line-height:1.5}
#cta .cta{margin-top:34px;display:flex;gap:14px;justify-content:center;flex-wrap:wrap}
#cta .btn.w{background:#fff;color:var(--red2)}
#cta .btn.o{background:transparent;border:1.5px solid rgba(255,255,255,.6);color:#fff}

/* ===== MOTION POLISH (whole-page) ===== */
/* cursor-tracked specular sheen on glass surfaces (nav pill, product cards, hero stats) */
.sheen{position:relative}
/* specular sheen — a soft cool glass reflection that tracks the cursor.
   Two layers: a broad ambient wash + a tighter glint, both cool-tinted (glassy),
   low opacity, wide falloff → reads as light catching real glass, not a white spotlight. */
.sheen::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:4;
 background:
   radial-gradient(420px 420px at var(--mx,50%) var(--my,50%),rgba(150,205,255,.13),rgba(120,175,255,.05) 38%,transparent 66%),
   radial-gradient(120px 120px at var(--mx,50%) var(--my,50%),rgba(255,255,255,.14),transparent 60%);
 opacity:0;transition:opacity .7s cubic-bezier(.22,1,.36,1);mix-blend-mode:screen}
.sheen:hover::after{opacity:1}
@media(hover:none),(prefers-reduced-transparency:reduce){.sheen::after{display:none}}
/* product cards — edge glow + lift on hover (transform/opacity + shadow only) */
.pcard{transition:box-shadow .45s cubic-bezier(.22,1,.36,1),border-color .45s cubic-bezier(.22,1,.36,1),transform .45s cubic-bezier(.22,1,.36,1)}
.pcard:hover{border-color:rgba(255,255,255,.30);box-shadow:0 28px 64px rgba(0,0,0,.46),inset 0 0 0 1px rgba(255,255,255,.14)}
/* section reveal — mask-wipe (light through glass); overrides the .rv fade */
.rv-wipe.in{animation:wipeIn .95s cubic-bezier(.22,1,.36,1) both}
/* The vertical insets are NEGATIVE on purpose. animation-fill-mode:both leaves the final keyframe
   applied for good, and `inset(0 0 0 0)` clips to the border box — which cut the descenders off the
   last line of every wiped heading, because display line-height here is ~1.0 and Zodiak's g/y/p
   reach below the line box. Opening the mask above and below costs nothing: the wipe is horizontal,
   so only the right edge needs to travel. */
@keyframes wipeIn{
 from{opacity:0;clip-path:inset(-0.3em 100% -0.4em 0);transform:translateY(6px)}
 to  {opacity:1;clip-path:inset(-0.3em -0.3em -0.4em 0);transform:none}}
@media(prefers-reduced-motion:reduce){.rv-wipe,.rv-wipe.in{opacity:1;animation:none;clip-path:none;transform:none}}
/* Elementor editor canvas: .rv starts at opacity:0 until motion.js adds .in.
   That JS does not re-bind reliably after widget re-renders, so the element
   stays invisible and unselectable. Force the settled state in the editor
   iframe only — body.elementor-editor-active is never on the live site. */
body.elementor-editor-active .rv,
body.elementor-editor-active .rv.in,
body.elementor-editor-active .rv-wipe,
body.elementor-editor-active .rv-wipe.in{
  opacity:1;
  animation:none;
  clip-path:none;
  transform:none}

/* ===== AMBIENT SPECTRAL ATMOSPHERE (layered mesh) ===== */
/* Not isolated corner blobs — overlapping multi-point colour fields blended together
   (background-blend-mode:screen) into a continuous spectral MESH, then screen-blended onto
   the section's own gradient base. Reads as light dispersing through glass, cool-biased,
   richer and more organic than a flat radial. Text sits on .wrap (z1) above; glow origins
   sit past the text measure so 4.5:1 body contrast holds. Cool→neutral→warm(CTA)→navy arc. */
:root{--spec-cyan:#5EC8FF;--spec-peri:#7B8CFF;--spec-blue:#4678FF;--spec-violet:#8A6BE6;--spec-magenta:#B478FF}
/* per-section gradient bases — sequenced colour across the scroll */
#products{background:linear-gradient(168deg,#0F1626 0%,#12192B 55%,#0C1220 100%)}
#applications{background:linear-gradient(168deg,#0B1018 0%,#0E1422 100%)}
#process{background:linear-gradient(168deg,#0E141F 0%,#0C111B 100%)}
#glassband{background:linear-gradient(168deg,#0E141F 0%,#0B0F18 100%)}
/* the mesh layer */
#products,#applications,#process,#glassband{position:relative;isolation:isolate}
#products::before,#applications::before,#process::before,#glassband::before{
 content:"";position:absolute;inset:-8% -4%;z-index:0;pointer-events:none;
 background-repeat:no-repeat;background-blend-mode:screen;mix-blend-mode:screen}
#products>.wrap,#applications>.wrap,#process>.wrap,#glassband>.wrap{position:relative;z-index:1}
/* Products — the brightest, most "glass" ambient (5-point mesh) */
#products::before{background-image:
 radial-gradient(46vw 42vw at 86% -6%,rgba(94,200,255,.30),transparent 60%),
 radial-gradient(40vw 40vw at 60% 18%,rgba(123,140,255,.18),transparent 58%),
 radial-gradient(50vw 46vw at 2% 96%,rgba(70,120,255,.22),transparent 60%),
 radial-gradient(34vw 34vw at 34% 66%,rgba(138,107,230,.14),transparent 62%),
 radial-gradient(30vw 28vw at 100% 70%,rgba(94,200,255,.14),transparent 64%);
 animation:atmosDrift 25.6s ease-in-out infinite alternate;will-change:transform}
/* Applications — deeper, violet-led mesh */
#applications::before{background-image:
 radial-gradient(48vw 44vw at 104% 0%,rgba(138,107,230,.28),transparent 60%),
 radial-gradient(40vw 40vw at 80% 30%,rgba(123,140,255,.16),transparent 60%),
 radial-gradient(48vw 46vw at -6% 92%,rgba(70,120,255,.22),transparent 60%),
 radial-gradient(30vw 30vw at 20% 40%,rgba(94,200,255,.10),transparent 64%)}
/* Process — cyan-led, calmer mesh */
#process::before{background-image:
 radial-gradient(46vw 44vw at 98% 6%,rgba(94,200,255,.22),transparent 60%),
 radial-gradient(38vw 38vw at 70% 26%,rgba(123,140,255,.13),transparent 60%),
 radial-gradient(44vw 42vw at -2% 100%,rgba(138,107,230,.15),transparent 60%)}
#glassband::before{background-image:
 radial-gradient(46vw 40vw at 88% 46%,rgba(94,200,255,.16),transparent 62%),
 radial-gradient(34vw 34vw at 6% 60%,rgba(123,140,255,.10),transparent 62%)}
@keyframes atmosDrift{from{transform:translate3d(-1.4%,-1%,0) scale(1.03)}to{transform:translate3d(1.4%,1.2%,0) scale(1.07)}}
@media(prefers-reduced-motion:reduce){#products::before{animation:none}}
/* Hero — a cool aurora wash in the dark editorial zone (above the facade+scrim, below the text),
   so the top of the page carries the same spectral colour language without washing the headline. */
#hero-sec::after{content:"";position:absolute;z-index:2;left:-10%;top:0;width:70%;height:100%;pointer-events:none;
 mix-blend-mode:screen;background:
  radial-gradient(40vw 40vw at 12% 24%,rgba(94,200,255,.16),transparent 62%),
  radial-gradient(38vw 40vw at 30% 92%,rgba(138,107,230,.16),transparent 62%),
  radial-gradient(30vw 30vw at -6% 60%,rgba(70,120,255,.12),transparent 64%);
 animation:atmosDrift 24s ease-in-out infinite alternate;will-change:transform}
@media(prefers-reduced-motion:reduce){#hero-sec::after{animation:none}}
@media(max-width:820px){#hero-sec::after{width:100%;left:0;opacity:.7}}
/* film grain — kills gradient banding + adds a sandblasted-glass tactility (fixed, ~5%) */
body::after{content:"";position:fixed;inset:0;z-index:60;pointer-events:none;opacity:.05;mix-blend-mode:overlay;
 background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
@media(prefers-reduced-transparency:reduce){body::after{display:none}}
/* paper.design mesh-gradient canvases (applications, process) — replace the CSS ::before mesh there.
   BLEND AND STRENGTH ARE TOKENS, and the blend is the part that matters: screening a spectral wash
   onto paper is a no-op, because screen lightens toward white and paper is already there. Multiply
   keeps the same hues and reads as tinted light instead of washed light — the print inverse of the
   dark theme's aurora, not a different idea. Light multiplies at .42, dark screens at .55.
   The canvas PALETTE flips too, but a shader cannot be retinted by CSS: see the FX mount block in
   inc/enqueue.php, which rebuilds the mesh when the theme announces a change. */
.meshbg{position:absolute;inset:0;z-index:0;width:100%;height:100%;display:block;pointer-events:none;
 opacity:var(--meshA,.42);mix-blend-mode:var(--blend,multiply)}
#applications::before,#process::before{display:none}
@media(prefers-reduced-transparency:reduce){.meshbg{display:none}}
/* Elementor Container Mesh Background toggle (Style > Background) */
.trg-meshbg-yes,
.e-con:has(> .meshbg){position:relative;isolation:isolate}
.trg-meshbg-yes > .e-con-inner,
.trg-meshbg-yes > .elementor-element,
.e-con:has(> .meshbg) > .e-con-inner,
.e-con:has(> .meshbg) > .elementor-element{position:relative;z-index:1}
/* THE MESH COMES WITH ITS OWN GROUND. The canvas is a translucent wash — multiply on paper, screen
   on ink — so the surface under it is half the effect: the handoff's three-stop base is what turns
   the wash into depth instead of a stain, and it is graded across the section where a flat panel
   colour is not. So the toggle owns the background as well as the canvas; the two are one design
   decision and cannot be set independently without one of them being wrong.
   FORCED, over anything set in Style > Background — colour, image or gradient — because Elementor
   writes those onto the same element from its own per-page stylesheet, which loads after this one.
   A container that needs its own background is a container that should not have the toggle on. */
.trg-meshbg-yes,
[data-trg-meshbg=yes]{background:linear-gradient(168deg,#F1F3F8 0%,#E9EDF5 55%,#F5F5F1 100%) !important}
:root[data-theme=dark] .trg-meshbg-yes,
:root[data-theme=dark] [data-trg-meshbg=yes],
body.trg-dark .trg-meshbg-yes,
body.trg-dark [data-trg-meshbg=yes]{background:linear-gradient(168deg,#0F1626 0%,#12192B 55%,#0C1220 100%) !important}

/* ===== QUOTE FORM (contact.html) ===== */
/* Fields are inline-styled in the contact page markup; these rules add the states that
   inline styles can't express — focus, error, status, success — and nothing else. */
#quoteform input,#quoteform select,#quoteform textarea{transition:border-color .2s,background .2s,box-shadow .2s;outline:none}
#quoteform input:hover,#quoteform select:hover,#quoteform textarea:hover{border-color:rgba(246,245,242,.26)}
#quoteform input:focus-visible,#quoteform select:focus-visible,#quoteform textarea:focus-visible,
#quoteform input:focus,#quoteform select:focus,#quoteform textarea:focus{
 border-color:rgba(246,245,242,.5);background:rgba(255,255,255,.07);box-shadow:0 0 0 3px rgba(246,245,242,.12)}
#quoteform button:focus-visible{outline:2px solid var(--paper);outline-offset:3px}
#quoteform select option{background:var(--ink2);color:var(--paper)}
/* invalid: red rim + red message. --redtx on ink is 5.2:1, so the message is AA at 11px. */
#quoteform .q-bad{border-color:var(--red);background:rgba(255,58,66,.06)}
#quoteform .q-bad:focus{box-shadow:0 0 0 3px rgba(255,58,66,.18)}
.q-err{font-size:11px;line-height:1.5;letter-spacing:.04em;text-transform:none;color:var(--redtx);margin-top:8px}
.q-status{margin-top:20px;font-family:'Geist Mono',monospace;font-size:12px;letter-spacing:.06em;line-height:1.6;color:var(--muted)}
.q-status:empty{display:none}
.q-status.is-bad{color:var(--redtx)}
.q-status a{color:var(--paper);text-decoration:underline;text-underline-offset:3px}
/* honeypot — off-screen, never focusable, never announced */
.q-pot{position:absolute;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none}
/* success panel replaces the form outright — the request is done, not "submitted" */
.q-done{margin-top:48px;max-width:620px;border:1px solid var(--line);border-left:2px solid var(--red);
 border-radius:20px;padding:40px 36px;background:rgba(255,255,255,.04)}
.q-done-k{font-size:11px;letter-spacing:.16em;color:var(--redtx);margin-bottom:14px}
.q-done h3{font-family:'Zodiak',Georgia,serif;font-weight:700;font-size:clamp(24px,2.6vw,34px);line-height:1.15;color:var(--paper)}
.q-done p{margin-top:16px;font-size:15px;line-height:1.6;color:var(--muted)}
.q-done a{color:var(--paper);text-decoration:underline;text-underline-offset:3px}

/* ---------------------------------------------------------------------------
   LAYOUT GRIDS THAT USED TO BE INLINE STYLES.
   They were written as style="display:grid;grid-template-columns:…" on the element, which no
   @media block can override — so on a phone the About page kept a 1.15fr/.85fr split (a 174px
   column of body copy beside a 129px column of mono specs, both unreadable) and the standards row
   stayed four columns wide. Same failure mode as the inline transform that killed the hover zoom:
   an inline style always wins. Anything that must change with viewport belongs in a class.
   --------------------------------------------------------------------------- */
.split2{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(32px,6vw,80px);align-items:start}
.specs4{display:grid;grid-template-columns:repeat(4,1fr);gap:24px;border-top:1px solid var(--line);padding-top:28px}
@media(max-width:860px){
 .split2{grid-template-columns:1fr;gap:28px}
 .specs4{grid-template-columns:repeat(2,1fr);gap:22px}}
@media(max-width:420px){.specs4{grid-template-columns:1fr;gap:18px}}
/* process.html's "why the order matters" pair: two up, one up on a phone. It used to force
   `grid-template-columns:repeat(2,1fr)` inline, which beat .pgrid's own 560px breakpoint and left
   two half-width cards side by side on a 390px screen. */
.pgrid.two{grid-template-columns:repeat(2,1fr)}
@media(max-width:560px){.pgrid.two{grid-template-columns:1fr}}

/* TAP TARGETS ON TOUCH (measured 2026-07-26, mobile audit of all pages).
   WCAG 2.5.8 asks for 24x24 CSS px; Apple's own guidance is 44x44. Measured on a phone:
   the mobile menu's phone/email lines were 17px tall and every footer link 23px — the menu is the
   ONLY navigation a phone user has, and those two lines are the site's conversion path, so they
   were the worst possible place to be under-size. Padding rather than font-size, so nothing about
   the type system moves; the extra height is hit area.
   Deliberately NOT applied to mailto/tel links sitting inside a sentence: padding would break the
   line box, and WCAG exempts inline targets in a block of text for exactly that reason. */
/* QUOTE-FORM FIELDS. These were inline `style="…font-size:15px…"` attributes on every control,
   which is why the iOS auto-zoom fix silently did nothing the first time: an inline style beats
   any @media rule. Fourth time this pattern has bitten this site (hover zoom, About grids, the
   scroll cue, now this) — anything that must vary by viewport cannot live in a style attribute. */
.qlab{display:block;font-size:11px;color:var(--muted);margin-bottom:8px}
.qin{width:100%;box-sizing:border-box;background:rgba(255,255,255,.04);border:1px solid var(--line);
 border-radius:14px;padding:14px 16px;color:var(--paper);font-family:'Geist',sans-serif;font-size:15px}
.qin-area{resize:vertical}
.qin-file{font-size:14px}
/* iOS zooms the page in whenever a focused control is under 16px, and leaves it zoomed — on the
   site's only conversion surface. 16 is the browser's threshold, not a design preference. */
@media(hover:none),(pointer:coarse){.qin,.qin-file{font-size:16px}}
