/* ==========================================================================
   EDITORIAL LAYER
   Fourth and last stylesheet. Loaded after components.css, so it wins on order
   and needs no !important and no raised specificity.

   WHAT THIS IS: the typographic and motion vocabulary from walieldin.com,
   applied to HAZ MED's existing light palette. The palette, the contrast pairs
   and the sixteen SVG artworks are untouched by design — only the type
   treatment, the geometry (hairlines instead of shadows, mono labels, indexed
   sections) and the motion set change.

   WHAT IT DELIBERATELY DOES NOT DO: it does not restate a colour that
   tokens.css already defines, and it does not introduce a webfont. The mono
   face is the platform stack in --font-mono, which is free, instant, and
   carries the whole effect at this size.

   TWO RULES THAT RUN THROUGH EVERYTHING HERE:

   1. letter-spacing is Latin-only. Arabic is a joined script: spacing the
      letters breaks the connections and renders the word as disconnected
      glyphs. Every tracking rule below is therefore guarded by :lang(), and
      Arabic gets 0 — never a smaller negative number.

   2. The mono face is Latin-only too. --font-mono resolves to SF Mono / Menlo /
      Consolas, none of which carry Arabic, so an Arabic run in it falls through
      to a fallback with different metrics. Arabic labels keep --font-arabic and
      take the treatment through size, weight and the tick mark instead.
   ========================================================================== */

:root {
  /* Hairline: a border you read as a drawn line rather than as a box edge.
     Derived from the brand charcoal so it sits in the palette, not beside it. */
  --hairline: color-mix(in srgb, var(--neutral-700) 14%, transparent);
  --hairline-strong: color-mix(in srgb, var(--neutral-700) 26%, transparent);
  /* The faint construction grid. Light enough to read as paper texture.
     5% IS A CONTRAST BUDGET, NOT A TASTE SETTING. axe cannot compute contrast
     for text sitting over a pseudo element, so it abstains on every string in
     the page-hero and reports "incomplete" rather than a pass. Measured by
     hand against the darkest grid pixel (#EFF0F0 — 5% charcoal over
     --surface-alt), the tightest pair on the site is --text-muted at 4.81:1
     against AA's 4.50. Raising this figure eats that margin: at 10% the muted
     body text fails. Anyone changing --grid-line or --text-muted must
     re-measure this pair. */
  --grid-line: color-mix(in srgb, var(--neutral-700) 5%, transparent);
  --grid-size: 72px;

  /* Motion. Ambient drift is an order of magnitude slower than any UI timing
     in tokens.css — it must never read as a transition. */
  --dur-drift: 18s;
  --dur-sweep: 1100ms;
}

/* Browsers without color-mix() get flat fallbacks. Declared first so the
   color-mix() values above override where supported. */
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  :root {
    --hairline: #DDE0E0;
    --hairline-strong: #BFC4C4;
    --grid-line: #F1F2F2;
  }
}


/* ==========================================================================
   1. DISPLAY TYPE
   Inter Tight's character is tight tracking and a high optical size. IBM Plex
   Sans takes the same treatment without a second font download.
   ========================================================================== */

.h1:lang(en), .h2:lang(en),
html[lang="en"] .h1, html[lang="en"] .h2 {
  letter-spacing: -0.021em;
}
.h3:lang(en), html[lang="en"] .h3 { letter-spacing: -0.014em; }

/* Arabic: explicitly zero, not merely absent. A joined script with tracking
   applied breaks the letter connections, and this is the rule that prevents an
   inherited value from reaching it. */
html[lang="ar"] .h1,
html[lang="ar"] .h2,
html[lang="ar"] .h3,
html[lang="ar"] .h4 { letter-spacing: 0; }


/* ==========================================================================
   2. MONO LABELS
   The single most recognisable trait of the reference: small uppercase mono
   labels, widely tracked, preceded by a short accent tick.
   ========================================================================== */

.eyebrow {
  position: relative;
  font-family: var(--font-mono);
  font-weight: var(--fw-medium);
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
}

/* The tick. A drawn mark, not a bullet glyph — inline-start so it mirrors in
   RTL with no extra rule, and aria-hidden by construction (it is a pseudo
   element, so no assistive technology ever announces it). */
.eyebrow::before {
  content: "";
  display: inline-block;
  inline-size: 1.25em;
  block-size: 1px;
  vertical-align: 0.3em;
  margin-inline-end: 0.6em;
  background: var(--action-fill);
}

/* Arabic keeps its own face and no tracking, and takes the label treatment
   through weight and the tick instead. --font-mono has no Arabic glyphs. */
html[lang="ar"] .eyebrow {
  font-family: var(--font-arabic);
  letter-spacing: 0;
  font-weight: var(--fw-semibold);
}

.eyebrow--inverse::before { background: var(--text-inverse-accent); }

/* Order codes and figures already use --font-mono. Tabular numerals line the
   digits up in the spec tables, where a column of order codes is read down. */
.code, .num, .is-num { font-variant-numeric: tabular-nums; }


/* ==========================================================================
   3. INDEXED SECTIONS
   Numbered section heads — 01 / 02 / 03 — set in mono inside a hairline box.

   The number is written into the markup at build time rather than generated
   with a CSS counter, so a band without a head cannot shift the numbering of
   the ones after it, and the number a reader sees is the number in the source.

   It is a REAL element with aria-hidden, not a ::before with content: attr(data-index). Generated content is exposed to the accessibility tree by
   most screen readers, so the pseudo-element version announced "zero four"
   before every heading on the site. A pseudo element cannot be marked
   aria-hidden, and `content: attr(x) / ""` — the alt-text syntax that would
   suppress it — is unsupported in Firefox, where an unsupported value
   invalidates the whole declaration and the number would just disappear.

   It sits on .section-head, which every head has: only 5 of the 45 heads in
   this build carry an .eyebrow, so hanging it off the label would have
   numbered a ninth of the site.
   ========================================================================== */

.section-head--swept { position: relative; }

.section-head__index {
  position: absolute;
  inset-inline-end: calc(100% + 1.4em);
  inset-block-start: 0.15em;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
  letter-spacing: 0.04em;
  line-height: 1;
  color: var(--text-muted);
  border: 1px solid var(--hairline);
  padding: 0.25em 0.5em;
  /* Latin digits in both languages: the site uses Western numerals throughout,
     and --font-mono carries no Arabic glyphs in any case. */
  font-variant-numeric: tabular-nums;
}

/* The inverse bands. */
.band--ink .section-head__index {
  color: var(--text-inverse-muted);
  border-color: var(--border-inverse);
}

/* Below the wide breakpoint the hanging index would fall outside the
   container, so it joins the flow above the label instead. */
@media (max-width: 1279px) {
  .section-head__index {
    position: static;
    display: inline-block;
    margin-block-end: var(--space-3);
  }
}

/* A centred head has no margin to hang anything in. */
.section-head--center .section-head__index {
  position: static;
  display: inline-block;
  margin-block-end: var(--space-3);
}


/* ==========================================================================
   4. HAIRLINE GEOMETRY
   The reference draws boundaries with single lines and corner ticks rather
   than with shadows. On a light ground that reads as a technical drawing,
   which is the right register for spill-response specification.
   ========================================================================== */

/* `.band` is the section wrapper in this build — there is no `.section`
   element, which is worth saying because `.section-head` reads like one.
   `.band--bordered` already ships a top rule, so it is retuned to the hairline
   rather than duplicated under a new name. */
.band--bordered { border-block-start-color: var(--hairline); }

/* Cards: one hairline, no shadow at rest. The hover shadow in components.css
   still applies — it is the interaction, not the resting state. */
:root { --border-card: var(--hairline); }

/* Corner ticks on the product cards. Two short lines at the inline-start
   corners, which is the reference's way of marking a frame without closing it.
   Pointer-events none so they can never intercept the card's own link. */
.product-card { position: relative; }
.product-card::before,
.product-card::after {
  content: "";
  position: absolute;
  inline-size: 10px;
  block-size: 10px;
  pointer-events: none;
  border-color: var(--hairline-strong);
  border-style: solid;
  border-width: 0;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.product-card::before {
  inset-block-start: -1px;
  inset-inline-start: -1px;
  border-block-start-width: 2px;
  border-inline-start-width: 2px;
}
.product-card::after {
  inset-block-end: -1px;
  inset-inline-end: -1px;
  border-block-end-width: 2px;
  border-inline-end-width: 2px;
}
/* The ticks appear on hover AND on focus-within, so a keyboard user sees the
   same affordance a mouse user does. */
.product-card:hover::before,
.product-card:hover::after,
.product-card:focus-within::before,
.product-card:focus-within::after {
  opacity: 1;
  border-color: var(--action-edge);
}

/* The dark card variant: --hairline-strong is charcoal-derived and vanishes on
   ink, so the ticks take the inverse line colour and the inverse accent. */
.product-card--ink::before,
.product-card--ink::after { border-color: var(--border-inverse); }
.product-card--ink:hover::before,
.product-card--ink:hover::after,
.product-card--ink:focus-within::before,
.product-card--ink:focus-within::after { border-color: var(--text-inverse-accent); }


/* ==========================================================================
   5. THE CONSTRUCTION GRID
   A faint gradient grid behind selected sections. Purely a ground: it carries
   no information, so it is drawn on a pseudo element and hidden from assistive
   technology by construction.
   ========================================================================== */

.band--grid { position: relative; isolation: isolate; }
/* The grid is only ever applied to light grounds — see build-content.py for
   why the ink hero and CTA band are excluded — but the inverse override is kept
   so that adding `band--grid` to an ink band later draws a visible line rather
   than a charcoal one on charcoal. */
.band--ink.band--grid { --grid-line: color-mix(in srgb, var(--ink-line) 55%, transparent); }
/* Only `.container`, which is the single wrapper every grid-bearing block
   uses. A blanket `> *` was tried and reverted: it would have raised the
   hero's `.hero__media` from z-index -2 to 1 and painted the background
   artwork over the headline. */
.band--grid > .container { position: relative; z-index: 1; }
.band--grid::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
  background-size: var(--grid-size) var(--grid-size);
  /* Fades out before the section edge so the grid never collides with the
     hairline of the next section. */
  mask-image: radial-gradient(120% 100% at 50% 0%, #000 35%, transparent 78%);
  -webkit-mask-image: radial-gradient(120% 100% at 50% 0%, #000 35%, transparent 78%);
}


/* ==========================================================================
   6. MOTION
   Five ambient/entrance keyframes on top of the four in components.css.

   Every one is gated behind `.js` — so with scripting off nothing is hidden
   and nothing animates — and every one is switched OFF, not merely shortened,
   under prefers-reduced-motion at the end of this file.

   The restraint from components.css still governs: this site sells life-safety
   equipment. Motion may draw the eye to a sequence and confirm an interaction.
   It may never move the page under someone reading a dilution ratio.
   ========================================================================== */

/* Ambient drift. Three variants at different periods so two artworks in one
   viewport never move in lockstep, which is what makes drift read as
   mechanical rather than as air. Translation only — no rotation, because the
   artworks contain labelled technical diagrams. */
@keyframes haz-drift-a {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, -8px, 0); }
}
@keyframes haz-drift-b {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, 7px, 0); }
}
@keyframes haz-drift-c {
  0%, 100% { transform: translate3d(0, 0, 0); }
  33%      { transform: translate3d(3px, -5px, 0); }
  66%      { transform: translate3d(-3px, 4px, 0); }
}

/* The grid pans by exactly one cell, so the loop is seamless at any duration. */
@keyframes haz-grid-drift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: var(--grid-size) var(--grid-size),
                              var(--grid-size) var(--grid-size); }
}

/* The sweep: an accent band crossing a rule once, on reveal.
   MOVED FROM transform TO background-position, and the reason is worth keeping.
   As a transform on a 30%-wide band, translateX(100%) moves it by 100% of ITS
   OWN width — 30% of the rule — so the band parked permanently in the middle
   of the rule instead of leaving. Travelling far enough to clear the rule then
   needed overflow:hidden on the head, which would have clipped the hanging
   section index, since that is an absolutely positioned pseudo element sitting
   outside the container.
   A background-position sweep on a full-width, no-repeat 30% image solves both:
   nothing can spill, so nothing needs clipping.
   background-position has no logical-direction form, so RTL needs its own
   keyframes — the third documented [dir="rtl"] exception in this codebase,
   alongside transforms and gradients. */
@keyframes haz-sweep {
  from { background-position: -60% 0; }
  to   { background-position: 160% 0; }
}
@keyframes haz-sweep-rtl {
  from { background-position: 160% 0; }
  to   { background-position: -60% 0; }
}

/* Slide in from the inline start. Uses a signed multiplier so the same
   keyframe travels the correct way in both directions: --flip is already
   defined in the design system as 1 for LTR and -1 for RTL. */
@keyframes haz-slide {
  from { opacity: 0; transform: translateX(calc(var(--flip, 1) * -18px)); }
  to   { opacity: 1; transform: none; }
}

/* The section index counting in. */
@keyframes haz-count-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}


/* ---- 6a. Ambient drift on decorative artwork --------------------------
   Opt-in per element via data-drift, written at build time and applied only
   to the twelve brand compositions — never to a product photograph and never
   to a spec drawing that a reader may be measuring against. */
.js [data-drift="a"] { animation: haz-drift-a var(--dur-drift) var(--ease-in-out) infinite; }
.js [data-drift="b"] { animation: haz-drift-b calc(var(--dur-drift) * 1.35) var(--ease-in-out) infinite; }
.js [data-drift="c"] { animation: haz-drift-c calc(var(--dur-drift) * 1.7) var(--ease-in-out) infinite; }

/* will-change only on the drifting elements, and only three variants exist, so
   this cannot accumulate into the dozens of composited layers that a blanket
   will-change produces. */
.js [data-drift] { will-change: transform; }


/* ---- 6b. The grid, drifting -------------------------------------------- */
.js .band--grid::before {
  animation: haz-grid-drift 60s linear infinite;
}


/* ---- 6c. Sweep across the section-head rule ---------------------------- */
.section-head--swept {
  padding-block-start: var(--space-6);
  /* A real border rather than a ::before, which keeps ::after free for the
     sweep band and needs no extra element. `position: relative` is declared
     once in section 3, where the index depends on it. */
  border-block-start: 1px solid var(--hairline);
}
.band--ink .section-head--swept { border-block-start-color: var(--border-inverse); }

/* The band that crosses the rule once on reveal. It is the full width of the
   rule with a 30% no-repeat gradient parked off the start edge, so at rest
   nothing is drawn and nothing extends beyond the head. */
.section-head--swept::after {
  content: "";
  position: absolute;
  inset-block-start: -1px;
  inset-inline: 0;
  block-size: 1px;
  background-image: linear-gradient(to right, transparent, var(--action-fill), transparent);
  background-size: 30% 100%;
  background-repeat: no-repeat;
  background-position: -60% 0;
  pointer-events: none;
}
html[dir="rtl"] .section-head--swept::after { background-position: 160% 0; }
.js .is-revealed .section-head--swept::after,
.js .section-head--swept.is-revealed::after {
  animation: haz-sweep var(--dur-sweep) var(--ease-out) both;
  animation-delay: 120ms;
}
/* main.js puts `.js` on <html>, so the RTL variant is a COMPOUND selector on
   that one element — `html[dir="rtl"] .js` would require a descendant .js and
   match nothing. */
html[dir="rtl"].js .is-revealed .section-head--swept::after,
html[dir="rtl"].js .section-head--swept.is-revealed::after {
  animation-name: haz-sweep-rtl;
}


/* ---- 6e. The index counting in ---------------------------------------- */
.js .is-revealed .section-head__index,
.js .section-head.is-revealed .section-head__index {
  animation: haz-count-in var(--dur-base) var(--ease-out-firm) both;
}


/* ---- 6d. Inner-page hero entrance -------------------------------------
   components.css animates `.hero__content` on the two home pages, which left
   the 44 inner routes with no entrance at all. This closes that gap.

   On load, not on scroll: `.page-hero` is above the fold on every route, so a
   scroll trigger would either never fire or fire mid-paint. No JS and no data
   attribute — the stagger is nth-child, so it cannot get out of step with the
   markup. */
.js .page-hero > .container > * {
  animation: haz-slide var(--dur-reveal) var(--ease-out-firm) both;
}
.js .page-hero > .container > *:nth-child(1) { animation-delay:  50ms; }
.js .page-hero > .container > *:nth-child(2) { animation-delay: 120ms; }
.js .page-hero > .container > *:nth-child(3) { animation-delay: 190ms; }
.js .page-hero > .container > *:nth-child(n+4) { animation-delay: 240ms; }


/* ==========================================================================
   7. REDUCED MOTION
   Off, not faster. A vestibular trigger is not made safe by shortening it, and
   the ambient drift and the grid pan are the two things here most likely to be
   one — they never stop on their own.

   Everything in this file animates a pseudo element or a decorative attribute,
   so switching it off leaves the page complete. The one case that needs more
   than `animation: none` is the sweep, whose band is parked off-screen by a
   transform: it is hidden outright instead.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .js [data-drift="a"],
  .js [data-drift="b"],
  .js [data-drift="c"],
  .js [data-drift],
  .js .band--grid::before,
  .js .page-hero > .container > *,
  .js .is-revealed .section-head--swept::after,
  .js .section-head--swept.is-revealed::after,
  /* The RTL variant is listed explicitly. It only sets animation-name, so the
     `animation: none !important` above already outranks it — but that is an
     implicit interaction between a shorthand and a longhand, and if anyone
     ever drops the !important the Arabic sweep would animate under reduced
     motion while the English one did not. */
  html[dir="rtl"].js .is-revealed .section-head--swept::after,
  html[dir="rtl"].js .section-head--swept.is-revealed::after,
  .js .is-revealed .section-head__index,
  .js .section-head.is-revealed .section-head__index {
    animation: none !important;
    transition: none !important;
  }

  /* No display:none is needed for the sweep any more: its resting
     background-position parks the gradient outside the box, so switching the
     animation off leaves the rule plain. That was not true of the earlier
     transform-based version, which parked a visible stripe mid-rule. */

  /* Drift leaves no residual offset — the keyframes start and end at zero —
     but the property is cleared anyway so no compositor layer is retained. */
  .js [data-drift] { will-change: auto; transform: none; }

  /* The inner-page hero's animation supplied its opacity, so without it the
     hero would be invisible — the one place in this file where switching the
     animation off is not sufficient on its own. */
  .js .page-hero > .container > * { opacity: 1; transform: none; }
}


/* ==========================================================================
   8. PRINT
   The grid, the ticks and the sweeps are screen furniture. A printed
   specification sheet should carry the content and the hairlines only.
   ========================================================================== */

@media print {
  .band--grid::before,
  .section-head--swept::after,
  .product-card::before,
  .product-card::after { display: none; }
  .eyebrow::before { background: #000; }
}
