/* ============================================================
   Data Systems Catalogue — visual system v3
   Apple-inspired: quiet neutral canvas, SF-style type ramp, one
   green accent, frosted surfaces, soft depth, restrained motion.
   ============================================================ */

:root {
  /* Four tiers of text, every one of them readable. --faint was #86868b, which
     measures 3.62:1 on white and 3.32:1 on the canvas: below the 4.5:1 that
     normal-size body text has to clear, and it carried real content (filter
     labels, dates, source links, the footer). Darkening it forced --muted down
     with it so the hierarchy still reads as four steps rather than three.
     Ratios against #ffffff / --canvas #f5f5f7:
       --ink    16.1 / 14.8    --ink-2  9.7 / 8.9
       --muted   6.3 /  5.8    --faint  5.3 / 4.9  */
  --ink: #1d1d1f;
  --ink-2: #424245;
  --muted: #5f5f66;
  --faint: #6b6b73;
  --line: rgba(0, 0, 0, 0.08);
  --line-strong: rgba(0, 0, 0, 0.14);
  /* Apple's SF, and only Apple's SF. Copied stop for stop from the reference's
     --font-display / --font-sans / --font-mono, because "the same typeface"
     has to mean the same fallback order too: a stack that ends in Inter and
     Roboto is a different design on every machine that does not have SF.
     Said plainly, because the owner asked for SF site-wide and deserves to
     know what he is getting: SF is Apple's and cannot be served as a webfont
     by us or by anyone. On a Mac, an iPhone or an iPad every word on this site
     is genuine SF. On Windows and Android `system-ui` hands over to Segoe UI
     and Roboto, which is what the reference does too. Loading a lookalike
     would cost every visitor a font download to be slightly wrong. */
  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "SF Pro", system-ui, sans-serif;
  --font-display: var(--font-sans);
  --mono: ui-monospace, "SF Mono", Menlo, monospace;

  --canvas: #f5f5f7;
  /* The same paper at zero alpha. Fading to the keyword `transparent` means
     fading through rgba(0,0,0,0), which lays a grey fringe along every edge
     where paper meets sky; naming the colour keeps the ramp clean. It is a
     token rather than a literal so it cannot drift from --canvas. */
  --canvas-0: rgba(245, 245, 247, 0);
  --panel: #ffffff;
  --plate: #ffffff;
  --green: #0d9f6e;
  --green-deep: #087a54;
  --accent-grad: linear-gradient(135deg, #0d9f6e, #0abf85);

  /* ---- Merged from the CommScope Connect design system (Lovable) ----
     That project's palette earns its keep in two places. `--brand-lime` is
     sampled from the Data Systems logo's own leaf — the one colour that is
     genuinely ours — and the electric/teal pair gives dark surfaces the
     command-centre depth the portal has. Both stay decorative: lime on white
     fails text contrast, so the emerald pair above remains the accent that
     carries meaning, and these carry atmosphere. */
  --brand-lime: #b8e76a;
  --brand-lime-deep: #6fa326;
  --electric: oklch(0.61 0.22 259);
  --electric-soft: oklch(0.72 0.16 248);
  --teal: oklch(0.72 0.13 188);
  --mesh-grad:
    radial-gradient(circle at 20% 0%, rgba(20, 184, 166, 0.10), transparent 44%),
    radial-gradient(circle at 80% 100%, rgba(59, 130, 246, 0.12), transparent 50%);
  --deep-grad:
    radial-gradient(ellipse 72% 52% at 50% -10%, rgba(37, 99, 235, 0.30), transparent 60%),
    linear-gradient(180deg, #020714 0%, #060f1e 38%, #0b1626 100%);
  --sweep-grad: linear-gradient(90deg, transparent 0%, rgba(184, 231, 106, 0.55) 50%, transparent 100%);
  --ease-confident: cubic-bezier(0.2, 0.8, 0.2, 1);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 2px 6px rgba(0, 0, 0, 0.05), 0 14px 34px rgba(0, 0, 0, 0.09);
  --shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.08), 0 30px 70px rgba(0, 0, 0, 0.14);
  --r-sm: 12px;
  --r-md: 18px;
  --r-lg: 26px;
  /* Motion tokens.
     `--spring` is the generated spring curve from springs.css (real physics
     sampled into a CSS linear() easing) with the old hand-picked bezier kept as
     the fallback, so every existing `var(--spring)` rule inherits genuine
     overshoot-and-settle and the page still animates if that file is missing.
     Springs drive transforms only — opacity and colour have no momentum, and
     springing them turns a fade into a flicker, so those keep `--ease`. */
  --ease: cubic-bezier(0.25, 0.1, 0.25, 1);
  --spring: var(--spring-snappy, cubic-bezier(0.34, 1.3, 0.5, 1));
  --pad-x: clamp(20px, 5.5vw, 96px);
  /* The stacking order, named. Values are the ones the page already shipped
     with, so nothing visually moves; the point is that the next overlay picks
     a name from this ladder instead of typing 999 and hoping. */
  --z-float: 30;
  --z-header: 40;
  --z-drawer: 50;
  --z-suggest: 60;
  --z-tray: 70;
  --z-toast: 80;
  --z-palette: 90;
  --z-skip: 100;
  --z-dialog: 1000;
  /* The reading and content width. Five rules were already using this token
     and nothing defined it, so every one of them was silently on its own
     fallback and the page had as many content widths as fallbacks. Defining it
     makes the token true; 1180px is the value the majority already fell back
     to, so nothing moves except the outliers. */
  --measure: 1180px;
}

* { box-sizing: border-box; }

/* The `hidden` attribute is only a UA-level `display: none`, so any class rule
   that sets `display` silently defeats it. That is not a cosmetic problem: the
   command palette is a fixed, full-viewport overlay, and while `hidden` was
   being ignored it sat invisibly over the page and swallowed every click on it.
   Anything marked hidden is hidden, whatever else it is styled to be. */
[hidden] { display: none !important; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: 92px;
  -webkit-text-size-adjust: 100%;
  /* `clip`, on both html and body, and neither of those is arbitrary.
     `overflow-x: hidden` on body alone does not do what it reads as. When one
     axis is `hidden` and the other computes to `visible`, the visible axis
     becomes `auto` and the element turns into a scroll container, which stops
     the value propagating to the viewport. The page was therefore still
     draggable sideways: scrollTo(600) left scrollX at 167, with the brand
     marquee as the overhang.
     `hidden` on html would fix the scrolling and break the sticky header, since
     a scroll container is what `position: sticky` resolves against. `clip`
     clips without creating one, so the header still sticks and the page cannot
     be dragged. */
  overflow-x: clip;
}

body {
  margin: 0;
  overflow-x: clip;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font-sans);
  line-height: 1.47;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* The reference's stylistic sets, which it names as one of the two details
     most often dropped in a port. On SF these change the shape of a, g and the
     digits; without them the same family reads slightly wider and softer than
     the original does. */
  font-feature-settings: "ss01", "ss02", "cv02", "cv03", "cv04", "cv11";
}

/* Headings carry --font-display as they do in the reference. It resolves to
   the same stack today; keeping the two names apart is what makes it a
   one-line change if a display face is ever licensed. */
h1, h2, h3, h4, h5, h6 { font-family: var(--font-display); }


a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
img { max-width: 100%; }
::selection { background: rgba(13, 159, 110, 0.2); }

:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
  border-radius: 6px;
}

h1, h2, h3, h4, p, dl { margin-top: 0; }

/* Sized so the two-clause headline holds two lines rather than three: at 88px
   it grew to 264px tall and pushed the search field under the fold. */
/* Metrics taken from the reference rather than eyeballed. Its note on this was
   blunt and correct: a heading set at the default -0.02em reads generic
   whatever family loads, because the tightness is the identity, not the face.
   The size ramp is kept a little shorter than the reference's 4.8rem ceiling
   so the two-clause headline still holds two lines rather than three; at 88px
   it grew to 264px tall and pushed the search field under the fold. */
h1 {
  margin-bottom: 18px;
  font-size: clamp(38px, 4.8vw, 68px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.045em;
}

/* Emphasis is a weight and a colour, never a clipped gradient. A transparent
   text fill is one unsupported property away from an invisible headline, and
   the hero's neon treatment needs a solid glyph underneath it before it can
   glow at all: you cannot light a letter that is a hole. */
h1 em {
  font-style: normal;
  color: #4d7c0f;
}

h2 {
  font-size: clamp(30px, 3.6vw, 52px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.035em;
}

h3 { letter-spacing: -0.015em; }

.sr-only, .skip-link {
  position: absolute;
  width: 1px !important; height: 1px !important;
  min-width: 0 !important;
  max-width: 1px !important;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.skip-link:focus {
  z-index: var(--z-skip);
  width: auto; height: auto;
  left: 18px; top: 18px;
  clip: auto;
  padding: 12px 18px;
  border-radius: 999px;
  background: var(--ink);
  color: #fff;
  font-weight: 600;
}

/* ---------- Ambient background ---------- */

.aurora {
  position: fixed;
  inset: 0;
  width: 100vw;
  max-width: 100vw;
  overflow: hidden;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(900px 540px at 85% -10%, rgba(13, 159, 110, 0.07), transparent 60%),
    radial-gradient(760px 480px at 4% 8%, rgba(10, 132, 255, 0.05), transparent 55%),
    var(--canvas);
}

.grid-veil, .grain { display: none; }

/* Atmospheric orbs, revived with the CommScope Connect treatment: two soft
   radials in electric and teal and one in the logo's own lime, drifting on a
   40-second orbit. They sit behind everything at single-digit opacity — depth,
   not decoration — and the global reduced-motion rule parks them. */
.blob {
  position: absolute;
  width: 46vw;
  max-width: 720px;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.5;
  animation: orbit-aurora 40s var(--ease-confident, ease-in-out) infinite;
  will-change: transform;
}

.blob-a {
  top: -18%; right: -8%;
  background: radial-gradient(circle, rgba(59, 130, 246, 0.16), transparent 65%);
}

.blob-b {
  top: 22%; left: -14%;
  background: radial-gradient(circle, rgba(20, 184, 166, 0.13), transparent 65%);
  animation-delay: -13s;
}

.blob-c {
  top: 58%; right: 6%;
  width: 30vw;
  background: radial-gradient(circle, rgba(184, 231, 106, 0.12), transparent 65%);
  animation-delay: -27s;
}

@keyframes orbit-aurora {
  0%, 100% { transform: translate(-6%, -4%) scale(1); }
  50% { transform: translate(5%, 3%) scale(1.08); }
}

/* A note that governs the whole sheet: the live field is one canvas fixed
   behind the entire page, so anything carrying backdrop-filter has its blur
   recomputed on every frame the field draws, everywhere, all the way down.
   That is what made the site feel heavy well below the hero. Blurs are gone
   from every surface the field shows through, and the grounds underneath
   were raised to carry the legibility the blur had been lending them. Two
   remain, on the photo badge and the dialog close button, because a blur a
   few dozen pixels across is not what a compositor chokes on. */

/* ---------- Header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: rgba(251, 251, 253, 0.94);
  border-bottom: 1px solid transparent;
  transition: border-color 300ms var(--ease), background 300ms var(--ease);
}

.site-header.is-stuck {
  border-bottom-color: var(--line);
  background: rgba(251, 251, 253, 0.92);
}

.top-strip {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 18px;
  min-height: 34px;
  padding: 5px var(--pad-x);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: 13px;
}

.top-strip a { color: var(--green-deep); font-weight: 600; transition: opacity 160ms var(--ease); }
.top-strip a:hover { opacity: 0.65; }

.pulse-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 0 0 rgba(13, 159, 110, 0.4);
  animation: pulse-confident 2.4s ease-in-out infinite;
}

/* The portal's live-indicator pulse: scale and opacity breathe together, which
   reads as "alive" where the old expanding ring read as "alert". */
@keyframes pulse-confident {
  0%, 100% { opacity: 0.55; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.18); }
}

@keyframes pulse {
  70% { box-shadow: 0 0 0 9px rgba(13, 159, 110, 0); }
  100% { box-shadow: 0 0 0 0 rgba(13, 159, 110, 0); }
}

.nav-shell {
  min-height: 72px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(12px, 1.6vw, 32px);
  padding: 10px var(--pad-x);
}

/* No min-width:0 here — the grid column must reserve the full
   wordmark window, otherwise nav links overlap the logo. */
.brand { display: flex; align-items: center; }

/* Wordmark SVG carries the full identity — no box, no duplicate
   text. Its ink occupies only ~80% x 46% of the viewBox (measured),
   so it is oversized inside a fixed clipping window: 40px of ink
   height needs an 88px render (40 / 0.456) → 164px of ink width.
   flex-basis + min-width pin the window against flex/grid shrink. */
.brand-mark {
  position: relative;
  display: block;
  flex: 0 0 164px;
  width: 164px;
  min-width: 164px;
  height: 40px;
  overflow: hidden;
  background: none;
  border: 0;
  box-shadow: none;
}

.brand-mark img {
  position: absolute;
  left: 50%; top: 50%;
  height: 88px;
  width: auto;
  max-width: none;
  transform: translate(-50%, -50%);
}
.brand-text { display: none; }

/* min-width:0 + overflow guard: the links are nowrap, so without these they
   overflow this grid column and paint over the wordmark and the search box. */
.nav-links {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  /* `overflow: hidden` was quietly amputating the nav: adding About pushed
     "Categories" to "ories" and "Contact" to "Cc" on a 1440px screen and
     nobody was told. A nav that cannot fit must shrink, not bleed. */
  overflow: visible;
  flex-wrap: nowrap;
  gap: clamp(2px, 0.6vw, 8px);
  color: var(--ink-2);
  font-size: clamp(12.5px, 0.95vw, 14px);
  font-weight: 500;
}

.nav-links a {
  padding: 8px clamp(6px, 0.7vw, 11px);
  border-radius: 999px;
  white-space: nowrap;
  transition: background 180ms var(--ease), color 180ms var(--ease);
}

.nav-links a:hover { background: rgba(0, 0, 0, 0.05); color: var(--ink); }

.nav-actions { display: flex; align-items: center; gap: 10px; }

/* ---------- Search ---------- */

.header-search { position: relative; width: clamp(180px, 17vw, 300px); }

.search-icon {
  position: absolute;
  left: 14px; top: 50%;
  width: 16px; height: 16px;
  transform: translateY(-50%);
  fill: none;
  stroke: var(--faint);
  stroke-width: 2;
  stroke-linecap: round;
  pointer-events: none;
}

.header-search input {
  width: 100%;
  height: 40px;
  /* Asymmetric because the two ends hold different things. Left is the 16px
     search glyph at 14px in, so 40px clears it. Right holds the ⌘K badge,
     which starts 13px from the edge and is 20-30px wide depending on how the
     label sets — 40px of padding ended 3px short of it, so the placeholder ran
     underneath and the field read "Search a brand or p⌘K". A caret arriving at
     the end of a typed part code hit the same wall. */
  padding: 0 54px 0 40px;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.05);
  color: var(--ink);
  outline: none;
  transition: background 200ms var(--ease), box-shadow 200ms var(--ease);
}

.header-search input::placeholder { color: var(--faint); }

.header-search input:focus {
  background: #fff;
  box-shadow: 0 0 0 3.5px rgba(13, 159, 110, 0.22), var(--shadow-sm);
}

.header-search input::-webkit-search-cancel-button { -webkit-appearance: none; }

.search-kbd {
  position: absolute;
  right: 13px; top: 50%;
  transform: translateY(-50%);
  min-width: 20px; height: 20px;
  padding: 0 5px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: none;
  color: var(--faint);
  font-family: inherit;
  font-size: 10px;
  font-weight: 600;
  cursor: pointer;
  transition: color 180ms var(--ease), border-color 180ms var(--ease), background 180ms var(--ease);
}

.search-kbd:hover {
  color: var(--green-deep);
  border-color: rgba(13, 159, 110, 0.4);
  background: rgba(13, 159, 110, 0.08);
}

.search-kbd:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }

/* ---------- Suggestions ---------- */

.suggest {
  position: absolute;
  left: 0; right: 0;
  top: calc(100% + 10px);
  z-index: var(--z-suggest);
  display: none;
  max-height: 60vh;
  overflow: auto;
  padding: 7px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.92);
  box-shadow: var(--shadow-lg);
}

.suggest.is-open { display: block; animation: pop-in 240ms var(--spring); }

@keyframes pop-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.985); }
}

.suggest button {
  width: 100%;
  display: grid;
  grid-template-columns: 44px 1fr;
  gap: 12px;
  align-items: center;
  padding: 8px;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  text-align: left;
  transition: background 130ms var(--ease);
}

.suggest button:hover,
.suggest button.is-active { background: rgba(0, 0, 0, 0.05); }

.suggest .s-thumb {
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: 10px;
  background: var(--plate);
  border: 1px solid var(--line);
}

.suggest .s-thumb img { width: 100%; height: 100%; }
.suggest .s-thumb.pending { color: var(--faint); font-size: 9px; font-weight: 700; }
.suggest .s-name {
  display: block;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.suggest .s-meta {
  display: block;
  color: var(--muted);
  font-size: 11px;
  font-family: var(--mono);
}
.suggest .s-empty { margin: 0; padding: 16px; color: var(--muted); font-size: 13px; }

/* ---------- Buttons ---------- */

.button,
.toolbar-actions button,
.filter-header button,
.drawer-tools button,
.mobile-menu,
.dialog-close,
.drawer-header button {
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 18px;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.05);
  color: var(--ink);
  font-size: 14px;
  font-weight: 590;
  white-space: nowrap;
  transition: background 180ms var(--ease), transform 180ms var(--spring), box-shadow 180ms var(--ease), color 180ms var(--ease);
}

.button:hover,
.toolbar-actions button:hover,
.filter-header button:hover,
.drawer-tools button:hover { background: rgba(0, 0, 0, 0.09); }

.button:active { transform: scale(0.965); }

.button.lg { min-height: 50px; padding: 0 28px; font-size: 16px; }

.button.primary {
  background: var(--green);
  color: #fff;
  box-shadow: 0 2px 10px rgba(13, 159, 110, 0.28);
  position: relative;
  overflow: hidden;
}

/* Lovable's scan-sweep, repurposed as a one-shot sheen on hover. The lime
   catches the logo leaf without ever carrying text, so contrast is safe. */
.button.primary::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--sweep-grad);
  opacity: 0.35;
  transform: translateX(-120%);
  pointer-events: none;
}

.button.primary:hover::after { animation: btn-sheen 0.7s var(--ease-confident); }

@keyframes btn-sheen {
  from { transform: translateX(-120%); }
  to { transform: translateX(160%); }
}

.button.primary:hover {
  background: var(--green-deep);
  box-shadow: 0 6px 20px rgba(13, 159, 110, 0.36);
  transform: translateY(-1px);
}

.ico { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.button.primary .ico, .whatsapp-float svg { fill: currentColor; stroke: none; }

.count-pill {
  min-width: 21px; height: 21px;
  display: grid;
  place-items: center;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--green);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  transition: transform 300ms var(--spring);
}

.cart.bump .count-pill { transform: scale(1.4); }

.mobile-menu {
  display: none;
  width: 42px;
  padding: 0;
  flex-direction: column;
  gap: 4px;
}

.mobile-menu span {
  width: 16px; height: 1.8px;
  margin: 0;
  border-radius: 2px;
  background: var(--ink);
  transition: transform 240ms var(--ease), opacity 240ms var(--ease);
}

.mobile-menu[aria-expanded="true"] span:nth-child(1) { transform: translateY(5.8px) rotate(45deg); }
.mobile-menu[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.mobile-menu[aria-expanded="true"] span:nth-child(3) { transform: translateY(-5.8px) rotate(-45deg); }

/* ---------- Scroll reveals ---------- */

/* Sections lift into place as they come into view.
 *
 * The hidden state is gated behind `html.js`, set by an inline script in the
 * document head. Without that gate a reader whose JavaScript failed to run
 * would get a page of invisible sections, because the class that reveals them
 * (`.in`) is applied by an IntersectionObserver. Gating it means the hidden
 * state only ever exists in a document that can undo it, and the head script
 * runs before first paint so nothing flashes on the way in.
 */
.reveal {
  opacity: 1;
  transform: none;
  transition:
    opacity 620ms var(--ease-out, var(--ease)),
    transform var(--spring-gentle-duration, 800ms) var(--spring-gentle, var(--ease));
}

html.js .reveal:not(.in) {
  opacity: 0;
  transform: translateY(18px);
}

.reveal.in { opacity: 1; transform: none; }

/* ---------- Hero ---------- */

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 16px;
  color: var(--green-deep);
  font-size: 13px;
  font-weight: 650;
  letter-spacing: 0.02em;
  text-transform: none;
}

/* ---------- Hero ----------
   A dark band, then the light catalogue. The signal field is the CommScope
   Connect look rebuilt as static SVG plus layered gradients: their version
   runs a canvas particle loop every frame, which is exactly the main-thread
   work that was taken off this page to get the first product card down to 1.5s
   on a phone. Nothing here executes on the device.
   The band stops at the hero deliberately. Below it sit 9,187 product cards
   that people read part codes off, and those stay on white. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(340px, 430px);
  align-items: center;
  gap: clamp(36px, 5vw, 80px);
  /* Trimmed from 128px: the search field is the primary action and belongs well
     inside the first screen, not 550px down it. */
  padding: clamp(40px, 5vw, 78px) var(--pad-x) clamp(46px, 5vw, 72px);
  border: 0;
  isolation: isolate;
  /* The wash under the field. A lime bloom top right where the headline ends,
     a blue counterweight bottom left so the band is not lit from one side only,
     and a darkening at the foot so the join to the white catalogue is a clean
     line rather than a gradient that peters out. */
  background:
    radial-gradient(120% 90% at 78% 6%, rgba(184, 231, 106, 0.15), transparent 58%),
    radial-gradient(ellipse 78% 52% at 50% -12%, rgba(37, 99, 235, 0.34), transparent 62%),
    radial-gradient(100% 80% at 8% 100%, rgba(27, 95, 217, 0.16), transparent 60%),
    linear-gradient(180deg, #030916 0%, #060f1e 46%, #0a1524 100%);
}

/* The Signal Field: a measuring grid, orthogonal routes that turn at right
   angles the way tracks are laid out on a board, and the nodes those routes
   terminate in. Three depths, each drifting on its own path at its own speed.
   One layer moving is invisible; three moving against each other is what reads
   as a live field.
   This replaced a starfield. Scattered stars said "space"; a grid and routes
   say "infrastructure", which is what the desk actually sells.
   An earlier pass drifted a single layer 1.4% over 120 seconds, which is about
   0.02% per second: animated on paper, motionless to a reader.
   All of it is transform and opacity, so it is composited and costs no main
   thread, and the global reduced-motion rule parks every bit of it. */
.field {
  position: absolute;
  inset: -10% -6%;
  z-index: -1;
  pointer-events: none;
  background-repeat: repeat;
  will-change: transform, opacity;
}

/* Masked away before it reaches the headline, so the copy never sits on ruled
   paper. It drifts exactly one tile, which is why the loop has no seam. */
/* Periods are the whole argument here, and the first port got them wrong.
   The reference this came from drifts at 90s, 64s and 48s, which measured at
   6 to 9 pixels of travel over three seconds on layers sitting at 16% and 50%
   opacity. That is animated on paper and motionless to a reader, which is
   exactly the failure the starfield had before it, and shipping it again was a
   straight regression. The periods below are roughly a third of that, with the
   amplitudes roughly doubled.
   The grid is the exception and stays slow on purpose: it is a measuring grid,
   and a ruler that visibly slides is a distraction rather than a background. */
.field-grid {
  background-image: url("/catalog/assets/signal-grid.svg");
  background-size: 128px 128px;
  opacity: 0.16;
  -webkit-mask-image: radial-gradient(120% 100% at 50% 0%, #000 35%, transparent 78%);
  mask-image: radial-gradient(120% 100% at 50% 0%, #000 35%, transparent 78%);
  animation: field-drift-grid 34s linear infinite;
}


/* The grid travels exactly one tile, so the loop closes on itself invisibly. */
@keyframes field-drift-grid {
  to { transform: translate3d(-128px, -128px, 0); }
}

/* The same grid tile again, at whisper strength under the top of the catalogue,
   so the dark hero and the light product grid read as one page rather than two.
   The reference set this at 0.5 and let it run the full height of the section.
   Measured, that put the section eyebrow at 3.95:1 and the supporting line at
   3.70:1, both below AA. At 0.16, faded out inside 200px, the grid still reads
   as texture and every piece of text over it clears 4.5:1. The catalogue is
   where people read part codes off the screen, and that comes first. */
.paper-field {
  position: relative;
  isolation: isolate;
}

.paper-field::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url("/catalog/assets/signal-grid.svg");
  background-size: 128px 128px;
  opacity: 0.16;
  -webkit-mask-image: linear-gradient(180deg, #000, transparent 200px);
  mask-image: linear-gradient(180deg, #000, transparent 200px);
}

.paper-field > * { position: relative; z-index: 1; }

/* ---------- A second dark band ----------
   The hero is not the only place the field belongs. A band the reader meets
   again further down is what makes the page feel like one designed thing rather
   than a dark header stuck on a white document.
   It is rationed deliberately: bands carry brand and identity, never product
   cards. Everything a buyer reads a part code off stays on the light ground,
   which is what the accessibility pass depends on. */
.ink-band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  color: #ffffff;
  background:
    radial-gradient(120% 90% at 82% 0%, rgba(184, 231, 106, 0.13), transparent 58%),
    radial-gradient(100% 80% at 10% 100%, rgba(27, 95, 217, 0.15), transparent 60%),
    linear-gradient(180deg, #060f1e 0%, #050b16 52%, #040a12 100%);
  background-color: #050b16;
}

.ink-band > * { position: relative; z-index: 1; }
.ink-band h2 { color: #ffffff; }
.ink-band .sub, .ink-band p { color: rgba(233, 240, 250, 0.72); }
.ink-band .section-heading .sub { color: rgba(240, 246, 255, 0.95); }
.ink-band .eyebrow { color: var(--brand-lime); }

/* The numbered band eyebrow, to the reference's own measurements.
 *
 * Its answer on this was specific enough to copy rather than interpret: mono,
 * 11px, uppercase, tracking 0.28em, lime, and a middot rather than a period
 * between the number and the word. The numbering runs sequentially down the
 * dark bands, which is the part that carries information — a reader arriving
 * mid-page can see where they are in a sequence rather than in a heap of
 * sections. The catalogue takes no number because it is the light band, and on
 * light ground the reference's eyebrow is blue and unnumbered.
 *
 * The tracking differs between the two on purpose, 0.28em on dark against
 * 0.22em on light: lime on black needs more air than blue on white. */
.eyebrow.band-number {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--brand-lime);
  font-feature-settings: "ss01", "ss02";
}

/* And on a light ground it is blue, unnumbered, and set tighter. Only the
   catalogue qualifies today; the rule is here so a new light band inherits the
   right treatment instead of borrowing the dark one. */
.section:not(.ink-band) .eyebrow {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--green-deep);
}

/* Text on the band, scoped to .hero-copy rather than .hero. The hero also holds
   the white featured panel, and everything inside that card has to keep its
   dark-on-white colours: lightening .panel-note by scoping to .hero put grey
   text on a white background at roughly 1.4:1. */
.hero-copy .eyebrow { color: var(--brand-lime); }
.hero-copy h1 { color: #ffffff; }

/* Starry neon, third recipe, and this one is the classic because the first
 * two were both fighting the same hidden problem.
 *
 * Attempt one hung a 44px drop-shadow on the whole h1: a lit cloud behind the
 * text, not glowing text. Attempt two moved tight drop-shadows onto the two
 * spans, which should have worked and did not, and the reason is the fills.
 * Both lines were gradient-clipped, so the glyphs were pale, semi-transparent
 * shapes; drop-shadow rasterises the glyph and blurs THAT, and a pale
 * letterform blurred at 26px is fog whatever the radius stack says. The owner
 * called it dark, big and not luminous, and all three were exactly right.
 *
 * Real neon is not a gradient. It is a solid tube, near-white at the core,
 * with a saturated halo around it. In CSS that is solid opaque text plus a
 * text-shadow stack in the tube colour at 4, 12, 26 and 52px. text-shadow was
 * unusable while the fills were clipped, because a shadow paints through a
 * transparent fill; on solid glyphs it is both the right tool and the cheap
 * one, and the halo compounds instead of washing out.
 *
 * So the gradient clip is retired, the letters go solid and luminous, and the
 * colour lives in the halo: line one neon green as the owner asked, line two
 * indigo to answer it, each breathing on its own phase. */
.hero-copy h1 .metal {
  color: #f4ffd8;
  -webkit-text-fill-color: #f4ffd8;
  background: none;
  text-shadow:
    0 0 4px rgba(217, 249, 157, 0.95),
    0 0 12px #a3e635,
    0 0 26px #84cc16,
    0 0 52px rgba(132, 204, 22, 0.55);
  animation: neon-green 3.6s ease-in-out infinite;
}

/* The indigo line runs to two rows, and the halo of the upper row lands on top
   of the halo of the lower one. Two 52px washes stacked in the leading read as
   one pale cloud with letters somewhere inside it, which is the fog this whole
   pass exists to remove. So the second tube is deliberately not a recolour of
   the first: the outer radius comes in, the deep end goes to a saturated
   violet rather than a lighter periwinkle, and the core stays tight. Same
   brightness at the letter, far less of it in the gap between rows. */
.hero-copy h1 em {
  color: #eef1ff;
  -webkit-text-fill-color: #eef1ff;
  background: none;
  text-shadow:
    0 0 4px rgba(199, 210, 254, 0.98),
    0 0 11px #818cf8,
    0 0 22px #4f46e5,
    0 0 38px rgba(79, 70, 229, 0.45);
  animation: neon-indigo 3.6s ease-in-out infinite;
  animation-delay: 1.2s;
}

@keyframes neon-green {
  0%, 100% {
    text-shadow:
      0 0 4px rgba(217, 249, 157, 0.95),
      0 0 12px #a3e635,
      0 0 26px #84cc16,
      0 0 52px rgba(132, 204, 22, 0.55);
  }
  50% {
    text-shadow:
      0 0 6px rgba(236, 252, 203, 1),
      0 0 16px #bef264,
      0 0 34px #a3e635,
      0 0 66px rgba(132, 204, 22, 0.8);
  }
}

@keyframes neon-indigo {
  0%, 100% {
    text-shadow:
      0 0 4px rgba(199, 210, 254, 0.98),
      0 0 11px #818cf8,
      0 0 22px #4f46e5,
      0 0 38px rgba(79, 70, 229, 0.45);
  }
  50% {
    text-shadow:
      0 0 6px rgba(224, 231, 255, 1),
      0 0 15px #a5b4fc,
      0 0 28px #6366f1,
      0 0 48px rgba(79, 70, 229, 0.62);
  }
}

.hero-copy .eyebrow {
  text-shadow: 0 0 6px rgba(184, 231, 106, 0.9), 0 0 16px rgba(184, 231, 106, 0.5);
  animation: neon-eyebrow 4.2s ease-in-out infinite;
  animation-delay: 0.7s;
}

@keyframes neon-eyebrow {
  0%, 100% { text-shadow: 0 0 6px rgba(184, 231, 106, 0.9), 0 0 16px rgba(184, 231, 106, 0.5); }
  50% { text-shadow: 0 0 8px rgba(214, 242, 154, 1), 0 0 24px rgba(184, 231, 106, 0.75); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-copy h1 .metal,
  .hero-copy h1 em,
  .hero-copy .eyebrow { animation: none; }
}

.hero-copy .lede { color: rgba(233, 240, 250, 0.88); }

.hero-copy h1 em {
  /* Its own line, always. The two clauses carry two different tube colours, so
     where the line falls is not typographic taste — it decides whether the
     reader sees green over indigo or a smear through the middle of a word. The
     old headline happened to break in the right place because its first clause
     was long; the new one is short enough that "behind" climbed onto line one
     and took half the treatment with it.
     Line break only. Colour and glow are set once, in the neon block above. */
  display: block;
}

/* Chips read as glass on the dark band instead of as white pills.

   Scoped to .hero, and that is the whole fix. This rule was written when the
   hero went dark and it has never once applied: an identical `.hero-chips
   button` selector sits about thirty lines further down setting a 70% white
   pill, and on equal specificity the later rule wins. So the intent was
   recorded here, the opposite shipped, and the comment above quietly described
   something nobody had ever seen. One class of extra specificity settles it.

   Glass rather than white is also the right answer now for a reason that did
   not exist when it was written: every other surface on a dark band became
   dark glass in the depth-of-field pass, so a row of bright white pills under
   a neon headline was the last piece of light UI left on a near-black page. */
.hero .hero-chips button {
  border-color: rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.08);
  color: rgba(240, 246, 255, 0.92);
}

.hero .hero-chips button:hover {
  border-color: rgba(184, 231, 106, 0.75);
  background: rgba(184, 231, 106, 0.16);
  color: #ffffff;
}

.hero-chips button:hover {
  border-color: rgba(184, 231, 106, 0.75);
  background: rgba(184, 231, 106, 0.16);
  color: #ffffff;
}

.hero .hero-secondary .button.ghost {
  border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(255, 255, 255, 0.1);
  color: #ffffff;
}

.hero .hero-secondary .button.ghost:hover { background: rgba(255, 255, 255, 0.18); }

.lede, .sub { color: var(--ink-2); }
.hero-copy .lede { max-width: 34ch; font-size: clamp(17px, 1.5vw, 21px); line-height: 1.5; }
.sub { max-width: 60ch; margin-top: 12px; font-size: 16px; }

.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }

.hero-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 26px 0 0;
  padding: 0;
  list-style: none;
}

.hero-chips button {
  min-height: 34px;
  padding: 0 15px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.7);
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 550;
  /* Named properties rather than `all`, and the reason is the focus ring.
     `all` transitions outline-width too, so tabbing to a chip animated the
     ring from 0 to 2px over 180ms: at the instant focus arrived there was no
     ring, and a keyboard reader moving at speed never saw one.

     This is also why the first audit of this control reported it as having no
     focus state at all. It has one. It simply was not there yet when the
     sample was taken — and if a machine reading computed style the moment
     after the keypress cannot find the ring, neither can a person. A focus
     indicator is the one thing on a page that has to be instantaneous;
     everything else here can ease. */
  transition:
    border-color 180ms var(--ease),
    background 180ms var(--ease),
    color 180ms var(--ease);
}

.hero-chips button:hover {
  border-color: var(--green);
  color: var(--green-deep);
  background: rgba(13, 159, 110, 0.06);
  transform: translateY(-1px);
}

/* ---------- Hero search ---------- */

/* The dominant control on the page. Buyers arrive with a part code, so this is
   deliberately larger than the header field and reads as the primary action. */
.hero-search {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  width: min(100%, 620px);
  margin: 30px 0 0;
  padding: 7px 7px 7px 20px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  /* Ninety-two percent white over the field: there is almost nothing left for
     a blur to show, and it sat over the canvas paying for one every frame. */
  background: rgba(255, 255, 255, 0.96);
  box-shadow: var(--shadow-sm);
  transition: border-color 220ms var(--ease), box-shadow 260ms var(--ease), transform 260ms var(--spring);
}

.hero-search:hover { box-shadow: var(--shadow-md); }

.hero-search:focus-within {
  border-color: rgba(13, 159, 110, 0.55);
  box-shadow: 0 0 0 4px rgba(13, 159, 110, 0.12), var(--shadow-md);
}

.hs-icon {
  width: 20px; height: 20px;
  flex: 0 0 auto;
  fill: none;
  stroke: var(--muted);
  stroke-width: 2;
  stroke-linecap: round;
  transition: stroke 220ms var(--ease);
}

.hero-search:focus-within .hs-icon { stroke: var(--green-deep); }

.hero-search input {
  min-width: 0;
  min-height: 46px;
  border: 0;
  background: none;
  font-size: 17px;
  letter-spacing: -0.005em;
  outline: none;
  appearance: none;
}

.hero-search input::placeholder { color: var(--faint); }
.hero-search input::-webkit-search-cancel-button { appearance: none; }

.hs-go {
  min-height: 46px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0 22px;
  border: 0;
  border-radius: 999px;
  background: var(--green);
  color: #fff;
  font-size: 15px;
  font-weight: 620;
  white-space: nowrap;
  box-shadow: 0 2px 10px rgba(13, 159, 110, 0.26);
  transition: background 200ms var(--ease), box-shadow 200ms var(--ease), transform 200ms var(--spring);
}

.hs-go:hover { background: var(--green-deep); box-shadow: 0 6px 18px rgba(13, 159, 110, 0.34); }
.hs-go:active { transform: scale(0.96); }

.hs-go svg {
  width: 16px; height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 220ms var(--spring);
}

.hs-go:hover svg { transform: translateX(3px); }

/* ---------- Hero secondary row ---------- */

.hero-secondary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 20px;
  margin-top: 26px;
}

/* Hero panel */

/* The hero panel, rebuilt to the reference's answer rather than to my guess.
 *
 * What stood here was a white glass card holding four product photographs on
 * a rotating clock. The owner circled it as looking wrong and the reference's
 * agent, asked directly, was blunter: "I never put white product tiles on the
 * dark hero. Product photography — which is white-background by rule — appears
 * on the white section below the hero, never floated on the dark wall." The
 * four launch cards are its stated hero content, and their treatment below is
 * copied from its own numbers: 1rem radius, 10% white border, 4% white fill,
 * backdrop blur, and a half-step lift on hover with the border warming to
 * lime. Photography now appears only where its white ground disappears into
 * the page's own. */
.hero-panel {
  position: relative;
  display: grid;
  gap: 12px;
  padding: 18px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--r-lg);
  /* No backdrop-filter here, and none on the tiles inside. A blurred backdrop
     is recomputed whenever what is behind it changes, and what is behind this
     panel is a canvas that repaints sixty times a second: the browser was
     running a fourteen pixel gaussian over the whole panel, and an eight pixel
     one over each of the four tiles inside it, on every frame of the field.
     That is what made the hero feel heavy, not the orbs. A flat translucent
     ground over a dark field looks the same and costs nothing per frame. */
  background: rgba(12, 20, 36, 0.62);
  box-shadow: none;
}

.hero-panel::before { display: none; }
.panel-glow { display: none; }

.hero-gallery {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 4px;
}

.hero-launch {
  display: grid;
  gap: 6px;
  justify-items: start;
  text-align: left;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.05);
  color: #ffffff;
  transition: transform 300ms var(--ease), border-color 300ms var(--ease), background 300ms var(--ease);
}

.hero-launch:hover {
  transform: translateY(-2px);
  border-color: rgba(184, 231, 106, 0.6);
  background: rgba(255, 255, 255, 0.07);
}

.hero-launch .cat-glyph { width: 30px; height: 30px; }
/* The glyphs are stroke art with no fill of their own; unstyled they inherit a
   black fill and read as blobs on dark glass. Same treatment as the spotlight
   tiles, in lime. */
.hero-launch .cat-glyph .glyph {
  width: 26px;
  height: 26px;
  fill: none;
  stroke: var(--brand-lime);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.hero-launch b { font-size: 13px; font-weight: 600; letter-spacing: -0.01em; }
.hero-launch small { font-size: 12px; color: rgba(255, 255, 255, 0.58); line-height: 1.45; }
.hero-launch em {
  font-style: normal;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  color: var(--brand-lime);
}

.hero-points {
  grid-column: 1 / -1;
  display: grid;
  gap: 11px;
  margin: 6px 4px 2px;
  padding: 0;
  list-style: none;
}

.hero-points li {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  align-items: start;
  gap: 11px;
  color: var(--ink-2);
  font-size: 15px;
  line-height: 1.45;
}

.hero-points li > span {
  position: relative;
  width: 18px; height: 18px;
  margin-top: 2px;
  display: block;
  border-radius: 50%;
  background: rgba(13, 159, 110, 0.12);
}

/* Tick drawn from two borders, so the list needs no icon font or inline SVG. */
.hero-points li > span::before {
  content: "";
  position: absolute;
  left: 5px; top: 5px;
  width: 7px; height: 4px;
  border-left: 1.8px solid var(--green-deep);
  border-bottom: 1.8px solid var(--green-deep);
  transform: rotate(-45deg);
}

.panel-note {
  grid-column: 1 / -1;
  margin: 2px 4px 0;
  color: var(--muted);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.5;
}

/* ---------- Brand logo marquee ---------- */

.logo-marquee {
  position: relative;
  max-width: 100vw;
  contain: paint;
  padding: clamp(18px, 2.5vw, 30px) 0 clamp(36px, 4vw, 56px);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

.marquee-track {
  display: flex;
  align-items: center;
  gap: clamp(14px, 1.4vw, 22px);
  width: max-content;
  animation: marquee 52s linear infinite;
}

.logo-marquee:hover .marquee-track { animation-play-state: paused; }

@keyframes marquee {
  to { transform: translateX(-50%); }
}

/* Each mark sits on its own plate, in its own colours, at the size the
   optical-scale sheet gives it. Three changes, all from the same complaint.

   Colour: the strip was grayscale at 72% opacity, which is the convention for
   a logo wall nobody is meant to look at. These are the brands the desk is
   built on, and washing them out to grey made the strip read as a piece of
   furniture rather than as content.

   Size: it ran at 20 to 27px against a wall of 38px marks two sections below,
   so the same logo appeared twice on one page at half the size.

   Plates: the marks now sit on the same light plate the wall uses, which is
   what lets a dark wordmark and a bright one share a row without one of them
   disappearing. */
.marquee-mark {
  display: grid;
  place-items: center;
  min-width: 132px;
  height: 54px;
  padding: 0 16px;
  border: 1px solid var(--line, rgba(0, 0, 0, 0.1));
  border-radius: var(--r-tile, 16px);
  background: #ffffff;
  cursor: pointer;
  transition: transform 260ms var(--ease), box-shadow 260ms var(--ease), border-color 260ms var(--ease);
}

.marquee-mark img {
  width: auto;
  height: auto;
  max-width: 108px;
  max-height: 30px;
  object-fit: contain;
}

.marquee-mark:hover,
.marquee-mark:focus-visible {
  transform: translateY(-2px);
  border-color: rgba(13, 159, 110, 0.45);
  box-shadow: 0 6px 20px rgba(4, 10, 20, 0.14);
}

/* The strip says what it is. Without this it was an unlabelled ribbon and the
   only honest reading of it was that something was scrolling for no reason. */
.marquee-head {
  max-width: var(--measure, 1180px);
  margin: 0 auto clamp(14px, 1.6vw, 22px);
  padding: 0 var(--pad-x);
}

.marquee-head h2 {
  margin: 0 0 4px;
  font-size: clamp(1.05rem, 1.5vw, 1.35rem);
  font-weight: 640;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.marquee-head p {
  margin: 0;
  font-size: 14px;
  color: var(--muted);
}

/* ---------- Sections ---------- */

.section { padding: clamp(56px, 7vw, 104px) var(--pad-x); }
.section-heading { max-width: 820px; margin-bottom: 36px; }

.category-entry { padding-top: clamp(34px, 4.5vw, 72px); }

.category-spotlight {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
}

@media (min-width: 1241px) {
  .category-spotlight { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.category-spotlight button {
  min-width: 0;
  min-height: 168px;
  display: grid;
  align-content: start;
  gap: 10px;
  padding: 20px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: linear-gradient(180deg, #fff, #fbfbfd);
  text-align: left;
  box-shadow: var(--shadow-sm);
  transition: transform 260ms var(--spring), border-color 260ms var(--ease), box-shadow 260ms var(--ease);
}

.category-spotlight button:hover {
  transform: translateY(-4px);
  border-color: rgba(13, 159, 110, 0.24);
  box-shadow: var(--shadow-md);
}

.category-spotlight .cat-glyph {
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 13px;
  background: rgba(13, 159, 110, 0.09);
}

.category-spotlight .cat-glyph .glyph {
  width: 23px;
  height: 23px;
  fill: none;
  stroke: var(--green-deep);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 320ms var(--spring);
}

.category-spotlight button:hover .cat-glyph .glyph { transform: scale(1.1); }

.category-spotlight b {
  color: var(--ink);
  font-size: 16px;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

.category-spotlight small {
  color: var(--muted);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.45;
}

.category-spotlight em {
  color: var(--green-deep);
  font-size: 11px;
  font-style: normal;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* ---------- Solutions ---------- */

.solution-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
}

.solution-card {
  min-height: 224px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 24px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--panel);
  cursor: pointer;
  transition: transform 340ms var(--spring), box-shadow 340ms var(--ease), border-color 340ms var(--ease);
}

.solution-card:hover {
  transform: translateY(-5px);
  box-shadow: var(--shadow-md);
  border-color: transparent;
}

.solution-card.is-active {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(13, 159, 110, 0.14), var(--shadow-md);
}

/* .solution-index is gone with the "01".."08" chips it drew. The owner's ruling
   on that pattern was explicit: a reader must not be shown the desk's internal
   numbering. The starter cards now lead with their own name. */

.solution-card h3 {
  display: flex;
  align-items: center;
  margin: 0 0 8px;
  font-size: 19px;
  font-weight: 650;
}

.solution-card p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.5; }

.solution-card ul {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 14px 0 18px;
  padding: 0;
  list-style: none;
}

.solution-card li {
  padding: 4px 11px;
  border-radius: 999px;
  background: var(--canvas);
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 550;
}

.solution-card .button { width: 100%; }

/* ---------- Catalogue ---------- */

.split {
  display: grid;
  grid-template-columns: 296px minmax(0, 1fr);
  gap: 28px;
  align-items: start;
  background: none;
  border: 0;
}

.filters {
  position: sticky;
  top: 106px;
  display: grid;
  gap: 18px;
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
}

.filter-header, .results-toolbar, .drawer-header, .product-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.filter-header h2, .results-toolbar h2, .drawer-header h2 { margin: 0; }
.filter-header h2, .drawer-header h2 { font-size: 21px; }

.filter-header button { min-height: 32px; padding: 0 14px; font-size: 13px; }

/* Desktop shows the filter panel open; the toggle only exists on narrow screens. */
.filter-body { display: grid; gap: 18px; }
/* Needs to out-specify `.filter-header button`, which sets display:inline-flex. */
.filter-header .filter-toggle { display: none; margin-left: auto; }

.filter-toggle::after {
  content: "";
  width: 7px; height: 7px;
  margin-left: 8px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translateY(-1px);
  transition: transform 200ms var(--ease);
}

.filters.is-open .filter-toggle::after { transform: rotate(225deg) translateY(-2px); }

/* Once a family filter is meaningless (no category or brand chosen) the select
   is disabled rather than offering 700 unscoped options. */
.filters select:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.filter-guide {
  display: grid;
  gap: 8px;
  padding: 12px;
  border: 1px solid rgba(13, 159, 110, 0.14);
  border-radius: 14px;
  background: rgba(13, 159, 110, 0.055);
}

/* The guide is filled in once the catalogue loads. Until then it rendered as a
   hollow green sliver, which looked like a broken box rather than a pending one. */
.filter-guide:empty { display: none; }

/* Stacked, not wrapped: three pills never fit one row of a 226px panel, so the
   third dropped to a second line and the group read as ragged rather than as
   an ordered sequence. */
.filter-steps {
  display: grid;
  gap: 5px;
}

.filter-steps span {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 26px;
  padding: 0 10px 0 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.72);
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
}

/* The 1-2-3 numerals in the filter trail went the same way, and for the same
   reason: they described the desk's own sequence rather than the reader's
   position in it. The trail still shows what has been chosen, which is the
   part that carried information. */

.filter-steps span.is-done {
  background: #fff;
  color: var(--green-deep);
}


.filter-guide p {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.4;
}

.filter-guide strong { color: var(--ink); }

.filters label { display: grid; gap: 8px; }

.filters label > span, .seg legend {
  color: var(--faint);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.filters input, .filters select, .sort-wrap select, .rfq-drawer textarea {
  width: 100%;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: #fff;
  color: var(--ink);
  outline: none;
  transition: border-color 180ms var(--ease), box-shadow 180ms var(--ease);
}

.filters input:focus, .filters select:focus, .sort-wrap select:focus, .rfq-drawer textarea:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3.5px rgba(13, 159, 110, 0.16);
}

.filters input, .filters select { height: 42px; padding: 0 13px; }

.filters select, .sort-wrap select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236e6e73' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 15px;
  padding-right: 36px;
}

.seg { margin: 0; padding: 0; border: 0; display: grid; gap: 8px; }
.seg legend { padding: 0; margin: 0; }

.seg-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 3px;
  padding: 3px;
  border-radius: 11px;
  background: rgba(0, 0, 0, 0.055);
}

.seg-row button {
  min-height: 34px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--muted);
  font-size: 13px;
  font-weight: 590;
  padding: 0 8px;
  transition: all 200ms var(--ease);
}

.seg-row button.is-on {
  background: #fff;
  color: var(--ink);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}

.quality-note {
  padding-top: 16px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 13px;
  line-height: 1.55;
}

.internal-only { display: none !important; }

/* ---------- BOQ workbench ---------- */

/* ---------- View transitions ----------
   The browser's own cross-fade between two states of the grid. Left near its
   default shape on purpose: this should read as the result set settling, not as
   an effect, so it is short and carries no movement of its own. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 260ms;
  animation-timing-function: var(--ease-out, ease-out);
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none; }
}

/* ---------- Scroll-linked motion ----------
   Framer's "transform on scroll" and Motion's scroll timelines, done natively:
   `animation-timeline: view()` ties an animation's progress to where the
   element is in the viewport, with no scroll listener and no library. Browsers
   without it simply skip the block — the content is already in its finished
   state, so nothing depends on the effect running.

   Only applied where it earns its place: the desk columns and the category
   tiles, which are large surfaces a reader scrolls past deliberately. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .category-spotlight button,
    .desk-grid > * {
      animation: scroll-lift linear both;
      animation-timeline: view();
      /* Runs across the element's entry only, and finishes well before the
         middle of the viewport — an effect still resolving under the reader's
         eye is a distraction, not a flourish. */
      animation-range: entry 0% entry 62%;
    }
  }

  @keyframes scroll-lift {
    from { opacity: 0; transform: translateY(28px) scale(0.985); }
    to { opacity: 1; transform: none; }
  }
}

/* ---------- Compare ---------- */

.compare-action {
  flex: 0 0 auto;
  width: 36px;
  display: grid;
  place-items: center;
  /* An 8% black border is invisible at this size, and this button wraps onto a
     line of its own on a narrow card. The result read as a stray quotation
     mark under every product rather than as a control: a 16px grey icon
     floating in white with no edge around it. The stronger line is what makes
     it a button. */
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  background: none;
  cursor: pointer;
  transition: background 180ms var(--ease), border-color 180ms var(--ease), transform 180ms var(--spring);
}

.compare-action svg {
  width: 16px; height: 16px;
  fill: none; stroke: var(--faint); stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}

.compare-action:hover { border-color: rgba(13, 159, 110, 0.4); background: rgba(13, 159, 110, 0.07); }
.compare-action:hover svg { stroke: var(--green-deep); }
.compare-action:active { transform: scale(0.94); }
.compare-action.on { border-color: var(--green); background: rgba(13, 159, 110, 0.14); }
.compare-action.on svg { stroke: var(--green-deep); }

.compare-tray {
  position: fixed;
  left: 50%;
  bottom: 20px;
  z-index: var(--z-tray);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100vw - 24px);
  padding: 10px 12px 10px 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  box-shadow: var(--shadow-lg);
  animation: tray-in var(--spring-bouncy-duration, 620ms) var(--spring-bouncy, var(--spring));
}

@keyframes tray-in {
  from { opacity: 0; transform: translate(-50%, 22px) scale(0.96); }
}

.compare-title {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}

.compare-chips { display: flex; gap: 6px; overflow-x: auto; }

.compare-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px 5px 11px;
  border-radius: 999px;
  background: var(--plate);
  font-family: var(--mono);
  font-size: 12px;
  white-space: nowrap;
}

.compare-chip button {
  width: 18px; height: 18px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.08);
  color: var(--muted);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.compare-chip button:hover { background: rgba(0, 0, 0, 0.16); color: var(--ink); }
.compare-tray .button { min-height: 34px; padding: 0 16px; font-size: 13px; }
.compare-tray .button:disabled { opacity: 0.5; cursor: default; }

.compare-dismiss {
  border: 0;
  background: none;
  color: var(--faint);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.compare-dismiss:hover { color: var(--ink); }

.compare-view { padding: clamp(22px, 3vw, 40px); }
.compare-view h2 { margin: 0 0 18px; }
.compare-scroll { overflow-x: auto; }

.compare-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.compare-table th, .compare-table td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}
.compare-table thead th { border-bottom: 2px solid var(--line-strong); }
.compare-table thead th strong { display: block; margin-top: 8px; font-size: 14px; }
.compare-table thead th code { font-size: 11px; color: var(--faint); }
.compare-table tbody th { width: 150px; color: var(--muted); font-weight: 600; }
.compare-media { display: block; height: 96px; }
.compare-media img { width: 100%; height: 100%; object-fit: contain; }
.compare-media .no-photo-plate { height: 96px; padding: 8px; }
.compare-media .no-photo-plate .plate-family,
.compare-media .no-photo-plate .plate-group { display: none; }

/* Rows where the products actually differ are the reason to be here. */
.compare-table tr.differs td { background: rgba(13, 159, 110, 0.055); font-weight: 560; }
.compare-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }

/* ---------- Shortcuts sheet ---------- */

.shortcuts {
  width: min(460px, calc(100vw - 32px));
  padding: 26px 28px;
  border: 0;
  border-radius: var(--r-lg);
  background: var(--panel);
  box-shadow: var(--shadow-lg);
}

.shortcuts::backdrop { background: rgba(12, 18, 24, 0.4); backdrop-filter: blur(6px); }
.shortcuts h2 { margin: 0 0 16px; font-size: 20px; }
.shortcuts dl { margin: 0 0 20px; display: grid; gap: 10px; }
.shortcuts dl > div { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.shortcuts dt { display: flex; gap: 4px; }
.shortcuts dd { margin: 0; color: var(--ink-2); font-size: 14px; }

.shortcuts kbd,
.shortcuts-key {
  min-width: 24px;
  padding: 3px 7px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--plate);
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
}

/* ---------- Copyable part code ---------- */

.detail-sku {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  margin: 0 0 10px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: var(--plate);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 13px;
  cursor: copy;
  transition: border-color 180ms var(--ease), color 180ms var(--ease);
}

.detail-sku span { opacity: 0; font-size: 12px; transition: opacity 180ms var(--ease); }
.detail-sku:hover { border-color: var(--line-strong); color: var(--ink); }
.detail-sku:hover span { opacity: 0.7; }

/* ---------- Back to top ---------- */

.to-top {
  position: fixed;
  right: 20px;
  bottom: 96px;
  z-index: var(--z-suggest);
  width: 42px; height: 42px;
  display: grid;
  place-items: center;
  /* An 8% black border is invisible at this size, and this button wraps onto a
     line of its own on a narrow card. The result read as a stray quotation
     mark under every product rather than as a control: a 16px grey icon
     floating in white with no edge around it. */
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--panel);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  animation: fade-in 240ms var(--ease-out, var(--ease));
  transition: transform 220ms var(--spring), box-shadow 220ms var(--ease);
}

.to-top svg { width: 18px; height: 18px; fill: none; stroke: var(--ink-2); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.to-top:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.to-top:active { transform: scale(0.94); }

@media (max-width: 620px) {
  .to-top { bottom: 84px; right: 14px; }
  .compare-tray { bottom: 12px; padding-left: 14px; gap: 8px; }
  .compare-title { display: none; }
}

/* ---------- Command palette ---------- */

.palette {
  position: fixed;
  inset: 0;
  z-index: var(--z-palette);
  display: grid;
  place-items: start center;
  padding: clamp(60px, 12vh, 140px) 16px 16px;
  background: rgba(12, 18, 24, 0.88);
  animation: fade-in 180ms var(--ease-out, var(--ease));
}

@keyframes fade-in { from { opacity: 0; } }

.palette-card {
  width: min(680px, 100%);
  max-height: min(70vh, 620px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--r-lg);
  background: var(--panel);
  box-shadow: var(--shadow-lg);
  animation: palette-in var(--spring-bouncy-duration, 620ms) var(--spring-bouncy, var(--spring));
}

@keyframes palette-in {
  from { opacity: 0; transform: translateY(-16px) scale(0.97); }
}

.palette-field {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--line);
}

.palette-field svg {
  width: 20px; height: 20px;
  fill: none; stroke: var(--faint); stroke-width: 1.9;
  stroke-linecap: round;
}

.palette-field input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  font-size: 17px;
  color: var(--ink);
  outline: none;
}

.palette-field input::placeholder { color: var(--faint); }

.palette-field button {
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: none;
  color: var(--faint);
  font-size: 11px;
  font-weight: 650;
}

.palette-results { overflow-y: auto; padding: 8px; }

.palette-group {
  margin: 12px 10px 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--faint);
}

.palette-row {
  width: 100%;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 11px 12px;
  border: 0;
  border-radius: 10px;
  background: none;
  text-align: left;
  cursor: pointer;
}

/* Highlight follows the keyboard, so it must not also follow the mouse — two
   competing highlights read as a bug. */
.palette-row.is-active { background: rgba(13, 159, 110, 0.11); }
.palette-label { font-size: 15px; font-weight: 560; color: var(--ink); }
.palette-hint { flex: 0 0 auto; font-size: 12px; color: var(--faint); font-family: var(--mono); }
.palette-empty { padding: 26px 16px; margin: 0; color: var(--muted); font-size: 14px; text-align: center; }

.palette-foot {
  margin: 0;
  padding: 10px 16px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  color: var(--faint);
}

.palette-foot kbd {
  display: inline-block;
  min-width: 18px;
  padding: 1px 5px;
  margin-right: 3px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--plate);
  font-family: inherit;
  font-size: 11px;
  text-align: center;
}

body.palette-open { overflow: hidden; }

/* ---------- Signals + Studio ----------
   Two columns of the desk's own content. Signals is a reading column, so it is
   set narrow and quiet; Studio is a rail of artwork that moves on its own. */

.section.desk-columns {
  background: var(--mesh-grad);
  border-radius: clamp(24px, 3vw, 40px);
}

/* One column of content since the poster rail left. Capping the band at a
   readable measure left half the section empty, which reads as a missing
   thing rather than as space, so the notes themselves take the width: two
   columns of cards on a wide screen, each its own comfortable measure, one
   column as soon as that stops being true. */
.desk-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(24px, 3vw, 52px);
  align-items: start;
}

.column-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 12px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--line);
}

.column-head h3 { margin: 0; font-size: 20px; letter-spacing: -0.01em; }

.column-note {
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}

.signal-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 10px;
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 1080px) {
  .signal-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
  /* Each card carries its own height: a longer note beside a shorter one
     leaves a ragged edge rather than stretching one to match the other. */
  .signal-list > li { align-self: start; }
}

.signal {
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--panel);
  animation: card-in var(--spring-smooth-duration, 560ms) var(--spring-smooth, var(--ease)) backwards;
  animation-delay: var(--enter-delay, 0ms);
  transition: border-color 240ms var(--ease), box-shadow 240ms var(--ease), transform 240ms var(--spring);
}

.signal:hover {
  border-color: rgba(13, 159, 110, 0.34);
  box-shadow: var(--shadow-sm);
  transform: translateY(-2px);
}

.signal-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  font-size: 11px;
  color: var(--faint);
}

.signal-tag {
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(13, 159, 110, 0.10);
  color: var(--green-deep);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.signal h4 { margin: 0 0 6px; font-size: 17px; line-height: 1.3; letter-spacing: -0.01em; }
.signal p { margin: 0; color: var(--ink-2); font-size: 14px; line-height: 1.55; }

.signal-actions {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 12px;
  font-size: 13px;
}

.signal-actions button {
  padding: 6px 14px;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.055);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  transition: background 180ms var(--ease), transform 180ms var(--spring);
}

.signal-actions button:hover { background: rgba(13, 159, 110, 0.14); color: var(--green-deep); }
.signal-actions button:active { transform: scale(0.96); }
.signal-actions a { color: var(--faint); font-weight: 600; }
.signal-actions a:hover { color: var(--green-deep); }


@media (max-width: 900px) {
  .desk-grid { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  /* A rail that moves on its own is exactly what this preference is about. */
}

.boq-workbench { padding-top: 30px; }

.boq-panel {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  gap: 18px;
  align-items: stretch;
}

.boq-input,
.boq-output {
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
}

.boq-input {
  display: grid;
  gap: 14px;
  padding: 22px;
}

.boq-input label,
.boq-output-head h3 {
  margin: 0;
  color: var(--ink);
  font-size: 16px;
  font-weight: 700;
}

.boq-input textarea {
  min-height: 250px;
  width: 100%;
  resize: vertical;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-radius: 18px;
  background: #fff;
  color: var(--ink);
  line-height: 1.45;
}

.boq-input textarea:focus {
  border-color: rgba(13, 159, 110, 0.48);
  box-shadow: 0 0 0 4px rgba(13, 159, 110, 0.1);
  outline: 0;
}

.boq-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.file-button {
  min-height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 16px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.05);
  color: var(--ink);
  font-weight: 650;
  cursor: pointer;
}

.file-button input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.boq-output {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  overflow: hidden;
}

.boq-output-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--line);
}

.boq-matches {
  display: grid;
  gap: 0;
  max-height: 430px;
  overflow: auto;
}

.boq-matches > p {
  margin: 0;
  padding: 22px;
  color: var(--muted);
}

.boq-match {
  display: grid;
  grid-template-columns: 54px minmax(0, 1fr) auto;
  gap: 12px;
  align-items: center;
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}

.boq-match:hover { background: rgba(0, 0, 0, 0.025); }
.boq-match h4 { margin: 0 0 3px; font-size: 14px; font-family: var(--mono); }
.boq-match p { margin: 0; color: var(--ink-2); font-size: 13px; line-height: 1.35; }
.boq-match small { display: block; margin-top: 4px; color: var(--faint); font-size: 11px; overflow-wrap: anywhere; }
.boq-match > span { color: var(--green-deep); font-weight: 800; }

.boq-unmatched {
  padding: 14px 20px 18px;
  background: #fbfbfd;
  border-top: 1px solid var(--line);
  color: var(--muted);
}

.boq-unmatched strong {
  display: block;
  margin-bottom: 8px;
  color: var(--ink);
  font-size: 13px;
}

.boq-unmatched p {
  margin: 4px 0;
  font-size: 12px;
  overflow-wrap: anywhere;
}

.catalogue-results { min-width: 0; }
.results-toolbar { margin-bottom: 16px; flex-wrap: wrap; }
.results-toolbar .eyebrow { margin-bottom: 6px; }

/* Section headings run to 52px, which is right for a statement and wrong for a
   result count sitting beside a sort control. */
.results-toolbar h2 {
  font-size: clamp(26px, 2.4vw, 34px);
  letter-spacing: -0.022em;
  font-variant-numeric: tabular-nums;
}

.result-note {
  margin: 5px 0 0;
  color: var(--muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.toolbar-actions { display: flex; gap: 10px; align-items: center; }

.sort-wrap { display: inline-flex; }
.sort-wrap select {
  height: 40px;
  min-width: 195px;
  border: 0;
  border-radius: 999px;
  background-color: rgba(0, 0, 0, 0.05);
  font-weight: 590;
  font-size: 14px;
  padding-left: 16px;
}

/* Filter chips */

.chip-row { display: flex; flex-wrap: wrap; gap: 8px; min-height: 0; margin-bottom: 16px; }
.chip-row:empty { display: none; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  padding: 0 6px 0 13px;
  border-radius: 999px;
  background: rgba(13, 159, 110, 0.1);
  color: var(--green-deep);
  font-size: 13px;
  font-weight: 600;
  animation: pop-in 220ms var(--spring);
}

.chip b { color: rgba(8, 122, 84, 0.6); font-weight: 600; }

.chip button {
  width: 21px; height: 21px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(8, 122, 84, 0.14);
  color: var(--green-deep);
  font-size: 13px;
  line-height: 1;
  transition: background 140ms var(--ease);
}

.chip button:hover { background: rgba(8, 122, 84, 0.28); }

/* ---------- Product detail ---------- */

.detail-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.detail-copy h2 {
  margin: 14px 0 6px;
  font-size: clamp(22px, 2.4vw, 29px);
  line-height: 1.18;
  letter-spacing: -0.02em;
}

.detail-sku {
  margin: 0;
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 13px;
}

.detail-tags { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }

.detail-tags .avail {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 10px;
  border: 1px solid rgba(10, 163, 109, 0.2);
  border-radius: 999px;
  background: rgba(10, 163, 109, 0.08);
  color: var(--green-deep);
  font-size: 11px;
  font-weight: 700;
}

.detail-tags .avail.tone-neutral {
  border-color: var(--line-strong);
  background: rgba(0, 0, 0, 0.035);
  color: var(--muted);
}

.detail-cta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 20px 0 8px;
}

.detail-phone { margin: 0 0 4px; color: var(--muted); font-size: 13px; }
.detail-phone a { color: var(--green-deep); font-weight: 650; }

.detail-section {
  margin-top: 22px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.detail-section h3 {
  margin: 0 0 8px;
  color: var(--muted);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.detail-section p {
  margin: 0;
  color: var(--ink);
  font-size: 15px;
  line-height: 1.65;
}

/* Provenance for quoted manufacturer copy: the buyer can see where the text came
   from and open the page it was taken from. */
.detail-section p.detail-source {
  margin-top: 10px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}

.detail-source a {
  color: var(--green-deep);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: rgba(13, 159, 110, 0.3);
  text-underline-offset: 2px;
}

.detail-source a:hover { text-decoration-color: var(--green); }

.resource-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.resource-links a {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
  box-shadow: var(--shadow-xs);
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
}

.resource-links a:hover {
  border-color: rgba(0, 166, 118, 0.4);
  color: var(--green-deep);
}

.resource-note {
  margin: 18px 0 0;
  padding: 12px 14px;
  border: 1px solid rgba(13, 159, 110, 0.16);
  border-radius: 16px;
  background: rgba(13, 159, 110, 0.055);
  color: var(--muted);
  font-size: 12.8px;
  line-height: 1.55;
}

/* auto-fit, so a product with a single spec gets a full-width row instead of a
   half-width card next to an empty column. */
.spec-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 8px;
  margin: 0;
}

.spec-list div {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: rgba(247, 249, 251, 0.72);
}

.spec-list dt {
  margin-bottom: 4px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.spec-list dd {
  margin: 0;
  color: var(--ink);
  font-size: 14px;
  font-weight: 650;
  overflow-wrap: anywhere;
}

.detail-utils { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }

.detail-utils button {
  min-height: 34px;
  padding: 0 13px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: #fff;
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 620;
}

.detail-utils button:hover { border-color: var(--green); color: var(--green-deep); }

/* ---------- Project-starter recommendation ----------
   Shown for review above the grid. It never mutates the RFQ on its own. */

.recommendation {
  display: grid;
  gap: 12px;
  margin-bottom: 18px;
  padding: 18px;
  border: 1px solid rgba(13, 159, 110, 0.2);
  border-radius: var(--r-lg);
  background: rgba(13, 159, 110, 0.045);
  animation: pop-in 260ms var(--spring);
}

.recommendation[hidden] { display: none; }

.rec-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}

.rec-head h3 { margin: 0; font-size: 19px; }
.rec-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.rec-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.rec-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.rec-list button {
  flex: 1;
  min-width: 0;
  display: grid;
  gap: 2px;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: #fff;
  text-align: left;
  transition: border-color 180ms var(--ease), box-shadow 180ms var(--ease);
}

.rec-list button:hover { border-color: var(--green); box-shadow: var(--shadow-sm); }
.rec-list b { font-family: var(--mono); font-size: 12px; }
.rec-list small { color: var(--muted); font-size: 11px; }

.rec-in {
  flex: 0 0 auto;
  color: var(--green-deep);
  font-size: 11px;
  font-weight: 750;
}

.rec-note { margin: 0; color: var(--muted); font-size: 13px; }

/* ---------- Product cards ---------- */

.product-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
}

.product-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--panel);
  transition: transform 380ms var(--spring), box-shadow 380ms var(--ease), border-color 380ms var(--ease);
  /* Entrance runs on the no-overshoot spring: a dense grid of cards that each
     overshoot reads as wobble, not polish. `backwards` holds the card hidden
     through its stagger delay instead of flashing it first. */
  animation: card-in var(--spring-smooth-duration, 560ms) var(--spring-smooth, var(--ease)) backwards;
  animation-delay: var(--enter-delay, 0ms);
}

/* Cards that were already on screen before this render — see renderProducts. */
.product-card.is-settled { animation: none; }

/* A card without a photo keeps the same shape as one with a photo: the
   category plate fills the frame, so the grid stays flush either way. */

@keyframes card-in {
  from { opacity: 0; transform: translateY(14px) scale(0.985); }
}

.product-card:hover {
  transform: translateY(-6px);
  border-color: transparent;
  box-shadow: var(--shadow-lg);
}

.image-frame {
  position: relative;
  width: 100%;
  height: 236px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 14px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: var(--plate);
  color: inherit;
  text-decoration: none;
}

.image-frame img {
  width: 100%; height: 100%;
  object-fit: contain;
  align-self: center;
  display: block;
  transform: scale(1);
  transform-origin: center;
  transition: transform 700ms var(--spring);
}

.product-card:hover .image-frame img { transform: scale(1.035); }

/* Photos settle in as they decode. A lazily-loaded grid otherwise snaps each
   image to full opacity the instant it arrives, which reads as flicker while
   scrolling past. Deliberately an animation on the `loaded` class rather than a
   default-hidden image: if that class never arrives, the photo is still
   visible. renderProducts sets it — synchronously for images already cached. */
.image-frame img.loaded { animation: photo-in 420ms var(--ease-out, var(--ease)) backwards; }

@keyframes photo-in {
  from { opacity: 0; transform: scale(1.02); }
}

/* Same height as a photo frame — a shorter pending frame made the grid ragged. */
.image-frame.pending {
  height: 236px;
  background: var(--plate);
}

/* No-photo state. The old dashed box with a cross read as "image failed to
   load"; this is a deliberate category plate instead — quiet, aligned, and
   honest that the photo is available on request. */
.no-photo-plate {
  position: relative;
  width: 100%; height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 20px;
  overflow: hidden;
  border-radius: var(--r-md);
  background:
    linear-gradient(158deg, #fdfdfe 0%, #f4f6f9 52%, #eef1f5 100%);
  color: var(--muted);
  text-align: center;
}

/* A soft off-centre highlight, so the plate reads as a designed surface rather
   than a flat grey box waiting for content. */
.no-photo-plate .plate-glow {
  position: absolute;
  inset: -30% -20% auto -20%;
  height: 150%;
  background: radial-gradient(50% 44% at 32% 22%, rgba(13, 159, 110, 0.09), transparent 72%);
  pointer-events: none;
}

.no-photo-plate .glyph {
  position: relative;
  width: 54px;
  height: 54px;
  fill: none;
  stroke: #98a4b0;
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 300ms var(--ease), transform 500ms var(--spring);
}

.product-card:hover .no-photo-plate .glyph {
  stroke: var(--green);
  transform: translateY(-2px) scale(1.04);
}

.no-photo-plate .plate-family {
  position: relative;
  max-width: 100%;
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.no-photo-plate .plate-group {
  position: relative;
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.62;
}

/* ---------- Licence plate ----------
   Licences, subscriptions, support and services have no physical form. The
   plate draws an entitlement card carrying the issuing manufacturer's mark:
   card-shaped so it can never read as a product photograph, branded so the
   buyer still knows whose entitlement it is. */

.no-photo-plate.is-licence {
  background: linear-gradient(158deg, #fbfcfe 0%, #eef2f7 55%, #e7ecf3 100%);
  gap: 12px;
}

.no-photo-plate.is-licence .plate-glow {
  background: radial-gradient(50% 44% at 30% 18%, rgba(24, 96, 180, 0.10), transparent 74%);
}

/* A sourcing route is not a unit either, so it gets type rather than a picture:
   the brand set between two rules, in the same restrained register as the
   entitlement card. The two together teach a reader that a structured plate
   means "this row is not one object", which is exactly what is true. Warm grey
   rather than the licence card's cool blue, so the two are told apart at a
   glance without either shouting. */
.no-photo-plate.is-route {
  background: linear-gradient(158deg, #fcfcfb 0%, #f2f1ee 55%, #ebe9e5 100%);
  gap: 12px;
}

.no-photo-plate.is-route .plate-glow {
  background: radial-gradient(50% 44% at 30% 18%, rgba(120, 96, 40, 0.09), transparent 74%);
}

.route-card {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  width: min(80%, 210px);
}

.route-rule {
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(28, 34, 46, 0.22), transparent);
}

.route-brand {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: #3d4453;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 150px;
}

.licence-card {
  position: relative;
  display: grid;
  place-items: center;
  width: min(74%, 190px);
  aspect-ratio: 1.586;               /* ID-1: the proportion every card shares */
  padding: 12px 16px;
  border-radius: 10px;
  background: linear-gradient(147deg, #ffffff 0%, #f4f7fb 46%, #e8eef6 100%);
  border: 1px solid rgba(19, 42, 68, 0.10);
  box-shadow:
    0 1px 1px rgba(16, 34, 56, 0.05),
    0 10px 22px -12px rgba(16, 34, 56, 0.35);
  overflow: hidden;
  transition: transform var(--spring-smooth-duration, 600ms) var(--spring-smooth, var(--spring)),
    box-shadow 320ms var(--ease);
}

/* A single diagonal highlight: enough to read as a laminated surface without
   turning into a novelty gradient. */
.licence-sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(118deg, transparent 30%, rgba(255, 255, 255, 0.85) 47%, transparent 62%);
  opacity: 0.7;
}

/* The contact pad every entitlement card in the world carries. Purely a shape
   cue — it names no scheme and imitates no issuer. */
.licence-chip {
  position: absolute;
  top: 14%;
  left: 11%;
  width: 17%;
  aspect-ratio: 1.32;
  border-radius: 3px;
  background: linear-gradient(155deg, #e8d9a8, #c9b16a 52%, #a98f47);
  box-shadow: inset 0 0 0 0.5px rgba(120, 96, 40, 0.5);
}

.licence-chip::after {
  content: "";
  position: absolute;
  inset: 26% 18%;
  border: 0.5px solid rgba(120, 96, 40, 0.55);
  border-left: 0;
  border-right: 0;
}

.licence-mark-slot {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  padding: 0 6%;
  margin-top: 6%;
}

/* The brand's own asset, sized to sit on the card rather than fill it. */
.licence-mark-slot .brand-logo,
.licence-mark-slot .subbrand-logo {
  max-width: 100%;
  max-height: 34px;
  width: auto;
  height: auto;
  opacity: 0.92;
}

.licence-mark-slot .brand-logo.sq { max-height: 30px; }

/* Brands with no approved logo asset set their name instead — never an
   improvised mark. */
.licence-mark-slot .brand-line {
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--ink-2);
  text-align: center;
  line-height: 1.25;
}

.licence-rule {
  position: absolute;
  left: 11%;
  right: 11%;
  bottom: 16%;
  height: 4px;
  border-radius: 2px;
  background: repeating-linear-gradient(90deg,
    rgba(19, 42, 68, 0.16) 0 12px, transparent 12px 18px);
}

.product-card:hover .licence-card {
  transform: translateY(-3px) rotate(-0.6deg);
  box-shadow:
    0 2px 3px rgba(16, 34, 56, 0.06),
    0 18px 34px -14px rgba(16, 34, 56, 0.42);
}

/* The sheet shows the same card, larger. */
.detail-media .licence-card { width: min(66%, 300px); }
.detail-media .licence-mark-slot .brand-logo { max-height: 54px; }

/* Small glyph plate for RFQ rows, BOQ matches, and related products. */
.rfq-thumb.pending {
  display: grid;
  place-items: center;
  background: linear-gradient(180deg, #fcfcfd, #f4f6f8);
}

.rfq-thumb.pending .glyph {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.photo-badge {
  position: absolute;
  left: 12px; top: 12px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 0;
  border: 0;
  padding: 5px 11px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(10px);
  color: var(--ink-2);
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  box-shadow: var(--shadow-sm);
}

.photo-badge::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--green);
}

.photo-badge.pending::before { background: #f0a03c; }
.photo-badge.use_family_image { color: #5f6f7f; }
.photo-badge.use_family_image::before { background: #7a8794; }
.photo-badge.use_official_candidate_image { color: #1769aa; }
.photo-badge.use_official_candidate_image::before { background: #2483d1; }

.brand-ribbon { display: none; }

.product-body {
  flex: 1;
  display: grid;
  gap: 7px;
  align-content: start;
  padding: 16px 18px 10px;
}

.brand-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 24px;
}

.line-label {
  color: var(--muted);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.product-card h3 {
  margin: 2px 0 0;
  font-size: 16px;
  font-weight: 640;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

/* Two lines maximum: a single long part name otherwise stretches every card in
   its row, which is what made the grid look uneven. */
.product-card h3 a {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--ink);
}
.product-card h3 a:hover { color: var(--green-deep); }

.product-card .sku {
  align-self: start;
  padding: 2px 8px;
  border-radius: 7px;
  background: rgba(0, 0, 0, 0.045);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 11px;
  overflow-wrap: anywhere;
}

.product-family, .product-meta {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.45;
}

.card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 4px;
}

.card-tags .avail {
  display: inline-flex;
  align-items: center;
  min-height: 21px;
  padding: 0 8px;
  border: 1px solid rgba(10, 163, 109, 0.18);
  border-radius: 999px;
  background: rgba(10, 163, 109, 0.075);
  color: var(--green-deep);
  font-size: 10px;
  font-weight: 700;
}

.card-tags .avail.tone-neutral {
  border-color: var(--line-strong);
  background: rgba(0, 0, 0, 0.03);
  color: var(--muted);
}

/* One primary action, two quiet ones — the previous three equal-weight buttons
   gave the card no focal point. */
.card-actions {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 8px;
  padding: 0 18px 16px;
}

.card-actions button,
.card-actions a {
  min-height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 620;
  white-space: nowrap;
  transition: background 180ms var(--ease), color 180ms var(--ease), border-color 180ms var(--ease), transform 160ms var(--spring);
}

.card-actions .primary-action {
  background: var(--ink);
  color: #fff;
}

.card-actions .primary-action:hover { background: var(--green-deep); }
.card-actions .primary-action:active { transform: scale(0.97); }
.card-actions .primary-action.added { background: rgba(10, 163, 109, 0.12); color: var(--green-deep); }

.card-actions .view-action,
.card-actions .wa-action {
  border-color: var(--line-strong);
  background: #fff;
  color: var(--ink-2);
}

.card-actions .view-action:hover,
.card-actions .wa-action:hover { border-color: var(--green); color: var(--green-deep); }


/* Brand marks sit inside a fixed window so a wide wordmark and a square glyph
   occupy the same visual weight on every card. */
.brand-logo {
  height: 22px;
  width: auto;
  max-width: 132px;
  object-fit: contain;
  object-position: left center;
}

.brand-logo.sq { height: 26px; max-width: 34px; }

/* A two-line lockup carries a tagline under the wordmark, so at the shared 22px
   the wordmark itself lands about 14px and reads as a smudge next to Logitech
   or Fortinet set on one line. Taller box, same optical weight. */
.brand-logo.stacked { height: 32px; max-width: 116px; }

/* CommScope's two lines are the only marks in this catalogue that are not
   SVG, and until now they were also the only ones with their own padding baked
   into the file: the NETCONNECT lockup was 600x315 with the actual wordmark
   occupying 40% of it, so inside an 88x46 box the mark rendered about 55px
   across. The owner read that as a missing logo, on two of the largest brands
   on the site. The files are trimmed to their ink now and the box is sized for
   what a two-line lockup needs rather than for a single-line wordmark. */
.subbrand-logo {
  height: 34px;
  width: auto;
  max-width: 168px;
  object-fit: contain;
  object-position: left center;
}

.subbrand-logo.systimax { height: 36px; }

.brand-line {
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* Skeletons + empty state */

.skeleton {
  height: 430px;
  border: 0;
  border-radius: var(--r-lg);
  background: linear-gradient(100deg, #ececf0 32%, #f6f6f8 50%, #ececf0 68%);
  background-size: 220% 100%;
  animation: shimmer 1.3s linear infinite;
}

@keyframes shimmer { to { background-position: -120% 0; } }

.empty-state {
  padding: 70px 24px;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--panel);
  text-align: center;
}

.empty-state h3 { margin: 0 0 8px; font-size: 22px; }
.empty-state p { color: var(--muted); margin-bottom: 22px; }
.empty-state .button {
  display: inline-flex;
  width: auto;
  margin: 4px;
}

.grid-footer { display: grid; place-items: center; gap: 8px; padding-top: 30px; }
#scrollSentinel { display: block; width: 100%; height: 1px; }

/* ---------- Brand cloud ---------- */

.industry-row { display: flex; flex-wrap: wrap; gap: 10px; }

/* ---------- The brand wall ----------
   A printed brand wall, not a row of pills. Every mark sits on a white plate of
   one fixed height with the artwork scaled to a share of it, so a tall stacked
   lockup and a wide wordmark carry the same optical weight and nothing is
   recoloured, retraced or improvised to get there.
   The plate is what makes this work on the dark band: half these marks are
   near-black wordmarks that would disappear on ink, and giving them a plate is
   the only treatment that keeps them legible without touching the artwork.
   The count is the reason it is a wall rather than decoration: every plate is a
   filter, and the number says how much material sits behind it. */
.brand-cloud { display: block; }

/* The two shelves of the wall, in the owner's own words. A tier title is a
   quiet mono label, not a heading fighting the section's one h2. */
.brand-tier-title {
  margin: 26px 0 14px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--brand-lime);
}

.brand-tier-title:first-child { margin-top: 0; }

.brand-tier-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
}

/* The whole tile is one clean white plate. Half the marks arrive with their
   own white background baked into the file and half arrive transparent, and
   on a translucent tile the two render as two different designs. On solid
   white the difference disappears, which is the entire fix. */
.brand-cloud button {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  gap: 10px;
  place-items: center;
  min-height: 96px;
  padding: 16px 14px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--r-sm);
  background: #ffffff;
  transition:
    transform 260ms var(--spring),
    border-color 220ms var(--ease),
    background-color 220ms var(--ease);
}

.brand-cloud button:hover {
  transform: translateY(-3px);
  border-color: rgba(184, 231, 106, 0.7);
  background: #ffffff;
  box-shadow: 0 14px 34px -18px rgba(184, 231, 106, 0.5);
}

.brand-plate {
  display: grid;
  place-items: center;
  width: 100%;
  height: 62px;
  padding-inline: 14px;
  /* The tile itself is the white plate now; this inner box only centres. */
  background: none;
  box-shadow: none;
}

/* Scaled to a share of the plate rather than set at a fixed height, which is
   what stops a wide wordmark filling the plate edge to edge while a compact
   glyph floats in the middle of one. */
.brand-plate .brand-logo { height: 26px; max-width: 100%; width: auto; object-fit: contain; }
.brand-plate .brand-logo.sq { height: 28px; max-width: 34px; }
.brand-plate .brand-logo.stacked { height: 34px; max-width: 100%; }

/* Brands with no approved mark keep their name in type on the same plate, so a
   missing asset reads as a considered choice rather than a hole in the wall. */
/* A name set in type, at the same optical presence as the artwork beside it.
   Toshiba and Palo Alto Networks have no approved artwork in this repository
   and the egress policy blocks fetching any, so their plates set the name.
   At 13px that read as an apology; at this size, with this weight, it reads as
   a decision. The moment authentic files land in assets/brands they take over
   with no code change. */
/* Symbol plus company name, for the marks that are a bare glyph. The glyph
   is the manufacturer's, the name is a typeset label beside it. */
.brand-lockup {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

.brand-lockup-name {
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.015em;
  color: #1d1d1f;
  white-space: nowrap;
}

/* Matched to the owner's reference lockups. TP-Link sets its name lowercase
   in near-black beside the teal symbol; Ubiquiti stacks the U over UBIQUITI
   in the brand blue with the wide tracking its wordmark carries. */
.brand-lockup { max-width: 100%; }

.brand-lockup.is-tplink .brand-lockup-name {
  font-size: 17px;
  font-weight: 800;
  letter-spacing: -0.02em;
}

/* Written at the depth of the generated size sheet, which would otherwise
   set the glyph to its solo 52px and push the name off the tile's edge:
   the owner's screenshot showed tp-link sliced mid-word. In a lockup the
   glyph shares the plate with the name, so it takes the smaller size. */
.brand-plate .brand-lockup .brand-logo[data-mark="tp-link.svg"] {
  width: auto;
  height: 30px;
}

.brand-plate .brand-lockup .brand-logo[data-mark="ubiquiti.svg"] {
  width: auto;
  height: 26px;
}

.brand-lockup.is-ubiquiti {
  flex-direction: column;
  gap: 3px;
}

.brand-lockup.is-ubiquiti .brand-logo { height: 26px; }

.brand-lockup.is-ubiquiti .brand-lockup-name {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: #0559c9;
}

.brand-plate .brand-line {
  font-size: 17px;
  font-weight: 750;
  letter-spacing: -0.015em;
  color: #1d1d1f;
  text-align: center;
}

.brand-cloud b {
  font-size: 11px;
  font-weight: 550;
  color: rgba(233, 240, 250, 0.62);
  font-variant-numeric: tabular-nums;
}

.industry-row span {
  min-height: 46px;
  display: inline-flex;
  align-items: center;
  padding: 0 22px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  font-weight: 550;
  color: var(--ink-2);
}

/* ---------- The bulletin board, and the desk's list ----------
   A dark band, because this is the desk speaking rather than the catalogue
   listing: identity and voice belong on the field, part codes belong on paper.
   Everything inside is injected by bulletin.js, so every rule here has to hold
   for zero notes as well as twelve — the board is a grid that collapses to
   nothing rather than a layout that needs filling. */

.bulletin-band {
  margin: 0 var(--pad-x) clamp(56px, 6vw, 88px);
  padding: clamp(38px, 5vw, 68px) clamp(26px, 4vw, 58px);
  border-radius: clamp(24px, 3vw, 40px);
}

.bulletin-band .section-heading { max-width: 60ch; margin-bottom: 26px; }
.bulletin-band .section-heading h2 { letter-spacing: -0.024em; }

/* The subject filters. A tab row rather than a select, because there are at
   most five and a reader should be able to see the whole shape of what the
   desk writes about without opening anything. */
.bulletin-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 24px;
}

.bulletin-filters button {
  font: inherit;
  font-size: 14px;
  font-weight: 520;
  padding: 8px 15px;
  border-radius: 999px;
  border: 1px solid rgba(184, 231, 106, 0.22);
  background: rgba(255, 255, 255, 0.05);
  color: rgba(233, 240, 250, 0.82);
  cursor: pointer;
  transition: background 180ms var(--ease), color 180ms var(--ease), border-color 180ms var(--ease);
}

.bulletin-filters button:hover {
  background: rgba(184, 231, 106, 0.12);
  color: #fff;
}

.bulletin-filters button[aria-selected="true"] {
  background: var(--brand-lime);
  border-color: var(--brand-lime);
  color: #0a1408;
  font-weight: 600;
}

/* `min(300px, 100%)` rather than a bare 300px. On a 390px phone the band's own
   margin and padding leave under 300px of content width, and a grid track with
   a hard minimum wider than its container does not shrink: it pushes the page
   sideways. Measured at 140px of horizontal overflow before this. */
.bulletin-board {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: 16px;
}

.bulletin-note {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 22px 22px 20px;
  border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, 0.09);
  background: rgba(255, 255, 255, 0.045);
  /* The lime edge marks the desk's own writing, the way the contact band's
     sweep marks the desk's own line. It is the only lime that is load-bearing
     here; everything else on the card is white at a set alpha. */
  border-top: 2px solid rgba(184, 231, 106, 0.55);
  transition: background 200ms var(--ease), border-color 200ms var(--ease);
}

.bulletin-note:hover { background: rgba(255, 255, 255, 0.07); }

.note-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.note-topic {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-lime);
}

.note-head time {
  font-family: var(--mono);
  font-size: 11px;
  color: rgba(233, 240, 250, 0.55);
  font-variant-numeric: tabular-nums;
}

.bulletin-note h3 {
  margin: 0;
  font-size: 18px;
  line-height: 1.28;
  letter-spacing: -0.012em;
  color: #fff;
  text-wrap: balance;
}

.bulletin-band .note-summary {
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
  color: rgba(233, 240, 250, 0.78);
}

.bulletin-note details { margin-top: auto; padding-top: 4px; }

.bulletin-note summary {
  font-size: 14px;
  font-weight: 560;
  color: var(--brand-lime);
  cursor: pointer;
  list-style: none;
  width: fit-content;
  padding: 4px 0;
}

.bulletin-note summary::-webkit-details-marker { display: none; }
.bulletin-note summary:hover { color: #fff; }

/* The control has to say what it will do next, not what it did. Two labels
   rather than one, because "Read the note" still sitting above an open note is
   a button that has stopped describing itself. */
.bulletin-note summary .s-open { display: none; }
.bulletin-note details[open] summary .s-shut { display: none; }
.bulletin-note details[open] summary .s-open { display: inline; }
.bulletin-note summary .s-shut::after { content: " \2193"; }
.bulletin-note summary .s-open::after { content: " \2191"; }

.bulletin-band .note-body p {
  margin: 0 0 10px;
  font-size: 15px;
  line-height: 1.62;
  color: rgba(233, 240, 250, 0.8);
}

.bulletin-band .note-source {
  margin: 6px 0 0;
  font-size: 13px;
  color: rgba(233, 240, 250, 0.5);
}

.bulletin-band .note-source a { color: rgba(184, 231, 106, 0.9); }

/* ---------- The mailing list ---------- */

.subscribe-card {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(20px, 3vw, 44px);
  align-items: start;
  margin-top: 28px;
  padding: clamp(24px, 3vw, 36px);
  border-radius: var(--r-lg);
  border: 1px solid rgba(184, 231, 106, 0.2);
  background:
    radial-gradient(120% 140% at 100% 0%, rgba(184, 231, 106, 0.09), transparent 62%),
    rgba(255, 255, 255, 0.04);
}

.subscribe-copy h3 {
  margin: 0 0 8px;
  font-size: clamp(20px, 2.2vw, 25px);
  letter-spacing: -0.018em;
  color: #fff;
}

.bulletin-band .subscribe-copy p {
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  color: rgba(233, 240, 250, 0.7);
}

.subscribe-form { display: flex; flex-direction: column; gap: 14px; }

.subscribe-row { display: flex; flex-wrap: wrap; gap: 10px; }

.subscribe-row input {
  flex: 1 1 220px;
  min-width: 0;
  font: inherit;
  font-size: 15px;
  padding: 12px 15px;
  border-radius: var(--r-sm);
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(4, 10, 20, 0.55);
  color: #fff;
}

.subscribe-row input::placeholder { color: rgba(233, 240, 250, 0.4); }
.subscribe-row input:focus-visible {
  outline: 2px solid var(--brand-lime);
  outline-offset: 2px;
  border-color: transparent;
}

.subscribe-topics {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin: 0;
  padding: 0;
  border: 0;
}

.subscribe-topics legend {
  padding: 0 0 8px;
  font-size: 13px;
  color: rgba(233, 240, 250, 0.55);
}

/* The label is the tap target, not the 16px box inside it, so it carries the
   height a thumb needs. */
.subscribe-topics label {
  display: flex;
  align-items: center;
  min-height: 32px;
  gap: 7px;
  font-size: 14px;
  color: rgba(233, 240, 250, 0.82);
  cursor: pointer;
}

.subscribe-topics input { accent-color: var(--brand-lime-deep); width: 16px; height: 16px; }

.bulletin-band .subscribe-status {
  margin: 0;
  min-height: 20px;
  font-size: 14px;
  color: rgba(184, 231, 106, 0.92);
}

.bulletin-band .subscribe-status a { color: var(--brand-lime); font-weight: 560; }

@media (max-width: 720px) {
  .subscribe-card { grid-template-columns: 1fr; }
}

/* ---------- One note, on its own page ----------
   Served by the worker at /bulletin/<slug>, so this is the only part of the
   site whose markup lives in TypeScript rather than in index.html. It gets a
   single column and nothing else: a reader who arrived from a search result
   wants the note, and every other thing on the page is a chance to lose them
   before the first paragraph. */

body.note-page {
  background:
    radial-gradient(120% 80% at 80% 0%, rgba(184, 231, 106, 0.10), transparent 58%),
    radial-gradient(90% 70% at 5% 100%, rgba(27, 95, 217, 0.13), transparent 60%),
    #050b16;
  color: #fff;
  min-height: 100vh;
  /* iOS Safari's 100vh includes the retracted toolbar, so the page jumped on
     scroll. dvh tracks the visible viewport; the vh line stays as the
     fallback for browsers without it. */
  min-height: 100dvh;
}

.note-sheet {
  max-width: 68ch;
  margin: 0 auto;
  padding: clamp(28px, 6vw, 72px) clamp(20px, 5vw, 32px) clamp(56px, 8vw, 96px);
}

.note-back {
  display: inline-block;
  margin-bottom: clamp(24px, 4vw, 40px);
  font-size: 14px;
  font-weight: 560;
  color: var(--brand-lime);
}

.note-back::before { content: "\2190  "; }
.note-back:hover { color: #fff; }

.note-kicker {
  margin: 0 0 12px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--brand-lime);
}

/* The subject is a link back to the board, filtered to it. Somebody who liked
   this note wants the others on the same thing, and that is one click rather
   than a scroll and a guess. */
.note-kicker a { color: inherit; }
.note-kicker a:hover { color: #fff; }

.note-kicker time {
  margin-left: 10px;
  color: rgba(233, 240, 250, 0.5);
  letter-spacing: 0.1em;
}

.note-sheet h1 {
  margin: 0 0 16px;
  font-size: clamp(30px, 5.4vw, 50px);
  line-height: 1.08;
  letter-spacing: -0.032em;
  text-wrap: balance;
}

/* The standfirst. Larger than the body and a shade brighter, because it is the
   sentence that decides whether the rest gets read. */
.note-stand {
  margin: 0 0 clamp(26px, 4vw, 40px);
  padding-bottom: clamp(22px, 3vw, 32px);
  border-bottom: 1px solid rgba(184, 231, 106, 0.22);
  font-size: clamp(17px, 2.1vw, 20px);
  line-height: 1.5;
  color: rgba(240, 246, 255, 0.94);
}

.note-prose p {
  margin: 0 0 20px;
  font-size: clamp(16px, 1.9vw, 17.5px);
  line-height: 1.72;
  color: rgba(233, 240, 250, 0.86);
}

.note-credit {
  margin: 28px 0 0;
  font-size: 13px;
  color: rgba(233, 240, 250, 0.5);
}

.note-credit a { color: rgba(184, 231, 106, 0.9); }

.note-cta {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-top: clamp(40px, 6vw, 64px);
  padding: clamp(22px, 3vw, 30px);
  border-radius: var(--r-lg);
  border: 1px solid rgba(184, 231, 106, 0.2);
  background: rgba(255, 255, 255, 0.04);
}

.note-cta p {
  flex: 1 1 240px;
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
  color: rgba(233, 240, 250, 0.82);
}

/* The two buttons stay a pair. Left loose in the parent flex they wrap
   independently, which puts one beside the sentence and drops the other onto
   its own line looking like an afterthought. */
.note-cta-actions {
  display: flex;
  flex: 0 1 auto;
  flex-wrap: wrap;
  gap: 10px;
}

.note-page .button.ghost {
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  border-color: transparent;
}

.note-page .button.ghost:hover { background: rgba(255, 255, 255, 0.18); }

/* The honeypot.
   Deliberately NOT `display: none` and NOT `type="hidden"`: both are the first
   thing a form-filling bot learns to skip. This is off-canvas and unreachable
   by keyboard, so a person never meets it and a script that fills every input
   it can find fills this one. `aria-hidden` on the wrapper keeps it away from
   screen readers, which is the one audience that would otherwise be caught by
   its own field. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---------- a saved list, as a page ----------
   Wider than a note, because this is a table somebody reads across rather than
   prose they read down. */

.list-sheet { max-width: 820px; }

.list-code {
  font-family: var(--mono);
  letter-spacing: 0.14em;
  color: rgba(233, 240, 250, 0.62);
}

.list-table {
  width: 100%;
  margin: clamp(24px, 4vw, 36px) 0 0;
  border-collapse: collapse;
  font-size: 15px;
}

.list-table th {
  padding: 0 12px 10px;
  text-align: left;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(233, 240, 250, 0.5);
  border-bottom: 1px solid rgba(184, 231, 106, 0.24);
}

.list-table td {
  padding: 13px 12px;
  vertical-align: top;
  line-height: 1.45;
  color: rgba(233, 240, 250, 0.88);
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}

/* Quantities line up as a column of numbers, not as text that happens to be
   numeric. */
.list-qty {
  width: 4.5em;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: #fff;
}

.list-sku {
  /* Wide enough for a real part code. The catalogue holds names like
     DS-ICS-V2-1292-ROTARY-SWITCH-PANEL-MOUNT, and at 12em those broke across
     four lines while the description column sat half empty beside them. */
  width: 17em;
  font-family: var(--mono);
  font-size: 14px;
  color: var(--brand-lime);
  word-break: break-word;
}

.list-note {
  margin: 22px 0 0;
  padding: 14px 16px;
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.045);
  font-size: 15px;
  line-height: 1.6;
  color: rgba(233, 240, 250, 0.82);
}

.list-note strong { color: #fff; }

@media (max-width: 560px) {
  /* Part code above description rather than beside it: at this width three
     columns give the description about twelve characters. */
  .list-table, .list-table tbody, .list-table tr, .list-table td { display: block; width: auto; }
  .list-table thead { display: none; }
  .list-table tr {
    padding: 12px 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  }
  .list-table td { padding: 2px 0; border: 0; }
  .list-qty::after { content: " x"; color: rgba(233, 240, 250, 0.5); }
  .list-qty { display: inline-block; }
  .list-sku { display: inline-block; margin-left: 8px; }
}

/* The list, on the note itself. A reader who arrived from a search result and
   read to the end is the best subscriber the desk will get, and asking them
   here costs nothing they were going to do anyway. */
.note-join {
  margin-top: clamp(40px, 6vw, 64px);
  padding: clamp(24px, 3.5vw, 34px);
  border-radius: var(--r-lg);
  border: 1px solid rgba(184, 231, 106, 0.24);
  background:
    radial-gradient(130% 150% at 100% 0%, rgba(184, 231, 106, 0.1), transparent 62%),
    rgba(255, 255, 255, 0.04);
}

.note-join h2 {
  margin: 0 0 8px;
  font-size: clamp(20px, 2.4vw, 26px);
  letter-spacing: -0.02em;
  color: #fff;
}

.note-join p {
  margin: 0 0 18px;
  max-width: 52ch;
  font-size: 15px;
  line-height: 1.6;
  color: rgba(233, 240, 250, 0.72);
}

.note-join-row { display: flex; flex-wrap: wrap; gap: 10px; }

.note-join-row input {
  flex: 1 1 220px;
  min-width: 0;
  font: inherit;
  font-size: 15px;
  padding: 12px 15px;
  border-radius: var(--r-sm);
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(4, 10, 20, 0.55);
  color: #fff;
}

.note-join-row input::placeholder { color: rgba(233, 240, 250, 0.4); }

.note-join-row input:focus-visible {
  outline: 2px solid var(--brand-lime);
  outline-offset: 2px;
  border-color: transparent;
}

/* The two cards at the foot of a note are a pair, not two sections. The full
   margin belongs above the first one, marking the end of the reading. */
.note-join + .note-cta { margin-top: 14px; }

/* What to read next. Two, because a page that offers everything offers
   nothing, and this exists to keep somebody here rather than to be an index. */
.note-next {
  display: grid;
  gap: 10px;
  margin-top: clamp(36px, 5vw, 56px);
}

.note-next h2 {
  margin: 0 0 4px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(233, 240, 250, 0.5);
}

.note-next a {
  display: grid;
  gap: 5px;
  padding: 16px 18px;
  border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, 0.09);
  background: rgba(255, 255, 255, 0.035);
  transition: background 180ms var(--ease), border-color 180ms var(--ease);
}

.note-next a:hover {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(184, 231, 106, 0.34);
}

.note-next-topic {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-lime);
}

.note-next-title {
  font-size: 16px;
  font-weight: 600;
  line-height: 1.3;
  color: #fff;
}

.note-next-all {
  justify-items: start;
  font-size: 14px;
  font-weight: 560;
  color: var(--brand-lime);
  background: none;
  border: 0;
  padding: 6px 0 0;
}

.note-next-all:hover { background: none; color: #fff; }

/* The board's link out to a note's own page. Quieter than the open control
   next to it, because opening in place is what most readers want and the
   permalink is for the one who wants to send it to somebody. */
/* Padded to a thumb rather than sized to its text. At 12.5px the link was 18px
   tall, which is a link a phone user aims at and misses. The padding is inside
   the card's own flow so nothing moves on the desktop layout. */
.note-permalink {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 4px 0;
  font-size: 13px;
  color: rgba(233, 240, 250, 0.55);
  white-space: nowrap;
}

.note-permalink:hover { color: var(--brand-lime); }

/* Sits under the open control, not beside it. `details` already carries the
   `margin-top: auto` that pins the pair to the bottom of a card, and a second
   auto margin here would split the free space between them instead. */
.note-controls {
  display: flex;
  align-items: baseline;
  justify-content: flex-start;
  gap: 14px;
  padding-top: 8px;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
  margin-top: 10px;
}

/* ---------- Contact band ---------- */

.contact-band {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 28px;
  margin: 0 var(--pad-x) clamp(56px, 6vw, 88px);
  padding: clamp(38px, 5vw, 68px);
  border-radius: clamp(24px, 3vw, 40px);
  /* The CommScope Connect hero gradient: deep navy with an electric bloom at
     the top edge. The one dark surface on the page now reads as the same
     command-centre material as the partner portal, rather than flat black. */
  background: var(--deep-grad, #101013);
  color: #fff;
  position: relative;
  overflow: hidden;
}

/* A single lime scan line drifts across the band's top edge — the portal's
   signature sweep, at the pace of an ambient effect rather than an alert. */
.contact-band::before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 40%;
  height: 1.5px;
  background: var(--sweep-grad);
  animation: band-sweep 7s var(--ease-confident, ease-in-out) infinite;
  pointer-events: none;
}

@keyframes band-sweep {
  0% { transform: translateX(-110%); opacity: 0; }
  18% { opacity: 1; }
  60% { opacity: 1; }
  100% { transform: translateX(280%); opacity: 0; }
}

.contact-band .eyebrow { color: #34d399; }
.contact-band h2 { max-width: 24ch; margin-bottom: 6px; letter-spacing: -0.024em; }
.contact-band .sub { color: rgba(255, 255, 255, 0.55); }

.contact-actions { display: flex; flex-wrap: wrap; gap: 12px; }
.contact-band .button.ghost { background: rgba(255, 255, 255, 0.1); color: #fff; }
.contact-band .button.ghost:hover { background: rgba(255, 255, 255, 0.18); }

.site-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 24px var(--pad-x) 44px;
  border: 0;
  color: var(--faint);
  font-size: 13px;
}

.site-footer img { height: 22px; width: auto; opacity: 0.85; }

/* ---------- RFQ drawer ---------- */

.rfq-drawer {
  position: fixed;
  inset: 0 0 0 auto;
  z-index: var(--z-drawer);
  width: min(430px, 100vw);
  display: none;
  gap: 14px;
  padding: 22px;
  /* Safety net for short viewports: the list gives way first, and only if the
     form itself still does not fit does the whole panel scroll. */
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Nearly opaque: the panel is a working surface with form fields in it, and at
     0.92 the brand marquee behind it showed through as grey smudges. */
  background: rgba(251, 251, 253, 0.975);
  border-left: 1px solid var(--line);
  box-shadow: -24px 0 70px rgba(0, 0, 0, 0.14);
  transform: translateX(105%);
  transition: transform 420ms var(--spring);
}

/* Flex, not the grid template: with a 1fr list row a single line item left a
   ~250px hole above the tools. As a flex column the list sizes to its content
   and scrolls only once it runs out of room. */
.rfq-drawer.is-open { display: flex; flex-direction: column; transform: translateX(0); }

.drawer-header button, .dialog-close {
  width: 38px;
  min-height: 38px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.06);
  font-size: 19px;
  line-height: 1;
}

.rfq-items {
  flex: 0 1 auto;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  display: grid;
  align-content: start;
  gap: 10px;
  padding-right: 2px;
}

/* Everything below the list keeps its natural height; the list is the only part
   that gives way when the drawer runs short. */
.drawer-header,
.drawer-tools,
.rfq-buyer,
.rfq-drawer textarea,
.drawer-actions { flex: 0 0 auto; }

.rfq-summary {
  display: grid;
  gap: 3px;
  padding: 12px 13px;
  border: 1px solid rgba(13, 159, 110, 0.18);
  border-radius: var(--r-md);
  background: rgba(13, 159, 110, 0.07);
}

.rfq-summary strong {
  font-size: 14px;
  color: var(--ink);
}

.rfq-summary span {
  color: var(--muted);
  font-size: 13px;
}

.rfq-item {
  display: grid;
  grid-template-columns: 52px 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: #fff;
  animation: card-in 300ms var(--ease) both;
}

.rfq-item img, .rfq-thumb {
  width: 52px; height: 52px;
  border-radius: 10px;
  background: var(--plate);
}

.rfq-item img { object-fit: contain; }
.rfq-thumb { display: grid; place-items: center; color: var(--faint); font-size: 10px; font-weight: 650; }

.rfq-item h3 { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }
.rfq-item p { margin: 3px 0 0; color: var(--muted); font-size: 11px; font-family: var(--mono); }

.qty-control {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.05);
}

.qty-control button {
  width: 26px; height: 26px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  font-size: 15px;
  transition: background 140ms var(--ease);
}

.qty-control button:hover { background: #fff; box-shadow: var(--shadow-sm); }

.qty-control input {
  width: 32px;
  border: 0;
  background: transparent;
  text-align: center;
  font-size: 14px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  outline: none;
  -moz-appearance: textfield;
}

.qty-control input::-webkit-outer-spin-button,
.qty-control input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.drawer-tools { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.drawer-tools button { min-height: 38px; padding: 0 8px; font-size: 13px; }

.rfq-drawer textarea { min-height: 76px; padding: 13px; resize: vertical; }
.drawer-actions { display: grid; grid-template-columns: 1fr; gap: 10px; }
.drawer-actions .button { width: 100%; }

.drawer-note {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  text-align: center;
}

.drawer-note a { color: var(--green-deep); font-weight: 600; }

/* The written route's reply line. It carries the reference the sender will
   quote if they ring, so it is set in mono and given its own height whether or
   not it has anything to say: a line that appears from nowhere pushes the call
   button out from under a thumb that was already reaching for it. */
.drawer-status {
  margin: 0;
  min-height: 18px;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.4;
  text-align: center;
  color: var(--green-deep);
}

.drawer-status a { color: var(--green-deep); font-weight: 600; }

.rfq-optin {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 2px 2px 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-2);
  cursor: pointer;
}

.rfq-optin input {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  accent-color: var(--green);
}

/* Buyer details: a quote needs a name, a company, and a delivery city, so the
   desk can price and ship without a round trip. */
.rfq-buyer {
  display: grid;
  gap: 10px;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: rgba(13, 159, 110, 0.04);
}

.rfq-buyer-head {
  margin: 0;
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 650;
}

.rfq-buyer-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.rfq-buyer-grid label {
  display: grid;
  gap: 5px;
  min-width: 0;
}

.rfq-buyer-grid span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.rfq-buyer-grid input {
  width: 100%;
  min-height: 38px;
  padding: 0 11px;
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  background: #fff;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
}

.rfq-buyer-grid input:focus {
  outline: none;
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(13, 159, 110, 0.14);
}

/* ---------- Product dialog ---------- */

/* The sheet is taller than the viewport on almost every product, and with a
   single non-scrolling box the user agent's own max-height simply clipped
   everything past the fold — technical specs, OEM links and related products
   were all unreachable. The content scrolls inside .dialog-scroll instead, so
   the rounded corners still clip and the close button stays put. */
.product-dialog {
  width: min(980px, calc(100vw - 28px));
  max-height: min(920px, calc(100vh - 32px));
  max-height: min(920px, calc(100dvh - 32px));
  display: flex;
  flex-direction: column;
  position: relative;
  padding: 0;
  border: 0;
  border-radius: var(--r-lg);
  background: #fff;
  color: var(--ink);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.dialog-scroll {
  min-height: 0;
  flex: 1 1 auto;
  overflow: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.product-dialog[open] { animation: dialog-in 380ms var(--spring); }

@keyframes dialog-in {
  from { opacity: 0; transform: translateY(22px) scale(0.97); }
}

.product-dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
}

body.dialog-fallback-open { overflow: hidden; }

.product-dialog.is-fallback-open {
  position: fixed;
  inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  max-height: calc(100vh - 28px);
  max-height: calc(100dvh - 28px);
  z-index: var(--z-dialog);
}

body.dialog-fallback-open::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-dialog) - 1);
  background: rgba(0, 0, 0, 0.55);
}

/* Anchored to the dialog, which no longer scrolls, so it stays reachable however
   far down the sheet the reader is. */
.dialog-close {
  position: absolute;
  right: 16px; top: 16px;
  z-index: 5;
  background: rgba(255, 255, 255, 0.88);
  backdrop-filter: blur(10px);
  box-shadow: var(--shadow-sm);
}

.detail-grid { display: grid; grid-template-columns: minmax(280px, 430px) minmax(0, 1fr); }
.detail-grid.no-photo-detail {
  grid-template-columns: minmax(0, 1fr);
}

.detail-media {
  min-height: 460px;
  display: grid;
  place-items: start center;
  padding: 34px;
  background: var(--plate);
}

/* Sticky so the part stays in view while the specs scroll past it — on a
   procurement sheet you read the table against the photo. */
.detail-media img {
  position: sticky;
  top: 0;
  max-width: 100%;
  max-height: 380px;
  object-fit: contain;
  align-self: start;
}
.detail-media.pending {
  min-height: 300px;
  padding: 24px;
  background: linear-gradient(180deg, #fcfcfd, #f4f6f8);
}

.detail-media .no-photo-plate .glyph { width: 64px; height: 64px; }

.no-photo-detail .detail-media {
  border-bottom: 1px solid var(--line);
}

.detail-copy { padding: 44px 38px 36px; }

.detail-brand-logo {
  display: block;
  height: 24px;
  width: auto;
  max-width: 150px;
  object-fit: contain;
  object-position: left center;
  margin-bottom: 16px;
}

.detail-brand-logo.subbrand-logo {
  height: 46px;
  max-width: 260px;
  margin-bottom: 18px;
}

.detail-brand-logo.subbrand-logo.systimax { height: 54px; }

.marquee-subbrand {
  height: 32px;
  min-width: 150px;
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  flex: 0 0 auto;
  margin: 0 24px;
  line-height: 1;
  text-transform: uppercase;
}

.marquee-subbrand b {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: 0.02em;
}

.marquee-subbrand small {
  color: var(--muted);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.06em;
}

.marquee-subbrand.systimax b { color: #f05a28; }
.marquee-subbrand.netconnect b { color: #1b365d; }

.marquee-subbrand-logo {
  height: 38px;
  width: auto;
  max-width: 210px;
  object-fit: contain;
  mix-blend-mode: multiply;
  filter: grayscale(1) opacity(0.78);
  flex: 0 0 auto;
  margin: 0 24px;
}

.detail-copy h2 { margin-bottom: 10px; font-size: clamp(26px, 2.4vw, 34px); }
.detail-copy > p { color: var(--muted); }

.detail-list { display: grid; gap: 0; margin: 26px 0; }

.detail-list div {
  display: grid;
  grid-template-columns: 130px 1fr;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}

.detail-list div:first-child { border-top: 1px solid var(--line); }
.detail-list dt { color: var(--muted); font-size: 14px; font-weight: 550; }
.detail-list dd { margin: 0; font-size: 14px; font-weight: 500; overflow-wrap: anywhere; }

.related-products {
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.related-products h3 {
  margin: 0 0 12px;
  font-size: 15px;
  color: var(--ink-2);
}

.related-products > div {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.related-products button {
  min-width: 0;
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  padding: 9px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: #fff;
  text-align: left;
  transition: border-color 180ms var(--ease), transform 180ms var(--ease), box-shadow 180ms var(--ease);
}

.related-products button:hover {
  transform: translateY(-1px);
  border-color: rgba(13, 159, 110, 0.28);
  box-shadow: var(--shadow-sm);
}

.related-products .rfq-thumb,
.related-products img {
  width: 42px;
  height: 42px;
  border-radius: 10px;
  object-fit: contain;
}

.related-products span {
  min-width: 0;
  display: grid;
  gap: 2px;
}

.related-products b,
.related-products small {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.related-products b {
  font-family: var(--mono);
  font-size: 11px;
}

.related-products small {
  color: var(--muted);
  font-size: 11px;
}

/* ---------- Floating WhatsApp + toasts ---------- */

/* Bottom-right, not bottom-left: on the left it sat on top of the hero copy and
   the phone link at short viewport heights. */
/* Landmark wrapper only. Its single child is fixed-positioned, so the wrapper
   is already a zero-height block and needs no layout rules. Deliberately not
   `display: contents`, which can drop the element, and with it the landmark,
   out of the accessibility tree. */
.float-actions { margin: 0; padding: 0; }

.whatsapp-float {
  position: fixed;
  right: max(20px, env(safe-area-inset-right, 0px));
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-float);
  /* Icon-only. The pill with its label sat on top of the last column's
     card actions at common scroll positions; a 58px circle covers half the
     area and reads identically, because the glyph is the whole message. */
  width: 58px;
  height: 58px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 999px;
  /* WhatsApp's own #25d366 puts white text at 1.98:1, which is unreadable by
     any measure. This is our call-to-action rather than WhatsApp's product, so
     it takes the brand green that the header button already uses and clears
     5.3:1. The glyph still says which app it opens. */
  background: var(--green-deep);
  color: #fff;
  font-weight: 650;
  box-shadow: 0 10px 30px rgba(8, 122, 84, 0.34);
  transition: transform 260ms var(--spring), box-shadow 260ms var(--ease);
}

.whatsapp-float:hover { transform: translateY(-3px) scale(1.03); box-shadow: 0 16px 42px rgba(8, 122, 84, 0.44); }
.whatsapp-float svg { width: 21px; height: 21px; flex: 0 0 auto; }

.toast-stack {
  position: fixed;
  left: 50%; bottom: 28px;
  z-index: var(--z-toast);
  display: grid;
  gap: 8px;
  transform: translateX(-50%);
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 20px;
  border-radius: 999px;
  background: rgba(29, 29, 31, 0.92);
  color: #fff;
  font-size: 14px;
  font-weight: 550;
  box-shadow: var(--shadow-lg);
  animation: toast-in 340ms var(--spring);
}

.toast::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 50%;
  background: #34d399;
}

.toast.out { animation: toast-out 260ms var(--ease) forwards; }

@keyframes toast-in { from { opacity: 0; transform: translateY(16px) scale(0.96); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(10px); } }


.suggest .s-thumb img { object-fit: contain; }

/* ---------- Responsive ---------- */

@media (max-width: 1240px) {
  .split { grid-template-columns: 262px minmax(0, 1fr); }
  .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .solution-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .category-spotlight { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Collapse to the menu button well before the links run out of room. */
@media (max-width: 1180px) {
  .nav-shell { grid-template-columns: auto 1fr; row-gap: 10px; }

  .nav-links {
    display: none;
    order: 3;
    grid-column: 1 / -1;
    justify-content: flex-start;
    flex-wrap: wrap;
    padding-bottom: 8px;
  }

  .nav-links.is-open { display: flex; animation: pop-in 240ms var(--spring); }
  .mobile-menu { display: inline-flex; order: 2; justify-self: end; }
  .nav-actions { order: 4; grid-column: 1 / -1; }
  .header-search { flex: 1; width: auto; }

  .hero { grid-template-columns: 1fr; }
  /* Once the hero stacks, the panel must stack with it. Keeping the two-column
     split here parked the four launch cards in the left half of the band and
     left the right half empty, which is how the 834px iPad rendered until the
     first proper tablet review caught it. */
  .hero-panel { grid-template-columns: 1fr; }
  .solution-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .category-spotlight { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; }
  .boq-panel { grid-template-columns: 1fr; }

  /* Filters collapse behind a toggle so products, not four dropdowns, are the
     first thing on screen. */
  .filters { position: static; padding: 14px 16px; gap: 0; }
  .filter-header .filter-toggle { display: inline-flex; }
  .filter-body { display: none; }
  .filters.is-open .filter-body {
    display: grid;
    gap: 16px;
    padding-top: 16px;
    animation: pop-in 220ms var(--spring);
  }
  .detail-grid { grid-template-columns: 1fr; }
  .detail-media { min-height: 300px; }
  .detail-copy { padding: 28px 22px; }
  /* Icon-only circle on small screens, so it covers as little of the page as a
     floating action can. */
  .whatsapp-float span { display: none; }
  .whatsapp-float { width: 52px; padding: 0; }
}

@media (max-width: 620px) {
  .logo-marquee { display: none; }
  .search-kbd { display: none; }

  /* Header compression. Two rows are unavoidable at this width, but the stack
     was 145px — a sixth of an iPhone viewport — before the page even started.
     Trimming the strip, the row padding and the wordmark brings it under 120px. */
  .top-strip {
    justify-content: center;
    gap: 12px;
    min-height: 28px;
    padding: 3px var(--pad-x);
    font-size: 12px;
  }
  .top-strip span:not(.pulse-dot) { display: none; }
  .nav-shell { min-height: 0; row-gap: 8px; padding: 8px var(--pad-x) 10px; }
  /* Window and render height stay locked to the wordmark's measured ink ratio
     (ink width ≈ 1.86 × render height), or the "D" clips against the window. */
  .brand-mark { flex-basis: 130px; width: 130px; min-width: 130px; height: 32px; }
  .brand-mark img { height: 68px; }
  .mobile-menu { min-height: 38px; }
  .header-search input { height: 38px; }

  /* Search and RFQ share one row, and the header's WhatsApp button is dropped:
     the top strip and the floating button already offer it, and the extra row
     cost ~50px of a 844px-tall viewport. */
  .nav-actions { flex-wrap: nowrap; gap: 8px; }
  .nav-actions .header-search { flex: 1 1 auto; min-width: 0; }
  .nav-actions .button.primary { display: none; }
  .nav-actions .cart { flex: 0 0 auto; min-height: 38px; }

  /* The compressed mobile header measures ~125px, so anchored sections need that
     much clearance or they scroll in underneath it. */
  html { scroll-padding-top: 138px; }

  .product-grid, .solution-grid, .category-spotlight { grid-template-columns: 1fr; }
  .category-spotlight button { min-height: 112px; }
  .product-grid { gap: 18px; }
  .product-card {
    border-radius: 24px;
    box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
  }
  .image-frame {
    height: 210px;
    padding: 18px;
  }
  .image-frame.pending { height: 210px; }
  .no-photo-plate .glyph { width: 40px; height: 40px; }
  .photo-badge {
    left: 14px;
    top: 14px;
    max-width: calc(100% - 28px);
    white-space: nowrap;
  }
  .product-body {
    padding: 18px 20px 14px;
    gap: 10px;
  }
  .brand-row {
    min-height: 42px;
    align-items: flex-start;
  }
  .brand-logo {
    height: 22px;
    max-width: 148px;
  }
  .brand-logo.sq { height: 24px; }
  .brand-line {
    max-width: 180px;
    font-size: 12px;
    overflow-wrap: anywhere;
  }
  .product-card h3 {
    font-size: 17px;
    line-height: 1.25;
    letter-spacing: -0.01em;
  }
  .product-meta { font-size: 13px; }
  .card-actions {
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    padding: 0 20px calc(20px + env(safe-area-inset-bottom, 0px));
  }
  /* Primary takes the full width; the two quiet actions share the next row. */
  .card-actions .primary-action { grid-column: 1 / -1; }
  .card-actions button,
  .card-actions a {
    min-height: 46px;
    padding: 0 12px;
    font-size: 15px;
  }
  .product-dialog {
    width: 100vw;
    max-width: 100vw;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border-radius: 0;
  }
  .detail-media {
    min-height: 260px;
    padding: 28px 20px;
  }
  .detail-copy {
    padding: 26px 20px calc(34px + env(safe-area-inset-bottom, 0px));
  }
  .detail-list div {
    grid-template-columns: 96px 1fr;
  }
  .detail-copy .hero-actions {
    display: grid;
    grid-template-columns: 1fr;
  }
  /* The submit label costs ~65px of a 347px field, which is enough to truncate
     the placeholder. The arrow keeps the affordance; the button carries an
     aria-label so the accessible name survives. */
  .hero-search { padding-left: 16px; gap: 8px; }
  .hero-search input { font-size: 16px; }
  .hs-go { padding: 0; width: 46px; }
  .hs-go span { display: none; }

  .hero { padding-top: 44px; }
  .hero-panel { grid-template-columns: 1fr; }
  /* Never a fixed height. The gallery is a two by two grid of tiles whose
     labels wrap to more lines the narrower the phone gets, so 240px was a
     ceiling the content grew straight through: the tiles overflowed the box
     and landed on top of the three claims underneath, which is what the owner
     photographed. The grid sizes to what is in it. */
  .hero-gallery { height: auto; min-height: 240px; }
  .hero-copy .lede { font-size: 17px; }

  .results-toolbar, .contact-band { align-items: flex-start; flex-direction: column; }
  .toolbar-actions { width: 100%; }
  .toolbar-actions .sort-wrap { flex: 1; }
  .toolbar-actions .sort-wrap select { width: 100%; }

  .drawer-tools { grid-template-columns: 1fr 1fr; }
  h1 { font-size: 34px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    /* Delays are zeroed too, not just durations. Staggered entrances hold their
       element hidden through the delay (animation-fill-mode: backwards), so a
       zero-duration animation with a 300ms delay is still 300ms of blank grid —
       the exact thing this query exists to prevent. */
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
  .marquee-track { animation: none; }
  /* Stated rather than left to the blanket rule above. That one would run each
     field layer once at 0.01ms and leave it parked wherever its last keyframe
     put it, which for the spark is off the right edge. Parking them at rest
     keeps the composition the designed one, and the field is legible as a
     still: it was drawn to be. */
  .field { animation: none !important; transform: none !important; }
}

.view-pill {
  flex: 0 0 auto;
  min-height: 28px;
  padding: 0 12px;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.05);
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 600;
  transition: background 160ms var(--ease);
}
.view-pill:hover { background: rgba(0, 0, 0, 0.1); }

.partner-hub {
  position: relative;
  overflow: hidden;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  background:
    linear-gradient(135deg, rgba(9, 14, 22, 0.94), rgba(18, 27, 38, 0.9)),
    radial-gradient(circle at 80% 20%, rgba(37, 211, 102, 0.18), transparent 32%);
  color: #fff;
}

/* ============================================================
   Premium motion + interaction pass
   ============================================================ */

:root {
  --blue: #0a84ff;
  --violet: #6e5cff;
  --gold: #c49338;
  --premium-shadow: 0 18px 46px rgba(18, 24, 34, 0.11), 0 2px 8px rgba(18, 24, 34, 0.05);
}

/* The line that used to be here read:
     h1, h2, h3, .brand-line, .line-label, .eyebrow { letter-spacing: 0; }
   It arrived with the premium-motion pass and quietly flattened the type
   system from below. `.eyebrow` here ties the tracking rules above on
   specificity, so being 4,000 lines later it won: the mono eyebrows were
   designed at 0.22em and 0.28em of tracking and have been rendering at none.
   The display headings only survived because their selectors carry a class.
   A reset this broad has no job a token cannot do better; deleted rather than
   scoped, so the type system has one owner again. */

.aurora {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(245, 245, 247, 0.92) 46%, #f5f5f7),
    radial-gradient(1200px 520px at 50% -12%, rgba(13, 159, 110, 0.16), transparent 62%),
    linear-gradient(116deg, rgba(10, 132, 255, 0.07), transparent 38%, rgba(196, 147, 56, 0.08) 78%, transparent);
}

.grid-veil {
  display: block;
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(29, 29, 31, 0.035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(29, 29, 31, 0.035) 1px, transparent 1px);
  background-size: 76px 76px;
  mask-image: linear-gradient(180deg, rgba(0,0,0,0.56), transparent 58%);
}

.grain {
  display: block;
  position: absolute;
  inset: 0;
  opacity: 0.05;
  background-image:
    repeating-linear-gradient(0deg, rgba(0,0,0,0.28) 0 1px, transparent 1px 3px);
  mix-blend-mode: multiply;
}

.site-header {
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) inset;
}

.nav-links a {
  position: relative;
}

.nav-links a::after {
  content: "";
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 4px;
  height: 2px;
  border-radius: 999px;
  background: var(--accent-grad);
  opacity: 0;
  transform: scaleX(0.4);
  transition: opacity 180ms var(--ease), transform 260ms var(--spring);
}

.nav-links a:hover::after {
  opacity: 1;
  transform: scaleX(1);
}

/* The hidden state for `.reveal` lives with the rest of the reveal rules near
   the top of this file, gated behind `html.js`. It used to be repeated here,
   ungated and later in the cascade, which meant it won: with JavaScript
   disabled every section heading on the page stayed at opacity 0 forever,
   because the class that reveals them is only ever added by an observer. */

/* Hero entrance. Runs on load rather than on scroll — the hero is already in
   view — and staggers off --step so the eye lands on the headline, then the
   search field. Short and shallow on purpose: 520ms, 14px, no bounce. */
.reveal-in {
  animation: reveal-in 520ms var(--ease) both;
  animation-delay: calc(var(--step, 0) * 70ms);
}

@keyframes reveal-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}

.hero {
  position: relative;
}

/* A 1px teal-and-lime rule used to run across the foot of the hero. It was
   written when the page was a stack of bands and its job was to mark where one
   surface ended and the next began. There are no bands now — one sky runs the
   whole document — so all it did was draw the ruled line everything else was
   built to avoid, at the very top of the page where it is seen first. Measured
   at the join it lifted a single pixel row from 62 to 99. Gone.
   (`.hero::before` is still used, as a scrim over the whole hero, in the phone
   block further down. It could never work while this rule existed: an absolute
   box with top and bottom both zero and an explicit `height: 1px` obeys the
   height, so the phone scrim was one pixel tall.) */

.hero-copy {
  position: relative;
}

.hero-copy::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -30px;
  width: min(520px, 76vw);
  height: 1px;
  background: linear-gradient(90deg, rgba(159, 232, 112, 0.75), transparent);
  transform-origin: left center;
  animation: line-sweep 3.6s var(--ease) infinite;
}

@keyframes line-sweep {
  0%, 38%, 100% { transform: scaleX(0.2); opacity: 0.18; }
  58% { transform: scaleX(1); opacity: 0.72; }
}


.hero-panel {
  isolation: isolate;
  overflow: hidden;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.94), rgba(255,255,255,0.76)),
    linear-gradient(135deg, rgba(13,159,110,0.12), rgba(10,132,255,0.08));
  box-shadow: var(--premium-shadow);
}

/* The panel sheen is gone. It swept a 74%-white diagonal across the panel
   every eight seconds, which is the gloss a phone case gets in a product
   render, and the owner's word for it was cheap. He is right: the panel holds
   the categories a visitor is meant to read, and a bright band crossing them
   on a timer is decoration competing with the one thing on that surface worth
   looking at. Nothing replaces it. The panel is already lifted off the field
   by its own shadow and its own light ground. */


@keyframes tile-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-5px); }
}

.category-spotlight button,
.solution-card,
.boq-input,
.boq-output,
.product-card,
.brand-cloud button {
  position: relative;
  isolation: isolate;
}

/* `.filters` is deliberately left out of the group above. It is `position:
   sticky` with `top: 106px`; resetting it to `relative` kept that offset and
   pushed the panel 106px down out of flow, landing it on top of the results
   heading in the single-column layout. It only needs the stacking context. */
.filters { isolation: isolate; }

.category-spotlight button::before,
.solution-card::before,
.product-card::before,
.brand-cloud button::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(260px 180px at var(--mx, 50%) var(--my, 0%), rgba(13,159,110,0.16), transparent 64%);
  opacity: 0;
  transition: opacity 260ms var(--ease);
}

/* The pointer-follow glow is emerald because it lives on white everywhere else,
   and emerald is the accent that carries meaning on a light ground. The brand
   wall is the one place it sits on ink, where emerald goes muddy next to the
   lime already in the field behind it. Same mechanism, band's own colour. */
.brand-cloud button::before {
  background: radial-gradient(260px 180px at var(--mx, 50%) var(--my, 0%), rgba(184, 231, 106, 0.2), transparent 64%);
}

.category-spotlight button:hover::before,
.solution-card:hover::before,
.product-card:hover::before,
.brand-cloud button:hover::before {
  opacity: 1;
}

.product-card {
  transform-style: preserve-3d;
  box-shadow: 0 1px 0 rgba(255,255,255,0.7) inset, 0 1px 2px rgba(0,0,0,0.02);
}

.product-card:hover {
  transform: translateY(-8px);
}

.image-frame {
  background:
    linear-gradient(180deg, #ffffff 0%, #f7f9fb 100%);
}

.image-frame::after {
  content: "";
  position: absolute;
  left: 13%;
  right: 13%;
  bottom: 16px;
  height: 16px;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(0,0,0,0.11), transparent 68%);
  filter: blur(5px);
  opacity: 0.42;
  transform: scaleX(0.72);
  transition: opacity 260ms var(--ease), transform 380ms var(--spring);
}

.image-frame img {
  position: relative;
  z-index: 1;
  filter: drop-shadow(0 10px 18px rgba(0,0,0,0.08));
}

.product-card:hover .image-frame::after {
  opacity: 0.68;
  transform: scaleX(0.86);
}

.product-card:hover .image-frame img {
  transform: translateY(-3px) scale(1.04);
}

.card-actions .primary-action {
  background:
    linear-gradient(180deg, rgba(255,255,255,0.12), transparent),
    var(--ink);
  box-shadow: 0 8px 18px rgba(29,29,31,0.13);
}

.card-actions .primary-action:hover {
  box-shadow: 0 12px 24px rgba(8,122,84,0.20);
}

.filters,
.boq-input,
.boq-output,
.recommendation,
.contact-band,
.partner-hub {
  box-shadow: var(--premium-shadow);
}

.filter-guide {
  background:
    linear-gradient(180deg, rgba(255,255,255,0.82), rgba(236,249,244,0.76));
}

.boq-input textarea {
  background:
    linear-gradient(#fff, #fff) padding-box,
    linear-gradient(135deg, rgba(13,159,110,0.24), rgba(10,132,255,0.14)) border-box;
  border-color: transparent;
}

.boq-match {
  transition: background 160ms var(--ease), transform 200ms var(--spring);
}

.boq-match:hover {
  transform: translateX(3px);
}

.rfq-drawer.is-open {
  animation: drawer-enter 420ms var(--spring) both;
}

@keyframes drawer-enter {
  from { transform: translateX(24px); opacity: 0; }
}

.whatsapp-float {
  animation: whatsapp-breathe 3.2s var(--ease) infinite;
}

@keyframes whatsapp-breathe {
  0%, 100% { transform: translateY(0); box-shadow: 0 14px 34px rgba(13,159,110,0.24); }
  50% { transform: translateY(-3px); box-shadow: 0 18px 42px rgba(13,159,110,0.32); }
}

.toast {
  border: 1px solid rgba(255,255,255,0.16);
  background:
    linear-gradient(180deg, rgba(45,49,57,0.98), rgba(24,28,34,0.98));
}

@media (max-width: 720px) {
  .hero-copy::after { display: none; }
  .hero-panel { border-radius: 28px; }
  .product-card { border-radius: 28px; }
  .card-actions {
    grid-template-columns: 1fr 1fr;
  }
  .card-actions .primary-action {
    grid-column: 1 / -1;
  }
  .image-frame {
    height: 248px;
  }
  .spec-list {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero-copy::after,
  h1 em,
  .hero-panel::after,
  .whatsapp-float {
    animation: none !important;
  }
  .reveal {
    opacity: 1 !important;
    transform: none !important;
  }
}

.partner-hub .section-heading .sub,
.partner-hub .eyebrow { color: rgba(255, 255, 255, 0.68); }

/* Three equal cards in a row is a stock layout, and numbering them 01/02/03
   makes it a stock layout wearing a costume. These are three plain statements:
   the boxes come off, a hairline groups them, and the first column carries more
   weight than the other two so the row is read rather than scanned. */
.hub-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: 0 40px;
}

.hub-grid article {
  padding: 26px 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.18);
}

/* The fourth card is the Watchtower's doorway and spans the row: in a
   1.4fr/1fr/1fr grid it would otherwise wrap into the first column alone and
   leave two empty cells beside it, which reads as a layout accident rather
   than an invitation. */
.hub-grid article:nth-child(4) { grid-column: 1 / -1; }

.hub-grid article a {
  color: var(--brand-lime, var(--brand-lime));
  text-decoration: underline;
  text-underline-offset: 3px;
}

.hub-grid h3 { margin: 0 0 12px; font-size: 22px; }
.hub-grid p { margin: 0; color: rgba(255, 255, 255, 0.7); line-height: 1.65; }
.hub-grid article:first-child h3 { font-size: 26px; }
.hub-grid article:first-child p { color: rgba(255, 255, 255, 0.82); }

/* Footer wordmark: same whitespace-cropping window at smaller scale */
.site-footer { position: relative; }
.site-footer img {
  height: 48px;
  width: auto;
  margin: -14px 0;
  opacity: 0.9;
}

@media (max-width: 900px) {
  .hub-grid { grid-template-columns: 1fr; gap: 0; }
  .hub-grid article { padding: 22px 0; }
  .hub-grid article:first-child h3 { font-size: 22px; }
}

/* ---------- Touch feedback ---------- */

/* Hover states never fire on a touchscreen, and a tapped card keeps its -6px
   lift until something else is tapped. On touch devices the lift is dropped and
   replaced with an immediate press response, so taps feel acknowledged. */
@media (hover: none) {
  .product-card:hover { transform: none; }
  .product-card:active {
    transform: scale(0.985);
    box-shadow: var(--shadow-md);
    transition-duration: 90ms;
  }

  .hero-chips button:hover,
  .button.primary:hover { transform: none; }

  .hero-chips button:active,
  .category-spotlight button:active,
  .brand-cloud button:active,
  .solution-card:active,
  .view-pill:active { transform: scale(0.97); }

  .card-actions button:active,
  .card-actions a:active,
  .whatsapp-float:active { transform: scale(0.955); }
}

/* Every decorative animation added above is opt-out. The blanket rule earlier in
   this file neutralises durations; these are named so the fill-mode `both` on
   .reveal-in cannot leave anything mid-transform. */
@media (prefers-reduced-motion: reduce) {
  .reveal-in {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .hs-go:hover svg,
  .product-card:hover { transform: none !important; }
}

/* ---------- Modal scroll lock ---------- */

/* Native showModal does not stop the page behind it from scrolling, so a trackpad
   flick over the backdrop moved the catalogue underneath the sheet. Both the
   native and the fallback path take this lock. */
/* Pinned, not just clipped. overflow: hidden leaves the document scrollable in
   principle, and showModal() asks the browser to scroll the dialog into view,
   which with smooth scrolling on the root glides the whole page to the top
   behind the sheet. Fixing the body at its current offset means there is no
   scroll left to animate. The offset itself is set inline by app.js, because
   only it knows where the reader was. */
body.dialog-open {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  overflow: hidden;
}

/* ---------- Long-list scroll performance ---------- */

/* The grid can hold several hundred cards, each with an image, a plate and three
   actions. Skipping layout and paint for the ones off screen keeps scrolling on
   the main thread cheap. contain-intrinsic-size is the measured card height, so
   the scrollbar stays honest and anchors do not drift. */
@supports (content-visibility: auto) {
  .product-grid > .product-card {
    content-visibility: auto;
    contain-intrinsic-size: auto 430px;
  }
}

/* ---------- Print ---------- */

/* A quote list gets printed or saved to PDF and walked into a meeting, and a
   product sheet gets attached to an email thread. Both were unusable on paper:
   the fixed header and the floating button printed over the content, the sheet
   printed at 30% of the page inside a scroll container, and the dark buttons ate
   a quarter of a toner cartridge. */
@media print {
  @page { margin: 14mm; }

  :root { --canvas: #fff; --canvas-0: rgba(255, 255, 255, 0); }

  html, body { background: #fff; }

  /* Ambient and navigational furniture carries no information on paper. */
  .aurora,
  .site-header,
  .whatsapp-float,
  .toast-stack,
  .logo-marquee,
  .skip-link,
  .hero-search,
  .hero-chips,
  .hero-secondary,
  .filters,
  .results-toolbar .toolbar-actions,
  .card-actions,
  .detail-cta,
  .detail-utils,
  .drawer-tools,
  .drawer-actions,
  .dialog-close,
  .load-more,
  .related-products,
  .partner-hub,
  .site-footer nav { display: none !important; }

  /* Nothing may clip or scroll: the page is as tall as it needs to be. */
  .product-dialog,
  .dialog-scroll,
  .rfq-drawer,
  .rfq-items {
    position: static !important;
    max-height: none !important;
    height: auto !important;
    overflow: visible !important;
    box-shadow: none !important;
    border: 0 !important;
    transform: none !important;
    backdrop-filter: none !important;
  }

  .product-dialog { width: 100% !important; }
  .product-dialog::backdrop { display: none; }

  /* When a sheet or the quote list is open, print that and nothing else. */
  body.dialog-open > *:not(.product-dialog),
  body:has(.rfq-drawer.is-open) > *:not(.rfq-drawer) { display: none !important; }

  .rfq-drawer.is-open { display: block !important; width: 100% !important; }

  /* Single column, and never split a line item or a card across a page break. */
  .detail-grid,
  .product-grid,
  .spec-list,
  .rfq-buyer-grid { grid-template-columns: 1fr !important; }

  .product-card,
  .rfq-item,
  .detail-section { break-inside: avoid; }

  .product-card {
    border: 1px solid #ccc !important;
    box-shadow: none !important;
  }

  .image-frame { height: 150px !important; }

  /* Paper has no hover and no colour budget. */
  a { color: #000 !important; text-decoration: none !important; }

  /* A printed link is dead unless the address is on the page — except for the
     ones where the address is noise: a wa.me deep link carries the whole
     prefilled message, and tel:/mailto: already read as the number or address. */
  .detail-source a::after,
  .resource-links a::after { content: " (" attr(href) ")"; font-size: 10px; word-break: break-all; }
  .resource-links a[href^="https://wa.me"]::after,
  .resource-links a[href^="tel:"]::after,
  .resource-links a[href^="mailto:"]::after { content: ""; }

  h1, h2 { font-size: 20pt !important; }
  h3 { font-size: 11pt !important; }
  body { font-size: 10.5pt; line-height: 1.4; }

  /* The desk's number should survive the print. */
  .detail-phone,
  .drawer-note { color: #000 !important; }
}

/* The sheet's heading is focused programmatically when the dialog opens so a
   screen reader announces the product rather than "Close product". It is not a
   keyboard target, so it takes no focus ring — on screen it read as an error
   state, and it was printing as a box around the part code. */
.detail-copy h2[tabindex="-1"]:focus,
.detail-copy h2[tabindex="-1"]:focus-visible { outline: none; }

/* ---------- Manufacturer summary ---------- */

/* One clear opening line, then scannable points. A procurement reader is deciding
   whether this part is the right one, not reading an essay. */
.detail-section p.detail-lede {
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
}

.detail-highlights {
  display: grid;
  gap: 9px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}

.detail-highlights li {
  position: relative;
  padding-left: 22px;
  color: var(--ink-2);
  font-size: 15px;
  line-height: 1.5;
}

/* A small square dot. A tick reads as a verified claim; the previous 10x2
   bar read as an en-dash, which the site's own copy rule bans, and a marker
   should obey the same language as the words beside it. */
.detail-highlights li::before {
  content: "";
  position: absolute;
  left: 5px; top: 8px;
  width: 5px; height: 5px;
  border-radius: 2px;
  border-radius: 1px;
  background: var(--green);
}

@media (max-width: 620px) {
  .detail-section p.detail-lede { font-size: 15px; }
  .detail-highlights li { font-size: 14px; }
}

/* ==========================================================================
   The frame, the atmosphere, and the phone           (P1 / P2 / P3)
   ==========================================================================

   Everything below exists because of one sentence the owner said twice: he
   wants the page to look exactly like the reference, and he overruled the
   argument that some of it was too much for a procurement site. So this is not
   a tasteful subset. It is the frame, the three-layer colour wash and the
   separate phone design, ported together.

   Two rules survive that instruction, and neither is taste. Product cards stay
   on a light ground, because that is where a buyer reads a part code off the
   screen and it is what the accessibility pass rests on; the reference does
   the same thing, its own results sit on white. And the atmosphere may not
   cost the first product card, which is measured at the end of this file's
   work rather than assumed.
   ========================================================================== */

/* ---------- P1: the frame ---------- */

/* The root goes graphite so that an overscroll bounce at either end of the
   document shows the page's own dark rather than a flash of white paper. Both
   ends of this document are now dark bands, so white was always the wrong
   answer there. */
html { background: #050b16; }

/* Display type, tightened. The reference sets its headline noticeably larger,
   heavier and much tighter than a default sans, and that tracking is most of
   why it reads as designed rather than typed. No webfont is involved: the app
   the owner likes ships the same system stack this project already uses, so
   the difference was never the typeface. */
.hero-copy h1,
.ink-band h2,
.section-heading h2 {
  font-size: clamp(2.2rem, 6vw, 4.8rem);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 1.02;
  text-wrap: balance;
}

.section-heading h2 { font-size: clamp(1.9rem, 4vw, 3.2rem); }

::selection { background: rgba(184, 231, 106, 0.32); color: #ffffff; }
.paper-field ::selection,
.section:not(.ink-band) ::selection { color: #05070c; }

/* The header stops being a white bar and becomes a lens over the field. Over
   the hero it is nearly transparent so the orbs run behind it; once the reader
   has moved 8px it settles to graphite and grows a hairline, which is the only
   thing separating it from the content beneath. */
/* The reference gets away with a header at 30% black because its hero starts at
   the top of the document and the header floats on it. Ours does not overlap
   the hero, so at rest the header is over light paper and 30% black rendered as
   a grey shelf above the band. It is nearly opaque at rest for that reason, and
   becomes more transparent once the reader has scrolled, because by then there
   is a dark band behind it and the lens effect can actually happen. */
.site-header {
  background: rgba(4, 10, 20, 0.94);
  border-bottom-color: transparent;
}

.site-header.is-stuck {
  /* 0.8 relied on the backdrop blur to stop content reading through, and a
     capture pipeline that skips backdrop-filter showed body text ghosting
     inside the header. Near-opaque holds the glass look and needs no rescue. */
  background: rgba(5, 11, 22, 0.92);
  border-bottom-color: rgba(255, 255, 255, 0.1);
}

/* The lens is a desktop luxury. On a phone the brand wall's white plates
   scroll directly behind the header, and at 80% they ghost through it as pale
   rectangles drifting under the search field — caught in a phone capture, not
   invented. Near-opaque there; the effect is not worth a smudged header on
   the device most visitors hold. */
@media (max-width: 768px) {
  /* Opaque, in both states. The lens was already a desktop luxury here, and
     with the backdrop blur gone there is nothing left to soften what shows
     through: at 0.94 the hero's own chips read as grey words floating inside
     the header, which is what the owner photographed. A header is a surface,
     not a window. */
  .site-header,
  .site-header.is-stuck { background: #050b16; }
}

.site-header .top-strip {
  border-bottom-color: rgba(255, 255, 255, 0.08);
  color: rgba(233, 240, 250, 0.66);
}

.site-header .top-strip a { color: var(--brand-lime, var(--brand-lime)); }
.site-header .nav-links a { color: rgba(233, 240, 250, 0.82); }
.site-header .nav-links a:hover { background: rgba(255, 255, 255, 0.08); color: #ffffff; }

.site-header .header-search {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.14);
}

.site-header .header-search input { color: #ffffff; }
.site-header .header-search input::placeholder { color: rgba(233, 240, 250, 0.5); }
.site-header .search-icon { stroke: rgba(233, 240, 250, 0.6); }

.site-header .search-kbd {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.16);
  color: rgba(233, 240, 250, 0.72);
}

.site-header .button.ghost {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.06);
}

.site-header .button.ghost:hover { background: rgba(255, 255, 255, 0.14); }
.site-header .mobile-menu span { background: #ffffff; }

/* Film grain over the whole document at 1.5%. Pure gradients read as flat
   digital colour; a little noise is what stops six overlapping blurred orbs
   looking like a screensaver. It is one fixed element, composited, and it
   never takes a pointer event. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-suggest);
  pointer-events: none;
  opacity: 0.015;
  mix-blend-mode: overlay;
  background-size: 160px 160px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 0.7 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* Where a dark band meets the band above it, a 120px fade instead of a butt
   join. Without this a run of dark sections reads as a stack of cards; with it
   the page reads as one continuous surface with content floating on it. */
.ink-band::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 120px;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(3, 9, 22, 0.85), transparent);
}

/* ---------- P2: the atmosphere ---------- */


/* ---------- P3: the phone ---------- */

/* This is not a set of tweaks to the desktop design, it is the separate mobile
   design ported alongside it, and it ships in the same commit as P2 on
   purpose. Six 700px orbs each blurred 55px, a conic layer blurred 70px and a
   backdrop-filtered header, all composited on a phone GPU, is how you get a
   band that scrolls at eight frames a second and a Safari tab that reloads
   itself. Everything below is the bill for the layer above. */
@media (max-width: 768px) {
  /* Backdrop filters are the single most expensive thing on this page on iOS,
     because each one forces a readback of everything painted beneath it. The
     header keeps its colour and loses the blur. */
  .site-header,
  .site-header .header-search,
  .hero-panel,
  .compare-tray {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }

  /* Opaque, not 94%. This rule is the last one to touch the header on a
     phone, so it is the one that decides: at 94% the hero's own chips read
     as grey words floating inside the header, which is what the owner
     photographed. The blur that used to hide that is gone, and a header is a
     surface rather than a window. */
  .site-header { background: #050b16; }

  

  

  /* With the orbs held still and brought into the visible half, the headline is
     now sitting on live colour rather than on dark. A scrim under the copy
     buys the contrast back without dimming the band itself. */
  .hero::before,
  .ink-band > .section-heading::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background: linear-gradient(180deg, rgba(3, 9, 22, 0.85), rgba(3, 9, 22, 0));
  }

  .hero-copy { position: relative; z-index: 2; }

  /* This used to flatten the headline to plain white on a phone, back when the
     treatment was a clipped gradient and unreadable at 390px over a moving
     wash. The neon is the opposite case: solid glyphs with a halo behind them
     are more legible on the small screen, not less, so the phone keeps the
     colour. Only the outermost halo comes off — a 52px radius at phone type
     sizes bleeds into the line above it. */
  /* The pulse comes off with it. Not for taste: a keyframe that animates
     text-shadow outranks a static declaration, so leaving it running would put
     the 52px desktop halo back on every other frame and undo the line above. */
  .hero-copy h1 .metal {
    animation: none;
    text-shadow:
      0 0 4px rgba(217, 249, 157, 0.95),
      0 0 11px #a3e635,
      0 0 22px #84cc16;
  }

  .hero-copy h1 em {
    animation: none;
    text-shadow:
      0 0 4px rgba(199, 210, 254, 0.95),
      0 0 11px #818cf8,
      0 0 22px #6366f1;
  }

  .hero { min-height: 100svh; }

  /* The grain is a fixed full-viewport composited layer and contributes almost
     nothing at this size. */
  body::after { display: none; }
}

/* Reduced motion parks the whole wash rather than merely slowing it. The bands
   are composed to read as a still, which is the reason the canvas draws one
   frame and stops rather than being removed. */
@media (prefers-reduced-motion: reduce) {
  
}

/* The wordmark on the dark header.
   datasystems-white.svg is in the repository and is the obvious asset for this,
   but it is broken in the way netconnect.svg was: rendered and measured, its
   ink occupies 5.5% of the frame against the black file's 80%, because only the
   leaf draws and the lettering does not. So the black mark is used and inverted
   here instead.
   The filter is invert plus a half turn of hue rather than a flat
   brightness(0) invert(1). Both put the lettering in white; only this one keeps
   the leaf lime, and the leaf is the one piece of colour in the identity. */
.site-header .brand-mark img { filter: invert(1) hue-rotate(180deg); }

/* The hero's field is inset -10% -6% so its drift never shows an edge, which
   was harmless while the band was a quiet gradient. With six blurred orbs and a
   conic aurora in there it spills a full colour wash over the brand marquee
   sitting underneath, and twenty greyscale logos on a moving rainbow is not a
   trust strip. Clip it to the band it belongs to. */
.hero { overflow: hidden; }

/* The field must stay out of flow inside a band.
   `.ink-band > *` sets position:relative on every direct child so the content
   sits above the background, and it has the same specificity as `.field` while
   appearing later in this file, so it silently won and the field became an
   in-flow element. On the contact band, which is a two column layout, that drew
   the whole canvas as a small rectangle in the left column. */
.ink-band > .field {
  position: absolute;
  z-index: -1;
}

/* Text and surfaces on the newly darkened bands.
   Five sections that were written for light paper are now dark, and only h2,
   .sub and .eyebrow had band-scoped colours. Everything else, the card
   headings, list items and inline links, stayed at ink on ink. */
.ink-band h3,
.ink-band h4,
.footer-grid h3,
.ink-band h5,
.ink-band strong,
.ink-band b { color: #ffffff; }

.ink-band li,
.ink-band dd,
.ink-band figcaption,
.ink-band small { color: rgba(233, 240, 250, 0.72); }

.ink-band a:not(.button):not(.brand) { color: var(--brand-lime, var(--brand-lime)); }

/* Cards on a band become glass rather than white slabs. A white card on a live
   colour field reads as a hole punched in the page. */
.ink-band article,
.ink-band .card,
.ink-band .panel,
.ink-band .hub-grid > *,
.ink-band .solution-card,
.ink-band .industry-row > span {
  background: rgba(255, 255, 255, 0.045);
  border-color: rgba(255, 255, 255, 0.1);
  color: rgba(233, 240, 250, 0.82);
  box-shadow: none;
}

.ink-band .button.ghost {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.24);
  background: rgba(255, 255, 255, 0.06);
}

.ink-band .button.ghost:hover { background: rgba(255, 255, 255, 0.14); }

/* ---------- The brands a category is deepest in ---------- */

/* Counted from the catalogue, four per tile. On the reference these are the
   most informative thing on a category tile and on ours they have the
   advantage of being true: they are a reading of the 9,187 rows rather than a
   list somebody typed and then stopped maintaining. */
.cat-brands {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 10px;
}

.cat-brands span {
  padding: 3px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.025);
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  transition: border-color var(--t-fast) var(--ease-signal),
              color var(--t-fast) var(--ease-signal);
}

.category-spotlight button:hover .cat-brands span {
  border-color: rgba(13, 159, 110, 0.4);
  color: var(--ink);
}

.ink-band .cat-brands span {
  border-color: rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.05);
  color: rgba(233, 240, 250, 0.78);
}

/* ---------- P4: one motion vocabulary ---------- */

/* Three durations and one curve, used everywhere. The page had accumulated a
   dozen ad hoc timings, and the reason the reference feels composed rather
   than busy is not that it animates less, it is that everything on it moves on
   the same terms. */
:root {
  --t-fast: 0.35s;
  --t-mid: 0.6s;
  --t-slow: 0.8s;
  --ease-signal: cubic-bezier(0.16, 1, 0.3, 1);
}

/* The reveal gains a blur that clears.
   Translate alone reads as "the element slid in". Translate plus a defocus
   that resolves reads as "the element arrived", which is the difference
   between the two pages. It is deliberately a small blur: 8px on a heading is
   atmosphere, 20px is a transition effect and draws attention to itself.
   The blur is dropped the moment the element is in, so nothing on the page is
   left carrying a filter it no longer needs. A permanent filter on a section
   promotes it to its own layer for the rest of the session. */
html.js .reveal:not(.in) {
  opacity: 0;
  transform: translateY(28px);
  filter: blur(8px);
}

.reveal {
  transition:
    opacity var(--t-mid) var(--ease-signal),
    transform var(--t-slow) var(--ease-signal),
    filter var(--t-mid) var(--ease-signal);
}

.reveal.in {
  opacity: 1;
  transform: none;
  filter: none;
}

/* A single pulse travelling the rule between sections, once, when the section
   arrives. The reference runs these continuously; ours fire on arrival and
   stop, because a page with seven of them looping is a page with seven things
   asking for attention while somebody is trying to read a part code. */
.ink-band > .section-heading::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -18px;
  width: 100%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(184, 231, 106, 0.9), transparent);
  background-size: 42% 100%;
  background-repeat: no-repeat;
  opacity: 0;
}

.ink-band > .section-heading { position: relative; }

.ink-band > .section-heading.in::after {
  animation: signal-travel 1.6s var(--ease-signal) 0.25s 1 both;
}

@keyframes signal-travel {
  0% { background-position: -45% 0; opacity: 0; }
  15% { opacity: 1; }
  85% { opacity: 1; }
  100% { background-position: 145% 0; opacity: 0; }
}

/* The eyebrow catches the light once as its section arrives, and then never
   again. A looping shimmer on a label that says "02 · CATEGORIES" is a
   distraction; one pass is a flourish. */
/* Scoped entirely to `.in`, so an eyebrow only becomes gradient-filled text at
   the moment it is about to be animated. A permanently transparent text fill is
   one unsupported property away from an invisible label, and the label is the
   only thing naming the section. */
.ink-band .section-heading.in .eyebrow {
  background-image: linear-gradient(
    100deg,
    currentColor 0%, currentColor 38%,
    #ffffff 48%, #ffffff 52%,
    currentColor 62%, currentColor 100%
  );
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: eyebrow-shimmer 1.5s var(--ease-signal) 0.4s 1 both;
}

@keyframes eyebrow-shimmer {
  from { background-position: 130% 0; }
  to { background-position: -30% 0; }
}

@media (prefers-reduced-motion: reduce) {
  html.js .reveal:not(.in) { filter: none; }
  .ink-band > .section-heading.in::after,
  .ink-band .section-heading.in .eyebrow { animation: none; }
}

/* ==========================================================================
   Two surfaces move to the dark ground, and the signature layer   (P5)
   ==========================================================================

   The category tiles and the BOQ console were the two things held back from
   the first band pass, on the grounds that both are read rather than looked
   at. Held, not refused: the reference has both on dark, the owner has asked
   twice for the page to look exactly like it, and neither carries a product
   card. So both go dark, and the contrast is measured rather than eyeballed.
   Only the 9,187 product cards stay on light paper, which was always the real
   rule underneath the placeholder one.
   ========================================================================== */

/* ---------- Category tiles on the band ---------- */

.ink-band .category-spotlight button {
  background: rgba(255, 255, 255, 0.045);
  border-color: rgba(255, 255, 255, 0.1);
  box-shadow: none;
}

.ink-band .category-spotlight button:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(184, 231, 106, 0.45);
}

.ink-band .category-spotlight b { color: #ffffff; }
/* 8.9:1 on the band ground. The tile blurb is the line that decides whether
   somebody clicks, so it is held well clear of AA rather than at it. */
.ink-band .category-spotlight small { color: rgba(233, 240, 250, 0.78); }
.ink-band .category-spotlight em { color: var(--brand-lime, var(--brand-lime)); }

.ink-band .category-spotlight .cat-glyph {
  background: rgba(184, 231, 106, 0.12);
  border-color: rgba(184, 231, 106, 0.22);
}

.ink-band .category-spotlight .cat-glyph .glyph { stroke: var(--brand-lime); }

/* ---------- The BOQ console ---------- */

/* The one place on the page somebody types. The reference puts its equivalent
   on dark and it works, but a paste area is not a decorative panel: the text
   in it is the reader's own and has to stay legible at 14px mono while a
   colour field moves behind the glass. Hence a heavier surface than the other
   cards on the band, and a solid field rather than a translucent one. */
.ink-band .boq-input,
.ink-band .boq-output {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.12);
  box-shadow: none;
}

.ink-band .boq-input label,
.ink-band .boq-output h3 { color: #ffffff; }

.ink-band .boq-input textarea {
  background: rgba(4, 10, 20, 0.72);
  border-color: rgba(255, 255, 255, 0.14);
  color: #eef4ff;
}

.ink-band .boq-input textarea::placeholder { color: rgba(233, 240, 250, 0.42); }

.ink-band .boq-input textarea:focus {
  border-color: rgba(184, 231, 106, 0.55);
  outline-color: rgba(184, 231, 106, 0.55);
}

.ink-band .boq-matches,
.ink-band .boq-unmatched { color: rgba(233, 240, 250, 0.8); }
/* 0.64 measured at 4.62:1 against the brightest ground the wash produces,
   which clears AA by a hair. The wash moves, and one sample is one moment, so
   this is the placeholder line held at 0.74 for headroom instead. */
.ink-band .boq-matches p { color: rgba(233, 240, 250, 0.74); }

.ink-band .file-button {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.2);
  color: #ffffff;
}

/* Corner brackets, from the reference's HUD frame. Four 20px angles rather
   than a full border: the panel reads as an instrument without gaining a box,
   and on a page whose whole subject is network hardware that is the right
   borrowed vocabulary. */
.ink-band .boq-panel > div { position: relative; }

.ink-band .boq-panel > div::before,
.ink-band .boq-panel > div::after {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  pointer-events: none;
  border-color: rgba(103, 232, 249, 0.55);
  border-style: solid;
}

.ink-band .boq-panel > div::before {
  top: 12px; left: 12px;
  border-width: 1px 0 0 1px;
}

.ink-band .boq-panel > div::after {
  bottom: 12px; right: 12px;
  border-width: 0 1px 1px 0;
}

/* ---------- P5: the signature layer ---------- */

/* The liquid-metal headline is gone, and this is the note explaining why,
   because it was the single rule keeping the neon from working.
   It poured a chrome gradient through the first hero line by painting the text
   fill transparent and clipping a background to the glyphs. Beautiful on its
   own; fatal underneath a glow. Every later attempt to light the headline was
   lighting a hole — a pale, semi-transparent letterform with nothing solid to
   throw light — which is exactly what the owner saw and named: dark, oversized,
   not luminous. Living far down the file, it also silently outranked the neon
   rules written six hundred lines above it, so each fix looked correct in the
   source and did nothing on the page.
   The headline's colour now has exactly one home, up in the neon block:
   solid tube, halo in text-shadow. Nothing down here may touch its fill. */

/* A sensor sweep behind the headline, one turn a minute, blurred past the
   point of being a shape. It is the slowest thing on the page on purpose:
   somewhere below the level of a moving object and at the level of the room
   being lit from a rotating source. */
.hero-copy::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -12%;
  left: -14%;
  width: 620px;
  height: 620px;
  max-width: 90vw;
  max-height: 90vw;
  border-radius: 9999px;
  filter: blur(38px);
  opacity: 0.55;
  pointer-events: none;
  will-change: transform;
  animation: data-ring 60s linear infinite;
  background: conic-gradient(
    from 0deg,
    rgba(34, 211, 238, 0) 0deg,
    rgba(34, 211, 238, 0.45) 60deg,
    rgba(59, 130, 246, 0.32) 130deg,
    rgba(34, 211, 238, 0) 200deg,
    rgba(165, 243, 252, 0.32) 280deg,
    rgba(34, 211, 238, 0) 360deg
  );
}

.hero-copy { position: relative; }

@keyframes data-ring {
  to { transform: rotate(360deg); }
}

/* Letter-spacing breathing on the headline: five thousandths of an em over
   twelve seconds. Far too small to see happening and just large enough to stop
   the headline reading as a static image. */
.hero-copy h1 { animation: tracking-breathe 12s ease-in-out infinite; }

@keyframes tracking-breathe {
  0%, 100% { letter-spacing: -0.045em; }
  50% { letter-spacing: -0.040em; }
}

/* The band scanline is gone. It was the reference's HUD sweep, one thin line
   crossing every band every six seconds, and the owner named it exactly:
   "there is something like a line passing". On their page it reads as a
   sensor; on a page whose bands now carry a live aurora it reads as a seam in
   the artwork, which is precisely how it was described. Removed rather than
   slowed, because a slower seam is still a seam.
   Its mobile and reduced-motion gates went with it. */


/* ---------- what a phone gets of all this ---------- */

@media (max-width: 768px) {
  /* The radar ring is a 620px blurred conic layer rotating forever behind the
     first thing anybody reads. It is the most expensive single effect here and
     the least visible at 390px, where most of it is off-screen. */
  .hero-copy::before { display: none; }

  /* One scanline per band across seven bands is seven permanently animating
     composited layers. Kept on the hero, dropped everywhere else, so the
     gesture is still on the page without being on all of it. */

  /* Letter-spacing is not a composited property: it relayouts the headline
     every frame, which on a phone is the difference between a smooth scroll
     and a janky one. */
  .hero-copy h1 { animation: none; }

  
}

@media (prefers-reduced-motion: reduce) {
  .hero-copy h1,
  .hero-copy::before,
  .hero-copy h1 .metal { animation: none !important; }
}

/* ==========================================================================
   The touch layer                                                    (P6)
   ==========================================================================
   touch.js supplies the numbers; these rules are the physics. Everything here
   is transform, opacity and a custom property, so nothing the pointer does
   can cause layout. */

/* The magnet. touch.js writes an inline translate capped at 5px; this
   transition is what makes it land like a spring rather than a jump, and what
   eases the button home when the cursor leaves. The overshoot curve is the
   reference's own. */
.magnet {
  transition: transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
  will-change: transform;
}

/* The idle sweep. Every 6.5 seconds a light crosses the primary action, once,
   quickly. It is the only looping motion on the light ground and it earns the
   exception: the primary CTA is the one element whose job is to be noticed.
   Paused the moment the cursor arrives, because by then it has worked. */
.button.primary { position: relative; overflow: hidden; }

.button.primary::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.45) 50%, transparent 70%);
  transform: translateX(-120%);
  animation: cta-idle-sweep 6.5s ease-in-out infinite;
  mix-blend-mode: screen;
  pointer-events: none;
  opacity: 0;
}

.button.primary:hover::after { animation-play-state: paused; opacity: 0; }

@keyframes cta-idle-sweep {
  0%, 88%, 100% { transform: translateX(-120%); opacity: 0; }
  92% { opacity: 0.8; }
  96% { transform: translateX(120%); opacity: 0.8; }
  97% { opacity: 0; }
}

/* The specular light. touch.js keeps --tx/--ty under the cursor; the gradient
   lives here, on the dark tiles only. On light paper a white light at overlay
   blend is invisible work, so the light ground never pays for it. */
.ink-band .category-spotlight button,
.ink-band .brand-cloud button,
.ink-band .solution-card {
  position: relative;
}

.ink-band .category-spotlight button::after,
.ink-band .solution-card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    circle 220px at var(--tx, 50%) var(--ty, 50%),
    rgba(255, 255, 255, 0.16),
    transparent 60%
  );
  opacity: 0;
  transition: opacity 0.35s ease;
  pointer-events: none;
  mix-blend-mode: overlay;
}

.ink-band .category-spotlight button:hover::after,
.ink-band .solution-card:hover::after {
  opacity: 1;
}

/* The sonar ring. Sized and placed by touch.js, gone 520ms later. */
.tap-ripple-ring {
  position: absolute;
  border: 1px solid rgba(165, 243, 252, 0.85);
  border-radius: 9999px;
  box-shadow: 0 0 12px rgba(34, 211, 238, 0.6);
  transform: scale(0);
  pointer-events: none;
  animation: tap-ripple-expand 520ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}

@keyframes tap-ripple-expand {
  from { transform: scale(0); opacity: 0.9; }
  to { transform: scale(1); opacity: 0; }
}

/* ---------- the hero stage ---------- */

/* The gallery holds eight real catalogue picks and shows four, rotating on the
   reference's 5.4s clock. The swap is a fade on the whole set rather than a
   shuffle of individual cards, because four images trading places reads as a
   glitch where one surface dimming and returning reads as a rotation. */
.hero-gallery { transition: opacity 0.45s var(--ease-signal); }


/* What a phone and reduced motion get: no magnet, no sweep, no light. The
   ripple stays on touch because acknowledging a tap is its whole job, and the
   stage stops turning under reduced motion (touch.js side). */
@media (hover: none), (prefers-reduced-motion: reduce) {
  .button.primary::after { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .magnet { transition: none; }
  .tap-ripple-ring { animation: none; opacity: 0; }
  .hero-gallery { transition: none; }
}

/* ---------- The footer becomes the sitemap ---------- */

/* The reference closes on four columns over the dark ground and ours now does
   the same, with one difference of substance: its links point at facade
   routes, ours point back into this page, where a brand link filters 9,187
   real rows. The registrations column is the one piece of pure text on this
   page that does trust work all by itself. */
.ink-footer {
  display: block;
  padding: clamp(48px, 6vw, 80px) var(--pad-x) 32px;
  color: rgba(233, 240, 250, 0.72);
  background: linear-gradient(180deg, #040a12 0%, #030710 100%);
}

/* The wordmark window trick from the header, same inversion, same reason:
   datasystems-white.svg draws only the leaf. */
/* The mark's ink fills only ~46% of its frame, so the frame is oversized to
   render the lettering at a readable weight, same arithmetic as the header. */
.ink-footer .footer-brand img {
  height: 76px;
  width: auto;
  margin: -18px 0 6px -10px;
  opacity: 1;
  filter: invert(1) hue-rotate(180deg);
}

.footer-grid {
  display: grid;
  grid-template-columns: minmax(260px, 1.4fr) repeat(3, minmax(150px, 1fr));
  gap: clamp(28px, 4vw, 56px);
  align-items: start;
  max-width: 1280px;
  margin: 0 auto;
}

.footer-brand p {
  margin: 0 0 18px;
  font-size: 14px;
  line-height: 1.65;
  max-width: 34ch;
}

.footer-contact { display: flex; flex-wrap: wrap; gap: 10px; }

.footer-contact .button.ghost {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.05);
}

.footer-grid h3 {
  margin: 4px 0 14px;
  color: rgba(233, 240, 250, 0.5);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.footer-grid nav { display: flex; flex-direction: column; gap: 9px; }

.footer-grid nav a {
  color: rgba(233, 240, 250, 0.82);
  text-decoration: none;
  font-size: 14px;
  width: fit-content;
  transition: color var(--t-fast) var(--ease-signal);
}

.footer-grid nav a:hover { color: var(--brand-lime, var(--brand-lime)); }

.footer-branches { display: flex; flex-direction: column; gap: 14px; }

.footer-branches address {
  font-style: normal;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
  font-size: 13px;
}

.footer-branches b { color: #ffffff; font-size: 14px; }

/* Registration numbers are read character by character when they are read at
   all, which is mono's whole job. */
.footer-branches .gst {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: rgba(233, 240, 250, 0.6);
}

.footer-branches .pan-india { font-size: 13px; color: rgba(233, 240, 250, 0.55); }

/* The AI image disclosure. One sentence, on every page, in the footer where
   provenance statements live. It reads at the same muted level as the
   baseline: a fact the desk states plainly, not a warning box. */
.footer-disclosure,
.note-disclosure {
  max-width: 1280px;
  margin: 36px auto 0;
  font-size: 13px;
  line-height: 1.55;
  color: rgba(233, 240, 250, 0.55);
}

.note-disclosure {
  margin: 44px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  color: var(--faint);
}

.footer-baseline {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  max-width: 1280px;
  margin: 40px auto 0;
  padding-top: 22px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 13px;
  color: rgba(233, 240, 250, 0.5);
}

@media (max-width: 900px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .footer-brand { grid-column: 1 / -1; }
}

@media (max-width: 560px) {
  .footer-grid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Deep Field: the bands become sky rather than ground              (P9)
   ==========================================================================

   The owner's note on the last build was that the background read as black
   with things on it, and that a white panel floating on black looked vague
   rather than impressive. He asked for something closer to an aurora: light
   green leading, constantly moving, resolving through vivid cosmic colour.

   What was actually holding the page dark was one line. The wash under the
   first band pass came out as a pastel rainbow, and the fix at the time was a
   flat graphite veil at 62% laid back over everything. That fixed the rainbow
   by throwing away the colour, and it is why every band since has read as
   near-black. The veil is now a tenth of that and the legibility it was
   buying is bought properly instead: locally, behind the text, where it is
   actually needed, rather than globally over artwork nobody can then see.

   Four changes, in the order they matter:

     1. the ground stops being near-black and becomes deep sky, with real
        indigo and teal in it, so even an unlit corner has colour;
     2. two aurora curtains drift across it on long counter-periods, which is
        the light-green-into-everything the owner described. Curtains rather
        than more orbs: an aurora is a sheet, and sheets are what make a sky
        read as weather instead of as decoration;
     3. the veil drops from 0.62 to 0.06 and a scrim appears behind copy only;
     4. the hero panel stops being a white slab and becomes dark glass, so the
        product photographs inside it read as lit tiles on an instrument
        rather than as a hole cut in the sky.
   ========================================================================== */

/* 1. The ground. Deep sky, not black: an unlit corner of this is still a
      colour, and the two ends differ so the band has a top and a bottom. */


.ink-band {
  background:
    radial-gradient(120% 90% at 82% 0%, rgba(52, 211, 153, 0.14), transparent 58%),
    radial-gradient(100% 80% at 8% 100%, rgba(99, 102, 241, 0.18), transparent 62%),
    linear-gradient(180deg, #050e20 0%, #05071a 52%, #030c16 100%);
  background-color: #050e20;
}

.ink-footer {
  background:
    radial-gradient(90% 70% at 80% 0%, rgba(52, 211, 153, 0.1), transparent 60%),
    linear-gradient(180deg, #05101c 0%, #04091a 100%);
}


/* Position and scale both move, so the sheet folds as it travels rather than
   sliding past like a backdrop on rails. */
@keyframes curtain-a {
  0%, 100% { background-position: 0% 20%; transform: scale(1); }
  33% { background-position: 60% 80%; transform: scale(1.12); }
  66% { background-position: 100% 30%; transform: scale(1.04); }
}

@keyframes curtain-b {
  0%, 100% { background-position: 100% 80%; transform: scale(1.08); }
  50% { background-position: 20% 10%; transform: scale(1); }
}


/* 3. The veil becomes a vignette rather than a sheet. Two curtains on screen
      blend can only ever add light, so where they cross the band was going
      milky and losing its depth. A vignette puts the dark back at the edges
      and leaves the lit centre alone, which is how a sky actually looks. */


/* The per-band scrims that used to live here are gone with the per-band
   fields. There were four generations of them: a soft pool behind the copy, a
   directional ramp across the hero, a vertical ramp across each band, and a
   nearly opaque top fifth. Every one was an attempt to light seven separate
   rectangles consistently, which is not a solvable problem. One field with
   one veil, above, replaces all of it. */

/* Cards carry their own dark now, rather than being a 4.5% white film that
   inherits whatever the sky is doing behind them. This is what lets the
   aurora be as vivid as it is: a tile is legible because the tile is dark,
   not because the whole band was dimmed to make it so.

   The depth of field is the second half of that sentence, and it took a
   genuinely absurd bug to notice it was missing. An orb drifted over the full
   stop in "Choose the material first." and, at 58% glass behind an 8px blur,
   came through as a hard bright ring sitting exactly where the period was. It
   read as a letter. The heading said "first O" and the markup said nothing of
   the sort.

   That is a lens problem, not a brightness problem. Eight pixels of blur does
   not defocus a 6px bright core — it smears it slightly and leaves it legible
   as a dot, so the field stayed in focus at every depth of the page and
   competed with the type at every one. The fix is to make the glass behave
   like glass: 24px of blur puts anything behind a card several centimetres
   out of focus, and the saturate keeps the colour that survives that blur
   worth having, so an orb passing behind a tile becomes a soft wash of its own
   hue rather than a dot with an outline.

   Dimming was the alternative and it is the wrong one. The orbs stay exactly
   as bright as they were, and in the negative space between cards — which is
   most of every band — they are untouched. They just fall out of focus where a
   reader is reading, which is what happens to a background in a photograph and
   what should have been happening here all along. */
.ink-band article,
.ink-band .card,
.ink-band .panel,
.ink-band .hub-grid > *,
.ink-band .solution-card,
.ink-band .industry-row > span,
.ink-band .category-spotlight button,
.ink-band .boq-input,
.ink-band .boq-output {
  background: rgba(5, 11, 24, 0.93);
  border-color: rgba(255, 255, 255, 0.14);
}

.ink-band .category-spotlight button:hover {
  background: rgba(8, 16, 32, 0.72);
  border-color: rgba(184, 231, 106, 0.5);
}

/* The plate's count sits on the band rather than on the white plate above
   it, so the button carries enough dark of its own to hold the figure. */
.ink-band .brand-cloud button { background: rgba(5, 11, 24, 0.66); }

/* 4. The panel. A white slab on a lit sky is the one thing on the page that
      read as unfinished, because it neither joins the sky nor sits on it. As
      dark glass it becomes part of the instrument, and the product plates
      inside it stay light, which is where white belongs: under a photograph
      of a part, not around it. */
.hero-panel {
  background:
    linear-gradient(180deg, rgba(8, 17, 32, 0.82), rgba(5, 11, 23, 0.74)),
    linear-gradient(135deg, rgba(52, 211, 153, 0.1), rgba(59, 130, 246, 0.08));
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}

.hero-panel .hero-points li { color: rgba(233, 240, 250, 0.86); }
.hero-panel .hero-points li > span { background: rgba(52, 211, 153, 0.16); }
.hero-panel .panel-note { color: rgba(233, 240, 250, 0.82); }

/* Text that sits on open sky rather than on a card gets its own shadow.
   This is the only honest fix for type over moving imagery: a scrim can be
   measured at one instant and be wrong at the next, because the curtain has
   drifted, whereas a shadow travels with the glyph and is correct at every
   frame by construction. Two of these tokens were measured at 3.84 and then
   3.18 on consecutive runs for exactly that reason. */
.ink-band .section-heading .sub,
.ink-band .section-heading p,
.hero-copy .lede,
.hero-panel .panel-note,
.ink-band .industry-row > span {
  text-shadow: 0 1px 3px rgba(3, 8, 18, 0.9), 0 2px 14px rgba(3, 8, 18, 0.75);
}

.ink-band h2,
.ink-band h3,
.hero-copy h1,
.hero-copy .eyebrow,
.ink-band .eyebrow {
  text-shadow: 0 2px 18px rgba(3, 8, 18, 0.55);
}


/* ---------- the phone, and reduced motion ---------- */

@media (max-width: 768px) {
  
  
  

  /* With one still curtain the sky is calmer, so the scrim can be lighter. */
  .hero-copy::after,
  .ink-band > .section-heading::before { inset: -10% -6%; }
}

@media (prefers-reduced-motion: reduce) {
  
}

/* ==========================================================================
   One sky for the whole document                                    (P10)
   ==========================================================================

   The owner's verdict on the last build, and he was right: the background was
   a crappy job next to the reference, and the section beginnings were not
   visible. Both had the same cause, and it was architectural rather than a
   matter of taste.

   The reference mounts its background ONCE, at the app shell, fixed to the
   viewport, and every section is simply transparent over it. This port put a
   separate field inside each band: seven auroras, seven canvases, seven
   veils. Two consequences followed inevitably. Every band boundary was a hard
   horizontal seam, because two independently lit rectangles meet at an edge.
   And each band needed its own scrim for its own text, so the scrims differed
   from band to band and the joins showed twice over: once in the artwork and
   once in the veil.

   No amount of tuning fixes that. Nine passes of adjusting curtain opacity,
   veil strength and ramp shape were all rearranging the same mistake. What
   fixes it is doing what the reference does: one field, one veil, and
   sections that carry no ground of their own at all.

   So: .page-field is fixed to the viewport and holds the aurora, the orbs and
   the canvas. Every dark section is transparent. One global scrim sits over
   the whole field, so the veil over the type is identical everywhere and
   there is nothing left to seam. Light sections paint opaque over the top,
   which is how the catalogue keeps its paper.
   ========================================================================== */

/* Near black, and that is the entire ground.
 *
 * The correction here is a subtraction. I read "aurora light green changing
 * into vibrant universe colours" as an instruction to build a sky, so a
 * rotating conic aurora and six 700px blurred colour blobs went in behind the
 * canvas, and a sky bright enough to see is bright enough to bury the orbs, so
 * it needed a 50% graphite scrim, which buried them anyway.
 *
 * The reference never had a sky. Its background component is the canvas and
 * nothing else, over --graphite, oklch(0.15 0.02 258): effectively black with
 * a trace of blue. The orbs are not lit BY a background, they ARE the
 * background, and they read because nothing behind them is competing.
 *
 * Measured over the hero, four frames half a second apart, before and after:
 *
 *                    aurora + scrim     canvas on black
 *   pixels lit             84.0%              2.9%
 *   pixels carrying colour 17.4%              0.8%
 *   bright cores            0.53%             0.59%
 *   eyebrow contrast        4.87:1           10.17:1
 *
 * Read that honestly: this is a much DARKER page, not a brighter one, and
 * less of the frame is in motion. That is the point. The old field lit 84% of
 * the screen and put colour across 17% of it, and the orbs were lost inside
 * their own background — the same count of bright cores, spread over a wash
 * that never let any one of them read as a light. Now the only thing painting
 * is the thing that was meant to, so the same lights are discrete and legible
 * against black, and the type gained a contrast ratio it never had.
 */
.page-field {
  position: fixed;
  inset: 0;
  /* Pinned to the LARGEST the viewport ever gets, not the current one.
     A phone hides its URL bar as you scroll and shows it again as you scroll
     back, which changes the viewport height every few hundred pixels of
     travel. With inset:0 alone the field resized on each of those, and a
     resize reseeds it: the orbs stopped dead and re-formed somewhere else,
     over and over, for the whole length of a scroll. lvh is the tall state,
     so the height never changes, so nothing reseeds and the drift is
     continuous. The extra strip below the fold is never seen. */
  height: 100vh;
  height: 100lvh;
  z-index: -2;
  overflow: hidden;
  pointer-events: none;
  background: radial-gradient(130% 100% at 50% -10%, #0c1526 0%, #070a12 46%, #04050a 100%);
}

/* The canvas fills the fixed host. */
.page-field .field-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  -webkit-mask-image: none;
  mask-image: none;
}

/* Sections stop carrying grounds. This is the whole fix in four lines: with
   nothing painted per band, there is no band to see the edge of. */
.hero,
.ink-band,
.ink-band::before,
.ink-band::after,
.hero::after {
  background: none;
}

.hero,
.ink-band {
  background-color: transparent;
  overflow: visible;
}

/* The two remaining light grounds paint opaque, over the sky rather than
   through it, because a product grid is read and a part code is copied off
   it. That was always the one rule under all of this. */
#catalogue {
  position: relative;
  z-index: 0;
  background: var(--canvas);
}

/* The marquee used to be in that rule, painting opaque paper. It was built as
   a bridge between the dark bands above and the light catalogue below, back
   when only some of the page was dark. Every band went near-black after that,
   and the bridge stopped bridging: it became a pale slab sitting on a dark
   page with two masked edges, which is what the owner was describing when he
   said a grey scroller appears out of nowhere.
   It belongs to the dark page now. The transition into the catalogue is
   already drawn by #catalogue::before, which is an 80px gradient sitting above
   the grid, so nothing is lost by taking the slab out of the middle. */
.logo-marquee {
  position: relative;
  z-index: 0;
  background: transparent;
}

/* #desk used to be in that list and it was the seam. It is editorial content,
   it belongs on the sky, and it is a band now. Removing it from the rule is
   the fix; the later class-based override could never win, because an id
   outranks any number of classes however far down the file it sits. */
#desk { position: relative; z-index: 0; }

/* The join between sky and paper, in both directions, so neither meets the
   other at a ruled line. */
#catalogue::before {
  content: "";
  position: absolute;
  inset: -44px 0 auto 0;
  height: 44px;
  pointer-events: none;
  background: linear-gradient(180deg, var(--canvas-0), var(--canvas));
}

/* The marquee cannot use that. It clips itself three ways over — overflow
   hidden to hold the moving track, `contain: paint`, and a horizontal mask to
   fade the ends — so a fade parked outside its box is thrown away before it
   ever paints. Both of its edges have been ruled lines since the sky went in,
   and it sits directly under the hero, which makes it the first hard cut on
   the page and the one the owner kept pointing at.
   The fix has to live inside the box, so the ground itself is the gradient:
   sky at the very edge, paper by the time the logos start. Nothing bleeds
   outside, so nothing can be clipped. Logos keep full opacity because it is
   the background that melts, not the element. */
/* Nothing to melt any more: the strip is transparent and the field runs behind
   it like every other band. The plates carry their own white, which is what
   lets a dark wordmark and a bright one share a row. */
.logo-marquee { background: transparent; }

/* Light type, because the ground under it is the field now. */
.logo-marquee .marquee-head h2 { color: #ffffff; }
.logo-marquee .marquee-head p { color: rgba(233, 240, 250, 0.72); }

/* ---------- The brand sheet ---------- */

/* Same chassis as the product sheet, sized for prose rather than a spec
   table. The plate keeps its white ground inside the sheet for the same
   reason it keeps it on the wall: the artwork is not ours to recolour. */
.brand-dialog { max-width: 640px; }

.brand-profile-head {
  display: flex;
  align-items: center;
  gap: 18px;
  margin-bottom: 18px;
}

.brand-profile-head .brand-plate {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 132px;
  height: 76px;
  padding: 10px 14px;
  border-radius: 14px;
  background: #ffffff;
  border: 1px solid var(--line);
}

.brand-profile-head h2 { margin: 0 0 4px; font-size: 26px; }

.brand-line-note {
  margin: 0;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The story is the sheet now: the inventory arithmetic that used to sit
   under it is gone at the owner's instruction, so the prose takes the room
   and a size worth the click. */
.brand-story { margin: 0 0 22px; font-size: 17px; line-height: 1.75; color: var(--ink-2); }

.brand-desk {
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  margin-bottom: 20px;
}

.brand-desk h3 {
  margin: 0 0 8px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--green-deep);
}

.brand-desk p { margin: 0; line-height: 1.65; color: var(--ink-2); }
.brand-desk strong { display: block; margin-top: 10px; color: var(--ink); }

.brand-actions { display: flex; flex-wrap: wrap; gap: 12px; }

@media (max-width: 560px) {
  .brand-profile-head { flex-direction: column; align-items: flex-start; }
}

/* The quiet line under the contact band's hours: a reader whose project has
   slipped is exactly who is on this page at 11pm, and the link is pitched at
   them rather than at everyone. */
.contact-waiting a {
  color: rgba(184, 231, 106, 0.85);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.contact-waiting a:hover { color: #d6f29a; }

/* ---------- About: the company profile, on the page ---------- */

/* The owner's Canva profile, retold rather than reproduced. The original PDF
   is hosted for download beside it, which is exactly why this copy is allowed
   to be warmer than the document: nobody needs the page to BE the PDF when the
   PDF is one click away. Treatment is the reference's dark-glass system: mono
   stat tiles, three glass cards for before/during/after, and the mission line
   in lime, which is the one lime element this band spends. */
.about-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 28px;
}

.about-stats > div {
  display: grid;
  gap: 4px;
  padding: 16px 18px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.04);
}

.about-stats b {
  font-family: var(--mono);
  font-size: clamp(22px, 2.2vw, 30px);
  font-weight: 600;
  color: var(--brand-lime);
  letter-spacing: -0.01em;
  font-feature-settings: "ss01", "ss02";
}

.about-stats span {
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
}

.about-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

.about-grid article {
  padding: 20px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.04);
  transition: transform 300ms var(--ease), border-color 300ms var(--ease), background 300ms var(--ease);
}

.about-grid article:hover {
  transform: translateY(-2px);
  border-color: rgba(184, 231, 106, 0.6);
  background: rgba(255, 255, 255, 0.07);
}

.about-grid h3 { margin: 0 0 10px; font-size: 17px; color: #ffffff; }
.about-grid p { margin: 0; font-size: 14px; line-height: 1.65; color: rgba(255, 255, 255, 0.68); }

.about-close { max-width: 820px; margin-top: 30px; }
.about-close p { margin: 0 0 20px; color: rgba(233, 240, 250, 0.8); line-height: 1.7; }
.about-close em {
  display: block;
  margin-top: 12px;
  font-style: normal;
  font-weight: 650;
  color: var(--brand-lime);
}

.about-actions { display: flex; flex-wrap: wrap; gap: 12px; }

@media (max-width: 900px) {
  .about-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .about-grid { grid-template-columns: 1fr; }
}

/* ---------- what the phone gets ---------- */

@media (max-width: 768px) {
  /* One fixed background layer on a phone is cheaper than seven, which is the
     other quiet dividend of this change: the field is now viewport sized no
     matter how long the document is. */
  .page-field { background-attachment: scroll; }
}

/* The body's own ground has to go, or it paints over a field that sits behind
   it: a fixed layer at z-index -2 is still behind the body's background box.
   The root keeps the deep sky so an overscroll bounce at either end shows the
   page's own colour. */
html { background: #030d18; }
body { background: transparent; }

/* The old body aurora is gone from the markup; its rules would otherwise sit
   in the stylesheet forever describing elements that no longer exist. */
.aurora,
.blob,
.blob-a,
.blob-b,
.blob-c { display: none; }

/* The hero's copy sits on the most open part of the sky, and with the per-band
   ramp gone it measured 4.61:1 and 4.89:1: passing, but by less than any other
   token on the page. Rather than reintroduce a section-shaped scrim, which is
   the thing that caused the seams, the shadow under the type is deepened. A
   shadow travels with the glyph, so it costs the sky nothing. */
.hero-copy .eyebrow,
.hero-copy .lede {
  text-shadow: 0 1px 3px rgba(3, 8, 18, 0.95), 0 2px 16px rgba(3, 8, 18, 0.85);
}

.hero-copy .lede { color: rgba(240, 246, 255, 0.96); }
.hero-copy .eyebrow { color: #c6f07a; }

/* The seam the owner kept pointing at, found by measuring rather than by
   looking: a mean-row scan across the junction showed a 533-point jump from
   rgb(245,245,247) to rgb(72,59,72) in one pixel row. That was #desk, the
   signals section, which had been given an opaque light ground and
   so sat in the middle of the sky as a white block with a knife edge along the
   bottom of it. One continuous field behind the page cannot help with a
   section that paints over the top of it.

   Editorial content belongs on the sky, so #desk becomes a band like the rest
   and the page is left with exactly one light region: the catalogue, where
   part codes are read and copied. */
.section.desk-columns.ink-band { background: none; }

.ink-band .desk-grid > * {
  background: rgba(5, 11, 24, 0.9);
  border-color: rgba(255, 255, 255, 0.14);
}


/* Every light region now meets the sky on a fade at BOTH edges. The top fades
   existed; the bottom ones did not, which is precisely why the cut showed at
   the foot of a light block and not at its head. The marquee handles its own
   two edges in its background, above, because it clips. */
#catalogue::after {
  content: "";
  position: absolute;
  inset: auto 0 -44px 0;
  height: 44px;
  pointer-events: none;
  background: linear-gradient(180deg, var(--canvas), var(--canvas-0));
}


/* Everything else still carrying paper inside a band. Found by walking the
   DOM and measuring computed luminance rather than by reading the stylesheet,
   because these are five different components written at five different times
   and no grep would have caught the set. `.brand-plate` is deliberately
   excluded: white plates are how brand marks are meant to sit on a dark wall,
   and that one is the reference's own treatment. */
.ink-band li.signal,
.ink-band .boq-unmatched,
.ink-band .solution-card li,
.ink-band .solution-card ul > li {
  background: rgba(5, 11, 24, 0.55);
  border-color: rgba(255, 255, 255, 0.12);
  color: rgba(233, 240, 250, 0.86);
}

.ink-band li.signal p,
.ink-band li.signal .signal-body { color: rgba(233, 240, 250, 0.8); }
.ink-band li.signal h3,
.ink-band li.signal h4 { color: #ffffff; }

.ink-band .boq-unmatched { color: rgba(233, 240, 250, 0.82); }


/* ==========================================================================
   Reading pools                                                       (P8)
   ==========================================================================
   The depth-of-field pass above fixed every surface that HAS a surface. It
   does nothing for the copy that sits on bare field — section headings, their
   standfirsts, the band eyebrows — and that is the copy the reader meets
   first in every band. On the "Two sides of every project" band the field runs
   at full brightness straight through the paragraphs.

   The instinct is a band-wide veil. That was built once, shipped, and was
   wrong: it dimmed the orbs everywhere to solve a problem that exists only
   where the type is, and the owner named the result exactly — the orb play
   looked dull. So this is the opposite move. A pool is an ellipse of the
   page's own dark, sized to the text block, feathered to nothing well before
   its edge, and sitting behind the type only. Two-thirds of the way out it is
   already invisible; the negative space between blocks never sees it.

   Feathering is the entire trick, and the reason the stops are uneven. A
   linear fade to transparent leaves a visible oval — the eye finds the edge of
   a soft shape far more readily than it finds the shape itself. Easing the
   alpha down early and long means there is no edge to find, only copy that
   happens to sit on slightly deeper dark. */

.section-heading,
.contact-band > div:first-child {
  position: relative;
}

.section-heading::before,
.ink-band.contact-band > div:first-child::before {
  content: "";
  position: absolute;
  /* Well outside the text box. A pool the size of its block has its densest
     part under the type and its edge under the type too, which is the one
     place the edge must not be.

     Vertical inset in pixels and horizontal in per cent looks inconsistent and
     is deliberate. The first version used per cent for both, so the pool
     scaled with the block: a tall three-line heading got a generous one and
     "Built for real sites." — two lines, the shortest block on the page — got
     almost none. That is backwards. A short heading is large type sitting on
     bare field and needs the pool most, not least. Pixels vertically give
     every block the same feathering distance whatever its height; per cent
     horizontally still lets a wide block spread wider than a narrow one, which
     is the one axis where scaling with the box is the right instinct. */
  inset: -60px -9%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    62% 58% at 34% 50%,
    rgba(3, 8, 18, 0.82) 0%,
    rgba(3, 8, 18, 0.74) 26%,
    rgba(3, 8, 18, 0.52) 48%,
    rgba(3, 8, 18, 0.24) 68%,
    rgba(3, 8, 18, 0.07) 84%,
    rgba(3, 8, 18, 0) 100%
  );
}

/* The pool is anchored left because the type is. Centred headings get a
   centred pool, or the copy sits on the bright side of its own shadow. */
.section-heading.is-centred::before,
.section-heading[style*="center"]::before {
  background: radial-gradient(
    58% 58% at 50% 50%,
    rgba(3, 8, 18, 0.82) 0%,
    rgba(3, 8, 18, 0.74) 26%,
    rgba(3, 8, 18, 0.52) 48%,
    rgba(3, 8, 18, 0.24) 68%,
    rgba(3, 8, 18, 0.07) 84%,
    rgba(3, 8, 18, 0) 100%
  );
}

/* Light bands have no field behind them and would get a grey smudge. */
.section:not(.ink-band) .section-heading::before,
.paper-field .section-heading::before { display: none; }

/* A phone is nearly all reading area, so a pool per block would add up to the
   band-wide veil this pass exists to avoid. The cards there already carry
   their own dark, and the hero is the only place the field is meant to be the
   loudest thing on screen. */
@media (max-width: 768px) {
  .section-heading::before,
  .ink-band.contact-band > div:first-child::before { display: none; }
}


/* ==========================================================================
   Measure, balance, and where an anchor lands                          (P9)
   ==========================================================================
   Three separate things, grouped because they are all the same mistake: type
   set by the width of its container rather than by what a person can read. */

/* 1. Measure. A standfirst inside an 820px heading block runs to roughly 95
      characters at desktop, and the eye starts losing its place returning to
      the left margin somewhere past 75. The heading itself can stay wide — a
      three-word line has no return sweep to lose — so this caps the running
      text only. */
.section-heading .sub,
.section-heading > p,
.ink-band .hub-grid > * > p,
.about-band p,
.partner-hub .hub-grid p {
  max-width: 68ch;
}

/* 2. Balance. "If you are the one installing it" broke as five words and then
      a lone "it", which is the single most conspicuous thing a heading can do
      — the reader's eye is drawn to exactly the word carrying the least. The
      browser will distribute the words evenly across the lines if asked, and
      it is only worth asking on short blocks, which is why this is on headings
      and standfirsts rather than on body copy. */
.ink-band h3,
.hub-grid h3,
.solution-card h3,
.section-heading .sub {
  text-wrap: balance;
}

/* 3. Where an anchor lands. Every nav link is a same-page jump and the header
      is fixed, so before this the browser scrolled each target to y=0 and the
      header covered the heading the reader had just asked to see. They arrived
      at a section whose name was hidden behind the thing they clicked it in.
      The extra 24px is breathing room, so the heading arrives just below the
      header rather than welded to its underside.

      Both numbers are measured rather than assumed. The first draft said 64px,
      which is the nav row — but the header is not the nav row: a contact strip
      sits above it, and the rendered heights are 107px and 131px. A 64px
      offset still looked correct on this page only because section padding was
      covering the shortfall, which is precisely the kind of accident that
      breaks the day a section is given less padding. */
:root { --header-h: 107px; }

main > section[id],
#catalogue,
.site-footer[id] {
  scroll-margin-top: calc(var(--header-h) + 24px);
}

/* The strip and the nav sit on separate rows on a phone, so the header is
   taller there and an anchor landing under it is the same bug at a different
   height. */
@media (max-width: 768px) {
  :root { --header-h: 131px; }
}


/* ==========================================================================
   One card family, one rhythm, one focus ring                         (P10)
   ==========================================================================

   1. RADIUS. Measured across the page, the surfaces came back at 26, 12, 999,
      0 and 26 pixels. Three of those are deliberate and two were drift. A pill
      at 999 is a pill because it holds one word; a hub column at 0 is a ruled
      column and not a card at all. But a brand tile at 12 sitting two bands
      away from a solution card at 26 is the same object rounded two different
      ways, and that is the kind of difference a reader cannot name and can
      always feel.

      So radius becomes a scale with a reason rather than a number per
      component: large surfaces that hold a heading and a paragraph, small
      tiles that hold a mark or a label, and pills. A tile keeps a tighter
      radius than a card on purpose — curvature should read as constant around
      a shape, so a small box needs a smaller radius to look equally round —
      but it is now one deliberate step down rather than an unrelated value. */
:root {
  --r-card: 26px;
  --r-tile: 16px;
  --r-pill: 999px;
}

/* 2. GLASS. The brand tiles were mixed at 66% while every other surface on a
      band sat at 76%, so the one grid built entirely of other companies'
      logos was the one letting the field through hardest — behind artwork
      that is nothing but flat colour and fine lettering. */
.ink-band .brand-cloud button {
  /* Was dark glass at 76%, letting the field glow through every logo. The
     owner's ruling: every tile is one clean white plate, because half the
     marks ship with a white background baked in and half ship transparent,
     and only a solid white ground renders the two identically. */
  background: #ffffff;
  border-radius: var(--r-tile);
}

/* 3. RHYTHM. Junction gaps down the page measured 349, 230, 159, 202, 230,
      230, 230, 231, 231 and 141 pixels. The 230s are the page's real rhythm
      and everything else is drift, with two exceptions worth keeping: the
      opening gap under the hero is larger because the hero is the one band
      that should be allowed to end, and the contact band is an inset card
      rather than a band, so it carries its own margin and sits closer.

      The 159 came from a bare `padding-top: 30px` on the requirement bench —
      no comment, no reason findable in the history, and the effect was that
      the bench crowded the band above it while every other section breathed. */
.boq-workbench { padding-top: clamp(56px, 7vw, 104px); }

/* 4. FOCUS. Tabbing the page end to end found 31 distinct controls and five
      with no visible focus state whatsoever: every text input, every select,
      and one button. Not a faint ring — nothing. A keyboard reader filtering
      the catalogue had no way to know which field they were typing into.

      The cause is ordinary and worth naming: buttons and links inherited the
      global focus ring, and form controls did not, because their `:focus`
      rules set a background and a box-shadow and browsers drop the default
      outline the moment an author styles the element at all. The shadow that
      replaced it was `rgba(0, 0, 0, 0)` on the dark header — a focus ring made
      of nothing.

      focus-visible rather than focus, so a pointer user clicking into the
      search box does not get a ring they did not ask for, while a keyboard
      user always does. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--brand, var(--green));
  outline-offset: 2px;
}

/* On a dark band the brand green is the wrong ring: it is close in value to
   the ground and disappears at 2px. The headline's own lime carries far more
   luminance and is already the page's accent for "this one". */
.ink-band input:focus-visible,
.ink-band select:focus-visible,
.ink-band textarea:focus-visible,
.ink-band [tabindex]:focus-visible,
.site-header input:focus-visible,
.site-header select:focus-visible {
  outline-color: var(--brand-lime);
}

/* Windows high-contrast mode throws author colours away and keeps the
   outline, so this is the one place a system keyword beats a token. */
@media (forced-colors: active) {
  input:focus-visible,
  select:focus-visible,
  textarea:focus-visible,
  button:focus-visible,
  a:focus-visible { outline: 2px solid Highlight; }
}

/* The hero chips were the last control on the page with no focus state. They
   are the first thing after the search box in the tab order, so a keyboard
   reader met the page's dead spot immediately. They set `outline: solid` with
   a zero width somewhere in their own rules, which reads as styled and paints
   as nothing — the one failure mode a quick look at the source will not catch,
   because the property is present. */
.hero-chips button:focus-visible {
  outline: 2px solid var(--brand-lime);
  outline-offset: 2px;
}


/* ==========================================================================
   The phone, at arm's length                                          (P11)
   ==========================================================================
   Two things, and neither is an accessibility failure. That matters, because
   the temptation was to call them one and inflate the fix.

   The page was walked target by target against WCAG 2.5.8, which asks for
   24x24 CSS px unless a 24px circle centred on the target clears every other
   target's circle. Every control passes, most of them through that spacing
   exception: the footer's brand links are 21px tall and stacked with enough
   room between them that the rule is satisfied. There is no violation here to
   fix, and saying otherwise would be a way of making routine polish sound
   like a bug fix.

   What is true is that a phone is held at arm's length in one hand, and a
   34px chip under a moving thumb is a miss waiting to happen. 44px is Apple's
   number and it is a good one. So this is comfort, stated as comfort.

   The second is type size. A 10.5px availability badge is below what anybody
   reads comfortably on a phone, and these are the labels carrying the least
   redundant information on the card: a part code, a country, whether a line is
   frequently supplied. 12px is the floor. */

@media (max-width: 768px) {
  /* Reach. Padding rather than height where the control is text, so the target
     grows without the type growing with it. */
  .hero-chips button {
    min-height: 44px;
    padding: 0 18px;
  }

  .site-header .header-search input,
  .nav-actions .button,
  .mobile-menu {
    min-height: 44px;
  }

  .mobile-menu { min-width: 44px; }

  /* The in-content controls a reader actually drives the catalogue with: the
     filter drawer and its reset at 32px, and the buttons ending each signal
     card at 31px, which was the smallest anything got on this page.

     Selector depth is not decoration here. The first attempt at this rule was
     written as `.filter-toggle` and lost, because the 32px it was overriding
     is set by `.filters .filter-header button`: three classes and an element
     beat one class, so the shorter selector never applied and the control
     stayed 32px while the source read as though it had been fixed. */
  .filters .filter-header button,
  .signal-actions > button,
  .signal-actions > a,
  .button {
    min-height: 44px;
  }

  .site-header .brand {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Legibility. The floor, not a rescale: everything already at or above 12px
     is left exactly where the type scale put it. */
  /* Written at the depth of the rules they override, which is the whole
     lesson of this block. The first version listed these as bare classes and
     changed nothing: every one of them is sized by a two-class selector
     (`.card-tags .avail`, `.product-card .sku`, `.footer-branches .gst`,
     `.eyebrow.band-number`), so a single class never got near them. The
     stylesheet read as though the floor had been set and the phone still
     rendered 10.5px badges. */
  .card-tags .avail,
  .detail-tags .avail,
  .product-card .sku,
  .footer-branches .gst,
  .eyebrow.band-number,
  .eyebrow,
  .line-label,
  .column-note,
  .signal-tag,
  .compare-title,
  /* The three remaining spans, found by walking the DOM rather than guessing
     at class names: the brand chips inside a category tile, and the field
     labels in the filter drawer and the quote form. A form label at 11.5px is
     the worst of the set, because it is the only thing telling a reader what
     they are about to type into. */
  .cat-brands > span,
  .filter-body label > span,
  .rfq-buyer-grid label > span {
    font-size: 12px;
  }

  /* A band eyebrow is caps with wide tracking, and at 12px that tracking
     starts pulling the words apart. Tightened to match. */
  .eyebrow,
  .eyebrow.band-number { letter-spacing: 0.06em; }
}


/* The desk's own section on the product sheet. It reads as an answer rather
   than as one more outbound link, which is what it was when it sat as the last
   chip in the manufacturer's row. */
.detail-desk .desk-links {
  margin-top: 12px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

.detail-desk .desk-primary {
  background: var(--green, var(--green));
  border-color: transparent;
  color: #ffffff;
  font-weight: 600;
}

.detail-desk .desk-primary:hover {
  background: #0b8a60;
  color: #ffffff;
}


/* ---------- pick up where you left off ---------- */

/* The reader's own history, offered back as a quiet strip above the grid. It
   competes with nothing: one muted label, part codes as chips, and it is not
   there at all until this browser has opened at least two sheets. */
.recent-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 18px;
}

.recent-strip .recent-label {
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin-right: 4px;
}

.recent-strip a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: #ffffff;
  color: var(--ink-2);
  font-family: var(--mono, monospace);
  font-size: 13px;
  transition: border-color 180ms var(--ease), color 180ms var(--ease);
}

.recent-strip a:hover,
.recent-strip a:focus-visible {
  border-color: var(--green, var(--green));
  color: var(--ink);
}

/* Long product names still make usable chips. */
.recent-strip a { max-width: 240px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; line-height: 32px; }

/* ---------- brand and range pages ----------
   Server-rendered in the note page's language; these rules are only the parts
   a note does not have. The family list is the page's substance and its
   internal links, so it reads as content, not as tags. */

.landing-sheet { max-width: 760px; }

.landing-families { margin-top: clamp(30px, 4vw, 44px); }

.landing-families h2 {
  margin: 26px 0 12px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(233, 240, 250, 0.5);
}

.landing-families ul {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.landing-families li a {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 5px 13px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.045);
  font-size: 14px;
  color: rgba(233, 240, 250, 0.88);
  transition: border-color 180ms var(--ease), background 180ms var(--ease);
}

.landing-families li a:hover {
  border-color: rgba(184, 231, 106, 0.5);
  background: rgba(184, 231, 106, 0.1);
  color: #fff;
}

.landing-more {
  margin: 12px 0 0;
  font-size: 13px;
  color: rgba(233, 240, 250, 0.55);
}

/* The brand index: a name and its one-line identity, as a column. */
.landing-brandlist ul { flex-direction: column; gap: 6px; }

.landing-brandlist li a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  width: 100%;
  min-height: 44px;
  padding: 10px 16px;
  border-radius: var(--r-md);
}

.landing-brandlist b { font-size: 16px; color: #fff; }
.landing-brandlist span { font-size: 13px; color: rgba(233, 240, 250, 0.55); }


.whatsapp-float span { display: none; }


/* ---------- questions buyers ask ----------
   Native details/summary: crawlable text with zero script, which is the whole
   point of the section. The open state is the reader's, not an animation's. */
.faq-band { display: block; }
.faq-head { max-width: 62ch; margin-bottom: 22px; }
.faq-list { max-width: 74ch; display: grid; gap: 10px; }
.faq-list details {
  background: rgba(9, 16, 30, 0.72);
  border: 1px solid rgba(148, 163, 184, 0.22);
  border-radius: 12px;
  padding: 2px 18px;
}
.faq-list summary {
  cursor: pointer;
  padding: 14px 0;
  font-weight: 600;
  font-size: 16px;
  list-style-position: inside;
}
.faq-list summary:focus-visible { outline: 2px solid var(--lime, #b8e76a); outline-offset: 2px; }
.faq-list details > p { margin: 0 0 16px; color: #c7d2e4; line-height: 1.55; font-size: 15px; }

/* The hero names its city in one quiet line under the lede. */
.hero-place { margin: 10px 0 0; font-size: 15px; color: rgba(219, 229, 244, 0.78); max-width: 52ch; }

/* The landing pages' one-line letterhead. */
.note-contact { margin: 26px 0 0; font-size: 13.5px; color: rgba(219, 229, 244, 0.7); line-height: 1.6; }
.note-contact a { color: inherit; }
