/* ==========================================================================
   Layer8Secure — design system

   A precision instrument, not a brochure. Engineered dark ground, modular
   panels with real strokes, and a monospace data layer that carries most of
   the personality.

   Deliberately not the HatchTag language: that site is warm, soft-cornered,
   translucent and grotesk throughout. This one is cold, low-radius, opaque,
   and reads as a console.
   ========================================================================== */

:root {
  /* Engineered dark: a blue-grey black, not a warm one. */
  --bg:      #08090c;
  --bg-2:    #0b0d12;
  --panel:   #0e1116;
  --panel-2: #12161d;
  --panel-3: #171c25;

  --text:   #e8ecf2;
  --text-2: rgba(232,236,242,0.62);
  --text-3: rgba(232,236,242,0.40);
  --text-4: rgba(232,236,242,0.24);

  /* Strokes are the structure here — real 1px lines, not soft shadows. */
  --line:   #1b2029;
  --line-2: #242b36;
  --line-3: #313a49;
  /* The light catching the top edge of a raised surface. */
  --edge:   rgba(255,255,255,0.045);

  /* One signal: cold mint. Marks Layer8's own domain — the eighth layer,
     live status, focus. HatchTag's accent is warm amber; these two must not
     be mistakable for each other. */
  /* The second signal, and the only one. This system has been deliberately
     single-colour — mint means "ours, live, good" and nothing else competed
     with it. The scroll trace needed a fault state, and a fault drawn in the
     safe colour is not a fault. An instrument that can only ever show green
     is a logo, not an instrument.

     It appears in two places, and both mean the same thing — something
     hostile, arriving: the fault half of the signal trace, and the attack
     arcs on the hero globe. A second red would have been the mistake.
     Nothing else on this site is allowed to use it. */
  --alarm:      #ff6a4d;
  --alarm-line: rgba(255, 106, 77, 0.34);

  --sig:      #3adfa8;
  --sig-dim:  rgba(58,223,168,0.10);
  --sig-line: rgba(58,223,168,0.32);
  --sig-text: #6fecc0;

  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI",
          system-ui, Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
          "Liberation Mono", monospace;

  /* Low radius: machined, not pillowy. 999px is the other site's voice and
     does not belong on a panel, a card or a button here. The one exception is
     the shop's filter chips, which are toggles rather than surfaces — see
     .filter. */
  --r:    0.375rem;
  --r-lg: 0.5rem;

  --gutter:  clamp(1.25rem, 4vw, 3rem);
  --measure: 80rem;
  /* 4rem, not 3.5. The controls in the bar are 44px tall by touch-target
     rule; at 56px that left six pixels of air above and below them and the
     whole row read as squeezed. The room was the problem, not the parts. */
  --nav-h:   4rem;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --t-fast: 120ms;
  --t:      220ms;
  --t-slow: 420ms;

  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 2rem);

  /* Keep the scrollbar's space reserved even when there is no scrollbar.
   *
   * Opening the cart or the menu locks the page by setting overflow:hidden
   * on this element, which takes the scrollbar away and hands its 15px back
   * to the layout. Everything shifted right as the drawer opened and left
   * again as it closed — and on the home page it was worse than a shift,
   * because main changing width is exactly what the signal trace watches
   * for: the drawer was triggering a full rebuild of the path on the way in
   * and another on the way out.
   *
   * Reserving the gutter permanently means locking the scroll changes no
   * layout at all, so there is nothing to shift and nothing to rebuild.
   * Where scrollbars are overlays rather than gutters — macOS, iOS — the
   * reserved space is zero and this does nothing, which is correct: there
   * was never a shift to fix there. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  font-family: var(--sans);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img, svg { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
button, input, textarea { font: inherit; color: inherit; }

:focus-visible { outline: 1px solid var(--sig); outline-offset: 2px; border-radius: 2px; }
::selection { background: var(--sig-dim); color: #fff; }

.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.skip {
  position: fixed; top: 0.75rem; left: 0.75rem; z-index: 200;
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0.75rem 1rem;
  border-radius: var(--r);
  background: var(--sig); color: #04140e;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transform: translateY(-200%);
  transition: transform var(--t) var(--ease);
}
.skip:focus { transform: none; }

/* --- Backdrop -------------------------------------------------------------
   A machined grid with a faint cool wash. No big blurred colour blob — that
   reads "marketing site"; this should read "surface". */
.backdrop {
  position: fixed; inset: 0; z-index: -1;
  background:
    radial-gradient(ellipse 80% 50% at 50% -10%, rgba(58,223,168,0.055), transparent 70%),
    var(--bg);
  pointer-events: none;
}
.backdrop::after {
  content: "";
  position: absolute; inset: 0;
  background-image:
    linear-gradient(to right, rgba(255,255,255,0.016) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255,255,255,0.016) 1px, transparent 1px);
  background-size: 3rem 3rem;
  mask-image: radial-gradient(ellipse 100% 70% at 50% 0%, #000 0%, transparent 80%);
  -webkit-mask-image: radial-gradient(ellipse 100% 70% at 50% 0%, #000 0%, transparent 80%);
}

/* --- Layout --------------------------------------------------------------- */
.wrap {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
main { flex: 1; }

.section { padding-block: clamp(3.5rem, 7vw, 6rem); }
.section + .section { padding-top: 0; }

.rule { height: 1px; background: var(--line); border: 0; margin: 0; }

/* --- Panel ----------------------------------------------------------------
   The core object. Opaque, stroked, low radius, with a single hairline of
   light on the top edge. Nearly everything on the page lives in one. */
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: inset 0 1px 0 var(--edge);
  overflow: hidden;
}
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 1rem;
  background: var(--bg-2);
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
}
.panel-head .idx { color: var(--text-4); font-variant-numeric: tabular-nums; }

/* --- Type ----------------------------------------------------------------- */

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.d1 {
  font-size: clamp(2.125rem, 4.6vw, 3.5rem);
  line-height: 1.1;
  letter-spacing: -0.028em;
}
.d2 {
  font-size: clamp(1.625rem, 2.8vw, 2.25rem);
  line-height: 1.18;
  letter-spacing: -0.022em;
}
.d3 {
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.008em;
}

/* One phrase in the signal colour, at most twice per page. */
.mark { color: var(--sig-text); }

.lede {
  margin: 1.5rem 0 0;
  font-size: clamp(1rem, 1.2vw, 1.0625rem);
  line-height: 1.65;
  color: var(--text-2);
  max-width: 56ch;
  text-wrap: pretty;
}
.copy { margin: 0.75rem 0 0; color: var(--text-2); text-wrap: pretty; }
.sm { font-size: 0.875rem; }
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* --- Buttons -------------------------------------------------------------- */
/* Machined: low radius, mono, a real border, a hairline of light on top. */
.btn {
  --bg-btn: var(--sig);
  --fg-btn: #04140e;
  --bd-btn: var(--sig);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding: 0.75rem 1.25rem;
  border: 1px solid var(--bd-btn);
  border-radius: var(--r);
  background: var(--bg-btn);
  color: var(--fg-btn);
  font-size: 0.875rem;
  font-weight: 550;
  letter-spacing: -0.003em;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--t) var(--ease),
              border-color var(--t) var(--ease),
              color var(--t) var(--ease),
              transform var(--t-fast) var(--ease);
}
.btn:active, .btn.pressed { transform: translateY(1px); }
.btn:hover { --bg-btn: #5cecbc; --bd-btn: #5cecbc; }

.btn--line {
  --bg-btn: var(--panel-2);
  --fg-btn: var(--text);
  --bd-btn: var(--line-2);
  box-shadow: inset 0 1px 0 var(--edge);
}
.btn--line:hover { --bg-btn: var(--panel-3); --bd-btn: var(--line-3); }

.btn--block { width: 100%; }
.btn .arw { transition: transform var(--t) var(--ease); }
.btn:hover .arw { transform: translateX(2px); }

/* --- Header --------------------------------------------------------------- */
/* Opaque bar with a stroke. No glass, no blur. */
.site-head {
  position: sticky; top: 0; z-index: 100;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  background: rgba(8,9,12,0.96);
  border-bottom: 1px solid transparent;
  transition: border-color var(--t-slow) var(--ease);
}
.site-head[data-scrolled="true"] { border-bottom-color: var(--line); }

/* Three columns, not a flex row. The outer two are equal and flexible, so
   the middle one sits on the centre of the bar whatever the brand and the
   controls happen to measure — nudging it there with margins only works
   until one side changes width. */
.nav {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
  width: 100%;
}
.nav-links { justify-self: center; }
.nav-end {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  justify-self: end;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  justify-self: start;
  transition: opacity var(--t) var(--ease);
}
.brand:hover { opacity: 0.82; }
.brand img { width: 1.25rem; height: auto; }
/* One word, two weights: "layer8" in ink, "secure" in the signal colour.
   The seam does the work a slash or a tagline used to. */
.brand-name {
  display: flex;
  align-items: baseline;
  font-family: var(--mono);
  font-size: 0.9375rem;
  letter-spacing: 0.01em;
}
.brand-name b    { font-weight: 600; }
.brand-name span { font-weight: 500; color: var(--sig); }

.nav-links { display: flex; align-items: center; gap: 0.25rem; }
.nav-links a {
  padding: 0.75rem;
  border-radius: var(--r);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text-3);
  transition: color var(--t) var(--ease), background-color var(--t) var(--ease);
}
.nav-links a:hover { color: var(--text); background: var(--panel-2); }
.nav-links a[aria-current="page"] { color: var(--sig-text); background: var(--sig-dim); }

/* The list button stays in the header on every page — a shop that is only
   visible once you are already in it does not advertise itself. */
.head-cart { gap: 0.5rem; padding-inline: 0.75rem; }
.head-cart .cart-badge { margin-left: 0.125rem; }

.menu-btn {
  display: none;
  width: 2.75rem; height: 2.75rem;
  padding: 0;
  align-items: center; justify-content: center;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--panel-2);
  box-shadow: inset 0 1px 0 var(--edge);
  cursor: pointer;
  transition: background-color var(--t) var(--ease);
}
.menu-btn:hover { background: var(--panel-3); }
.menu-btn i { position: relative; width: 0.875rem; height: 0.5rem; display: block; }
.menu-btn i::before, .menu-btn i::after {
  content: "";
  position: absolute; left: 0; right: 0; height: 1px;
  background: currentColor;
  transition: transform var(--t) var(--ease);
}
.menu-btn i::before { top: 0; }
.menu-btn i::after  { bottom: 0; }
.menu-btn[aria-expanded="true"] i::before { transform: translateY(3.5px) rotate(45deg); }
.menu-btn[aria-expanded="true"] i::after  { transform: translateY(-3.5px) rotate(-45deg); }

/* --- Mobile panel --------------------------------------------------------- */
.scrim {
  position: fixed; inset: 0; z-index: 110;
  background: rgba(4,5,7,0.7);
  opacity: 0; visibility: hidden;
  transition: opacity var(--t) var(--ease), visibility var(--t);
}
.scrim[data-open="true"] { opacity: 1; visibility: visible; }

.sheet {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 120;
  width: min(20rem, 84vw);
  display: flex;
  flex-direction: column;
  padding: 1rem;
  background: var(--panel);
  border-left: 1px solid var(--line-2);
  transform: translate3d(100%, 0, 0);
  visibility: hidden;
  touch-action: none;
}
.sheet[data-open="true"] { visibility: visible; }

.sheet-label {
  margin: 0;
  padding: 0.5rem 0.5rem 1rem;
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-4);
  border-bottom: 1px solid var(--line);
}
.sheet nav { display: flex; flex-direction: column; padding-top: 0.5rem; }
.sheet nav a {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0.5rem;
  border-radius: var(--r);
  font-size: 1rem;
  font-weight: 500;
  transition: background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.sheet nav a em {
  font-style: normal;
  font-size: 0.625rem;
  color: var(--text-4);
  font-variant-numeric: tabular-nums;
}
.sheet nav a:active, .sheet nav a.pressed { background: var(--panel-2); color: var(--sig-text); }

/* --- Hero -----------------------------------------------------------------
   One centred column over a globe. It was two columns with a readout panel
   on the right; the figures from that panel are the stats under the
   headline now, and the panel's border is not competing with the headline
   for the eye. */
.hero {
  position: relative;
  padding-block: clamp(3rem, 8vh, 5rem) clamp(2.5rem, 5vw, 4rem);
  /* The globe layer is deliberately wider than the page so the rings can
     run off the sides; without this that overhang became a horizontal
     scrollbar on the whole document. */
  overflow: hidden;
}

/* The globe's layer.
 *
 * Absolutely placed and pointer-transparent so it cannot take a click off
 * the buttons sitting over it, and pinned wider than the copy column so the
 * rings can run past the text without the section growing a scrollbar. The
 * canvas inside it is created by the script and sized to this box.
 *
 * Height is tied to the section rather than the viewport: a hero that is
 * 100vh tall on a laptop is a hero that is 100vh tall on a phone in
 * landscape, and then the headline is below the fold. */
.hero-globe {
  /* Framing, read by the script. A banner behind text wants the planet
     small and high; a column of its own wants it large and centred. */
  --globe-rw: 0.19;
  --globe-rh: 0.34;
  --globe-cx: 0.5;
  --globe-cy: 0.30;

  position: absolute;
  inset: 1.5rem -10% auto -10%;
  height: clamp(20rem, 46vw, 34rem);
  min-width: 0;          /* never let the canvas set the track width */
  z-index: 0;
  pointer-events: none;
  /* The planet fades out before it reaches the copy, so the dots never
     fight the lede for contrast. Two stops, because a single one put the
     seam right through the headline. */
  -webkit-mask-image: linear-gradient(to bottom, #000 42%, rgba(0,0,0,0.55) 68%, transparent 92%);
          mask-image: linear-gradient(to bottom, #000 42%, rgba(0,0,0,0.55) 68%, transparent 92%);
}
/* The canvas fills its host and never sizes it.
   
   The script used to set the canvas's CSS width and height from the box it
   had measured, which is a loop: as a grid item the host's automatic
   minimum size is its content, so an over-wide canvas pushed the host back
   out to the full container width — 1134px inside a 528px column — and the
   globe stopped being in its column at all. The script now sets only the
   backing store, in device pixels, and the layout is decided here. */
.hero-globe canvas { display: block; width: 100%; height: 100%; }

.hero-copy {
  position: relative;
  z-index: 1;
  max-width: 44rem;
  margin-inline: auto;
  text-align: center;
}

/* Where they are, above the headline. The reference for this layout puts a
   small pill here; a border round two words is a lot of furniture for a
   locality, so it is the live dot and the words. */
.hero-where {
  position: relative;   /* over the trace layer, which sits at z-index 0 */
  z-index: 1;
  /* Clearance from the planet, which is drawn in the upper third of its
     box. This used to sit on .hero-copy, because the locality line was the
     first thing inside it; now that it is a sibling it is the first thing
     in the flow, and the clearance has to come with it — without it the
     line landed across the middle of the globe, over the dotted land and
     on top of the marker. */
  padding-top: clamp(6rem, 20vw, 13rem);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin: 0 0 1.25rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
}

.hero-copy .lede { margin-inline: auto; }
.hero-copy .hero-cta { justify-content: center; }

/* The figures. Tabular numerals so 24/7 and 300+ sit on the same rhythm,
   and the label under the number rather than over it: the number is what
   the eye lands on and the label is what explains it. */
/* Cells hug their content instead of taking a fixed share of the row.
   
   On equal columns the figures filled 19%, 58% and 37% of their cells, so the
   dividers sat at arbitrary distances from the ink they were separating and
   the row looked lopsided even though the grid was perfectly even. Sized to
   content, each rule sits the same distance from the number on either side of
   it, which is what the eye is actually reading. */
.hero-stats {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1rem, 4vw, 2.5rem);
  margin: clamp(2.5rem, 6vw, 4rem) auto 0;
  max-width: 40rem;
}
.hero-stat { min-width: 0; }
.hero-stats dd {
  margin: 0;
  font-size: clamp(1.75rem, 5vw, 2.75rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.hero-stats dt {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  line-height: 1.35;
  color: var(--text-3);
}

/* The dividers are on the gaps between the columns, not around them, so the
   row does not end in a hanging rule. */
@media (min-width: 30rem) {
  .hero-stat + .hero-stat {
    border-left: 1px solid var(--line);
    padding-left: clamp(1rem, 4vw, 2.5rem);
  }
}

/* Wide enough for two columns: the argument on the left, the planet on the
   right, rather than one stacked on the other.
   
   The globe stops being a backdrop here and becomes a sibling in the grid —
   position: relative, its own cell, vertically centred and much larger,
   because a column can carry a globe that a banner cannot. The bottom-wipe
   mask is wrong for that shape, so it is a radial one: the sphere fades at
   its own edge instead of being cut off by a horizon. */
@media (min-width: 64rem) {
  .hero {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    /* Column gap only. `gap` sets both, and the row gap was landing between
       the globe and the locality line under it — 72px of air inserted
       between a caption and the thing it captions. */
    column-gap: clamp(2rem, 5vw, 4.5rem);
    row-gap: 0;
    align-items: center;
  }

  .hero-copy {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
    text-align: left;
    margin-inline: 0;
    max-width: 34rem;
    padding-top: 0;
  }

  /* Under the planet, centred on it. It reads as a caption on the globe
     here rather than a label on the headline, which is the right
     relationship: the mark on the sphere and these two words are saying
     the same thing. */
  .hero-where {
    grid-column: 2;
    grid-row: 2;
    justify-content: center;
    margin: 0;
    padding-top: 0;
  }
  .hero-copy .lede { margin-inline: 0; }
  .hero-copy .hero-cta { justify-content: flex-start; }
  .hero-stats { margin-inline: 0; max-width: none; }

  .hero-globe {
    --globe-rw: 0.42;
    --globe-rh: 0.44;
    --globe-cy: 0.5;

    grid-column: 2;
    grid-row: 1;
    position: relative;
    inset: auto;

    /* Height from its own width, not from the viewport.
       
       The sphere is sized by whichever axis is smaller, and in this column
       that is always the width — so a box whose height came from vw kept
       growing after the sphere had stopped, and the locality line under it
       drifted further away the wider the screen got: 91px of gap at 1024,
       108 at 1440, 162 at 1920. Tied to the width the slack is a fixed
       proportion, so the caption sits the same distance below the planet at
       every size. */
    /* height must be released explicitly: an explicit height beats
       aspect-ratio, and the base rule sets one for the banner layout. */
    height: auto;
    aspect-ratio: 1 / 0.95;
    margin-bottom: 0.75rem;
    -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 56%, rgba(0,0,0,0.45) 76%, transparent 92%);
            mask-image: radial-gradient(circle at 50% 50%, #000 56%, rgba(0,0,0,0.45) 76%, transparent 92%);
  }
}

@media (max-width: 30rem) {
  /* Stacked, and the planet pulled in tighter — at 375px a globe sized off
     the width is most of the screen.
     
     Pushed down the page as well. It used to start above the top of the
     hero, so the thing cutting off the top of the planet was the header
     bar — which reads as a mistake, because a fixed chrome element
     clipping the artwork looks like the artwork is in the wrong place. Now
     it starts below the bar and the hero text runs over its lower third,
     so what interrupts it is the headline. The mask holds longer than it
     did for the same reason: it has to still be there when the words
     arrive, or there is nothing for them to overlap. */
  .hero-globe {
    inset: 2.25rem -18% auto -18%;
    height: 17rem;
    -webkit-mask-image: linear-gradient(to bottom, #000 58%, rgba(0,0,0,0.55) 82%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 58%, rgba(0,0,0,0.55) 82%, transparent 100%);
  }
  /* Measured, not guessed: at 9rem the locality line sat 73px below the
     bottom of the drawn sphere, which on a phone is a screenful of nothing
     between the picture and the first word. 4.25rem puts it just inside the
     fade, the same relationship the wide layout has. */
  /* Measured. The locality line sits just over the bottom of the drawn
     sphere and the headline follows below it, so what interrupts the
     planet is the words rather than the header bar. */
  .hero-where { padding-top: 6rem; }
  .hero-stats {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.75rem;
  }
  .hero-stats dt { font-size: 0.6875rem; }
}

/* text-wrap: normal so the authored <br> in the hero is obeyed rather than
   second-guessed by balance. */
.hero .d1 { max-width: none; text-wrap: normal; }

/* Except where the column is too narrow to hold the second half of it.
   
   The break was measured against a wide single column, and after the hero
   split in two that column only exists between 56 and 64rem. Below 56 the
   line never fitted; at 64 and up the copy is in a 34rem cell and it does
   not fit again — both leave "safe." stranded alone on a third line. So the
   authored break applies in that band and nowhere else, and outside it the
   browser evens the lines out instead. */
@media (max-width: 56rem), (min-width: 64rem) {
  .hero .d1 br { display: none; }
  .hero .d1 { text-wrap: balance; }
}
.hero-cta { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 2rem; }

/* The live dot. It marked a status line under the buttons; that line said
   the same three things the stats and the locality now say, so it went, and
   the dot moved up to the locality above the headline — which is the one
   place on the page where "we are here, and we are awake" belongs. */
.led {
  width: 0.375rem; height: 0.375rem;
  border-radius: 50%;
  background: var(--sig);
  box-shadow: 0 0 0 0.1875rem var(--sig-dim);
  flex: none;
}

/* Readout — the figures, presented as monitored values. */
/* --- The readout answers the pointer --------------------------------------
   The only piece of motion in the hero, and it earns its place by being in
   character: this panel is an instrument, so it behaves like one. It turns a
   few degrees to face you, a soft light tracks across it, and the row under
   the cursor lights up. Three degrees, not fifteen — at the size of a party
   trick it would compete with the headline, which is the one thing on this
   page that has to win.

   Tracking is 1:1 and instant while the pointer is on it. Direct
   manipulation that lags behind the hand feels broken, so the easing is
   saved for the release: let go and it settles back over 620ms.

   The selector carries [data-reveal] on purpose. That attribute already owns
   `transform` on this element, and `[data-reveal].in { transform: none }`
   would otherwise flatten the tilt the moment the panel finished revealing.
   Rather than fight it, the reveal offset is folded in as --reveal-y, so the
   panel still slides up on entry and still tilts afterwards. */
/* Under the hand: no transition, and promote it for the duration only. */
/* The light. The panel's own overflow clips it to the rounded corners, so it
   never needs to know what shape it is sitting in. */

/* --- Section head --------------------------------------------------------- */
.head { max-width: 46rem; margin-bottom: clamp(2rem, 4vw, 3rem); }

/* --- Capabilities — a ruled table, not a card grid ------------------------ */
.caps > .cap { border-bottom: 1px solid var(--line); }
.caps > .cap:last-child { border-bottom: 0; }
.cap {
  display: grid;
  grid-template-columns: 1.75rem 1fr;
  gap: 0.25rem 1rem;
  padding: 1.25rem 1rem;
  transition: background-color var(--t) var(--ease);
}
.cap:hover { background: var(--panel-2); }
/* Explicit placement: auto-placement drops the paragraph into the icon
   column and sets it one word per line. */
.cap-ico {
  grid-column: 1; grid-row: 1 / span 2;
  width: 1.125rem; height: 1.125rem;
  align-self: start;
  margin-top: 0.25rem;
  color: var(--text-4);
  transition: color var(--t) var(--ease);
}
.cap:hover .cap-ico { color: var(--sig); }
.cap .d3   { grid-column: 2; grid-row: 1; }
.cap .copy { grid-column: 2; grid-row: 2; margin-top: 0; }
@media (min-width: 60rem) {
  .cap { grid-template-columns: 1.75rem 14rem 1fr; gap: 1.25rem; align-items: baseline; }
  .cap-ico { grid-row: 1; }
  .cap .d3   { grid-column: 2; grid-row: 1; }
  .cap .copy { grid-column: 3; grid-row: 1; }
}

/* --- The layer stack — the signature piece --------------------------------
   A real table: index, surface, and who covers it. 01-07 are protocol;
   08 is the only row in the signal colour, because it is the only row this
   company is about. */
.stack .layer {
  display: grid;
  grid-template-columns: 3rem 1fr 7.5rem;
  gap: 1rem;
  align-items: center;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--line);
}
.stack .layer:last-child { border-bottom: 0; }
.layer-n {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--text-4);
  font-variant-numeric: tabular-nums;
}
.layer-name {
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--text-3);
}
.layer-state {
  justify-self: end;
  font-family: var(--mono);
  font-size: 0.5625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-4);
  border: 1px solid var(--line-2);
  border-radius: 2px;
  padding: 0.25rem 0.5rem;
  white-space: nowrap;
}

.layer--8 {
  background: var(--sig-dim);
  box-shadow: inset 1px 0 0 var(--sig);
  padding-block: 1.25rem !important;
}
.layer--8 .layer-n { color: var(--sig); }
.layer--8 .layer-name {
  font-family: var(--sans);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.012em;
  color: var(--text);
}
.layer--8 .layer-name small {
  display: block;
  margin-top: 0.5rem;
  font-size: 0.875rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-2);
  max-width: 52ch;
}
.layer--8 .layer-state {
  color: var(--sig-text);
  border-color: var(--sig-line);
  background: rgba(58,223,168,0.06);
}
@media (max-width: 40rem) {
  .stack .layer { grid-template-columns: 2.5rem 1fr; }
  .layer-state { display: none; }
}

/* --- Approach ------------------------------------------------------------- */
.steps > .step { border-bottom: 1px solid var(--line); }
.steps > .step:last-child { border-bottom: 0; }
.step {
  display: grid;
  grid-template-columns: 3rem 1fr;
  gap: 0.5rem 1rem;
  padding: 1.5rem 1rem;
}
/* Explicit placement: auto-placement would drop the paragraph into the
   3rem number column and set it one word per line. */
.step-n {
  grid-column: 1; grid-row: 1 / span 2;
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--sig);
  font-variant-numeric: tabular-nums;
}
.step h3    { grid-column: 2; grid-row: 1; font-size: 1rem; }
.step .copy { grid-column: 2; grid-row: 2; margin-top: 0; }
@media (min-width: 52rem) {
  .step { grid-template-columns: 3rem 15rem 1fr; gap: 1.5rem; align-items: baseline; }
  .step-n { grid-row: 1; }
  .step h3    { grid-column: 2; grid-row: 1; }
  .step .copy { grid-column: 3; grid-row: 1; }
}

/* --- Shop bar: filter + list ---------------------------------------------- */
.shop-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--line);
}
.filters { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* The one rounded control on the site. Everything else here is machined at
   0.375rem, but a filter chip is a token you toggle rather than a panel you
   read, and the pill is what says "tap me, I switch". */
.filter {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding: 0.75rem 1.125rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text-3);
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: color var(--t) var(--ease), border-color var(--t) var(--ease),
              background-color var(--t) var(--ease);
}
.filter:hover { color: var(--text); border-color: var(--line-3); }
.filter.is-on {
  color: var(--sig-text);
  border-color: var(--sig-line);
  background: var(--sig-dim);
}
.filter-n {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--text-4);
  font-variant-numeric: tabular-nums;
}
.filter.is-on .filter-n { color: var(--sig-text); }

.cart-badge {
  min-width: 1.25rem;
  padding: 0 0.25rem;
  border-radius: 999px;
  background: #04140e;
  color: var(--sig);
  font-family: var(--mono);
  font-size: 0.6875rem;
  line-height: 1.25rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.cart-badge[data-empty="true"] { opacity: 0.45; }

/* One short bump when a line is added. The drawer no longer opens, so this
   is what tells you the click landed. */
.cart-badge.is-bumped { animation: badge-bump 380ms var(--ease); }
@keyframes badge-bump {
  0%   { transform: scale(1); }
  36%  { transform: scale(1.28); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .cart-badge.is-bumped { animation: none; }
}

.cat-intro {
  margin: 1.25rem 0 0;
  max-width: 62ch;
  color: var(--text-2);
}

/* --- Product grid ---------------------------------------------------------
   Real product cards: a visual, the pitch, then the price and the control
   that adds it. Three columns of equal-height cards, because that is what a
   shop is and the console vocabulary still carries it. */
.shelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 1rem;
  margin-top: 1.75rem;
}
.prod {
  position: relative;            /* the flag is pinned to the card, not the tile */
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: inset 0 1px 0 var(--edge);
  overflow: hidden;
  transition: border-color var(--t) var(--ease);
}
.prod:hover { border-color: var(--line-3); }

/* `hidden` has to actually win. An author `display` on the element beats the
   browser's own [hidden] { display: none } at equal specificity, so the
   category filter was setting the attribute on eight cards and hiding none
   of them — the counts changed, the shelf did not. */
.prod[hidden] { display: none; }

/* The phone's route into the detail. There is no detail to open on a wide
   screen — the card already shows everything — so it does not exist there. */
.prod-peek { display: none; }

/* No stock photography: an authored line drawing on the machined grid,
   consistent stroke, sized like a product shot. */
.prod-tile {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 7;
  background:
    radial-gradient(ellipse 60% 70% at 50% 40%, rgba(58,223,168,0.07), transparent 70%),
    linear-gradient(to right, rgba(255,255,255,0.02) 1px, transparent 1px) 0 0 / 1.5rem 1.5rem,
    linear-gradient(to bottom, rgba(255,255,255,0.02) 1px, transparent 1px) 0 0 / 1.5rem 1.5rem,
    var(--bg-2);
  border-bottom: 1px solid var(--line);
  color: var(--text-3);
  transition: color var(--t) var(--ease);
}
.prod:hover .prod-tile { color: var(--sig); }
.prod-tile svg { width: 2.75rem; height: 2.75rem; }
.prod-body .flag { position: absolute; top: 0.75rem; left: 0.75rem; }

.prod-body { flex: 1; padding: 1.25rem; }
.prod-body .d3 { font-size: 1rem; }
.prod-body .copy { margin-top: 0.5rem; font-size: 0.875rem; }

/* "Good if ..." — lets a non-technical reader recognise their own situation. */
.who {
  margin: 0.75rem 0 0;
  padding-left: 0.75rem;
  border-left: 1px solid var(--sig-line);
  font-size: 0.875rem;
  color: var(--sig-text);
}
/* The industry term, demoted to a footnote rather than leading the card. */
.term { margin: 0.75rem 0 0; font-size: 0.75rem; color: var(--text-4); }

.flag {
  font-family: var(--mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sig-text);
  background: var(--sig-dim);
  border: 1px solid var(--sig-line);
  border-radius: 2px;
  padding: 0.25rem 0.5rem;
}

/* --- Buy block ------------------------------------------------------------ */
.prod-buy {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.75rem 1rem;
  padding: 1.25rem;
  border-top: 1px solid var(--line);
  background: var(--bg-2);
}
.price {
  font-family: var(--mono);
  font-size: 1.25rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}
.price small {
  display: block;
  margin-top: 0.25rem;
  font-family: var(--sans);
  font-size: 0.75rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-3);
}
.prod-add { grid-column: 1 / -1; }
.prod-add.is-added { --bg-btn: var(--panel-3); --bd-btn: var(--sig-line); --fg-btn: var(--sig-text); }

/* Quantity stepper: a real control, 44px tall, tabular figures. */
.qty {
  display: inline-grid;
  grid-template-columns: 2.25rem 3rem 2.25rem;
  align-items: stretch;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--panel);
}
.qty-btn {
  min-height: 2.75rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-2);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.qty-btn:hover { background: var(--panel-3); color: var(--text); }
.qty-in {
  min-width: 0;
  padding: 0;
  border: 0;
  border-inline: 1px solid var(--line);
  background: transparent;
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
  -moz-appearance: textfield;
}
.qty-in::-webkit-outer-spin-button,
.qty-in::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qty-in:focus { outline: none; background: var(--panel-3); }

.qty-noun {
  margin: 0;
  justify-self: end;
  font-size: 0.75rem;
  color: var(--text-4);
}
.qty--sm { grid-template-columns: 2rem 2.5rem 2rem; }
.qty--sm .qty-btn { min-height: 2.25rem; }

.shop-empty { margin: 2rem 0 0; color: var(--text-3); }
.note { margin: 1.75rem 0 0; font-size: 0.8125rem; color: var(--text-4); }

/* Home-page teaser: three picks, one per group. auto-fit would resolve to
   four tracks on a wide screen and leave the row ragged, so pin it to three. */
.shelf--teaser { grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
@media (min-width: 62rem) { .shelf--teaser { grid-template-columns: repeat(3, 1fr); } }

/* Declared before the phone block below, not after it. A later rule at the
   same specificity wins whether or not the earlier one sits in a media query,
   and when these lived at the bottom of the file they quietly undid the
   phone layout. */

/* --- The shop on a phone --------------------------------------------------
   A shop you scroll is not a shop you shop. The wide card carries a blurb, a
   "good if", a trade term, a stepper and a price — everything you need to
   decide, which is the right answer when three of them fit across a screen
   and the wrong one when each costs most of a viewport. Getting to the
   twelfth item meant half a minute of thumb.

   Below 40rem the same markup becomes a two-up grid that you scan: picture,
   name, price, add. Everything that was making the card tall is still in the
   DOM, hidden, and one tap lifts it into a sheet. Choose from the grid, read
   when something looks right. */
@media (max-width: 40rem) {

  .shelf {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
    margin-top: 1.25rem;
  }

  .prod {
    padding: 0;
    border-radius: var(--r);
  }

  /* The target. Covers the card down to the buy block, so a tap anywhere on
     the picture or the name opens the detail and a tap on Add just adds. */
  .prod-peek {
    display: block;
    position: absolute;
    inset: 0 0 auto 0;
    z-index: 2;
    height: calc(100% - 3.5rem);
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    border-radius: var(--r) var(--r) 0 0;
  }
  .prod-peek:focus-visible { outline: 2px solid var(--sig); outline-offset: -2px; }
  .prod:has(.prod-peek:active) { border-color: var(--line-3); }

  /* Squarer than the wide banner — at half a screen a 16/7 letterbox is a
     sliver — but not so tall that the picture outweighs the words. The mark
     grows with it, or it reads as a small thing lost in a large empty box. */
  .prod-tile { aspect-ratio: 4 / 3; }
  .prod-tile svg { width: 2.25rem; height: 2.25rem; }

  .prod-body { padding: 0.625rem 0.625rem 0; }

  /* Clamped, because a name that runs to five lines in one card and one in
     its neighbour is what makes a grid look broken — but at three lines, not
     two. These names are sentences rather than product codes, and cutting
     "Find out what is actually on your network" after "on your" costs the
     reader the whole point of writing them in plain English. */
  .prod-body .d3 {
    font-size: 0.875rem;
    line-height: 1.3;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
    /* Reserve all three lines whether the name uses them or not. Letting the
       box shrink to fit means a two-line name and a three-line name sit at
       different heights, and then so do their prices and their buttons —
       which is the difference between a grid you scan and a grid that looks
       broken. Costs about 18px on the shorter names; worth it. */
    min-height: calc(3 * 1.3em);
  }

  /* Still in the DOM — the sheet reads from here. */
  .prod-body .copy,
  .who,
  .term { display: none; }

  .prod-body .flag {
    position: absolute;
    top: 0.5rem;
    left: 0.5rem;
    z-index: 1;
  }

  .prod-buy {
    display: block;
    padding: 0.5rem 0.625rem 0.625rem;
    border-top: 0;
    background: none;
  }
  .price { font-size: 1rem; }
  .price small { font-size: 0.6875rem; margin-top: 0.125rem; }

  .prod-buy .qty,
  .prod-buy .qty-noun { display: none; }

  .prod-add {
    position: relative;
    z-index: 2;                    /* above the peek target */
    width: 100%;
    margin-top: 0.5rem;
    min-height: 2.5rem;
    padding-inline: 0.5rem;
    font-size: 0.8125rem;
  }

  /* --- The detail sheet --- */
  .peek {
    width: 100%;
    max-width: none;
    max-height: 88dvh;
    /* top auto, bottom zero: a dialog defaults to margin:auto and centres
       itself, and a sheet floating in the middle of the screen is a modal
       pretending to be a sheet. */
    margin: auto 0 0;
    padding: 0;
    border: 1px solid var(--line-2);
    border-bottom: 0;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    background: var(--panel);
    color: var(--text);
  }
  .peek::backdrop { background: rgba(4,5,7,0.72); }
  .peek[open] { display: flex; flex-direction: column; }

  .peek-grab {
    flex: none;
    width: 2.25rem;
    height: 0.25rem;
    margin: 0.625rem auto 0;
    border-radius: 999px;
    background: var(--line-3);
  }

  .peek-body {
    padding: 1rem 1.125rem 0;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
  }
  .peek-body .prod-tile {
    aspect-ratio: 16 / 7;
    border: 1px solid var(--line);
    border-radius: var(--r);
    margin-bottom: 0.875rem;
  }
  .peek-body .prod-tile svg { width: 2.5rem; height: 2.5rem; }
  .peek-body .flag {
    position: static;
    display: inline-block;
    margin-bottom: 0.5rem;
  }
  .peek-body h2 { font-size: 1.0625rem; line-height: 1.3; margin: 0; }
  .peek-body .copy,
  .peek-body .who,
  .peek-body .term { display: block; }
  .peek-body .copy { margin-top: 0.625rem; font-size: 0.875rem; }
  .peek-body .who  { margin-top: 0.875rem; }
  .peek-body .term { margin-top: 0.875rem; }

  /* The buy block comes back in full here — including the stepper, which is
     the one place on a phone where changing the number makes sense. */
  .peek-foot {
    flex: none;
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 0.75rem;
    padding: 1rem 1.125rem;
    padding-bottom: max(1rem, env(safe-area-inset-bottom));
    margin-top: 1rem;
    border-top: 1px solid var(--line);
    background: var(--panel-2);
  }
  .peek-foot .price { font-size: 1.125rem; }
  .peek-foot .price small { display: block; }
  .peek-foot .qty { display: inline-grid; }
  .peek-foot .qty-noun { display: none; }
  .peek-foot .btn { grid-column: 1 / -1; width: 100%; }

  /* Filters stay sticky under the header — twelve things is enough that the
     fastest route to one of them is to stop showing the other eight, and a
     filter you have to scroll back up to reach is a filter nobody uses. */
  .shop-bar {
    position: sticky;
    top: var(--nav-h);
    z-index: 5;
    padding-block: 0.5rem;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
  }

  /* All four on screen at once, wrapped onto two lines. A sideways scroller
     hides half the choices behind a gesture nobody is told about, and a
     filter you cannot see is not offering you anything.

     Each pill takes the width its own label needs rather than an equal share
     of the row: forcing four equal columns makes "Your people and data" the
     one that has to truncate, and a category you cannot finish reading is
     barely better than one you cannot see. Uneven rows are the cheaper
     compromise. */
  .filters { gap: 0.4375rem; }
  .filter { padding-inline: 0.875rem; }
  /* A safety net if a category is ever named something very long — it should
     never fire on the four that exist. */
  .filter-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .cat-intro { margin-top: 1rem; font-size: 0.875rem; }

  /* A six-line lede before the first product is four lines too many here. */
  .shop .head, #shop .head { margin-bottom: 1.5rem; }
  .shop .head .lede, #shop .head .lede {
    margin-top: 0.875rem;
    font-size: 0.9375rem;
    line-height: 1.55;
  }

  .note { margin-top: 1.25rem; }


  /* The home page shows three picks, and all three are on screen. A swipe
     rail hides two of them behind a gesture, and a teaser nobody swipes has
     teased nobody.

     Three does not divide by two, so the last one takes the full width rather
     than sitting as an orphan beside a gap. It gets a wider picture to suit
     the shape and drops the reserved name height, because it has no neighbour
     left to line up with. */
  .shelf--teaser { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .shelf--teaser .prod:last-child { grid-column: 1 / -1; }
  .shelf--teaser .prod:last-child .prod-tile { aspect-ratio: 16 / 6; }
  .shelf--teaser .prod:last-child .d3 { min-height: 0; }
}

@media (max-width: 40rem) and (prefers-reduced-motion: reduce) {
  .shelf--teaser { scroll-behavior: auto; }
}


.shop-more {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1rem;
  margin: 1.5rem 0 0;
}
.shop-more-note { font-size: 0.875rem; color: var(--text-3); }

/* --- Cart drawer ---------------------------------------------------------- */
.cart { padding: 0; }
.cart-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 0.75rem 0.75rem 1.25rem;
  background: var(--bg-2);
  border-bottom: 1px solid var(--line);
}
.cart-title { font-size: 0.9375rem; font-weight: 600; }

.cart-body { flex: 1; overflow-y: auto; padding: 1rem 1.25rem; }
.cart-empty { margin: 0; font-size: 0.875rem; color: var(--text-3); }
.cart-lines { list-style: none; margin: 0; padding: 0; }
.cart-line { padding: 1rem 0; border-bottom: 1px solid var(--line); }
.cart-line:first-child { padding-top: 0; }
.cart-line:last-child { border-bottom: 0; }
.cart-line-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}
.cart-line-name { font-size: 0.875rem; font-weight: 550; line-height: 1.35; }
.cart-x {
  flex: none;
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--r);
  background: transparent;
  color: var(--text-4);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.cart-x:hover { color: #ff7a6b; background: var(--panel-2); }
.cart-line-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.75rem;
}
.cart-line-sum {
  font-family: var(--mono);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}

.cart-foot {
  padding: 1.25rem;
  border-top: 1px solid var(--line);
  background: var(--bg-2);
}
/* Totals bucket by billing unit — a monthly fee and a one-time install are
   not the same number and must not be added into one. */
.cart-totals {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.5rem 1rem;
  margin: 0 0 1rem;
}
.cart-totals dt { font-size: 0.8125rem; color: var(--text-3); }
.cart-totals dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.9375rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.cart-note { margin: 0 0 1rem; font-size: 0.75rem; color: var(--text-4); }
.cart-clear {
  display: block;
  width: 100%;
  min-height: 2.75rem;
  margin-top: 0.75rem;
  padding: 0.75rem;
  border: 0;
  background: transparent;
  color: var(--text-4);
  font-size: 0.8125rem;
  cursor: pointer;
  transition: color var(--t) var(--ease);
}
.cart-clear:hover { color: var(--text-2); }

/* --- Contact -------------------------------------------------------------- */
.contact-grid { display: grid; gap: clamp(2rem, 4vw, 3rem); align-items: start; }
@media (min-width: 60rem) { .contact-grid { grid-template-columns: 1fr 22rem; } }

.detail { margin: 0; }
.detail .detail-row { padding: 1rem; border-bottom: 1px solid var(--line); }
.detail .detail-row:last-child { border-bottom: 0; }
.detail-row dt {
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-4);
}
.detail-row dd {
  margin: 0.5rem 0 0;
  font-family: var(--mono);
  font-size: 0.875rem;
}
.detail-row a { transition: color var(--t) var(--ease); }
.detail-row a:hover { color: var(--sig-text); }

/* --- Footer --------------------------------------------------------------- */
.site-foot {
  margin-top: clamp(3rem, 6vw, 4.5rem);
  border-top: 1px solid var(--line);
  padding-block: 2.5rem 1.5rem;
  background: var(--bg-2);
}
/* Three blocks: who we are, where to go, how to reach us. The brand takes
   the slack so the two lists sit together at the end rather than drifting
   apart across a wide screen. */
.foot-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 2.5rem 4rem;
  align-items: flex-start;
}
.foot-brand { flex: 1 1 18rem; }
.foot-brand .brand { margin-bottom: 0.875rem; }
.foot-brand p {
  margin: 0;
  max-width: 30ch;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--text-3);
}

/* Mono, uppercase, tiny: the same label voice the panels and the status line
   use, so the footer reads as part of the instrument rather than as a
   different site bolted to the bottom of it. */
.foot-col h4 {
  margin: 0 0 1rem;
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-4);
}
.foot-reach { flex: 0 1 16rem; }

/* Label above value, because an email address and a phone number are the
   two things worth being able to find without reading a sentence. */
.foot-detail { margin: 0; display: grid; gap: 0.875rem; }
.foot-detail dt {
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-4);
}
.foot-detail dd {
  margin: 0.1875rem 0 0;
  font-size: 0.875rem;
  color: var(--text-2);
}
.foot-detail a { transition: color var(--t) var(--ease); }
.foot-detail a:hover { color: var(--sig-text); }
.foot-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.foot-col a {
  display: block;
  font-size: 0.875rem;
  color: var(--text-2);
  transition: color var(--t) var(--ease);
}
.foot-col a:hover { color: var(--sig-text); }

/* Apple's 44pt minimum is a touch rule; a pointer does not need it. */
/* Every control the shop added reaches 44px where a finger is the input:
   steppers, the remove button, the clear action. */
@media (pointer: coarse) {
  .qty { grid-template-columns: 2.75rem 3.5rem 2.75rem; }
  .qty--sm { grid-template-columns: 2.75rem 3rem 2.75rem; }
  .qty--sm .qty-btn { min-height: 2.75rem; }
  .cart-x { width: 2.75rem; height: 2.75rem; }
}

/* Declared after the footer's own rules rather than up in the shop's phone
   block. The shop block sits earlier in this file, and a later rule at the
   same specificity wins whether or not the earlier one is in a media query —
   which is how `column-gap` lost to the `gap: 0` below and the two columns
   ran together as "CapabilitiesApproach". */
@media (max-width: 40rem) {
  .foot-grid { gap: 2rem; }
  /* Four index links at a 44px target each made a column taller than the
     block it belonged to. Two columns halve it without shrinking a target. */
  .foot-col ul { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .foot-brand p { max-width: none; }
}

@media (pointer: coarse) {
  /* Row gap zero so the 44px targets stack edge to edge; the column gap has
     to survive that, or the two columns have no space between them. */
  .foot-col ul { row-gap: 0; column-gap: 1.5rem; margin-block: -0.75rem; }
  .foot-col ul a { padding-block: 0.75rem; }
  /* 0.6875rem each side takes a 22px line to a 44px target. Half a rem left
     it at 38 and quietly broke the rule the rest of the site keeps. */
  .foot-detail dd a { display: inline-block; padding-block: 0.6875rem; margin-block: -0.6875rem; }
  .detail-row dd a { display: inline-block; padding-block: 0.75rem; margin-block: -0.75rem; }
}

.socials { display: flex; gap: 0.5rem; margin-top: 1.25rem; }
.socials a {
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--panel-2);
  transition: border-color var(--t) var(--ease);
}
.socials a:hover { border-color: var(--line-3); }
.socials img { width: 0.875rem; height: 0.875rem; opacity: 0.5; }

.foot-base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.06em;
  color: var(--text-4);
}
.foot-base nav { display: flex; gap: 1.25rem; }
.foot-base nav a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding-inline: 0.5rem;
  margin: -0.75rem -0.5rem;
  transition: color var(--t) var(--ease);
}
.foot-base nav a:hover { color: var(--text-2); }

/* --- Inquiry dialog ------------------------------------------------------- */
dialog.inquire {
  width: min(32rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--panel);
  color: var(--text);
  box-shadow: inset 0 1px 0 var(--edge), 0 32px 64px -24px rgba(0,0,0,0.8);
}
dialog.inquire::backdrop { background: rgba(4,5,7,0.72); }
.dlg-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.75rem 0.75rem 0.75rem 1rem;
  background: var(--bg-2);
  border-bottom: 1px solid var(--line);
}
.dlg-head .d3 { font-size: 0.9375rem; font-weight: 600; }
.dlg-x {
  width: 2.75rem; height: 2.75rem;
  display: grid; place-items: center;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r);
  background: transparent;
  color: var(--text-3);
  cursor: pointer;
  transition: color var(--t) var(--ease), background-color var(--t) var(--ease);
}
.dlg-x:hover { color: var(--text); background: var(--panel-2); }
.dlg-body { padding: 1.25rem; display: grid; gap: 1rem; }
.field { display: grid; gap: 0.5rem; }
.field label {
  font-size: 0.8125rem;
  font-weight: 550;
  color: var(--text-3);
}
.field input, .field textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--bg-2);
  font-size: 0.9375rem;
  transition: border-color var(--t) var(--ease);
}
.field input:focus, .field textarea:focus { outline: none; border-color: var(--sig); }
.field textarea { resize: vertical; min-height: 5rem; }
.err {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: #ff7a6b;
  min-height: 1.25rem;
}

/* --- Reveal --------------------------------------------------------------- */
/* A panel clips to its radius, so a child rising into place is chopped
   while it animates. Children of a panel fade in place instead. */
.panel [data-reveal] { transform: none !important; }

/* Nothing is hidden until something is known to be able to show it again.
   
   This was an unconditional `opacity: 0`, and every section on the page
   carries the attribute — so with JavaScript off or broken, the whole site
   rendered blank. The class is set by a one-line inline script in the head,
   which runs before anything paints. */
.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 0.5rem, 0);
  transition: opacity 520ms var(--ease), transform 520ms var(--ease);
  transition-delay: var(--delay, 0ms);
}
.js [data-reveal].in { opacity: 1; transform: none; }

/* Above the fold, the reveal is a CSS animation that starts with the page
   instead of waiting for an observer.
   
   The hero is what the largest-contentful-paint measures, and it was hidden
   until a deferred 29KB script had downloaded, parsed, run and had its
   IntersectionObserver fire. Nothing on the first screen should wait for
   JavaScript to become visible. Below the fold the observer is still the
   right tool — there is no point animating things nobody has scrolled to. */
.js [data-rise] {
  opacity: 0;
  animation: rise-in 560ms var(--ease) var(--delay, 0ms) forwards;
}
@keyframes rise-in {
  from { opacity: 0; transform: translate3d(0, 0.5rem, 0); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .js [data-rise] { animation: none; opacity: 1; transform: none; }
}


/* --- The signal trace -----------------------------------------------------
   A line of light that runs down the page as you scroll, bending to touch
   the things worth reading and switching them on as it arrives.

   It is on brand rather than decorative: this company sells the idea of a
   signal travelling through layers of a building, and an unlit circuit trace
   that powers up behind you is that idea drawn. It is also the reason the
   effect has to stay quiet — a bright line chasing the reader would fight the
   words it is pointing at.

   The whole thing is built by l8.js from the positions of [data-trace]
   elements. There are no coordinates in this file and none in the markup;
   hand-authored ones would be wrong at the first viewport that is not the one
   they were drawn for.

   One rule the lit state must keep: it is additive. Nothing on this page
   starts dimmer than its resting state and waits for the light to make it
   readable — the trace adds emphasis, it never withholds legibility. Scroll
   animation that hides content until you reach it is a trap dressed as a
   flourish. */

#main { position: relative; }

/* Content sits above the trace; the trace sits above the page backdrop. */
#main > .wrap { position: relative; z-index: 1; }

/* In front of the content rather than behind it. Same box, same viewBox. */
.trace--front { z-index: 2; }

.trace {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

/* The unpowered run. Visible, so the path ahead reads as a route rather than
   as a line appearing from nowhere. */
.trace-dim {
  fill: none;
  stroke: var(--line-2);
  stroke-width: 1;
  stroke-linecap: round;
}

/* The powered run. The dash offset is the only thing scroll touches. */
.trace-lit {
  fill: none;
  stroke: var(--sig);
  stroke-width: 1.25;
  stroke-linecap: round;
  /* Present enough to follow, quiet enough that the words still win. The
     line is pointing at the reading, not competing with it. */
  opacity: 0.62;
}

/* --- What the line is telling you ---
   Icons strung along the trace: what goes wrong before the shop, the same
   things put right after it. They sit on the line rather than beside it, so
   the trail reads as running through them.

   Off by default and switched on as the head arrives. Nothing here carries
   information that is not also in the words on the page — it is a drawing of
   the argument, not the argument. */
.trace-icon {
  opacity: 0;
  /* No transform-box or transform-origin: an SVG element defaults to its own
     local origin, which is exactly where the disc is centred. Setting
     fill-box here would take the caption into account and grow the icon from
     a point off to one side of itself. */
  transform: scale(0.6);
  transition: opacity 420ms var(--ease), transform 520ms var(--ease);
}
.trace-icon.is-on { opacity: 1; transform: scale(1); }


/* Solid, not tinted. These sit over whatever the page happens to put behind
   them — a panel, a rule, a paragraph — and a 12% wash would let all of it
   through. The colour is carried by the stroke and the drawing. */
.trace-icon-disc {
  fill: var(--bg-2);
  stroke: var(--line-2);
  stroke-width: 1;
  transition: stroke 420ms var(--ease);
}
.trace-icon-art .solid { fill: currentColor; stroke: none; }
.trace-icon-art {
  fill: none;
  stroke: var(--text-4);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 420ms var(--ease);
}
.trace-icon-cap {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  fill: var(--text-4);
  transition: fill 420ms var(--ease);
}

/* Things going wrong. */
.trace-icon--fault.is-on .trace-icon-disc { stroke: var(--alarm-line); }
.trace-icon--fault.is-on .trace-icon-art  { stroke: var(--alarm); }
.trace-icon--fault.is-on .trace-icon-cap  { fill: var(--alarm); }

/* The same things, handled. */
.trace-icon--fixed.is-on .trace-icon-disc { stroke: var(--sig-line); }
.trace-icon--fixed.is-on .trace-icon-art  { stroke: var(--sig); }
.trace-icon--fixed.is-on .trace-icon-cap  { fill: var(--sig-text); }

/* The leading edge. */
.trace-head {
  fill: var(--sig);
  opacity: 0;
  transition: opacity 320ms var(--ease);
}
.trace.is-live .trace-head { opacity: 1; }

/* The glow, without a filter.
   
   This was drop-shadow on the group. The lit path's dash offset changes on
   every scroll frame, so every frame asked for a fresh blurred raster of a
   viewport-sized region — the one genuinely expensive thing on the page
   while scrolling, and invisible to any JavaScript profile because it is
   paint, not script. A wider, fainter copy of the same path underneath
   costs one more stroke in a paint that was happening anyway. */
.trace-halo {
  fill: none;
  stroke: var(--sig);
  stroke-width: 4;
  stroke-linecap: round;
  opacity: 0.16;
}
.trace-head-halo {
  fill: var(--sig);
  opacity: 0;
  transition: opacity 320ms var(--ease);
}
.trace.is-live .trace-head-halo { opacity: 0.22; }

/* --- What the light does when it arrives ---
   A rule grows down the leading edge and the text comes up to full strength.
   Both are additive: unlit text is --text-2, which is the same colour it
   would be with no trace on the page at all. */
[data-trace] {
  position: relative;
  transition: color 420ms var(--ease);
}
[data-trace]::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -1.25rem;
  width: 2px;
  border-radius: 2px;
  background: var(--sig);
  transform: scaleY(0);
  transform-origin: top;
  opacity: 0;
  transition: transform 520ms var(--ease), opacity 320ms var(--ease);
}
[data-trace].is-lit::before { transform: scaleY(1); opacity: 0.55; }

/* The light actually reaching the words. A heading is already at full
   strength — --text is the resting colour of every h2 on this page — so
   raising the colour would have done nothing visible, which is what the
   first version of this did. A glow is the part that has somewhere to go.

   It sits behind the glyphs and changes no ink, so the text is exactly as
   legible lit as unlit; the light is added to the page, not taken away from
   the rest of it. */
[data-trace].is-lit {
  color: var(--text);
  text-shadow: 0 0 22px rgba(58, 223, 168, 0.38),
               0 0 6px rgba(58, 223, 168, 0.18);
}
[data-trace] { transition: color 420ms var(--ease), text-shadow 520ms var(--ease); }

/* A marked word inside a lit line goes the rest of the way. */
[data-trace].is-lit .mark { color: var(--sig); }

/* The rule needs somewhere to sit. Inside a panel there is no gutter to hang
   it in, so that case gets the glow and not the rule. */
.panel [data-trace]::before { left: -0.625rem; }


/* Nothing to trace for someone who asked the page to hold still. The script
   does not build the SVG at all in that case; this covers the lit classes if
   the preference changes after the fact. */
@media (prefers-reduced-motion: reduce) {
  .trace { display: none; }
  [data-trace]::before { display: none; }
  [data-trace] { transition: none; }
}

/* --- Responsive ----------------------------------------------------------- */
@media (max-width: 56rem) {

  /* No middle column to centre once the links are gone, so the grid gives
     way to two groups pushed to the edges. */
  .nav {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
  }
  .nav-links { display: none; }
  .menu-btn { display: inline-flex; }

  /* The two controls become one pair of matching squares. A wide pill next
     to a square button reads as two unrelated things that happen to be
     adjacent. */
  .nav-end { gap: 0.5rem; }
  .head-cart {
    position: relative;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    gap: 0;
  }
  .head-cart-label { display: none; }

  /* The count leaves the flow and sits on the corner of the button, the way
     it does in every app with a basket in it. Inline, it stretched the
     button wider than its neighbour for a character that is usually "0". */
  .head-cart .cart-badge {
    position: absolute;
    top: -0.3125rem;
    right: -0.3125rem;
    margin: 0;
    min-width: 1.125rem;
    line-height: 1.125rem;
    background: var(--sig);
    color: #04140e;
    /* A ring in the bar's own colour, so the badge reads as sitting on top
       of the button rather than welded to its edge. */
    box-shadow: 0 0 0 2px #08090c;
  }
  /* Nothing in the list, nothing to say. A grey "0" pinned to the corner of
     the button is a notification badge announcing the absence of news, and
     it is the first thing the eye goes to in an otherwise calm bar. It
     appears — and bumps — the moment there is a number worth reading. */
  .head-cart .cart-badge[data-empty="true"] { display: none; }
}

/* A narrow secondary button sitting under a wide primary one reads as a
   mistake. Below the wrap point they stack at equal width instead. */
@media (max-width: 30rem) {
  .hero-cta { flex-direction: column; align-items: stretch; }
  .hero-cta .btn { width: 100%; }
  .shop-more { flex-direction: column; align-items: stretch; }
  .shop-more .btn { width: 100%; }
  .shop-more-note { text-align: center; }
}

/* --- Preferences ---------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  .btn:active { transform: none; }
}

@media (prefers-contrast: more) {
  :root {
    --text-2: rgba(232,236,242,0.86);
    --text-3: rgba(232,236,242,0.70);
    --text-4: rgba(232,236,242,0.55);
    --line: #39424f; --line-2: #4a5462; --line-3: #5d6877;
  }
  .backdrop::after { display: none; }
}

@media print {
  .site-head, .backdrop, .scrim, .sheet, .hero-cta { display: none; }
  body { background: #fff; color: #000; }
}

/* --- Cookie consent ------------------------------------------------------
   Same engine as the main site (assets/consent.js at the domain root — one
   domain, one decision), a different instrument. Stroked, opaque, low radius,
   mono where the console would speak. Nothing here borrows the other site's
   pills or glass. */

.cc {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 300;
  padding: var(--gutter);
  padding-bottom: max(var(--gutter), env(safe-area-inset-bottom));
  transform: translateY(105%);
  transition: transform var(--t-slow) var(--ease);
}
.cc.is-up { transform: translateY(0); }

.cc-inner {
  max-width: var(--measure);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem 2rem;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--line-2);
  border-top: 1px solid var(--edge);
  border-radius: var(--r-lg);
  background: var(--panel);      /* opaque: this is a panel, not a sheet of glass */
  box-shadow: 0 -18px 48px -24px #000;
}

.cc-copy { flex: 1 1 24rem; min-width: 0; }

/* The one mono label. It reads as a system notice rather than a marketing
   interruption, which is closer to what it actually is. */
.cc-title {
  margin: 0 0 0.5rem;
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sig-text);
}
.cc-text {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--text-2);
  max-width: 58ch;
}

.cc-acts { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* The way to read what you are agreeing to, in both the banner and panel. */
.cc-more { margin: 0.75rem 0 0; font-size: 0.8125rem; }
.cc-more a {
  color: var(--sig-text);
  text-decoration: underline;
  text-underline-offset: 0.25em;
  text-decoration-color: var(--sig-line);
  transition: text-decoration-color var(--t) var(--ease);
}
.cc-more a:hover { text-decoration-color: var(--sig); }
.cc-body .cc-more { margin-top: 1rem; }

.cc-btn {
  appearance: none;
  border: 1px solid transparent;
  border-radius: var(--r);
  padding: 0.625rem 1.125rem;
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--t) var(--ease),
              border-color var(--t) var(--ease),
              color var(--t) var(--ease),
              transform var(--t-fast) var(--ease);
}
.cc-btn:active { transform: translateY(1px); }

/* Accept and reject are the same size, the same weight and the same padding.
   The fill differs and nothing else — a reject that is harder to find than
   an accept is not a choice, it is a funnel. */
.cc-btn--yes { background: var(--sig); border-color: var(--sig); color: #04140e; }
.cc-btn--yes:hover { background: #5cecbc; border-color: #5cecbc; }
.cc-btn--no  { background: var(--panel-2); border-color: var(--line-2); color: var(--text); }
.cc-btn--no:hover { background: var(--panel-3); border-color: var(--line-3); }
.cc-btn--quiet {
  background: transparent;
  color: var(--text-2);
  border-color: transparent;
  text-decoration: underline;
  text-underline-offset: 0.25em;
  text-decoration-color: var(--line-3);
  padding-inline: 0.5rem;
}
.cc-btn--quiet:hover { color: var(--sig-text); }

/* --- Preferences panel --- */
.cc-panel {
  width: min(34rem, calc(100vw - 2rem));
  max-height: min(80dvh, 44rem);
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 40px 100px -32px #000;
  overflow: hidden;
}
.cc-panel::backdrop { background: rgba(4,5,7,0.72); }
.cc-panel[open] { display: flex; flex-direction: column; }

.cc-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
}
.cc-panel-title {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
}

.cc-x {
  position: relative;
  appearance: none; border: 1px solid var(--line-2); background: var(--panel-3);
  width: 1.875rem; height: 1.875rem; border-radius: var(--r);
  display: grid; place-items: center; cursor: pointer; flex: none;
}
.cc-x:hover { border-color: var(--line-3); }
/* Drawn in CSS: one glyph is not worth a file. */
.cc-x::before, .cc-x::after {
  content: ''; position: absolute; width: 0.6875rem; height: 1px; background: var(--text-2);
}
.cc-x::before { transform: rotate(45deg); }
.cc-x::after  { transform: rotate(-45deg); }

/* overflow-x is pinned shut: a box that scrolls vertically cannot leave the
   other axis visible, so the browser promotes it to auto and reserves a
   horizontal bar nothing ever uses. */
.cc-body {
  padding: 1.25rem;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
}
.cc-lede { margin: 0 0 1rem; font-size: 0.8125rem; color: var(--text-2); }

.cc-gpc {
  margin: 0 0 1.25rem;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--sig-line);
  border-left-width: 2px;
  border-radius: var(--r);
  background: var(--sig-dim);
  font-size: 0.8125rem;
  color: var(--sig-text);
}

.cc-rows { display: grid; gap: 0.5rem; }
.cc-row {
  padding: 0.9375rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--panel-2);
}
.cc-row-head {
  position: relative;            /* the real input is pinned over the painted track */
  display: flex; align-items: center; gap: 0.75rem;
  cursor: pointer;
}
.cc-row-name { flex: 1; font-weight: 500; font-size: 0.875rem; }
.cc-row-text { margin: 0.5rem 0 0; font-size: 0.8125rem; line-height: 1.55; color: var(--text-2); }
.cc-row-note {
  margin: 0.5rem 0 0;
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-4);
}

/* The real control stays in the tab order and the accessibility tree; the
   painted one is decoration driven off its :checked state. */
/* Sits exactly on top of the track it drives. Pinned rather than left to
   float, because an absolutely positioned box with no offsets lands wherever
   its nearest positioned ancestor happens to be — which overflowed the panel
   and put a scrollbar under it. */
.cc-switch {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  opacity: 0; margin: 0; width: 2.5rem; height: 1.375rem; cursor: pointer;
}
.cc-track {
  flex: none; position: relative;
  width: 2.5rem; height: 1.375rem;
  border-radius: 999px;
  background: var(--panel-3);
  border: 1px solid var(--line-2);
  transition: background-color var(--t) var(--ease), border-color var(--t) var(--ease);
}
.cc-knob {
  position: absolute; top: 50%; left: 0.1875rem;
  width: 0.875rem; height: 0.875rem; border-radius: 999px;
  background: var(--text-3);
  transform: translateY(-50%);
  transition: transform var(--t) var(--ease), background-color var(--t) var(--ease);
}
.cc-switch:checked + .cc-track { background: var(--sig-dim); border-color: var(--sig-line); }
.cc-switch:checked + .cc-track .cc-knob { transform: translate(1.0625rem, -50%); background: var(--sig); }
.cc-switch:disabled + .cc-track { opacity: 0.6; }
.cc-switch:disabled, .cc-switch:disabled + .cc-track { cursor: default; }
.cc-switch:focus-visible + .cc-track { outline: 1px solid var(--sig); outline-offset: 2px; }

.cc-foot {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
  padding: 1rem 1.25rem;
  border-top: 1px solid var(--line);
  background: var(--panel-2);
}
.cc-foot .cc-btn { flex: 1 1 auto; }

/* The withdrawal control. Permanent, on every page, next to the legal links
   where someone looking for it will look. */
/* Geometry copied from the links beside it, including the touch target. */
.foot-base [data-consent-open] {
  appearance: none; border: 0; background: none;
  font: inherit; letter-spacing: inherit; color: inherit; cursor: pointer;
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding-inline: 0.5rem;
  margin: -0.75rem -0.5rem;
  transition: color var(--t) var(--ease);
}
.foot-base [data-consent-open]:hover { color: var(--text-2); }

@media (max-width: 48rem) {
  .cc-inner { padding: 1.125rem; }
  .cc-acts { width: 100%; }
  .cc-acts .cc-btn--yes, .cc-acts .cc-btn--no { flex: 1 1 9rem; }
  .cc-acts .cc-btn--quiet { flex: 1 1 100%; }
  .cc-foot { flex-direction: column; }
}

@media (pointer: coarse) {
  .cc-btn { min-height: 2.75rem; }
  .cc-x { width: 2.75rem; height: 2.75rem; }
}

@media (prefers-reduced-motion: reduce) {
  .cc, .cc-track, .cc-knob, .cc-btn { transition: none; }
}

@media (prefers-contrast: more) {
  .cc-inner, .cc-panel { border-color: var(--line-3); }
  .cc-row { border-color: var(--line-2); }
}

@media print { .cc, .cc-panel { display: none !important; } }

/* --- Privacy notice ------------------------------------------------------
   A specification sheet rather than a brochure page: one column, hard rules
   between sections, mono where the document is naming a thing rather than
   describing it. Same document, different instrument from the main site. */

.legal { max-width: 48rem; }

.legal-head { margin-bottom: clamp(2.5rem, 5vw, 3.5rem); }
.legal-head .lede { max-width: 54ch; }

.legal-date {
  margin: 1.5rem 0 0;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-4);
}

/* The summary earns a panel because it answers the question the rest of the
   page qualifies. One signal stroke on the leading edge, nothing more. */
.legal-tldr {
  padding: clamp(1.125rem, 3vw, 1.5rem);
  border: 1px solid var(--line);
  border-left: 2px solid var(--sig-line);
  border-radius: var(--r);
  background: var(--panel);
}
.legal-tldr h2 { margin-bottom: 0.875rem; }
.legal-tldr ul { margin: 0; padding-left: 1.125rem; display: grid; gap: 0.625rem; }
.legal-tldr li { color: var(--text-2); }
.legal-tldr li::marker { color: var(--sig); }

.legal-toc {
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.legal-toc-title {
  margin: 0 0 0.875rem;
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-4);
}
.legal-toc ol {
  margin: 0; padding: 0; list-style: none;
  columns: 2; column-gap: 2rem;
  font-size: 0.875rem;
}
.legal-toc li { break-inside: avoid; margin-bottom: 0.4375rem; }
.legal-toc a {
  color: var(--text-2);
  transition: color var(--t) var(--ease);
}
.legal-toc a:hover { color: var(--sig-text); }

.legal-body { margin-top: clamp(2.5rem, 5vw, 3.5rem); }

.legal-body h2 {
  margin-top: clamp(2.75rem, 5vw, 3.5rem);
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  scroll-margin-top: calc(var(--nav-h) + 2rem);
}
.legal-body > h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

.legal-h4 {
  margin: 1.75rem 0 0;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text);
}

.legal-body p { margin: 0.875rem 0 0; color: var(--text-2); text-wrap: pretty; }
.legal-body strong { color: var(--text); font-weight: 600; }

/* Prose wants ~68 characters a line. The panel that holds it is wider than
   that because the table needs the room, so the measure is set on the text
   rather than on the column. */
.legal-body p,
.legal-body .legal-list,
.legal-tldr ul,
.legal-tldr > p { max-width: 68ch; }

.legal-body em { color: var(--text); font-style: italic; }

.legal-body a:not(.legal-link-btn) {
  color: var(--sig-text);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--sig-line);
  transition: text-decoration-color var(--t) var(--ease);
}
.legal-body a:not(.legal-link-btn):hover { text-decoration-color: var(--sig); }

.legal-body code {
  font-family: var(--mono);
  font-size: 0.875em;
  padding: 0.1em 0.375em;
  border: 1px solid var(--line-2);
  border-radius: 3px;
  background: var(--panel-2);
  color: var(--text);
}

.legal-list {
  margin: 1rem 0 0; padding-left: 1.125rem;
  display: grid; gap: 0.75rem;
  color: var(--text-2);
}
.legal-list li::marker { color: var(--text-4); }

/* The "this is not legal advice" line. Set apart so it does not read as one
   more clause, because it is the opposite of one. */
.legal-note {
  margin-top: 1.5rem !important;
  padding: 0.875rem 1rem;
  border: 1px solid var(--line);
  border-left: 2px solid var(--line-3);
  border-radius: var(--r);
  background: var(--panel-2);
  font-size: 0.8125rem;
  color: var(--text-3) !important;
}

.legal-link-btn {
  appearance: none; border: 0; background: none; padding: 0;
  font: inherit; cursor: pointer;
  color: var(--sig-text);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--sig-line);
  transition: text-decoration-color var(--t) var(--ease);
}
.legal-link-btn:hover { text-decoration-color: var(--sig); }

/* Scrolls rather than crushing four columns onto a phone. */
.legal-table-wrap {
  margin-top: 1.25rem;
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--r);
}
.legal-table {
  width: 100%; min-width: 34rem;
  border-collapse: collapse;
  font-size: 0.8125rem;
  text-align: left;
}
.legal-table th, .legal-table td {
  padding: 0.6875rem 0.875rem;
  vertical-align: top;
  border-bottom: 1px solid var(--line);
}
.legal-table thead th {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-4);
  background: var(--panel-2);
  white-space: nowrap;
}
.legal-table tbody th { font-weight: 500; white-space: nowrap; }
.legal-table td { color: var(--text-2); }
.legal-table tr:last-child th, .legal-table tr:last-child td { border-bottom: 0; }
.legal-table code { background: none; border: 0; padding: 0; }

@media (max-width: 34rem) {
  .legal-toc ol { columns: 1; }
}

@media (prefers-contrast: more) {
  .legal-body h2, .legal-table-wrap, .legal-toc { border-color: var(--line-2); }
}

@media print {
  .legal-toc { display: none; }
  .legal-body h2 { break-after: avoid; }
  .legal-table-wrap { overflow: visible; }
}

/* --- About page -----------------------------------------------------------
   Everything about a person is optional, so each block here has to look
   deliberate on its own rather than relying on the one above it being
   present. See L8_PEOPLE in config.php. */

.person {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1.25rem, 3vw, 2rem);
  align-items: flex-start;
}
.person + .person {
  margin-top: clamp(1.75rem, 4vw, 2.5rem);
  padding-top: clamp(1.75rem, 4vw, 2.5rem);
  border-top: 1px solid var(--line);
}
.person-photo {
  flex: none;
  width: 7rem; height: 7rem;
  border-radius: 50%;
  object-fit: cover;
  background: var(--panel-2);
  box-shadow: 0 0 0 1px var(--line);
}
.person-body { flex: 1 1 18rem; min-width: 0; }
.person-role {
  margin: 0.25rem 0 0;
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--sig);
}

/* Credentials as labelled values, not badges. A badge is a picture of a
   claim; a licence number is the claim, and it can be checked. */
.person-creds {
  display: grid;
  gap: 0.625rem;
  margin: 1.25rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.person-creds > div {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 1rem;
  align-items: baseline;
}
.person-creds dt {
  flex: none;
  min-width: 10rem;
  font-size: 0.8125rem;
  color: var(--text-3);
}
.person-creds dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.875rem;
  color: var(--text);
}

@media (max-width: 30rem) {
  .person-creds dt { min-width: 0; flex-basis: 100%; }
}

/* The drift field on the about page. Fixed and behind everything: it is a
   backdrop, not a section, so it does not scroll with the copy and it never
   takes a pointer event - the handler listens on the window instead. */
.drift {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.drift canvas { display: block; width: 100%; height: 100% }

/* Nothing drifting for someone who asked the page to hold still. The script
   draws one static frame in that case rather than hiding it, so the texture
   is still there and only the motion is gone. */
