/* ==========================================================================
   OOGLE V5 — ART DIRECTION LAYER

   tokens.css defines the system. styles.css builds the components. This file
   is what sits on top: the grounds, the two motifs, and the small amount of
   motion the site is allowed.

   WHAT THIS FILE USED TO BE

   It was 1,050 lines. It had signal fields, node topologies, capability
   drawings, a resolver, an era ribbon, cropped background words, editorial
   topic artwork, parallax, ambient dash-travel and four background systems.
   Each piece was defensible. The sum was a company that looked like it sold
   network monitoring.

   It is now about a fifth of that, and the rule is a hard one:

     TWO MOTIFS.
       1. the blue keyline — a 2px rule, in styles.css, already the site's
          most repeated shape
       2. the pixel field — squares on a lattice, three placements, no more

   FOUR GROUNDS, and no pattern behind anything else:
       white · soft gray · deep navy · Oogle blue

   Everything here could be deleted and the site would still be a complete,
   accessible, well-typeset document. That was true before and it is the one
   thing worth keeping from the previous version of this file.
   ========================================================================== */


/* ==========================================================================
   1. THE PIXEL FIELD
   ========================================================================== */
.art-px {
  display: block; width: 100%; height: 100%;
  overflow: hidden;
  color: var(--blue-light); opacity: 0.85;
  /* V5.11: 90deg. The house rule in visual.py says "no diagonal -- a diagonal
     would be somebody else's idea", and this mask had been contradicting it
     by ten degrees on six plates. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 62%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 62%);
}
.px      { fill: currentColor; opacity: 0.16; }
.px-mid  { fill: currentColor; opacity: 0.26; }
.px-hi   { fill: currentColor; opacity: 0.42; }

/* The layer that holds it. Absolutely positioned inside a clipping parent,
   masked away from the half the reading sits on — a pattern under a paragraph
   is not texture, it is a pattern under a paragraph, and low opacity does not
   fix that. */
/* .art-layer marks anything decorative: it is aria-hidden, never focusable,
   and interact.py asserts both plus a hard cap of two per page. The V5.10
   artwork carries the class for exactly that reason. */
.art-layer {
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none; overflow: hidden;
}
.art-layer > svg { position: absolute; inset: 0; width: 100%; height: 100%; }


/* ==========================================================================
   2. THE FOUR GROUNDS
   White and soft gray come from styles.css (.on-paper). The two
   strong ones are here.
   ========================================================================== */

/* --- Oogle blue ----------------------------------------------------------
   The loudest thing in the system, so it is rationed to one appearance per
   page. Its gradient never goes lighter than #0E61D1, which is what keeps
   white on it at 5.75:1 everywhere in the field. */
.bluefield {
  position: relative; overflow: clip; isolation: isolate;
  background-color: var(--blue);
  /* V5.11: 180deg, not 162. Every ground in the system is orthogonal now.
     An 18-degree ramp is not a design idea, it is a default nobody chose. */
  background-image: linear-gradient(180deg, var(--blue) 0%, var(--blue-deep) 76%, var(--blue-far) 100%);
  color: #fff;
}
.bluefield > * { position: relative; z-index: 2; }
.bluefield .art-layer { color: var(--navy); opacity: 0.45; }
.bluefield .eyebrow { color: #E6F0FF; }
.bluefield .eyebrow::before { background: var(--cyan-soft); }
.bluefield h2, .bluefield h3, .bluefield .display, .bluefield .mega { color: #fff; }
.bluefield p, .bluefield .lead, .bluefield li { color: #E6F0FF; }
.bluefield .mono { color: #DCEBFF; }   /* 4.76:1 on --blue; #CFE2FF was 4.38 */
.bluefield a:not(.btn) { color: #fff; text-decoration: underline; text-underline-offset: 3px; }
.bluefield a:not(.btn):hover { text-decoration-color: var(--cyan-soft); }
.bluefield .btn-ghost { border-color: rgba(255, 255, 255, 0.62); color: #fff; }  /* 3.18:1 boundary */
.bluefield .btn-ghost:hover { border-color: #fff; background: rgba(255, 255, 255, 0.10); color: #fff; }
.bluefield .btn-solid, .bluefield .btn:not(.btn-ghost) {
  background: #fff; color: var(--blue-deep); border-color: #fff;
}
.bluefield .btn:not(.btn-ghost):hover { background: #E6F0FF; color: var(--blue-deep); }
.bluefield :focus-visible { outline-color: #fff; }
/* Anything that brings its own white surface keeps its own ink. */
.bluefield .panel, .bluefield .panel p, .bluefield .panel li { color: var(--ink-3); }
.bluefield .panel {
  background: var(--paper); border-color: transparent;
  box-shadow: 0 30px 70px -34px rgba(4, 20, 56, 0.55);
}
.bluefield .panel .mono { color: var(--ink-4); }
.bluefield .caps li, .bluefield .chip {
  background: rgba(255, 255, 255, 0.94); border-color: transparent; color: var(--ink-3);
}


/* ==========================================================================
   3. THE HERO

   V5.9.5 put a headline, a lead and two buttons on the left of a 1440px band
   and filled the remaining 31% with 220 five-pixel squares at an effective
   8-21% alpha. That measured out at under half a percent ink: it was not a
   device, it was dust, and at 390px it was nothing at all.

   What is there now is one object. The period in the Oogle wordmark, at
   display scale, bleeding off the right edge and off the top and bottom, so
   it reads as a fragment of something larger. Its leading edge comes apart
   into squares that step toward the headline and stop short of it. The mark
   and the motif are finally the same shape.

   The hero closes on a full-bleed ruled band carrying the four facts that
   answer "are these people real" before the visitor has scrolled at all.
   ========================================================================== */
.hero {
  /* The section is now a light page ground with a contained object on it.
     The .on-dark token re-point moved off this selector and onto .hero-panel
     in tokens.css, because the navy is the panel's, not the page's. */
  isolation: isolate;
  position: relative;
  --sec-pad: clamp(14px, 2vw, 28px);
  padding-block: var(--sec-pad) 0;
}

/* The panel. This is the object: one navy surface, one large radius, its own
   generous padding, and `overflow: clip` so the artwork is cut by the
   panel's corners instead of by the viewport. V5.10 let the object leave the
   frame on three sides on the theory that containment "would make it an
   illustration". It made the page look like a deck slide instead. */
.hero-panel {
  position: relative;
  /* `clip`, not `hidden`. Both crop the artwork to the panel's corners, but
     `hidden` makes the panel a scroll container -- which silently breaks any
     position: sticky inside it, and at 200% text zoom leaves overflow the
     reader cannot scroll to. */
  overflow: clip;
  border-radius: var(--radius-feature);
  background: var(--navy);
  box-shadow: var(--panel-lift);
}

.hero-stage {
  position: relative;
  padding: clamp(56px, 7.5vw, 108px) clamp(28px, 4.5vw, 80px) clamp(52px, 6.5vw, 92px);
}

/* The grid reserves the artwork's territory with an empty cell rather than
   letting the copy and the object share the same space and hope. Nothing can
   collide with the object at any width because nothing is ever placed there. */
.hero-grid {
  position: relative; z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
  gap: clamp(32px, 5vw, 64px);
  align-items: center;
  min-height: clamp(320px, 34vw, 460px);
}
.hero-blank { min-height: 1px; }

/* The field's slot. Full panel height, anchored right, clipped by the panel's
   own radius. It is a background system now, not an object placed in a box. */
.hero-art {
  position: absolute;
  z-index: 1;
  inset: 0 0 0 auto;
  translate: none;
  width: clamp(300px, 48%, 640px);
  min-height: 0;
  pointer-events: none;
  /* Plane one of two. The pointer parallax writes a transform straight onto
     this element and onto .sf-cells; the easing is a CSS transition, so there
     is no rAF loop, nothing to schedule and nothing to tear down. Six pixels
     of travel, which is meant to be felt rather than seen. */
  transition: transform 520ms var(--e-out), opacity var(--m-scene) var(--e-out);
}

/* ==========================================================================
   3a. THE SIGNAL FIELD

   Four layers on one lattice. The lattice is `--cell`, and --cell is the ONLY
   number that positions anything: the grid's pitch, the bands' left edge and
   width, and every lit cell's x and y are all expressed in it. A pixel
   therefore cannot be anywhere except on a grid intersection, which is the
   structural answer to V5.11's squares looking sprayed on.

   Nothing here animates a layout or paint property. The bands, the glow and
   the two parallax planes animate `transform`; the cells animate `opacity`.
   Both are interpolated on the compositor thread, so the field costs the main
   thread nothing per frame and keeps running at full rate even while the page
   is busy. No canvas, no shader, no library, no requestAnimationFrame.

   Markup comes from `visual.py::signal_field()` -- positions and phases only,
   deterministic from a seed, so the artwork is reviewable in a diff.
   ========================================================================== */
.sfield {
  /* --cell is a LENGTH, and it has to be.
     It shipped as `calc(100% / 13)` for one build, which looks right and is
     not: an unresolved percentage resolves against the WIDTH for `left` and
     `width`, and against the HEIGHT for `top` and `height`. So every lattice
     square and every lit cell came out a portrait rectangle whose aspect
     tracked the panel's -- 1:1.27 at 1440 and 1:1.86 at 900, where the field
     read as an equaliser rather than as pixels. The brand's shape is a square.
     `cqw` resolves against the field's inline size on both axes, so thirteen
     columns fill the field exactly AND the cell is square. The clamp is the
     fallback where container queries are missing: not an exact thirteen, but
     square, which is the property that matters. */
  --cell: clamp(26px, 3.2vw, 47px);
  position: absolute; inset: 0;
  /* `clip`, not `hidden`: same rule as every other panel on the site. */
  overflow: clip;
  /* The field fades in from the left so it is at full strength only where the
     type is not. A pulsing element behind body copy cannot pass contrast, and
     no amount of restraint fixes that -- so it is masked, not dimmed. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 42%, #000 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 42%, #000 100%);
}

@supports (container-type: inline-size) {
  .hero-art { container-type: inline-size; }
  .sfield { --cell: calc(100cqw / 13); }
}

/* --- the lattice. Two gradients, painted once, never animated. ------------ */
.sf-grid {
  position: absolute; inset: -1px;
  background-image:
    linear-gradient(90deg, rgba(120, 170, 230, 0.13) 1px, transparent 1px),
    linear-gradient(180deg, rgba(120, 170, 230, 0.10) 1px, transparent 1px);
  background-size: var(--cell) var(--cell);
}

/* --- the bands. The prism's DNA, cleaned up: no tear, no hard stops.
   ONE repeating gradient rather than three placed elements. Placed bands are
   anchored to a fixed origin, so at any box the lattice does not exactly fill
   they end up crowded into one half of the field with bare grid beside them.
   A repeating gradient has no origin to be wrong about: it fills whatever it
   is given, at any width, and it is one composited layer instead of three. */
.sf-bands {
  position: absolute; inset: -12% -100% -12% -100%;
  /* Soft shoulders, not hard stops. The first build used hard stops and one
     of them landed 90px in from the panel's right edge, drawing a full-height
     vertical seam and leaving the field terminating in a dark strip instead of
     bleeding off the corner. Every edge here ramps over 3% of the repeat --
     enough to remove the seam, not enough to become a blur. */
  background-image: linear-gradient(90deg,
      rgba(14, 97, 209, 0.26) 0%, rgba(14, 97, 209, 0.26) 20%,
      transparent 23%, transparent 35%,
      rgba(0, 191, 255, 0.13) 38%, rgba(0, 191, 255, 0.13) 51%,
      transparent 54%, transparent 66%,
      rgba(20, 50, 186, 0.28) 69%, rgba(20, 50, 186, 0.28) 97%,
      rgba(14, 97, 209, 0.26) 100%);
  background-size: calc(var(--cell) * 6.5) 100%;
  /* Vertical falloff so the bands are a field, not a barcode. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 24%, #000 76%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0%, #000 24%, #000 76%, transparent 100%);
}

/* --- the light. One soft field for depth, no blur filter. ---------------- */
.sf-glow {
  position: absolute; inset: -25%;
  background: radial-gradient(46% 38% at 62% 42%,
              rgba(0, 191, 255, 0.16) 0%,
              rgba(14, 97, 209, 0.10) 38%,
              transparent 72%);
}

/* --- the cells. The group pulses; the cell only sits still and differs.
   Four groups instead of fifteen loose cells: fifteen independently animated
   elements is fifteen composited layers and 27MB of layer memory for a
   decoration. Each cell keeps its own base opacity, so a group brightens a set
   of unequal squares rather than a block of identical ones. */
.sf-cells {
  position: absolute; inset: 0;
  /* Plane two of the parallax. The transform is written by JS directly onto
     this element, not inherited from a custom property on the panel: writing
     --px on .hero-panel dirtied 43 elements per pointer move. */
  transition: transform 640ms var(--e-out);
  /* The resting value, and the value a reduced-motion visitor sees. The
     keyframes swing either side of it, so switching the motion off leaves a
     complete lit grid rather than a blank one. */
  opacity: 0.26;
}
.sf-cells i {
  position: absolute;
  left: calc(var(--x) * var(--cell));
  top: calc(var(--y) * var(--cell));
  width: calc(var(--cell) - 1px);
  height: calc(var(--cell) - 1px);
  background: var(--cyan);
  opacity: var(--o, 0.8);
}

@media (prefers-reduced-motion: no-preference) {
  /* One pitch of travel, `linear infinite`: the gradient repeats every 6.5
     cells, so translating exactly that distance loops seamlessly and needs no
     `alternate`. */
  .sf-bands { animation: sfDrift 30s linear infinite; }
  .sf-glow  { animation: sfGlow 26s var(--e-inout) infinite alternate; }
  .sf-cells {
    animation: sfPulse var(--t) var(--e-inout) infinite;
    /* Negative delay: every group starts part-way through its own cycle, so
       the field is already mid-breath on the first frame. */
    animation-delay: calc(var(--d) * -1);
  }
  /* Nothing animates while it cannot be seen. The compositor otherwise ticks
     every one of these sixty times a second for the whole life of the tab,
     including after the visitor has scrolled a thousand pixels past them:
     3.2% of a core, indefinitely, for a picture nobody is looking at. */
  .is-still .sf-bands,
  .is-still .sf-glow,
  .is-still .sf-cells,
  .is-still .closer-art > img { animation-play-state: paused; }
}
@keyframes slideIn {
  from { transform: scale(1.05) translateY(-1.6%); }
  to   { transform: none; }
}
@keyframes sfDrift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(var(--cell) * -6.5), 0, 0); }
}
@keyframes sfGlow {
  from { transform: translate3d(-5%, -4%, 0); }
  to   { transform: translate3d(6%, 5%, 0); }
}
@keyframes sfPulse {
  /* 0.20-0.36, not 0.12-0.46. A 3.8x swing on the brightest mark in the
     composition is a blink; 1.8x is a breath, which is what was asked for. */
  0%, 100% { opacity: 0.20; }
  50%      { opacity: 0.36; }
}


.hero-copy { max-width: none; position: relative; }
.hero-copy .eyebrow { color: var(--cyan); }
.hero-copy h1 { margin-top: var(--s6); }
/* Two colours in one headline halved its authority: the light blue second
   line sat at a lower value than the first and the sentence came apart. One
   colour, one weight. The period is the only thing that changes. */
.hero h1 em { font-style: normal; color: inherit; }
.hero h1 em::after { content: ""; }
.hero-copy .lead {
  color: var(--snow-2); max-width: 32ch; margin-top: var(--s7);
}
.hero-cta { margin-top: var(--s9); }

/* ==========================================================================
   3b. THE FACT BAR
   Its own component, sitting under the hero with a deliberate gap. V5.11 had
   it as a rail welded to the bottom of the hero panel, which made four
   verifiable facts read as small print under a headline instead of as a
   statement of terms.
   ========================================================================== */
/* The gap is the whole argument for the component existing, so it is bigger
   than either radius. It shipped at 18px against a 32px hero corner: two dark
   rounded panels of identical width with their curves nearly touching read as
   one surface with a crack in it, not as two objects. */
.factbar { padding-block: clamp(28px, 3.4vw, 52px) 0; }
.facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  list-style: none; margin: 0; padding: 0;
  /* Darker than the hero's navy on purpose: two navy surfaces at the same
     value read as one surface interrupted. */
  background: #040914;
  border: 1px solid var(--line-dark);
  /* Matching the hero's radius, not one step under it. 32 above 24 with a thin
     gap between them looks like an oversight rather than a decision. */
  border-radius: var(--radius-feature);
  overflow: clip;
}
.facts li { min-width: 0; display: flex; }
/* The cell is a link. The bar had a hover lift on four <li> that went nowhere,
   which is a false affordance -- a surface that lights under the cursor is
   telling the visitor it is clickable. Every one of these four facts is
   verifiable on a page of this site, so the honest fix was to make the claim
   go to its evidence rather than to delete the interaction the brief asked
   for. */
.facts a {
  display: block; width: 100%;
  overflow-wrap: anywhere;
  padding: clamp(22px, 2.4vw, 32px) clamp(18px, 2vw, 30px);
  transition: background-color var(--m-state) var(--e-inout);
}
/* Inset, not full-bleed. A divider that runs edge to edge between four equal
   columns of two left-aligned lines is a table rule; the same line stopped
   short of the padding is a separator. */
.facts li + li { position: relative; }
.facts li + li::before {
  content: ""; position: absolute; left: 0; top: 22px; bottom: 22px;
  width: 1px; background: var(--line-dark);
}
.facts b {
  display: block;
  /* 18px, not 15. This is the bar's subject line, and at 15px against a 13px
     letterspaced uppercase mono the second line was wider, more textured and
     landed first -- the hierarchy read backwards. */
  font-size: var(--t-h4); font-weight: var(--w-bold); line-height: 1.2;
  color: var(--snow); letter-spacing: var(--tr-h4);
}
.facts span {
  display: block; margin-top: 9px;
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: var(--tr-mono); text-transform: uppercase;
  color: var(--snow-3); line-height: 1.45;
}
/* Informational, not a toy: the surface lifts one step and the second line
   gains contrast. Nothing moves. */
.facts a:hover { background: rgba(110, 160, 220, 0.07); }
.facts a:hover b { color: #fff; }
.facts a:hover span { color: var(--snow-2); }

@media (max-width: 900px) {
  .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .facts li:nth-child(odd)::before { content: none; }
  .facts li:nth-child(n+3) { border-top: 1px solid var(--line-dark); }
}
/* Phones: the bar keeps its breathing room but stops spending it twice. The
   hero and the four facts together are 115% of a 390x844 viewport at the
   generous padding, and these four lines exist to be read before the visitor
   scrolls. */
/* Phones: the bar keeps its breathing room but stops spending it twice. */
@media (max-width: 560px) {
  .facts a { padding: var(--s5); }
  .facts b { font-size: var(--t-sm); }
  .facts span { margin-top: 6px; line-height: 1.35; }
  .facts li + li::before { top: 16px; bottom: 16px; }
}
@media (max-width: 600px) { .facts { border-radius: var(--radius-panel); } }

@media (max-width: 64em) {
  .hero-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 0.62fr); }
  .hero-art { width: clamp(260px, 40%, 460px); }
  .hero-copy .lead { max-width: 40ch; }
}
/* Below 820 the field stops being a column beside the type and becomes the
   panel's own ground, present only in its lower half.

   The first V5.12 build gave it a 150px band under the CTAs. That cost the
   thing the facts exist for: hero plus fact bar went from 97.7% of a 390x844
   phone to 117%, so the four facts that answer "are these people real" before
   the visitor scrolls were below the fold on every phone. Seventeen percent of
   a viewport is not available for decoration.

   The lattice and the light stay, masked to fade in downward, so the panel has
   texture and depth and costs no height at all. The cells go: a pulsing square
   is a detail for a screen with room for details, and a cell that renders
   nothing while still ticking sixty times a second is cost with no picture. */
@media (max-width: 51.25em) {
  .hero-grid { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  .hero-blank { display: none; }
  .hero-art { inset: 0; width: auto; transform: none; }
  .sfield {
    --cell: clamp(26px, 7.5vw, 40px);
    -webkit-mask-image: linear-gradient(180deg, transparent 44%, #000 94%);
    mask-image: linear-gradient(180deg, transparent 44%, #000 94%);
  }
  /* display:none, not opacity:0 -- an animation on a display:none element does
     not run, which is the point.
     The bands go too. With the cells gone they were the only thing left, and a
     band gradient striped by vertical gridlines at a 28px pitch behind the
     lead and both buttons does not read as digital infrastructure. It reads as
     gradient banding: a rendering fault, not a design. The grid and the light
     are enough for texture on a phone. */
  .sf-cells, .sf-bands { display: none; }
}
@media (max-width: 600px) {
  .hero-panel { border-radius: var(--radius-panel); }
  .hero-copy .lead { max-width: 30ch; }
  /* Two up, not four stacked: the same four facts in half the height. */
}


/* ==========================================================================
   3c. THE INTERNAL PAGE HERO SYSTEM (V5.13)

   Five families, one component. Every internal page used to open with an
   eyebrow, a headline, a paragraph and a bordered note -- fifty-one times.
   The families differ by GROUND and by what sits in the second column; the
   grid, the radius, the padding and the entrance are shared, so the system is
   one set of rules rather than fifty-one designs.

     .phero-service    navy, the page's own diagram
     .phero-industry   pale, bordered, same diagram grammar
     .phero-company    the warm ground, used once on /about/
     .phero-portfolio  navy, real client screenshots in browser frames
     .phero-editorial  typography and a hairline, no artwork at all

   The fifth family is the point of the other four: hierarchy means a legal
   page and a contact form do NOT get a production, and giving every page the
   same intensity is how a site ends up looking like a template again.
   ========================================================================== */
.phero { --sec-pad: clamp(14px, 2vw, 28px); padding-block: var(--sec-pad) 0; }
.phero-panel {
  position: relative;
  isolation: isolate;
  overflow: clip;
  border-radius: var(--radius-feature);
  padding: clamp(34px, 4vw, 66px) clamp(26px, 4vw, 68px);
  box-shadow: var(--panel-lift);
}
.phero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
  gap: clamp(28px, 4.5vw, 72px);
  align-items: center;
}
.phero-copy { min-width: 0; position: relative; z-index: 2; }
/* `break-word`, and one value at every width.
   V5.13 set `normal` here so a headline's final full stop could not break
   onto a line of its own, on the reasoning that "the column has room and the
   panel clips nothing". Neither half held. Above 1000px the hero is two
   columns, and 33 pages join their last two words with &nbsp; -- on
   /who-we-help/education/ at 1920 the last line ran 108px PAST the copy
   column and printed its full stop on top of the artwork panel. Below 44em
   the fallback was `anywhere`, which broke words mid-token: at 375 the
   /website-maintenance/ headline set "are" and "that" on lines of their own
   and then a 10px line containing nothing but ".".
   `break-word` is the value that does what both were reaching for: it breaks
   only when a word genuinely cannot fit, so nothing leaves the column and
   nothing is split that did not have to be. An orphaned full stop is a
   typographic irritation; a headline sitting on the artwork is a broken
   page. */
/* V5.22: A FLOOR THAT GUARANTEES THE LONGEST WORD FITS ITS OWN COLUMN.
   --t-display bottoms out at 40px, but the column an internal h1 sits in is
   set by the panel and the hero grid, not by the viewport: 216px inside a
   320px screen, and 424px at 1024 where the copy column is roughly half a
   capped track. At those two widths eleven-letter words were wider than the
   line they were on and `overflow-wrap: break-word` split them mid-word --
   measured: "WooCommerc / e", "maintenan / ce", "emergenci / es",
   "cooperatin / g.", "informatio / n", "Sometime / s".

   `cqi` rather than `vw` because the CONTAINER is the constraint and the
   viewport is not: 12.5cqi solves at 320 and at 1024 with the same number,
   where no single vw value can (8.8vw at 320 is 5.4vw at 1024). The homepage
   .mega solved the same problem with min(--t-mega, 12.5vw) because its column
   really does track the viewport; this one does not.

   It only bites where it has to. At 375 and up it is within a couple of
   percent of --t-display and at 1440 it is above it, so nothing moves; at 320
   the headline sets at 31px instead of 40, which is the correct size for a
   line that is 248px wide. */
.phero-copy { container-type: inline-size; }
.phero-copy h1 {
  margin-top: var(--s6);
  max-width: 17ch;
  overflow-wrap: break-word;
  font-size: min(var(--t-display), 12.5cqi);
}
.phero-copy .lead { margin-top: var(--s7); max-width: 40ch; }
.phero-act { margin-top: var(--s8); align-items: center; }
/* The aside every service and audience page already carried. It stays in the
   copy column: the second column is the drawing now, and dropping a sentence
   of real content to make room for decoration is the wrong trade. */
.phero-note {
  margin-top: var(--s8); padding-left: var(--s5);
  border-left: 2px solid var(--keyline);
  max-width: 46ch;
}
.phero-note .mono { display: block; margin-bottom: var(--s3); }
.phero-note p { font-size: var(--t-sm); line-height: 1.6; }

/* The drawing's frame. A fixed aspect so a family reads the same from page to
   page whatever the copy length, and the lattice ground the whole site uses. */
/* The drawing sits on its own surface, and that surface is the full height of
   the panel. It was a fixed 640/500 box centred in whatever height the copy
   produced, which on a long page left 310px of empty navy above the drawing
   and 310px below it -- a thin wire graphic adrift in a rectangle. A defined
   area at panel height is a composition at any copy length. */
.phero-art {
  position: relative; z-index: 1;
  min-width: 0;
  align-self: stretch;
  min-height: 300px;
  display: grid; place-items: center;
  padding: clamp(12px, 1.6vw, 26px);
  border-radius: var(--radius-panel);
  background: var(--phero-art-bg);
  border: 1px solid var(--phero-art-edge);
}
.phero-art > .m { width: 100%; max-height: 100%; display: block; }
.phero-service, .phero-portfolio {
  --phero-art-bg: rgba(140, 180, 230, 0.035);
  --phero-art-edge: rgba(140, 180, 230, 0.12);
}
.phero-industry { --phero-art-bg: rgba(255, 255, 255, 0.62); --phero-art-edge: var(--surface-edge); }
.phero-company  { --phero-art-bg: rgba(255, 255, 255, 0.46); --phero-art-edge: var(--sand-2); }

/* --- the grounds ---------------------------------------------------------
   Each family sets its own ground and its own drawing palette in one place.
   A motif never knows which family it is in. */
.phero-service .phero-panel,
.phero-portfolio .phero-panel { background: var(--navy); }
.phero-industry .phero-panel {
  background: var(--paper-2);
  border: 1px solid var(--surface-edge);
  box-shadow: var(--lift-1);
}
.phero-company .phero-panel {
  background: var(--sand);
  border: 1px solid var(--sand-2);
  box-shadow: var(--lift-1);
}

/* The same lattice as the homepage field, at a fifth of the presence, masked
   to the drawing's side. It is what makes an internal panel feel like part of
   the same site rather than a flat rectangle with a picture in it. */
.phero-panel::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(90deg, var(--phero-grid-line) 1px, transparent 1px),
    linear-gradient(180deg, var(--phero-grid-line) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: linear-gradient(90deg, transparent 30%, #000 78%);
  mask-image: linear-gradient(90deg, transparent 30%, #000 78%);
}
.phero-service .phero-panel, .phero-portfolio .phero-panel {
  --phero-grid-line: rgba(120, 170, 230, 0.10);
}
.phero-industry .phero-panel { --phero-grid-line: rgba(30, 70, 130, 0.07); }
.phero-company .phero-panel  { --phero-grid-line: rgba(90, 70, 45, 0.06); }

/* --- the drawing palette, per ground ------------------------------------- */
.phero-service, .phero-portfolio {
  --phero-plane-solid: #0B1A2E;
  --m-stroke: rgba(155, 195, 240, 0.46);
  --m-hair:   rgba(155, 195, 240, 0.28);
  --m-fill:   rgba(155, 195, 240, 0.14);
  --m-bar:    rgba(185, 215, 248, 0.36);
  --m-key:    var(--blue-lift);
  --m-key-fill: rgba(42, 114, 220, 0.24);
  --m-hot:    var(--cyan);
}
.phero-industry {
  --phero-plane-solid: #FFFFFF;
  --m-stroke: rgba(20, 55, 105, 0.28);
  --m-hair:   rgba(20, 55, 105, 0.16);
  --m-fill:   rgba(20, 55, 105, 0.07);
  --m-bar:    rgba(20, 55, 105, 0.22);
  --m-key:    var(--blue);
  --m-key-fill: rgba(14, 97, 209, 0.14);
  --m-hot:    var(--blue-deep);
}
.phero-company {
  --phero-plane-solid: #FBFAF8;
  --m-stroke: rgba(70, 58, 42, 0.30);
  --m-hair:   rgba(70, 58, 42, 0.17);
  --m-fill:   rgba(70, 58, 42, 0.08);
  --m-bar:    rgba(60, 50, 38, 0.26);
  --m-key:    var(--blue);
  --m-key-fill: rgba(14, 97, 209, 0.16);
  --m-hot:    var(--blue-deep);
}

/* --- the drawing vocabulary ---------------------------------------------
   Fourteen diagrams, twenty-one classes. Every motif is built from these, so
   a new drawing needs no new CSS. */
.m-frame  { stroke: var(--m-stroke); stroke-width: 1.5; }
.m-frame-key { stroke: var(--m-key); stroke-width: 1.5; }
.m-plane  { stroke: var(--m-stroke); stroke-width: 1.5; fill: var(--m-fill); }
.m-plane-top { stroke: var(--m-key); fill: var(--m-key-fill); }
.m-line, .m-tick, .m-ring { stroke: var(--m-hair); stroke-width: 1; }
/* 2px. A 1px tick renders at 0.58px at 1024 and is simply not there. */
.m-tick2 { stroke: var(--m-hair); stroke-width: 2; }
.m-frame-hot { stroke: var(--m-hot); stroke-width: 1.5; }
.m-hot-ring { fill: none; stroke: var(--m-hot); stroke-width: 2; }
/* The front window in a two-window drawing is FILLED, or the back window's
   content shows straight through it and the two read as one muddle. */
.m-plane-solid { fill: var(--phero-plane-solid); }
.m-edge   { stroke: var(--m-key); stroke-width: 1.5; }
.m-split  { stroke: var(--m-hot); stroke-width: 2; }
.m-scanline { stroke: var(--m-hot); stroke-width: 2; }
.m-fill, .m-track, .m-row, .m-tile { fill: var(--m-fill); }
.m-fill-key, .m-row-key { fill: var(--m-key-fill); }
.m-bar, .m-dot, .m-cell, .m-off { fill: var(--m-bar); }
.m-bar-off { fill: var(--m-bar); }
.m-bar-key { fill: var(--m-key); }

.m-key { fill: var(--m-key); }
.m-cell-hot, .m-hot-fill, .m-hot-dot { fill: var(--m-hot); }
/* The broken half of the repair diagram is drawn a hair off the lattice. It
   is the only place on the site where something is deliberately misaligned,
   and it is misaligned because that is the subject. */
.m-repair .m-bar-off { transform: translateX(6px); }
.m-repair .m-bar-off:nth-of-type(even) { transform: translateX(-4px); }

/* --- the motion. One thing per drawing, and it is always the subject. ----- */
@media (prefers-reduced-motion: no-preference) {
  .m-scan     { animation: mScan 11s var(--e-inout) infinite alternate; }
  .m-ping-cell{ animation: mPing 5s var(--e-inout) infinite; }
  .m-fillbar  { animation: mFill 9s var(--e-inout) infinite alternate; }
  /* The highlight MOVES between positions rather than teleporting. V5.13
     first used steps(1, end), so a rank change happened in zero frames and
     read as a rendering glitch. */
  .m-rank     { animation: mRank 14s var(--e-inout) infinite; }
  .m-ping     { animation: mPing 6s var(--e-inout) infinite; }
  .m-packet   { animation: mPacket 8s var(--e-inout) infinite alternate; }
  .m-packet-lg{ animation: mPacketLg 11s var(--e-inout) infinite alternate; }
  .m-packet-sm{ animation: mPacketSm 9s var(--e-inout) infinite alternate; }
  .m-cta      { animation: mPing 7s var(--e-inout) infinite; }
  /* The era drawing's baseline draws once, on arrival, and then it is simply
     a line. A twenty-seven year timeline that loops would be a gimmick. */
  .m-nowline  { transform-origin: left center;
                transition: transform var(--m-demo) var(--e-out) 300ms; }
  .phero[data-anim="wait"] .m-nowline { transform: scaleX(0.02); }
  /* Nothing runs while it cannot be seen: the same class the homepage field
     uses, applied by the same observer. */
  .is-still .m-scan, .is-still .m-fillbar, .is-still .m-rank,
  .is-still .m-ping, .is-still .m-ping-cell, .is-still .m-packet,
  .is-still .m-cta { animation-play-state: paused; }
}
/* Rest states, declared OUTSIDE the motion query, so a reduced-motion visitor
   gets a finished composition rather than a paused one. The scan line parked
   at translateY(0) read as a stray cyan rule stuck to the top of the box; the
   packet parked outside the left-hand system, touching nothing. */
.m-scan   { transform: translateY(100px); }
.m-packet { transform: translateX(80px); }
.m-packet-lg { transform: translateX(120px); }
.m-packet-sm { transform: translateY(30px); }
.m-rank   { transform: translateY(68px); }
.m-fillbar { transform-origin: 60px center; transform: scaleX(0.72); }
.m-ping, .m-ping-cell, .m-cta { opacity: 1; }
@keyframes mScan   { from { transform: translateY(0); } to { transform: translateY(200px); } }
@keyframes mFill   { from { transform: scaleX(0.16); } to { transform: scaleX(1); } }
@keyframes mRank   { 0%, 20% { transform: translateY(68px); }
                     30%, 50% { transform: translateY(0); }
                     60%, 80% { transform: translateY(136px); }
                     90%, 100% { transform: translateY(68px); } }
@keyframes mPing   { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
/* The packet crosses the WHOLE bridge. It used to stop half way, inside the
   middle box, on a diagram that promises three systems talking. */
@keyframes mPacketLg { from { transform: translateX(0); } to { transform: translateX(400px); } }
@keyframes mPacketSm { from { transform: translateY(0); } to { transform: translateY(62px); } }
@keyframes mPacket { from { transform: translateX(0); } to { transform: translateX(160px); } }

/* --- the entrance. Same settle-on-load the homepage hero uses. ----------- */
@media (prefers-reduced-motion: no-preference) {
  .phero[data-anim="wait"] .phero-copy > *:not(h1) {
    opacity: 0; transform: translateY(var(--m-rise));
  }
  .phero[data-anim] .phero-copy > *:not(h1) {
    transition: opacity var(--m-enter) var(--e-out),
                transform var(--m-enter) var(--e-out);
  }
  .phero[data-anim="run"] .phero-copy > .lead { transition-delay: 70ms; }
  .phero[data-anim="run"] .phero-act { transition-delay: 140ms; }
  .phero[data-anim="run"] .phero-note { transition-delay: 210ms; }
  /* The drawing arrives last and only fades. It is the supporting element and
     it should never be the first thing that moves. */
  .phero[data-anim="wait"] .phero-art { opacity: 0; }
  .phero[data-anim] .phero-art { transition: opacity var(--m-scene) var(--e-out) 200ms; }
}

/* --- the portfolio family: real screenshots, real frames ----------------- */
.phero-shots {
  position: relative;
  aspect-ratio: 640 / 500;
  display: block;
  /* min-height and aspect-ratio fight: .phero-art sets a 300px floor for the
     drawing surface, and an aspect-ratio then computes the WIDTH from it. At
     320px under 200% text zoom that made this element 500px wide inside a
     216px column. An explicit aspect owns its own sizing. */
  min-height: 0;
  padding: 0; border: 0; background: none;
}
.ws {
  position: absolute; display: block; overflow: clip;
  border-radius: 10px;
  border: 1px solid var(--line-dark-2);
  background: var(--navy-2);
  box-shadow: 0 20px 50px -24px rgba(0, 0, 0, 0.75);
}
/* One aspect for all three, cropped from the top: these are windows onto
   pages, not full-page captures, and three frames of different heights read
   as an accident. */
.ws img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10;
          object-fit: cover; object-position: 50% 0; }
.ws { padding-top: 22px; }
.ws-lead { left: 4%; right: 4%; top: 14%; z-index: 3; }
.ws .ws-bar {
  position: absolute; inset: 0 0 auto 0; height: 22px;
  border-bottom: 1px solid var(--line-dark-2);
  display: flex; align-items: center; gap: 5px; padding-left: 10px;
}
.ws .ws-bar i { width: 5px; height: 5px; background: var(--snow-4); opacity: 0.5; }
.ws-b1 { left: 16%; right: 22%; top: 2%;  z-index: 2; }
.ws-b2 { left: 30%; right: 0;   bottom: 2%; z-index: 1; }
/* The two back frames used to drift 7.8px over 26 and 34 seconds. That is
   0.3px a second on the heaviest images on the page: code with no output. */

/* --- the editorial tier -------------------------------------------------
   The intake forms, the legal pages, 404 and every article. No panel, no
   drawing: one hairline lattice fading out under the headline, and the same
   settle-on-load entrance the other four families use, so the tier is
   recognisably part of the system without pretending to be a production.

   This is the half of the system that makes the other half work. If a privacy
   policy opened with a diagram, nothing on the site would mean anything. */
.phero-editorial { padding-block: clamp(28px, 4vw, 60px) clamp(20px, 3vw, 44px); }
.phero-editorial .phero-copy { max-width: var(--track-text); }
.phero-editorial h1 { max-width: 18ch; }
.phero-editorial .lead { max-width: 56ch; }
/* The quiet tier gets the same lattice as the panel families, at the same
   pitch, faded out downward. It is what makes "no drawing" read as a register
   rather than as an unfinished page. */
.phero-editorial { position: relative; isolation: isolate; }
.phero-editorial > * { position: relative; z-index: 1; }
.phero-editorial::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  left: 50%; transform: translateX(-50%);
  width: min(100%, calc(var(--track) + var(--gutter) * 2));
  background-image:
    linear-gradient(90deg, rgba(30, 70, 130, 0.055) 1px, transparent 1px),
    linear-gradient(180deg, rgba(30, 70, 130, 0.045) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, transparent 92%);
  mask-image: linear-gradient(180deg, transparent 0%, #000 22%, transparent 92%);
}

.phead, .art-head { position: relative; isolation: isolate; }
.phead > *, .art-head > * { position: relative; z-index: 1; }
.phead::before, .art-head::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  /* Inside the 1280 column the rest of the site obeys, and faded at the top
     edge. It shipped full-bleed with a hard horizontal cut 14px under the
     breadcrumb, which is the one place on the site where a background did not
     belong to the grid. */
  inset: 0;
  left: 50%; transform: translateX(-50%);
  width: min(100%, calc(var(--track) + var(--gutter) * 2));
  background-image:
    linear-gradient(90deg, rgba(30, 70, 130, 0.055) 1px, transparent 1px),
    linear-gradient(180deg, rgba(30, 70, 130, 0.045) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, transparent 92%);
  mask-image: linear-gradient(180deg, transparent 0%, #000 22%, transparent 92%);
}
@media (prefers-reduced-motion: no-preference) {
  .phead[data-anim="wait"] h1, .art-head[data-anim="wait"] h1,
  .phead[data-anim="wait"] .lead, .art-head[data-anim="wait"] .lead,
  .phead[data-anim="wait"] .phead-act, .phead[data-anim="wait"] .phead-side,
  .art-head[data-anim="wait"] .art-meta {
    opacity: 0; transform: translateY(var(--m-rise));
  }
  .phead[data-anim] h1, .art-head[data-anim] h1,
  .phead[data-anim] .lead, .art-head[data-anim] .lead,
  .phead[data-anim] .phead-act, .phead[data-anim] .phead-side,
  .art-head[data-anim] .art-meta {
    transition: opacity var(--m-enter) var(--e-out),
                transform var(--m-enter) var(--e-out);
  }
  .phead[data-anim="run"] .lead, .art-head[data-anim="run"] .lead { transition-delay: 70ms; }
  .phead[data-anim="run"] .phead-act { transition-delay: 140ms; }
  .phead[data-anim="run"] .phead-side,
  .art-head[data-anim="run"] .art-meta { transition-delay: 210ms; }
}

/* --- responsive ----------------------------------------------------------
   The drawing does not shrink to a stamp, it moves under the copy at a wider
   aspect and then leaves entirely on a phone. A diagram at 320px is a smudge,
   and a smudge is worse than nothing. */
/* `em`, not `px`. A two-column hero at 200% text zoom needs 1630px of minimum
   content width inside a 1165px panel, at every viewport, and the panel clips.
   An em breakpoint tracks the reader's own text size -- 976px at the default
   root, 1952px at 200% -- so the hero collapses for the reader who needs it to
   and nobody else. Same rule the header, the lanes and the related-links grid
   already follow. */
@media (max-width: 61em) {
  .phero-grid { grid-template-columns: minmax(0, 1fr); gap: clamp(24px, 4vw, 40px); }
  .phero-copy h1, .phero-copy .lead { max-width: 30ch; }
  .phero-copy .lead { max-width: 52ch; }
  .phero-art, .phero-shots { aspect-ratio: 16 / 9; }
  .phero-panel::before {
    -webkit-mask-image: linear-gradient(180deg, transparent 30%, #000 82%);
    mask-image: linear-gradient(180deg, transparent 30%, #000 82%);
  }
}
@media (max-width: 39em) {
  .phero-panel { border-radius: var(--radius-panel); padding: var(--s9) var(--s7); }
  .phero-art { display: none; }
  .phero-shots { display: block; aspect-ratio: 5 / 3; }
  .ws-b2 { display: none; }
}
/* V5.22: at 320 the panel's own 32px of inline padding sat on top of the
   wrap's 20px, so a headline had 216px of a 320px screen -- 67.5% -- to set
   in. Halving it at the two narrowest widths returns 32px to the line without
   touching the panel anywhere it has room. */
@media (max-width: 27em) {
  .phero-panel { padding-inline: var(--s5); }
}

/* ==========================================================================
   4. THE MOVEMENT

   The second axis. Every homepage movement hangs its eyebrow and its keyline
   in a narrow left column and starts the headline on a second axis further in.
   V5.9.5 began five consecutive sections with a left-aligned headline at the
   same x and the same size, which is why the page read as a stack rather than
   as a composition: there was only ever one axis to read along.

   Below 900px the label column lifts above the content and the keyline stays
   with it, so the device survives on a phone instead of collapsing to nothing.
   ========================================================================== */
.mvt {
  display: grid;
  grid-template-columns: minmax(0, 0.26fr) minmax(0, 1fr);
  gap: clamp(24px, 3.4vw, 64px);
  align-items: start;
}
.mvt-label { position: sticky; top: calc(var(--hdr-h) + var(--s8)); }
/* A movement that continues after a full-bleed break keeps the same second
   axis without repeating its label. */
.mvt-body-2 { grid-column: 2; }
@media (max-width: 56.25em) { .mvt-body-2 { grid-column: 1; } }
.mvt-label .eyebrow { margin-bottom: var(--s5); }
.mvt-label p { font-size: var(--t-sm); color: var(--text-faint); max-width: 24ch; }
.mvt-body > * + * { margin-top: var(--s8); }
@media (max-width: 56.25em) { .mvt-body > * + * { margin-top: var(--s6); } }
.mvt-head { max-width: 16ch; }
/* A second heading inside a movement. Used once, over the two published
   plans in the services movement, so they read as a named thing rather than
   as a fifth lane. */
.mvt-sub { max-width: 22ch; }
.mvt-lead { max-width: 46ch; margin-top: var(--s6) !important; }

@media (max-width: 900px) {
  .mvt { grid-template-columns: minmax(0, 1fr); gap: var(--s7); }
  .mvt-label { position: static; }
}

/* Section rhythm is set by the movement's job, not by one global value.
   V5.9.5 wrapped every section in an identical 144px frame while the content
   inside ranged 488px to 1633px, which flattened the music. */
/* These were clamp(88,11vw,176) / clamp(72,9vw,144) / clamp(56,7vw,104), which
   at 1440 resolved to 158 / 130 / 101 and at 390 to 88 / 72 / 56 — three values
   inside a 57px band, so the page still read as one metronome. The spread is
   real now: the proof movement gets nearly twice the air the quiet one does. */
.sec-proof  { --sec-pad: clamp(52px, 12vw, 184px); padding-block: var(--sec-pad); }
.sec-quiet  { --sec-pad: clamp(36px, 5vw, 84px);  padding-block: var(--sec-pad); }
.sec-band   { --sec-pad: clamp(44px, 6vw, 96px);  padding-block: var(--sec-pad); }

/* V5.11: a dark section is a contained panel now, not a full-bleed slab. Every
   coloured surface in the body follows the same grammar -- navy hero, navy
   feature, blue closer, sand movement, all objects on a light page. The only
   full-bleed grounds left are white, soft gray and the footer. */
.darkpanel {
  --dp-py: clamp(40px, 5vw, 84px);
  --dp-px: clamp(28px, 4vw, 72px);
  position: relative;
  isolation: isolate;
  border-radius: var(--radius-feature);
  overflow: clip;
  padding: var(--dp-py) var(--dp-px);
  box-shadow: var(--panel-lift);
}
@media (max-width: 600px) {
  .darkpanel { --dp-py: var(--s9); --dp-px: var(--s7); border-radius: var(--radius-panel); }
}


/* ==========================================================================
   5. THE FOUR LANES

   The four words that are the company. In V5.9.5 they were set at 30px, which
   was 1.4x the 21px paragraph beside them and smaller than every section
   heading on the page. They take the new 34px step now and they carry the
   weight.

   The tag lists under each lane are gone. /services/ and the mobile nav both
   own that architecture; repeating six terms here bought nothing and cost the
   row its shape.
   ========================================================================== */
/* V5.11: one panel, four rows. Boxing each lane would be four identical tiles,
   which is the card grid this release exists to avoid -- but leaving four bare
   rows on white made the section that names the entire company the flattest
   thing on the page. The GROUP is the surface; the rows keep their hairlines
   and their hover keyline. */
.lanes {
  border: 1px solid var(--surface-edge);
  border-radius: var(--radius-panel);
  background: var(--bg);
  padding-inline: clamp(20px, 2.6vw, 40px);
}
.lane {
  position: relative;
  border-bottom: 1px solid var(--rule);
}
.lane:last-child { border-bottom: 0; }
.lane-in {
  display: grid;
  /* Wide enough that "AI & Automation" sets on one line at 34px. It is one of
     the four words the whole company is described in; it does not wrap. */
  /* A FIXED third column. It was `auto`, so "Build services", "Support
     services", "Growth services" and "All services" each sized their own
     track and pushed the description column to four different left edges --
     a 17px rag down the most prominent panel on the homepage. */
  /* V5.20: a floor on the first track and a cap on the gap. Both were
     `vw`-driven after .wrap had already capped at 1280, so the headline
     column got NARROWER as the viewport got wider -- 263px at 1440, 250px at
     1920 -- and "AI & Automation" wrapped at every desktop width, which is
     the one thing the comment above says it must not do. The 150px third
     track was 3px short of "Support services" at every width, so the arrow
     glyph painted into the panel's right padding. */
  grid-template-columns: minmax(280px, 0.62fr) minmax(0, 1fr) 156px;
  gap: clamp(16px, 2.2vw, 32px);
  align-items: baseline;
  padding: clamp(22px, 2.6vw, 34px) 0;
}
.lane::after {
  content: ""; position: absolute; left: 0; bottom: -1px; height: 2px; width: 100%;
  background: var(--keyline);
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--m-panel) var(--e-inout);
}
.lane:hover::after, .lane:focus-within::after { transform: scaleX(1); }
.lane h3 {
  font-size: var(--t-h3); line-height: var(--lh-h3);
  color: var(--text); margin: 0;
}
.lane h3 a { color: inherit; }
.lane h3 a::after { content: ""; position: absolute; inset: 0; }
.lane:hover h3 a { color: var(--link); }
.lane p {
  font-size: var(--t-lead); color: var(--text-dim); line-height: 1.45;
  max-width: 40ch; margin: 0;
}
/* The words people actually type. V5.10 first cut these on the grounds that
   /services/ owns the architecture — but the architecture was not the point:
   this line is the homepage's only answer to "can they fix MY problem", and
   "malware", "migration" and "checkout" are what a visitor is scanning for. */
.lane-ex {
  display: block; margin-top: var(--s3);
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: var(--tr-mono); text-transform: uppercase;
  color: var(--text-faint); word-spacing: var(--ws-caps);
}
/* V5.22: this was `white-space: nowrap` in a fixed 156px column, so at 150%
   text zoom and above "Support services" needed 206px and at 200% it needed
   264px -- 41% of the label ran off the panel edge. body{overflow: hidden auto}
   meant there was no way to scroll to the cut-off text either, which is a
   WCAG 1.4.4 failure rather than a cosmetic one. Two short words are allowed
   to wrap; the arrow glyph stays with the last word via the pseudo-element,
   so nothing is orphaned. (.workcap .arrow keeps nowrap: it does not overflow
   and it sits on one line by design.) */
.lane .arrow { white-space: normal; }
/* `em`, not `px`: at 200% text zoom the four lane rows still had three tracks
   at 1024 and 1280 and the arrow column overflowed the panel. An em breakpoint
   tracks the reader's text size, which is what the header breakpoints already
   do for the same reason. 56em is 896px at the default root. */
@media (max-width: 56em) {
  .lane-in { grid-template-columns: minmax(0, 1fr); gap: var(--s2); align-items: start;
             padding: var(--s5) 0; }
  .lane p { font-size: var(--t-body); }
  /* The affordance moves INTO the heading rather than costing a fourth line.
     Hiding the arrow outright (as V5.10 first did) left four black headings
     with nothing to say they were tappable; giving it a row of its own cost
     40px x 4 on the page that could least afford it. */
  .lane .arrow { display: none; }
  .lane h3 a { display: inline-block; padding-block: 8px; }
  .lane h3 a::before {
    content: ""; float: right; width: 15px; height: 8px; margin: 0.55em 0 0 10px;
    background: currentColor; color: var(--link);
    -webkit-mask: var(--arrow-mask) no-repeat center / contain;
    mask: var(--arrow-mask) no-repeat center / contain;
  }
}


/* ==========================================================================
   6. THE WORK

   Oogle builds websites, so the websites are the argument. V5.9.5 put three of
   them behind a bordered plate with a caption bar under it — a frame around
   things that already have frames — and did not reach the first one until 33%
   of the way down the page.

   Now the first project is a full-bleed image with its caption hanging in the
   margin, two more sit in-track beneath it, and the remaining six are a ruled
   list of names. No plates. No caption bars. The screenshots carry the colour
   for this whole movement, which is why the section around them stays white.
   ========================================================================== */
.workfeat { position: relative; display: block; }
/* V5.11: contained. V5.10 emitted this at section level so the image touched
   both edges of the screen, then had to set `border-radius: 0` on it to stop
   the corners looking wrong at 100vw — a square-cornered full-width picture is
   a slab, not a designed object. It sits in the track now, at the feature
   radius, with the depth the rest of the system uses. */
.workfeat-lead { display: block; margin-block: clamp(28px, 3.5vw, 56px) clamp(20px, 2.5vw, 36px); }
/* The overflow-x: clip that used to be here existed only to contain the lead
   screenshot's 100vw bleed. The lead is in the track now, nothing in this
   section extends past it, and the clip was cutting 25px of real content at
   320px under 200% text zoom. */
.workfeat-lead .workfeat-media { border-radius: var(--radius-feature); box-shadow: var(--panel-lift); }
.workfeat-lead .wrap { display: block; }
.workfeat-lead .workcap { margin-top: var(--s5); }
.workfeat-media {
  border: 1px solid var(--rule);
  display: block; overflow: hidden;
  border-radius: var(--radius-card);
  background: none;
}
.workfeat-media img {
  display: block; width: 100%; height: auto;
  /* The captures are 1600x1000 and their bottom edge cuts a section in half.
     Cropping from the top shows the part of a home page that argues for
     itself — the masthead and the first screen — and drops the seam. */
  aspect-ratio: 16 / 10; object-fit: cover; object-position: 50% 0%;
  transition: transform var(--m-panel) var(--e-inout);
}
.workfeat-lead .workfeat-media img { aspect-ratio: 2 / 1; }
/* Native ratio: a 3/2 crop cut 6% off the bottom, which landed exactly
   through the x-height of a line of type in two of the three captures. */
.worktwo .workfeat-media img { aspect-ratio: 16 / 10; }
.workfeat:hover .workfeat-media img { transform: scale(1.014); }

.workcap {
  display: grid;
  /* minmax(0, auto) on the trailing track, not auto: an `auto` track cannot
     shrink below its content, so at 200% text zoom the domain link pushed the
     caption past the viewport. This lets it wrap instead. */
  grid-template-columns: auto minmax(0, 1fr) minmax(0, auto);
  gap: var(--s5);
  align-items: baseline;
  min-width: 0;
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
}
.workcap-n {
  font-family: var(--font-mono); font-size: var(--t-mono); font-weight: 500;
  letter-spacing: var(--tr-mono); color: var(--mark);
}
/* The name was inheriting the caption's 1.65 line-height and 100% width, so
   a 32px name sat 3px off a 15px span. It takes the display recipe like every
   other heading-scale string on the page. */
.workcap b {
  font-size: var(--t-h3); font-weight: var(--w-black);
  letter-spacing: var(--tr-display); font-stretch: var(--wd-display);
  line-height: var(--lh-h3); color: var(--text);
  margin-right: 0.4em;
}
.workcap span { font-size: var(--t-sm); color: var(--text-faint); }
.workcap > * { min-width: 0; overflow-wrap: break-word; }
.workcap .arrow { white-space: nowrap; }

/* Two supporting projects, in-track. */
.worktwo {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(22px, 3vw, 48px);
  margin-top: clamp(32px, 4vw, 64px);
}
.worktwo .workcap { grid-template-columns: auto minmax(0, 1fr); }
.worktwo .workcap b { font-size: var(--t-h4); }

/* The rest, as names. A list of clients is proof; six more thumbnails are
   inventory. */
/* Six names as six full-width rows was 668px of near-empty page immediately
   after the last screenshot — the longest dead stretch on the site. Two
   columns halve it and give the list a shape. */
.worklist {
  margin-top: clamp(32px, 4vw, 56px);
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(24px, 3vw, 56px);
  border-top: 1px solid var(--rule);
}
.worklist-row { display: block; border-bottom: 1px solid var(--rule); }
.worklist-in {
  display: grid; grid-template-columns: auto minmax(0, 1fr) minmax(0, auto);
  gap: var(--s5); align-items: baseline;
  padding: var(--s4) 0;
}
.worklist-row i { overflow-wrap: anywhere; }
.worklist-row b { font-size: var(--t-h4); font-weight: var(--w-black);
                  letter-spacing: var(--tr-display); color: var(--text); }
.worklist-row:hover b { color: var(--link); }
.worklist-in > span, .worklist-in > i {
  font-family: var(--font-mono); font-size: var(--t-mono); font-style: normal;
  letter-spacing: var(--tr-mono); text-transform: uppercase; color: var(--text-faint);
}
@media (max-width: 43.75em) {
  .worktwo { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .worklist-in { grid-template-columns: auto minmax(0, 1fr); gap: var(--s4);
                 padding: var(--s3) 0; }
  .workcap { grid-template-columns: auto minmax(0, 1fr); }
  /* Full row, not the second track. At 200% text zoom a domain like
     "semarineinc.com" is 273px of unbreakable text and the second track is
     228px, which was the last horizontal overflow on the site. */
  .workcap .arrow { grid-column: 1 / -1; }
  .workcap b { display: block; }
  .worklist-row i { display: none; }
  /* A 1600px desktop capture shown 350px wide is not evidence, it is a
     texture. On a phone ONE screenshot carries the argument at a size where
     the work is actually legible, and the two supporting projects become
     named rows like the six below them. Nothing is hidden: every link, name
     and platform survives, only the unreadable thumbnails go. */
  .workfeat-lead .workfeat-media img { aspect-ratio: 16 / 10; }
  .worktwo .workfeat-media { display: none; }
  .worktwo .workfeat { margin: 0; }
  .worktwo .workfeat { border-bottom: 1px solid var(--rule); }
  .worktwo .workfeat:first-child { border-top: 1px solid var(--rule); }
  .worktwo { gap: 0; margin-top: var(--s8); }
  .worktwo .workcap {
    margin-top: 0; padding: var(--s4) 0; border-top: 0;
    grid-template-columns: auto minmax(0, 1fr);
    padding: var(--s3) 0;
  }
  .worklist { margin-top: 0; border-top: 0; grid-template-columns: minmax(0, 1fr); }
  .workcap { margin-top: var(--s4); padding-top: var(--s4); gap: var(--s3); }
}


/* ==========================================================================
   7. THE STACK ILLUSTRATION

   The movement used to carry a wireframe "before and after" of a page whose
   stylesheet had stopped loading. It was drawn in CSS, it was not a real
   client site, and a fabricated transformation is a claim this site does not
   get to make. It is gone -- markup, stylesheet and its IntersectionObserver
   module in main.js with it.

   What sits here now is a supplied raster illustration of the stack the
   movement actually claims: design, development, hosting, email, DNS and
   security answered by one team. It carries its own alpha channel, so it
   goes STRAIGHT ONTO THE PANEL -- no frame, no card, no plate, no white
   rectangle. The artwork is the visual.

   The glow behind it is the one thing this stylesheet draws. The supplied
   file was cut to a hard alpha mask, which took the halo off with the
   background, and without it the artwork reads as pasted on rather than lit
   by the panel. It is a radial gradient with no edge anywhere in it, so it
   cannot be mistaken for a background box.
   ========================================================================== */
.split-art-fig {
  max-width: 600px;
  margin-inline: auto;
  /* The glow is painted BY the figure, not by a pseudo-element inset outside
     it. The first version used ::before at inset: -14% -11%, which looked
     right on a wide panel and, once the figure filled the whole column at
     600px, put 54px of itself outside .darkpanel -- and .darkpanel is
     overflow: clip, so audit.py correctly reported the panel as cropping its
     own content at 200% text zoom. A background reaches full transparency at
     74% of a 58%-wide radius, so it finishes well inside the box and there is
     nothing to overflow at any width. */
  background: radial-gradient(58% 52% at 52% 47%,
              rgba(74, 160, 235, 0.15) 0%,
              rgba(74, 160, 235, 0.055) 46%,
              transparent 74%);
}
/* width and height are on the <img> itself, so the box is reserved before a
   byte of the file arrives and the movement never shifts under the reader. */
.split-art-fig img { display: block; width: 100%; height: auto; }

/* The supplied "connected services" artwork (assets/img/one-team-*.webp,
   September 2026), which replaced the drawn SVG. It is a flat raster with
   its own navy ground, two or three levels bluer than the panel's, so two
   things make it belong to the panel rather than sit on it:

   1. mix-blend-mode: lighten. Per channel the panel shows through wherever
      it is the brighter of the two, which is nowhere the artwork is lit and
      everywhere it is flat: the panel's lattice runs on through the
      picture's dark ground, and the ground's slight blue lifts the panel by
      a value no eye can find.
   2. A mask that lets the edges go. The six tiles reach to within 6-10% of
      the frame, so the outer 6% on every side fades to nothing (two
      linear gradients, intersected): every tile and wire is whole, and
      there is no edge anywhere for the eye to find the rectangle by.

   No frame, no plate, no shadow, and the figure paints no glow of its own:
   the artwork carries one. */
.split-art-raster { background: none; position: relative; }
.split-art-raster img {
  mix-blend-mode: lighten;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent),
                      linear-gradient(180deg, transparent, #000 6%, #000 94%, transparent);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent),
              linear-gradient(180deg, transparent, #000 6%, #000 94%, transparent);
  mask-composite: intersect;
}
/* On a phone the figure takes 12px of the panel's padding on each side:
   the artwork has its own margin inside the frame, and 295px reads where
   271 was a thumbnail. Still 20px inside the panel's edge, nowhere near
   the corner radius. */
@media (max-width: 600px) {
  .split-art-raster { margin-inline: -12px; }
}
/* the small cluster at its foot */
.split-art-raster .pxs-br { --pxs-x: 6%; --pxs-y: 3%; }

/* the three beats beside the illustration */
/* Parity is not a design constraint. The roster grid is two up; with an odd
   number of names the last row left an empty cell and a rule that stopped
   halfway. It spans instead. */
.worklist-row:last-child:nth-child(odd) { grid-column: 1 / -1; }

.beats { list-style: none; margin: 0; padding: 0; }
.beats li {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s4);
  padding: var(--s5) 0; border-top: 1px solid var(--rule);
}
.beats li:last-child { border-bottom: 1px solid var(--rule); }
.beats i {
  font-family: var(--font-mono); font-style: normal; font-size: var(--t-mono);
  letter-spacing: var(--tr-mono); color: var(--mark); line-height: 1.7;
}
.beats b { font-weight: var(--w-bold); color: var(--text); }


/* ==========================================================================
   7b. SINCE 1999

   Longevity, without a timeline. One numeral at display scale, the eras as a
   single ruled line of mono, and the person who has been running it the whole
   time. The warm ground is the only place on the site that is not cool, which
   is what makes this the human movement rather than another band.
   ========================================================================== */
.sandfield { background: var(--sand); }
/* V5.11: the warm ground is a contained panel rather than a full-width stripe.
   The one place on the site that is not cool should be a THING you arrive at,
   not a band the page passes through. */
.era-panel {
  border-radius: var(--radius-feature);
  border: 1px solid var(--sand-2);
  padding: clamp(36px, 4.6vw, 80px);
  overflow: clip;
}
@media (max-width: 600px) { .era-panel { border-radius: var(--radius-panel); padding: var(--s8) var(--s7); } }
.sandfield .lead, .sandfield p { color: var(--ink-3); }

.era-in {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr);
  gap: clamp(32px, 5vw, 88px);
  align-items: center;
}
.era-year {
  display: block;
  /* The vw cap is not styling, it is a floor guard: at 200% text zoom the rem
     minimum resolves to 160px, which is wider than the panel it now sits in at
     390 and 430. min() keeps the normal rendering identical (21vw exceeds the
     clamp at every width the site is read at) and only bites under zoom. */
  font-size: min(clamp(5rem, 3.1rem + 8.4vw, 11rem), 21vw);
  font-weight: var(--w-black); letter-spacing: -0.055em; line-height: 0.82;
  font-stretch: var(--wd-display);
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.era-since {
  display: block; margin-bottom: var(--s3);
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: var(--tr-mono); text-transform: uppercase; color: var(--blue);
}
.era-copy { max-width: 44ch; margin-top: var(--s6) !important; }
/* The link is its own line rather than trailing the last sentence. The
   paragraph now ends on a deliberate beat ("Taking care of our clients
   doesn't."), and a link sharing that line takes the weight off it. */
.era-more { margin-top: var(--s5); }
.era-list {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  list-style: none; margin: var(--s7) 0 0; padding: var(--s5) 0 0;
  border-top: 1px solid var(--sand-2);
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: var(--tr-mono); text-transform: uppercase; color: var(--ink-4);
}
.era-portrait {
  border-radius: var(--radius-card); overflow: hidden; background: var(--sand-2);
}
.era-portrait img { display: block; width: 100%; height: auto; }
.era-name {
  margin-top: var(--s4); font-size: var(--t-sm); color: var(--ink-4);
}
.era-name b { color: var(--ink-2); font-weight: var(--w-bold); }
@media (max-width: 900px) {
  .era-in { grid-template-columns: minmax(0, 1fr); gap: var(--s7); }
  .era-portrait { max-width: 260px; }
  .era-copy { margin-top: var(--s5) !important; }
  .era-list { margin-top: var(--s5); }
}


/* ==========================================================================
   8. EDITORIAL PLATES
   Article cards need an image and there are no photographs. The plate is the
   pixel field on one of three grounds — enough to give a row of cards a beat,
   and it is not a diagram of anything.
   ========================================================================== */
.ed-art {
  position: relative; display: block; overflow: hidden;
  border: 1px solid var(--rule); border-radius: var(--radius-card);
  /* The base variant was --paper-3 with --blue pixels: near-white on a white
     page with a handful of faint marks in it, which read as a failed image
     load rather than as a plate. All three variants carry weight now. */
  background: var(--navy-3); color: var(--blue-light); border-color: transparent;
}
.ed-art > svg { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0.9; }
/* The featured image, where the article has one (September 2026): it fills
   the same box the plate did, 16:9 on every card so a row of them sits on
   one line. Hover is the site's card hover, a border in the blue and a
   1% lift of the picture; nothing zooms. */
.ed-art-img { background: var(--navy); }
.ed-art-img img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform var(--m-panel) var(--e-out);
}
.ed-art-img::before, .ed-art-img::after { content: none; }
.ed-feature .ed-art, .ed-card .ed-art { aspect-ratio: 16 / 9; }
.ed-card .ed-art { margin-bottom: var(--s5); }
.ed-art, .ed-art-img { transition: border-color var(--d-state) var(--ease), box-shadow var(--d-state) var(--ease); }
.ed-card:hover .ed-art-img, .ed-feature:hover .ed-art-img { border-color: var(--blue-lift); box-shadow: var(--lift-2); }
@media (prefers-reduced-motion: no-preference) {
  .ed-card:hover .ed-art-img img, .ed-feature:hover .ed-art-img img { transform: scale(1.015); }
}
.ed-grid > *:nth-child(3n+2) .ed-art { background: var(--navy-2); color: var(--blue-light); border-color: transparent; }
.ed-grid > *:nth-child(3n+3) .ed-art { background: var(--blue); color: var(--navy); border-color: transparent; }
.ed-cat {
  display: block; font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--mark);
  margin-bottom: var(--s3);
}


/* ==========================================================================
   9. MICROINTERACTIONS
   Hover should look designed for the element. No transform: scale anywhere
   except a photograph, where it is a crop rather than a bounce.
   ========================================================================== */
a.card { transition: border-color var(--d-state) var(--ease), background var(--d-state) var(--ease); }
a.card:hover, a.card:focus-visible {
  transform: none; box-shadow: none; background: var(--film);
  border-color: color-mix(in srgb, var(--mark) 35%, var(--rule));
}
a.card:focus-visible::before { transform: scaleY(1); }
a.card:hover .arrow::after { transform: translateX(4px); }

/* The header's links are .nav-lnk. The art direction pass wrote this rule
   against .nav-main, a class that does not exist anywhere on the site, so the
   navigation underline has never once appeared. Neither harness could catch
   it: a hover style that silently does nothing is invisible to a contrast
   check and to a behaviour assertion. */
.nav-lnk { position: relative; }
.nav-lnk::after {
  content: ""; position: absolute; left: 0; right: 100%; bottom: 2px;
  height: 1.5px; background: var(--blue);
  transition: right var(--d-state) var(--ease);
}
.nav-lnk:hover::after, .nav-lnk:focus-visible::after,
.nav-lnk[aria-current]::after, .nav-lnk[data-active]::after { right: 0; }

a.shot { transition: border-color var(--d-state) var(--ease), box-shadow var(--d-state) var(--ease); }
a.shot:hover, a.shot:focus-visible { transform: none; box-shadow: var(--lift-3); border-color: var(--line-2); }

.index a, .index-row { position: relative; }
.index a::before, .index-row::before {
  content: ""; position: absolute; left: 0; top: -1px; height: 2px; width: 100%;
  background: var(--mark); transform: scaleX(0); transform-origin: left center;
  transition: transform var(--d-state) var(--ease);
}
.index a:hover::before, .index a:focus-visible::before,
.index-row:hover::before, .index-row:focus-visible::before { transform: scaleX(1); }


/* ==========================================================================
   10. MOTION

   Two bugs shipped in V5.9.5 and both are fixed here.

   (a) @keyframes revealIn declared only a `to` frame, so its implicit `from`
       resolved from the element's BASE style — opacity 1, transform none. Every
       scroll reveal on the site interpolated 1 -> 1 and cut in over a single
       frame. Reveals are transitions now: a transition has no implicit-from
       hazard and cannot fail that way again.

   (b) .hero[data-anim] hid the headline, the lead, both buttons and the
       credential strip at opacity 0, and only JavaScript brought them back. With
       JavaScript off the entire hero was invisible, and the browser picked a
       phone number in the header as the LCP element. The hidden state is applied
       by JS now, exactly as the reveal system already did it.

   The amplitude rule: travel is >= 24px or the motion is deleted. V5.9.5 moved a
   104px headline by 12px and a 628px card by 14px, which is not a move, it is a
   fade wearing a transform. All distances come from --m-rise / --m-rise-lg, and
   prefers-reduced-motion collapses them to 0 in tokens.css — no component
   repeats that media query.

   Motion resolves toward order. Nothing bounces, floats, drifts or loops.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {

  /* A block arrives ------------------------------------------------------- */
  [data-reveal] {
    transition: opacity var(--m-enter) var(--e-out),
                transform var(--m-enter) var(--e-out);
  }
  [data-reveal="wait"] { opacity: 0; transform: translateY(var(--m-rise-lg)); }
  /* The "one team" artwork comes online rather than rising: a shorter
     travel, a scale from 0.97, and the scene duration. Once per load, like
     every reveal. */
  .split-art-fig[data-reveal] { transition-duration: var(--m-scene), var(--m-scene); }
  .split-art-fig[data-reveal="wait"] { transform: translateY(var(--m-rise)) scale(0.97); }

  /* A group's children arrive in sequence --------------------------------- */
  [data-rise] > * {
    transition: opacity var(--m-enter) var(--e-out),
                transform var(--m-enter) var(--e-out);
  }
  [data-rise="wait"] > * { opacity: 0; transform: translateY(var(--m-rise)); }
  [data-rise="in"] > *:nth-child(2) { transition-delay: calc(var(--m-step) * 1); }
  [data-rise="in"] > *:nth-child(3) { transition-delay: calc(var(--m-step) * 2); }
  [data-rise="in"] > *:nth-child(4) { transition-delay: calc(var(--m-step) * 3); }
  [data-rise="in"] > *:nth-child(5) { transition-delay: calc(var(--m-step) * 4); }
  [data-rise="in"] > *:nth-child(6) { transition-delay: calc(var(--m-step) * 5); }

  /* The keyline draws. The site's most repeated brand shape finally moves,
     and it moves the way a rule should: left to right, once. */
  [data-reveal] .eyebrow::before,
  [data-rise] .eyebrow::before {
    transition: transform var(--m-scene) var(--e-out) calc(var(--m-step) * 1);
  }
  [data-reveal="wait"] .eyebrow::before,
  [data-rise="wait"] .eyebrow::before { transform: scaleX(0); }

  /* A screenshot uncovers top to bottom, the way a page paints in. This
     replaces a 14px nudge on a 628px object, which nobody could see. */
  /* data-wipe is gone: the homepage feature is data-slide now, and a clip
     wipe on a full-track photograph read as the picture being uncovered
     rather than as the page it shows arriving. */

  /* The hero settles once on load. The hidden state is set by JS.

     The h1 is deliberately NOT in the stagger. It is the LCP element, and
     fading it in pushed Largest Contentful Paint from 144ms to 880ms on
     desktop for 620ms of decoration — the browser records LCP when the element
     reaches its final paint, so an entrance animation on the biggest thing on
     the page is a Core Web Vitals cost with nothing bought. The headline is
     simply there, and everything around it settles. */
  .hero[data-anim="wait"] .hero-copy > *:not(h1) {
    opacity: 0; transform: translateY(var(--m-rise));
  }
  .hero[data-anim] .hero-copy > *:not(h1) {
    transition: opacity var(--m-enter) var(--e-out),
                transform var(--m-enter) var(--e-out);
  }
  .hero[data-anim="run"] .hero-copy > .lead { transition-delay: 80ms; }
  .hero[data-anim="run"] .hero-cta { transition-delay: 160ms; }
  /* The field activates last and only fades: it is decoration, so it arrives
     after everything a visitor came to read. */
  .hero[data-anim="wait"] .hero-art { opacity: 0; }
  .hero[data-anim] .hero-art {
    transition: opacity var(--m-scene) var(--e-out) 240ms;
  }
}

/* --------------------------------------------------------------------------
   THE VOCABULARY (V5.12)

   Five named motions, each with one job. A page that fades everything up four
   times in a row has no motion design, it has a default.

     reveal   text arrives                  opacity + rise
     build    pixels, grids and interfaces   a clip that assembles in place
     slide    screenshots                    travel inside their own frame
     drift    background layers              slow, continuous, decorative only
     focus    hover                          one step, no travel

   reveal, build and slide RESOLVE: they play once and stop. drift is the only
   thing on the site allowed to loop, it is always decoration, it is always
   behind a mask or at single-digit alpha, and it is always the first thing
   `prefers-reduced-motion` switches off.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {

  /* --- build ------------------------------------------------------------
     The cell assembles from its leading edge rather than fading up: the whole
     surface arrives, border and divider included, left to right. Used where
     the subject is an interface or a set of facts being stated. */
  /* `:is(*, .m)` is a universal selector carrying class weight -- :is() takes
     the specificity of its most specific argument. Without it this rule is
     (0,1,0) and any component that declares its own transition on the same
     children beats it: `.facts li { transition: background-color }` is (0,1,1),
     so for one build the fact bar's own hover rule silently cancelled the only
     new motion in the vocabulary and the bar cut in rather than assembling.
     A component must be able to state its hover without deleting its entrance. */
  [data-build] > :is(*, .m) {
    transition: clip-path var(--m-scene) var(--e-snap),
                opacity var(--m-enter) var(--e-out),
                background-color var(--m-state) var(--e-inout);
  }
  /* Both ends explicit. `inset()` to `none` is a DISCRETE transition -- it
     cannot tween, so the stagger delays fired on an animation with no duration
     and the whole bar popped at once. */
  [data-build="wait"] > :is(*, .m) { clip-path: inset(0 100% 0 0); opacity: 0; }
  [data-build="in"] > :is(*, .m)   { clip-path: inset(0 0 0 0); }
  [data-build="in"] > *:nth-child(2) { transition-delay: 70ms; }
  [data-build="in"] > *:nth-child(3) { transition-delay: 140ms; }
  [data-build="in"] > *:nth-child(4) { transition-delay: 210ms; }
  [data-build="in"] > *:nth-child(5) { transition-delay: 280ms; }

  /* --- slide ------------------------------------------------------------
     A screenshot is a picture of a page, so it arrives the way a page does:
     it travels a little inside its own frame and settles. The frame never
     moves, which is what separates this from a fade-up wearing a transform. */
  /* An animation, not a transition. A transition on `transform` here is
     declared for the life of the element, so it also owned the HOVER on the
     three biggest interactive objects on the page: the portfolio screenshots'
     1.4% hover zoom took 1500ms instead of 240 and was imperceptible. A
     one-shot animation with no fill-mode reverts the moment it ends and hands
     the property back. */
  [data-slide] { overflow: clip; }
  [data-slide="in"] img { animation: slideIn var(--m-demo) var(--e-out) 1; }

  /* --- drift ------------------------------------------------------------
     Continuous, decorative, masked. The dark feature panel gets the same
     lattice as the hero at a fifth of the alpha, moving one cell every
     eighty seconds -- slow enough that it is texture rather than motion, and
     the only reason it moves at all is so the panel is not a flat rectangle
     of navy behind the stack illustration. */
  .darkpanel > * { position: relative; z-index: 1; }
  .darkpanel::before {
    content: ""; position: absolute; inset: -60px 0;
    pointer-events: none; z-index: 0;
    background-image:
      linear-gradient(90deg, rgba(120, 170, 230, 0.055) 1px, transparent 1px),
      linear-gradient(180deg, rgba(120, 170, 230, 0.04) 1px, transparent 1px);
    background-size: 46px 46px;
    -webkit-mask-image: radial-gradient(120% 90% at 78% 40%, #000 0%, transparent 72%);
    mask-image: radial-gradient(120% 90% at 78% 40%, #000 0%, transparent 72%);
    /* No animation. It had one: 46px over 80 seconds, which measured a mean
       change of 0.10 of 255 over forty seconds -- invisible, and a permanent
       composited layer plus a compositor tick for the life of the tab to
       deliver it. The lattice earns its place standing still. */
  }
  /* The closer's pixel edge breathes sideways rather than sitting still. It is
     already the quietest thing on the panel; this keeps it from reading as a
     printed border. */
    /* Halved. At -2.5% the closer's pixel band travelled 32px under the one
     call to action on the page -- 59 times more perceptible than the panel
     drift above, and calibrated in exactly the wrong direction. */
  .closer-art > img { animation: sfSlide 52s var(--e-inout) infinite alternate; }

  /* --- reveal, applied where a line is the subject ------------------------
     The rule above the eras draws itself rather than appearing: scaleX on the
     border, which is one composited transform and not a stroke repaint.
     It is on a ::before, not on .era-list itself -- .era-list is a row of
     eight mono words, and scaling THAT on X squashes type. */
  .era-list { position: relative; }
  .era-list::before {
    content: ""; position: absolute; left: 0; right: 0; top: -1px; height: 2px;
    background: var(--keyline); transform-origin: left center;
    transition: transform var(--m-demo) var(--e-out) 200ms;
  }
  [data-reveal="wait"] .era-list::before { transform: scaleX(0.02); }
}
@keyframes sfSlide {
  from { transform: translate3d(-1.2%, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

/* Hover is a transform, never a box-model property. V5.9.5 animated
   padding-left on .lane and .index-row, which is layout on every hover. */
.lane, .index-row, .worklist-row {
  transition: background-color var(--m-state) var(--e-inout);
}
.lane-in, .index-row-in, .worklist-in {
  transition: transform var(--m-state) var(--e-inout);
}
.lane:hover .lane-in,
.index-row:hover .index-row-in,
.worklist-row:hover .worklist-in { transform: translateX(var(--m-nudge)); }


/* ==========================================================================
   11. MOBILE
   ========================================================================== */
@media (max-width: 600px) {
  .hero-copy { max-width: none; }
}


/* ==========================================================================
   12. PRINT
   ========================================================================== */
@media print {
  .art-layer, .ed-art { display: none !important; }
  .bluefield { background: #fff !important; color: #000 !important; }
  .bluefield * { color: #000 !important; }
}


/* ==========================================================================
   13. TWO SMALL LAYOUTS
   ========================================================================== */
/* A heading with its link on the same line, so a section does not need an
   eyebrow above it and a link below it. */
.split-head {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  justify-content: space-between; gap: var(--s5) var(--s8);
}
.split-head > p { margin: 0; }

/* ==========================================================================
   14. NEEDS
   A two-column list of what something has to get right. Replaces the pattern
   this site was using five different ways: a heading, a paragraph, and a grid
   of bordered cards.
   ========================================================================== */
/* V5.11: each item is a surface. This is the most repeated component on the
   site -- five of them, twice a page, on thirty-one pages -- and it was a 2px
   keyline doing the entire job of separating one idea from the next. It is ONE
   grid, not four, which is the difference between intentional boxes and a card
   grid: the page gains definition without gaining a pattern. */
.needs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s5); }
/* White on the soft-gray section ground, not --surface-2: every .needs on the
   site sits inside an .on-paper section, and --surface-2 resolves to that same
   #F7F9FC, so the "surface" shipped at 1.00:1 against its own ground and was a
   bare outline. White gives it 1.05:1 -- deliberately near zero, so the eye
   finds the EDGE, which is what reads as precision rather than as a card
   floating above the page. */
.needs > div {
  background: var(--bg);
  border: 1px solid var(--surface-edge);
  border-radius: var(--radius-card);
  padding: var(--s7);
}
/* Five items in two columns leaves a hole every time. Parity is not a design
   constraint -- the same rule that fixed .worklist and .showcase. */
.needs > div:last-child:nth-child(odd) { grid-column: 1 / -1; }
/* The keyline is a fixed 34px mark, not the heading's own top border. Inside a
   box the border ran the full width of whatever the heading happened to be, so
   five items in a row drew five different lengths -- an accident reading as a
   decision. Same mark, same length, as .eyebrow::before. */
.needs h3 {
  font-size: var(--t-h3); letter-spacing: var(--tr-h3);
  font-weight: var(--w-semi); color: var(--text);
}
.needs h3::before {
  content: ""; display: block; width: 34px; height: 2px;
  background: var(--keyline); margin-bottom: var(--s5);
}
.needs p { font-size: var(--t-body); color: var(--text-dim); margin-top: var(--s4); line-height: 1.6; max-width: 40ch; }
@media (max-width: 47.5em) { .needs { grid-template-columns: minmax(0, 1fr); } }

/* A bare list, used where the items are short enough to need no explanation. */
.plainlist { display: grid; gap: var(--s3); max-width: 62ch; }
.plainlist li { position: relative; padding-left: 22px; color: var(--text-dim); font-size: var(--t-body); }
.plainlist li::before {
  content: ""; position: absolute; left: 0; top: 0.7em;
  width: 10px; height: 1px; background: var(--mark);
}

/* The expectations column on a service page. V5.11 gives it the surface it was
   always behaving like: it sits in a c5 beside prose, which is an aside, and an
   aside with a fill reads as an aside without having to be labelled one. */
.expect {
  background: var(--bg);
  border: 1px solid var(--surface-edge);
  border-radius: var(--radius-panel);
  padding: var(--s8);
}
.expect > :first-child::before {
  content: ""; display: block; width: 34px; height: 2px;
  background: var(--keyline); margin-bottom: var(--s5);
}
.expect ol { display: grid; gap: var(--s4); counter-reset: e; }
.expect ol li {
  position: relative; padding-left: 30px; counter-increment: e;
  font-size: var(--t-body); color: var(--text-2); line-height: 1.5;
}
.expect ol li::before {
  content: counter(e); position: absolute; left: 0; top: 0;
  font-family: var(--font-mono); font-size: var(--t-sm);
  color: var(--mark); font-variant-numeric: tabular-nums;
}


/* ==========================================================================
   15. THE WORK PAGE
   Image on top at full track, facts underneath. V5 alternated a 7/5 split,
   framed every capture in browser chrome, varied the crop every third row and
   dropped an oversized numeral behind each one. The screenshot is the
   argument; everything around it was decoration competing with it.
   ========================================================================== */
.case { display: block; }
.case + .case { margin-top: clamp(56px, 8vw, 120px); }
.case .shot {
  display: block; border: 1px solid var(--rule); border-radius: var(--radius-card);
  overflow: hidden; background: var(--surface-2); box-shadow: var(--lift-2);
}
.case .shot img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 9; object-fit: cover; object-position: 50% 0%;
}
/* Two columns, four rows, every item placed. The first version left the
   service chips and the live link on auto rows, which put them back into rows
   already occupied by the description — they rendered on top of each other. */
.case-meta {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  grid-template-rows: auto auto auto;   /* no 1fr: the services row is
                                           empty on most projects and a
                                           flexible track stretched it */
  gap: var(--s4) var(--s10); align-items: start;
  margin-top: var(--s8);
}
.case-meta h3 {
  grid-column: 1; grid-row: 1;
  font-size: var(--t-h2); letter-spacing: var(--tr-h2);
}
.case-tags {
  grid-column: 1; grid-row: 2;
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint);
}
.case-meta > p:not(.case-tags):not(.mt-6) {
  grid-column: 2; grid-row: 1 / 3;
  font-size: var(--t-lead); color: var(--text-dim); line-height: 1.5; max-width: 46ch;
}
.case-meta > .case-serv { grid-column: 2; grid-row: 3; margin-top: var(--s5); }
.case-meta > .mt-6 { grid-column: 1; grid-row: 3; margin-top: var(--s5); }
.case-serv { display: flex; flex-wrap: wrap; gap: 6px; }
.case-serv a {
  font-size: var(--t-xs); border: 1px solid var(--rule); border-radius: var(--radius-pill);
  padding: 5px 11px; color: var(--text-dim);
}
.case-serv a:hover { border-color: var(--mark); color: var(--link); }
@media (max-width: 860px) {
  .case-meta { grid-template-columns: minmax(0, 1fr); gap: var(--s4); }
  .case-meta > * { grid-column: 1 !important; grid-row: auto !important; }
  .case-meta h3 { font-size: var(--t-h3); }
}

/* --------------------------------------------------------------------------
   V5.11: THE WORK PAGE IS TIERED
   One lead project on a full-track surface at the feature radius, two cases at
   the card radius, then a repeating grid. The tier comes from the item's index
   in build.py, never from a hand-placed position, so the page holds its shape
   at eight projects and at twenty-five. V5.10 gave all nine the same treatment,
   which was count-agnostic but ran to 5,800px at nine and would run to 18,000
   at twenty-five -- past which the statement panel and the closer, the only two
   conversion surfaces on the page, are unreachable.
   -------------------------------------------------------------------------- */
.case-lead .shot-lead {
  border-radius: var(--radius-feature);
  box-shadow: var(--panel-lift);
}

/* The two supporting projects are a PAIR, so the lead is unambiguously the
   lead. V5.11 first shipped the lead at 2:1 and the two below it at 16:9 --
   which made the item flagged as the feature 80px shorter than the two that
   followed it, inverting the hierarchy it was meant to establish. */
.workpair {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(28px, 3.4vw, 56px);
  margin-top: clamp(48px, 7vw, 104px);
}
.workpair .case + .case { margin-top: 0; padding-top: 0; border-top: 0; }
.workpair .case-meta { grid-template-columns: minmax(0, 1fr); gap: var(--s4); margin-top: var(--s6); }
/* !important, the same way the 860px collapse does it: the placement rules
   above are `.case-meta > p:not(.case-tags):not(.mt-6)`, which outranks any
   descendant selector this class can write. Without it the description stayed
   in a column-2 that no longer exists, the browser created an implicit track,
   and the project name wrapped one letter at a time. */
.workpair .case-meta > * { grid-column: 1 !important; grid-row: auto !important; }
.workpair .case-meta h3 { font-size: var(--t-h3); }
.workpair .case-meta > p:not(.case-tags):not(.mt-6) { font-size: var(--t-body); }
.workpair .case-meta > .case-serv, .workpair .case-meta > .mt-6 { margin-top: var(--s5); }
@media (max-width: 47.5em) {
  .workpair { grid-template-columns: minmax(0, 1fr); gap: var(--s10); }
}
.case-lead .shot-lead img { aspect-ratio: 16 / 9; }
.case-lead .case-meta { margin-top: var(--s9); }
@media (max-width: 43.75em) {
  .case-lead .shot-lead { border-radius: var(--radius-panel); }
  .case-lead .shot-lead img { aspect-ratio: 16 / 10; }
}

/* The one repeating card grid on the site, and it earns it: the items are
   genuinely the same kind of thing, it is the only grid on the page, and it is
   what lets the portfolio grow without a redesign or a line of copy changing. */
.workgrid-head { margin-top: clamp(56px, 9vw, 128px); }
.workgrid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s8) var(--s6); margin-top: var(--s8);
}
.wcard { display: block; }
.wcard-media {
  display: block; overflow: hidden; border-radius: var(--radius-card);
  border: 1px solid var(--rule); background: var(--surface-2);
  box-shadow: var(--lift-1);
  transition: box-shadow var(--d-state) var(--ease), border-color var(--d-state) var(--ease);
}
.wcard-media img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; object-position: 50% 0%; }
.wcard:hover .wcard-media { box-shadow: var(--lift-2); border-color: var(--line-2); }
.wcard-name {
  display: block; margin-top: var(--s5); font-size: var(--t-h4);
  font-weight: var(--w-semi); letter-spacing: var(--tr-h4); color: var(--text);
}
.wcard:hover .wcard-name { color: var(--link); }
.wcard-tags {
  display: block; margin-top: var(--s2); font-size: var(--t-sm);
  color: var(--text-dim); line-height: 1.5;
}
.wcard .arrow { margin-top: var(--s4); font-size: var(--t-sm); }
@media (max-width: 67.5em) { .workgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 40em)  { .workgrid { grid-template-columns: minmax(0, 1fr); gap: var(--s8); } }


/* ==========================================================================
   16. THE SERVICES INDEX
   ========================================================================== */
.disc { display: block; }
.disc + .disc { margin-top: clamp(56px, 7vw, 104px); padding-top: clamp(56px, 7vw, 104px); border-top: 1px solid var(--rule); }
.disc-head { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--s6) var(--s10); align-items: baseline; }
.disc-head h2 { font-size: clamp(1.875rem, 1.3rem + 2.4vw, 3rem); letter-spacing: var(--tr-h2); }
.disc-head p { font-size: var(--t-lead); color: var(--text-dim); line-height: 1.5; max-width: 46ch; }
.disc .index { margin-top: var(--s10); }
@media (max-width: 53.75em) { .disc-head { grid-template-columns: minmax(0, 1fr); gap: var(--s4); } }

/* the audience index, without the topology marks it used to carry */
.audrow {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 40%) auto;
  gap: var(--s6); align-items: baseline;
  padding: var(--s7) 0; border-top: 1px solid var(--rule);
  position: relative; transition: translate var(--d-state) var(--ease);
}
.audrow:last-of-type { border-bottom: 1px solid var(--rule); }
.audrow::before {
  content: ""; position: absolute; left: 0; top: -1px; height: 2px; width: 100%;
  background: var(--mark); transform: scaleX(0); transform-origin: left center;
  transition: transform var(--d-state) var(--ease);
}
.audrow:hover::before, .audrow:focus-visible::before { transform: scaleX(1); }
.audrow:hover { translate: var(--s3); }   /* was padding-left: reflowed 3 columns */
.audrow h3 { font-size: var(--t-h3); letter-spacing: var(--tr-h3); color: var(--text); }
.audrow:hover h3 { color: var(--link); }
.audrow > p { font-size: var(--t-body); color: var(--text-dim); line-height: 1.5; }
.audrow-n { font-family: var(--font-mono); font-size: var(--t-mono); color: var(--text-faint); letter-spacing: 0.1em; }
@media (max-width: 860px) {
  .audrow { grid-template-columns: minmax(0, 1fr); gap: var(--s3); }
  .audrow-n { display: none; }
}


/* ==========================================================================
   18. THE CONTRAST MOVEMENT LAYOUT
   Copy on the left, the stack illustration on the right, and the link out
   sitting under the copy rather than under the picture. Three named areas
   rather than two columns, because the mobile order is not the desktop order
   read top to bottom: the illustration belongs AFTER the three beats and
   BEFORE the link, and areas are the only way to say that without moving the
   link out of the copy it belongs to in the markup.
   ========================================================================== */
.split-art {
  display: grid;
  /* The figure gets the larger share. .mvt-body is not the panel -- the
     eyebrow column takes ~250px off the left before this grid starts -- so at
     1440 there are 885px to divide, and an even split left the artwork at
     429px with labels smaller than any type on the page. 1 : 1.5 puts the
     supplied artwork at ~490 and still leaves the beats two lines each. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "copy fig"
    "cta  fig";
  column-gap: clamp(24px, 3vw, 48px);
  row-gap: var(--s8);
  align-items: start;
}
.split-art-copy { grid-area: copy; }
.split-art-fig  { grid-area: fig; align-self: center; }
.split-art-cta  { grid-area: cta; margin: 0; }
.split-art-copy > * + * { margin-top: var(--s7); }
.split-art .beats { margin-top: var(--s9); }

@media (max-width: 900px) {
  .split-art {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    grid-template-areas: "copy" "fig" "cta";
    row-gap: var(--s8);
  }
  .split-art-fig { max-width: 520px; }
  .split-art-copy > * + * { margin-top: var(--s6); }
  .split-art .beats { margin-top: var(--s6); }
  .beats li { padding: var(--s4) 0; }
}
/* The labels drawn inside the artwork are the smallest type in the movement,
   so on a phone the figure takes the whole column and no cap.

   It briefly took the panel's inline padding back too, with a -32px inline
   margin, for about 22% more width on a 390px screen. audit.py's 200% text
   zoom guard caught it: .darkpanel is overflow: clip, so anything past its
   padding box is content the panel is cropping, and the panel's own 24px
   corner radius really was cutting the artwork there. Sixty pixels is not
   worth an element that reports itself as clipped. */
@media (max-width: 600px) {
  .split-art-fig { max-width: none; }
}


/* ==========================================================================
   19. SHOWCASE, STEPS AND THE STATEMENT
   These three components left the homepage in V5.10 but they are still used
   on /approach/ (.steps3), /work/ (.statement) and the three audience pages
   that carry a project showcase. The V5.10 dead-class sweep read the
   homepage after it had already been rewritten and removed them; restored
   here verbatim from V5.9.5.
   ========================================================================== */



/* ==========================================================================
   5. SELECTED WORK
   The screenshots are the proof, so they are the biggest thing on the page
   and they carry three words of context each.
   ========================================================================== */
.showcase { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(20px, 2.6vw, 36px); }

/* Full-width only at one item or three-plus. At exactly two -- which is
   already the case on /who-we-help/professional-services/ -- the wide rule
   left one item stretched and one half-width beside an empty cell. */
.showcase > :first-child:not(:nth-last-child(2)) { grid-column: 1 / -1; }

.sc {
  display: block; position: relative;
  background: var(--surface-2); border: 1px solid var(--rule);
  border-radius: var(--radius-card); overflow: hidden;
  transition: border-color var(--d-state) var(--ease);
}

.sc-media { display: block; overflow: hidden; }

.sc-media img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 10; object-fit: cover; object-position: 50% 0%;
  transition: transform 800ms var(--ease);
}

.showcase > :first-child:not(:nth-last-child(2)) .sc-media img { aspect-ratio: 21 / 9; }

.sc-cap {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--s5);
  padding: var(--s5) var(--s6) var(--s6);
}

.sc-cap b { font-size: var(--t-h4); font-weight: var(--w-semi);
            letter-spacing: var(--tr-h4); color: var(--text); }

.sc-cap span { font-size: var(--t-sm); color: var(--text-faint); }

.sc:hover { border-color: var(--line-2); }

.sc:hover b { color: var(--link); }
@media (prefers-reduced-motion: no-preference) {
  .sc:hover .sc-media img, .sc:focus-visible .sc-media img { transform: scale(1.03); }}
@media (max-width: 720px) {
  .showcase { grid-template-columns: minmax(0, 1fr); }
  .showcase > :first-child .sc-media img { aspect-ratio: 16 / 10; }}



/* ==========================================================================
   6. THREE STEPS
   The whole of the approach, on the homepage and on /approach/. It used to be
   three sections of methodology with a drawing that got progressively more
   ordered. It is three sentences.
   ========================================================================== */
.steps3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s9) var(--s8); }

.steps3 > li { position: relative; padding-top: var(--s7); border-top: 2px solid var(--keyline); }

.steps3 h3 {
  font-size: clamp(1.375rem, 1.1rem + 1.2vw, 1.875rem);
  letter-spacing: var(--tr-h3); color: var(--text); font-weight: var(--w-bold);
}

.steps3 p { font-size: var(--t-body); color: var(--text-dim); margin-top: var(--s4); line-height: 1.6; max-width: 34ch; }
@media (max-width: 50em) { .steps3 { grid-template-columns: minmax(0, 1fr); gap: var(--s8); }}



/* ==========================================================================
   7. THE STATEMENT
   One large sentence on its own ground, used where a section would otherwise
   be a heading, a paragraph and three cards. Sometimes the best section is a
   headline and nothing else.
   ========================================================================== */
/* The statement panel on /work/. Contained, orthogonal ramp, generous padding:
   the same object grammar as the hero and the closer, in the third ground. */
.statement-panel {
  border-radius: var(--radius-feature);
  padding: clamp(40px, 5vw, 84px);
  box-shadow: var(--panel-lift);
}
@media (max-width: 600px) { .statement-panel { border-radius: var(--radius-panel); padding: var(--s8) var(--s7); } }
.statement {
  font-size: clamp(1.75rem, 1.1rem + 2.6vw, 3.25rem);
  line-height: 1.14; letter-spacing: var(--tr-h2);
  font-weight: var(--w-bold); max-width: 20ch; text-wrap: balance;
}

@media (prefers-reduced-motion: reduce) {
  .sc-media img { transition: none; }}


/* ==========================================================================
   20. THE CLOSER'S OBJECT

   Declared here, not in styles.css, and this is not cosmetic. `.art-layer`
   sets `inset: 0` and lives in this file, which loads last — so when the
   component was written in styles.css its `top: 50%` / `bottom: 0` were both
   overridden by `inset: 0` at equal specificity. The square then pinned to the
   top of the band and `translate: -50%` lifted it clean out of the section, so
   on phones the artwork sat behind the headline and left 190px of empty blue
   underneath it. Anything that positions inside an .art-layer has to be
   declared after it.
   ========================================================================== */
/* The dissolve. A detail, not an object: it runs along the panel's bottom
   boundary at single-digit alpha and is the same drawing at every width. */
.closer-art {
  position: absolute; z-index: 0;
  inset: auto 0 0 0; top: auto;
  height: clamp(64px, 8vw, 116px);
  pointer-events: none;
}
/* `cover`, never `fill`: a pixel is a square, and stretching the band to the
   panel's width would flatten every one of them on a phone. */
.closer-art > img {
  position: static; display: block; width: 105%; height: 100%;
  object-fit: cover; object-position: 50% 100%;
}



/* ==========================================================================
   21. THE PIXEL SYSTEM (September 2026)

   The third placement of the pixel motif, after the hero field and the
   closer's edge: compositions of squares, in the site's own blue, navy and
   gray, that assemble around a panel or a heading as it scrolls into view.

   THE MARKUP is one aria-hidden <span class="pxs pxs-<variant> pxs-<place>">
   holding a handful of <i> cells (oogle-core/inc/pixels.php). Everything
   else is here: each cell's size, position, tone and order of arrival. A
   cell is a square, never rounded, on the lattice: the brand's shape.

   SEVEN COMPOSITIONS, so that no two placements on a page repeat:
     cascade   32 / 16 / 8 / 4, the small ones arriving first
     backdrop  a large translucent square behind, a 12 on its edge, a 6
     trail     five small squares stepping away in an L
     rail      a column of seven small squares at a container's side
     corner    a 24 and two 12s forming a broken L
     duo       a 40 and a 20, two tones, corner to corner
     scatter   six cells of unequal sizes, loosely placed

   PLACEMENT is a corner or a side of the host (tl tr bl br l r). The right-
   hand placements mirror the composition through `direction: rtl` on the
   span, so a cell's inset-inline-start reads from the right edge; the
   bottom placements flip the vertical axis with --fy. The host carries
   .has-pxs, which is position: relative and overflow-x: clip, so a cluster
   that steps outside a panel can never widen the page.

   DEPTH. Foreground cells are solid and small; background cells are large
   and translucent and sit under the content (z-index 0 inside the host's
   own stacking context). Nothing is ever placed where text is: the offsets
   put every composition in a panel's padding, a section's gutter or the
   space between panels.

   MOTION. A cell starts at 45% scale, a few pixels off its resting place
   and invisible; when the host crosses into the viewport each cell
   settles into place with a short stagger. It plays again on the way back
   up: the reveal flag is set by one observer and cleared by a second one
   with a wider margin, so a host sitting on the boundary cannot flicker.
   Under reduced motion every cell is simply drawn in its final place.
   ========================================================================== */
.has-pxs { position: relative; overflow-x: clip; }
.pxs {
  position: absolute; z-index: 2;
  width: 0; height: 0; pointer-events: none;
  /* the scale the whole composition is drawn at: 1 on desktop, smaller on
     a phone, so a 40px square is a 24px square where the page is 360 wide */
  --pxs-k: 1;
  --fy: 1;
  /* tones on the light ground */
  --c1: var(--blue);
  --c2: var(--blue-deep);
  --c3: var(--navy);
  --c4: var(--line-2);
  --c5: var(--blue-film-2);
}
.pxs-on-dark {
  --c1: var(--cyan); --c2: var(--blue-light); --c3: var(--blue); --c4: var(--navy-5); --c5: var(--dark-film-2);
}
.pxs-on-blue {
  --c1: #fff; --c2: var(--cyan-soft); --c3: var(--blue-far); --c4: rgba(255,255,255,0.45); --c5: rgba(255,255,255,0.14);
}
.pxs-behind { z-index: 0; }
.pxs i {
  position: absolute;
  inset-inline-start: calc(var(--x) * var(--pxs-k));
  top: calc(var(--y) * var(--pxs-k) * var(--fy));
  margin-top: calc((var(--fy) - 1) / 2 * var(--s) * var(--pxs-k));
  width: calc(var(--s) * var(--pxs-k)); height: calc(var(--s) * var(--pxs-k));
  background: var(--c);
  opacity: var(--o, 1);
}
/* where the origin sits on the host */
.pxs-tl { top: var(--pxs-y, 0px); left: var(--pxs-x, 0px); }
.pxs-tr { top: var(--pxs-y, 0px); right: var(--pxs-x, 0px); direction: rtl; }
.pxs-bl { bottom: var(--pxs-y, 0px); left: var(--pxs-x, 0px); --fy: -1; }
.pxs-br { bottom: var(--pxs-y, 0px); right: var(--pxs-x, 0px); direction: rtl; --fy: -1; }
.pxs-l  { top: var(--pxs-y, 40%); left: var(--pxs-x, 0px); }
.pxs-r  { top: var(--pxs-y, 40%); right: var(--pxs-x, 0px); direction: rtl; }

/* On a section the origin is the edge of the content track, not of the
   viewport, and the section's own padding is added so a corner placement
   lands at the corner of the panel or heading inside rather than at the
   section's outer edge. --sec-pad is each rhythm's top padding, declared
   beside the rhythm itself (styles.css and section 4 above). */
.pxs-track.pxs-tl, .pxs-track.pxs-bl, .pxs-track.pxs-l { left: calc(max(var(--gutter), (100% - var(--track)) / 2) + var(--pxs-x, 0px)); }
.pxs-track.pxs-tr, .pxs-track.pxs-br, .pxs-track.pxs-r { right: calc(max(var(--gutter), (100% - var(--track)) / 2) + var(--pxs-x, 0px)); }
.pxs-track.pxs-tl, .pxs-track.pxs-tr { top: calc(var(--sec-pad, 0px) + var(--pxs-y, 0px)); }
.pxs-track.pxs-bl, .pxs-track.pxs-br { bottom: calc(var(--sec-pad, 0px) + var(--pxs-y, 0px)); }
.pxs-track.pxs-l, .pxs-track.pxs-r  { top: var(--pxs-y, 38%); }

/* The three code-level placements. The homepage hero's trail sits just
   outside the panel's bottom-left corner; the interior hero's sits above
   its top-right; the closer's cascade hangs off the blue panel's top-right
   corner with its rail in the left gutter beside the panel. */
.hero .pxs-trail { --pxs-x: -6px; --pxs-y: -10px; }
.pxs-phero { --pxs-x: 6px; --pxs-y: -6px; }
.pxs-closer { --pxs-x: 4px; --pxs-y: -4px; }
.closer .pxs-rail { --pxs-x: -18px; --pxs-y: 46%; }

/* --- cascade: 32 / 16 / 8 / 4 ---------------------------------------------
   The big one sits on the corner; the smaller ones step away from it along
   the edge. Small arrives first, so it reads as pixels gathering into the
   square rather than a square shedding them. */
.pxs-cascade i:nth-child(1) { --s: 32px; --x: -18px; --y: -18px; --c: var(--c1); --k: 3; }
.pxs-cascade i:nth-child(2) { --s: 16px; --x: 12px;  --y: 12px;  --c: var(--c2); --k: 2; }
.pxs-cascade i:nth-child(3) { --s: 8px;  --x: 34px;  --y: 4px;   --c: var(--c3); --k: 1; --o: 0.8; }
.pxs-cascade i:nth-child(4) { --s: 4px;  --x: 48px;  --y: 14px;  --c: var(--c3); --k: 0; --o: 0.6; }

/* --- backdrop: one large translucent square, a 12 on its edge, a 6 -------- */
.pxs-backdrop i:nth-child(1) { --s: 56px; --x: -22px; --y: -22px; --c: var(--c5); --k: 0; }
.pxs-backdrop i:nth-child(2) { --s: 12px; --x: 30px;  --y: 30px;  --c: var(--c1); --k: 1; }
.pxs-backdrop i:nth-child(3) { --s: 6px;  --x: 46px;  --y: 20px;  --c: var(--c2); --k: 2; }

/* --- trail: five small squares stepping away in an L ---------------------- */
.pxs-trail i:nth-child(1) { --s: 8px; --x: 0px;  --y: 0px;  --c: var(--c1); --k: 0; }
.pxs-trail i:nth-child(2) { --s: 6px; --x: 12px; --y: 0px;  --c: var(--c1); --k: 1; --o: 0.85; }
.pxs-trail i:nth-child(3) { --s: 4px; --x: 22px; --y: 0px;  --c: var(--c2); --k: 2; --o: 0.7; }
.pxs-trail i:nth-child(4) { --s: 6px; --x: 0px;  --y: 12px; --c: var(--c3); --k: 3; --o: 0.6; }
.pxs-trail i:nth-child(5) { --s: 4px; --x: 0px;  --y: 22px; --c: var(--c3); --k: 4; --o: 0.45; }

/* --- rail: a column of seven at a container's side ------------------------- */
.pxs-rail i { --s: 6px; --x: 0px; --c: var(--c4); }
.pxs-rail i:nth-child(1) { --y: 0px;   --k: 0; }
.pxs-rail i:nth-child(2) { --y: 14px;  --k: 1; --s: 4px; }
.pxs-rail i:nth-child(3) { --y: 28px;  --k: 2; }
.pxs-rail i:nth-child(4) { --y: 42px;  --k: 3; --c: var(--c1); }
.pxs-rail i:nth-child(5) { --y: 56px;  --k: 4; --s: 4px; }
.pxs-rail i:nth-child(6) { --y: 70px;  --k: 5; }
.pxs-rail i:nth-child(7) { --y: 84px;  --k: 6; --s: 4px; }

/* --- corner: a 24 and two 12s in a broken L ------------------------------- */
.pxs-corner i:nth-child(1) { --s: 24px; --x: -12px; --y: -12px; --c: var(--c1); --k: 0; }
.pxs-corner i:nth-child(2) { --s: 12px; --x: 16px;  --y: -12px; --c: var(--c3); --k: 1; --o: 0.8; }
.pxs-corner i:nth-child(3) { --s: 12px; --x: -12px; --y: 16px;  --c: var(--c2); --k: 2; }

/* --- duo: a 40 and a 20, two tones, corner to corner ---------------------- */
.pxs-duo i:nth-child(1) { --s: 40px; --x: -20px; --y: -8px;  --c: var(--c1); --k: 0; }
.pxs-duo i:nth-child(2) { --s: 20px; --x: 20px;  --y: -28px; --c: var(--c2); --k: 1; }

/* --- scatter: six cells of unequal sizes, loosely placed ------------------- */
.pxs-scatter i:nth-child(1) { --s: 16px; --x: 0px;  --y: 0px;  --c: var(--c1); --k: 2; }
.pxs-scatter i:nth-child(2) { --s: 6px;  --x: 24px; --y: 6px;  --c: var(--c3); --k: 0; --o: 0.7; }
.pxs-scatter i:nth-child(3) { --s: 10px; --x: 8px;  --y: 26px; --c: var(--c2); --k: 4; }
.pxs-scatter i:nth-child(4) { --s: 4px;  --x: 36px; --y: 22px; --c: var(--c3); --k: 1; --o: 0.5; }
.pxs-scatter i:nth-child(5) { --s: 24px; --x: 30px; --y: 36px; --c: var(--c5); --k: 3; }
.pxs-scatter i:nth-child(6) { --s: 6px;  --x: -14px; --y: 18px; --c: var(--c4); --k: 5; }

/* --- the motion ----------------------------------------------------------
   Three tiers, so the page does not fire everything at one speed:
     fast ambient        trail, rail         small cells, ~300ms, 55-60ms apart
     normal ambient      corner, duo, cascade  ~420-480ms, 90-120ms apart
     noticeable cluster  backdrop, scatter     ~600ms, 100-140ms apart
   (The builders below are the fourth tier, at 1.0-1.8s a piece.) */
.pxs { --pxs-dur: 420ms; --pxs-step: 90ms; }
.pxs-trail, .pxs-rail { --pxs-dur: 300ms; --pxs-step: 58ms; }
.pxs-duo { --pxs-dur: 460ms; --pxs-step: 120ms; }
.pxs-cascade { --pxs-dur: 480ms; --pxs-step: 110ms; }
.pxs-backdrop { --pxs-dur: 620ms; --pxs-step: 140ms; }
.pxs-scatter { --pxs-dur: 600ms; --pxs-step: 100ms; }
@media (prefers-reduced-motion: no-preference) {
  html.js .pxs i {
    opacity: 0;
    transform: translate(calc(var(--mx, 0px) * -1), calc(var(--my, 6px) * var(--fy))) scale(0.45);
    transition: opacity calc(var(--pxs-dur) * 0.85) var(--e-out), transform var(--pxs-dur) var(--e-out);
  }
  html.js [data-pxs-in] i {
    opacity: var(--o, 1);
    transform: none;
    transition-delay: calc(var(--k, 0) * var(--pxs-step) + var(--pxs-wait, 0ms));
  }
  /* The big translucent squares drift a few pixels as the host crosses
     the viewport. `translate` is a separate property from the transform
     the entrance uses, so the two never fight; scroll-driven, so there is
     no scroll handler and no layout read. */
  @supports (animation-timeline: view()) {
    .pxs-backdrop i:nth-child(1), .pxs-scatter i:nth-child(5), .pxs-duo i:nth-child(1) {
      animation: pxsDrift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
  }
}
@keyframes pxsDrift { from { translate: 0 12px; } to { translate: 0 -12px; } }

/* --- the section-edge stair --------------------------------------------------
   A ruled section opens with three squares stepping down from its hairline
   at the left edge of the content track: the smallest composition, drawn by
   pseudo-elements, on every interior page. */
[data-px] { position: relative; }
[data-px="edge"]::before, [data-px="edge"]::after {
  content: ""; position: absolute; z-index: 1; pointer-events: none;
  background: var(--blue); opacity: 0.5;
}
[data-px="edge"]::before { width: 10px; height: 10px; top: 2px; left: max(var(--gutter), calc((100% - var(--track)) / 2)); }
[data-px="edge"]::after {
  width: 22px; height: 10px; top: 14px;
  left: calc(max(var(--gutter), calc((100% - var(--track)) / 2)) + 12px);
  -webkit-mask: linear-gradient(90deg, #000 0 10px, transparent 10px 12px, #000 12px 22px);
  mask: linear-gradient(90deg, #000 0 10px, transparent 10px 12px, #000 12px 22px);
}
@media (prefers-reduced-motion: no-preference) {
  html.js [data-px="edge"]:not([data-px-in])::before { opacity: 0; transform: translateY(14px); }
  html.js [data-px="edge"]:not([data-px-in])::after  { opacity: 0; clip-path: inset(0 100% 0 0); }
  [data-px="edge"]::before { transition: opacity var(--m-enter) var(--e-out), transform var(--m-enter) var(--e-out); }
  [data-px="edge"]::after  { clip-path: inset(0 0 0 0); transition: opacity var(--m-enter) var(--e-out) 140ms, clip-path var(--m-scene) var(--e-snap) 140ms; }
}

/* --- the path cards: a hover cluster ----------------------------------------- */
.path { position: relative; }
.path::after {
  content: ""; position: absolute; top: 18px; right: 18px; z-index: 1;
  width: 22px; height: 10px; pointer-events: none;
  background: var(--mark); opacity: 0;
  -webkit-mask: linear-gradient(90deg, #000 0 10px, transparent 10px 12px, #000 12px 22px);
  mask: linear-gradient(90deg, #000 0 10px, transparent 10px 12px, #000 12px 22px);
  transition: opacity var(--d-state) var(--ease);
}
.path:hover::after, .path:focus-visible::after { opacity: 0.6; }

/* --- narrow screens --------------------------------------------------------
   Fewer, smaller, and never outside the page. The rails go (they live in
   the gutters a phone does not have); the scatter and cascade lose their
   outermost cells; everything is drawn at 60%; and the right-hand
   placements tuck inside the host instead of hanging past its edge. */
@media (max-width: 62em) {
  .pxs { --pxs-k: 0.75; }
  .pxs-rail { display: none; }
}
@media (max-width: 40em) {
  .pxs { --pxs-k: 0.6; }
  .pxs-scatter i:nth-child(6), .pxs-cascade i:nth-child(4), .pxs-trail i:nth-child(5) { display: none; }
  .pxs-tr, .pxs-r, .pxs-br { --pxs-x: 10px; }
  .pxs-tl, .pxs-l, .pxs-bl { --pxs-x: 10px; }
}

/* ==========================================================================
   21b. THE PIXEL BUILDERS

   The second kind of pixel motion: not squares settling around a panel but
   a composition the visitor watches being put together. A solid bar grows
   into place, then a field of small squares fills in beside it one at a
   time, and the whole thing takes a second or so. It plays ONCE per page
   load (main.js "builders": one observer, unobserve on arrival), it starts
   only when most of it is on screen, and when it is built it stays built.

   The geometry is data (oogle-core/inc/pixels.php, OOGLE_PXB): every bar
   and cell carries its position, size and arrival time as custom
   properties, and this section only knows how to draw a bar, draw a cell,
   grow one and settle the other. Five compositions: tower, dissolve,
   keystone, resolve, stair. Two of them (dissolve, resolve) are the motif
   the brand can keep: a solid line and the pixels it is made of.

   PLACEMENT is the same six points as the ambient compositions and the
   same track anchor. The span has the composition's own width and height,
   so a bottom placement is drawn upward from the edge and a right-hand one
   is mirrored through direction: rtl, bars included.

   PHONES get a smaller composition, not a shrunken one: cells flagged .m
   are dropped, bars use their --wm/--hm, and the side placements go with
   the gutters they live in. Under reduced motion, or with scripting off,
   the finished composition is simply drawn.
   ========================================================================== */
.pxb {
  position: absolute; z-index: 2; pointer-events: none;
  width: calc(var(--pxb-w) * var(--pxs-k)); height: calc(var(--pxb-h) * var(--pxs-k));
  --pxs-k: 1;
  --c1: var(--blue); --c2: var(--blue-deep); --c3: var(--navy);
  --c4: var(--line-2); --c5: var(--blue-film-2); --c6: var(--blue-pale);
}
.pxb.pxs-on-dark {
  --c1: var(--blue-light); --c2: var(--blue); --c3: var(--cyan);
  --c4: var(--navy-5); --c5: var(--dark-film-2); --c6: rgba(111, 178, 247, 0.4);
}
.pxb.pxs-on-blue {
  --c1: #fff; --c2: var(--cyan-soft); --c3: var(--blue-far);
  --c4: rgba(255,255,255,0.45); --c5: rgba(255,255,255,0.14); --c6: rgba(255,255,255,0.28);
}
.pxb b, .pxb i {
  position: absolute; display: block;
  inset-inline-start: calc(var(--x) * var(--pxs-k));
  top: calc(var(--y) * var(--pxs-k));
}
.pxb b { width: calc(var(--w) * var(--pxs-k)); height: calc(var(--h) * var(--pxs-k)); transform-origin: var(--ox) var(--oy); }
.pxb i { width: calc(var(--s) * var(--pxs-k)); height: calc(var(--s) * var(--pxs-k)); }
.pxb .t1 { background: var(--c1); }
.pxb .t2 { background: var(--c2); }
.pxb .t3 { background: var(--c3); }
.pxb .t4 { background: var(--c4); }
.pxb .t5 { background: var(--c5); }
.pxb .t6 { background: var(--c6); }

/* where it sits on the host */
.pxb-tl { top: var(--pxs-y, 0px); left: var(--pxs-x, 0px); }
.pxb-tr { top: var(--pxs-y, 0px); right: var(--pxs-x, 0px); direction: rtl; }
.pxb-bl { bottom: var(--pxs-y, 0px); left: var(--pxs-x, 0px); }
.pxb-br { bottom: var(--pxs-y, 0px); right: var(--pxs-x, 0px); direction: rtl; }
.pxb-l  { top: var(--pxs-y, 38%); left: var(--pxs-x, 0px); }
.pxb-r  { top: var(--pxs-y, 38%); right: var(--pxs-x, 0px); direction: rtl; }
.pxb-tr b, .pxb-br b, .pxb-r b { transform-origin: var(--oxm) var(--oy); }
.pxs-track.pxb-tl, .pxs-track.pxb-bl, .pxs-track.pxb-l { left: calc(max(var(--gutter), (100% - var(--track)) / 2) + var(--pxs-x, 0px)); }
.pxs-track.pxb-tr, .pxs-track.pxb-br, .pxs-track.pxb-r { right: calc(max(var(--gutter), (100% - var(--track)) / 2) + var(--pxs-x, 0px)); }
.pxs-track.pxb-tl, .pxs-track.pxb-tr { top: calc(var(--sec-pad, 0px) + var(--pxs-y, 0px)); }
.pxs-track.pxb-bl, .pxs-track.pxb-br { bottom: calc(var(--sec-pad, 0px) + var(--pxs-y, 0px)); }
.pxs-track.pxb-l, .pxs-track.pxb-r  { top: var(--pxs-y, 38%); }

/* The homes. Each offset puts the composition in space the layout already
   leaves empty: the eyebrow column beside the services panel, the eyebrow
   column inside the "one team" panel, the strip of section padding just
   under the checkup panel and the "since 1999" panel. */
/* The two that stand in an empty column are drawn larger: they have the
   room, and a piece that is watched being built should be worth watching. */
.pxb-tower.pxb-l    { --pxs-y: 34%; --pxs-x: 2px; --pxs-k: 1.3; }
.sec-band .pxb-keystone.pxb-l { --pxs-y: 40%; --pxs-x: clamp(28px, 4vw, 64px); --pxs-k: 1.25; }
.pxb-resolve.pxb-bl { --pxs-y: -26px; --pxs-x: 2px; }
.pxb-dissolve.pxb-br { --pxs-y: -34px; --pxs-x: 2px; }
.pxb-stair.pxb-bl   { --pxs-y: -70px; --pxs-x: 2px; }
.pxb-stair.pxb-tr, .pxb-keystone.pxb-tr, .pxb-tower.pxb-tr { --pxs-y: -18px; --pxs-x: -10px; }

/* --- the build -------------------------------------------------------------
   Waiting: a bar is scaled to nothing along the axis it grows on, a cell is
   invisible and small. Arriving: each takes its own --d from the data, so
   the bar grows first (500-700ms, the snap curve: it lands, it does not
   float) and the cells follow in the wave the data describes. */
@media (prefers-reduced-motion: no-preference) {
  html.js .pxb .pxb-x  { transform: scaleX(0); }
  html.js .pxb .pxb-y  { transform: scaleY(0); }
  html.js .pxb .pxb-xy { transform: scale(0); }
  html.js .pxb i { opacity: 0; transform: scale(0.35); }
  html.js .pxb[data-pxb-in] b { transform: none; transition: transform 680ms var(--e-snap) var(--d, 0ms); }
  html.js .pxb[data-pxb-in] .pxb-xy { transition-duration: 520ms; }
  html.js .pxb[data-pxb-in] i {
    opacity: 1; transform: none;
    transition: opacity 260ms var(--e-out) var(--d, 0ms), transform 340ms var(--e-out) var(--d, 0ms);
  }
}

/* --- narrow screens -------------------------------------------------------- */
@media (max-width: 62em) {
  .pxb { --pxs-k: 0.8; }
  .pxb-l, .pxb-r { display: none; }
}
@media (max-width: 40em) {
  .pxb { --pxs-k: 0.7; }
  .pxb i.m { display: none; }
  .pxb b { width: calc(var(--wm) * var(--pxs-k)); height: calc(var(--hm) * var(--pxs-k)); }
  /* the tower's bar is shorter on a phone; the grid sits at its top, so
     nothing moves */
  .pxb-tr, .pxb-br, .pxb-r { --pxs-x: 12px; }
  .pxb-tl, .pxb-bl, .pxb-l { --pxs-x: 12px; }
}

/* ==========================================================================
   22. THE HISTORY PANEL (/about/)

   The warm ground again, because this is the other human movement on the
   site: where the name has been. A ruled sequence of eras on the left, the
   archival mark on a white plate on the right, and a closing line under
   both. The plate is white because the mark was drawn for white; putting a
   green tractor straight onto sand would have muddied a logo that is the
   whole reason the panel exists.
   ========================================================================== */
.history {
  border-radius: var(--radius-feature);
  border: 1px solid var(--sand-2);
  padding: clamp(36px, 4.6vw, 80px);
  overflow: clip;
}
.history-head { max-width: 58ch; }
.history-head .eyebrow { margin-bottom: var(--s6); }
.history-head .lead { margin-top: var(--s5); max-width: 52ch; }
.history-body {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(32px, 5vw, 80px);
  align-items: start;
  margin-top: clamp(32px, 4vw, 56px);
}
.history-eras { display: grid; }
.history-era {
  display: grid; grid-template-columns: 88px minmax(0, 1fr);
  gap: var(--s4) var(--s5);
  padding: var(--s6) 0;
  border-top: 1px solid var(--sand-2);
}
.history-era:last-child { border-bottom: 1px solid var(--sand-2); }
.history-year {
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: var(--tr-mono); text-transform: uppercase; color: var(--blue);
  padding-top: 6px;
}
.history-era-in h3 { color: var(--ink); }
.history-era-in p { margin-top: var(--s3); font-size: var(--t-sm); color: var(--ink-3); line-height: 1.62; max-width: 52ch; }
.history-fig { margin: 0; position: sticky; top: calc(var(--hdr-h) + var(--s8)); }
.history-plate {
  position: relative;
  background: var(--paper);
  border: 1px solid var(--sand-2);
  border-radius: var(--radius-card);
  padding: clamp(48px, 5vw, 64px) clamp(24px, 3vw, 44px) clamp(24px, 3vw, 44px);
  box-shadow: var(--lift-2);
}
.history-plate img { display: block; width: 100%; height: auto; }
.history-plate-l {
  position: absolute; top: 14px; left: 16px;
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: var(--tr-mono); text-transform: uppercase; color: var(--ink-5);
}
.history-fig figcaption {
  margin-top: var(--s4); font-size: var(--t-xs); color: var(--ink-4); line-height: 1.55;
  max-width: 44ch;
}
.history-close {
  margin-top: clamp(32px, 4vw, 56px); padding-top: var(--s7);
  border-top: 2px solid var(--keyline);
  display: grid; grid-template-columns: minmax(0, 0.4fr) minmax(0, 1fr);
  gap: var(--s4) var(--s8);
}
.history-close h3 { color: var(--ink); }
.history-close p { font-size: var(--t-body); color: var(--ink-3); line-height: 1.65; max-width: 60ch; }
@media (max-width: 900px) {
  .history-body { grid-template-columns: minmax(0, 1fr); }
  .history-fig { position: static; max-width: 460px; }
  .history-close { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 600px) {
  .history { border-radius: var(--radius-panel); padding: var(--s8) var(--s5); }
  .history-era { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
  .history-year { padding-top: 0; }
}


/* ==========================================================================
   23. THE WHO-WE-HELP STRIP (homepage)
   A heading, one line, and the nine industries as pills. It sits between
   the four lanes and the "one team" panel: the visitor has just read what
   we do, and this is the one-line answer to whether we do it for people
   like them.
   ========================================================================== */
.astrip {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.4fr);
  gap: var(--s6) clamp(24px, 4vw, 64px);
  align-items: start;
  padding: clamp(24px, 3vw, 40px);
  border: 1px solid var(--surface-edge);
  border-radius: var(--radius-panel);
  background: var(--surface-2);
}
.astrip-head .eyebrow { margin-bottom: var(--s4); }
.astrip-head .h3 { max-width: 18ch; }
.astrip-lead { margin-top: var(--s3); font-size: var(--t-sm); color: var(--text-dim); max-width: 36ch; }
.astrip-list { align-self: start; padding-top: 6px; }
.astrip-list li > a:not(.btn):not(.arrow):not(.tel):not(.nav-lnk),
.astrip-list li > a:not(.btn):not(.arrow):not(.tel):hover { text-decoration: none; }
.astrip-more { grid-column: 2; margin-top: calc(var(--s3) * -1); }
@media (max-width: 56em) {
  .astrip { grid-template-columns: minmax(0, 1fr); }
  .astrip-more { grid-column: 1; }
}
.mvt + .astrip { margin-top: clamp(40px, 5vw, 72px); }

/* ==========================================================================
   24. HOW THE BILLING WORKS (/pricing/)

   Three columns, three different objects, because the three shapes of an
   invoice are different things and the first version drew them as three
   identical walls of text. Monthly is the priced one: it stands on the blue
   film and carries the two figures as a small table with the ownership term
   in a ruled note under them. Quoted is the process: what gets a scope, as
   chips, and the four steps that make one, as a rail of squares. By the hour
   is the small work: what it is, and the promise that the rate and the time
   are said before it starts. The rate is never printed.

   The arrangement is a band and two cards, not three columns: monthly has
   the most to say (two figures with what each covers, the ownership term)
   and runs the full width as a three-column band; quoted and by the hour
   sit under it side by side. Three equal columns left the two shorter
   ones with a third of their height empty, which is exactly the dead
   space this pass exists to remove. */
.bill {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-areas: "monthly monthly" "quoted hourly";
  gap: var(--s5);
  align-items: stretch;
}
.bill-monthly { grid-area: monthly; }
.bill-quoted  { grid-area: quoted; }
.bill-hourly  { grid-area: hourly; }
/* The band: the head, the lead and the moving note on the left, the two
   figures in the middle, the ownership term on the right. */
.bill-col.bill-monthly {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.25fr) minmax(0, 1fr);
  grid-template-areas: "head prices term" "lead prices note" "link prices note";
  grid-template-rows: auto auto 1fr;
  column-gap: clamp(24px, 3vw, 48px); row-gap: var(--s4);
  align-content: start;
}
.bill-monthly .bill-head   { grid-area: head; }
.bill-monthly .bill-lead   { grid-area: lead; }
.bill-monthly .bill-sub    { grid-area: note; align-self: start; }
.bill-monthly .bill-prices { grid-area: prices; align-self: start; }
.bill-monthly .bill-term   { grid-area: term; align-self: start; }
.bill-monthly .bill-link   { grid-area: link; align-self: end; margin-top: var(--s2); }
.bill-col {
  position: relative;
  display: flex; flex-direction: column; gap: var(--s5);
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--surface-edge);
  border-radius: var(--radius-panel);
  padding: var(--s7);
  overflow: clip;
}
/* A small stair of squares in the corner of each: the brand's pixel, one
   per shape, at three different corners so the three do not repeat. */
.bill-col::after {
  content: ""; position: absolute; width: 22px; height: 10px; pointer-events: none;
  background: var(--mark); opacity: 0.45;
  -webkit-mask: linear-gradient(90deg, #000 0 10px, transparent 10px 12px, #000 12px 22px);
  mask: linear-gradient(90deg, #000 0 10px, transparent 10px 12px, #000 12px 22px);
}
.bill-monthly::after { bottom: 18px; right: 18px; }
.bill-quoted::after  { bottom: 18px; right: 18px; }
.bill-hourly::after  { bottom: 18px; left: 18px; }
.bill-monthly { background: linear-gradient(var(--blue-film), var(--blue-film)), var(--surface); border-color: rgba(14, 97, 209, 0.22); }
.bill-hourly  { background: var(--surface-2); }

.bill-head { display: flex; align-items: flex-start; gap: var(--s4); }
.bill-n {
  flex: none;
  display: inline-grid; place-items: center;
  width: 40px; height: 40px;
  background: var(--navy); color: var(--snow);
  border-radius: var(--radius-chip);
  font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: 0.06em;
}
.bill-monthly .bill-n { background: var(--blue); }
.bill-head-t { min-width: 0; }
.bill-head-t .card-meta { margin-bottom: 4px; }
.bill-h { font-size: var(--t-h4); font-weight: var(--w-black); letter-spacing: var(--tr-h4); color: var(--text); line-height: 1.2; }
.bill-lead { font-size: var(--t-body); font-weight: var(--w-bold); color: var(--text-2); line-height: 1.45; }

/* The two figures, as a table: name and a word on the left, the figure on
   the right, a hairline between. The figure is the site's own .price at
   the smaller size. */
.bill-prices { display: grid; gap: var(--s3); }
.bill-price {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s4);
  padding: var(--s4) var(--s5);
  background: var(--surface); border: 1px solid var(--surface-edge); border-radius: var(--radius-inset);
}
.bill-price dt { font-size: var(--t-body); font-weight: var(--w-bold); color: var(--text); display: grid; gap: 4px; min-width: 0; }
.bill-price-sub { font-size: var(--t-xs); font-weight: var(--w-book); color: var(--text-dim); line-height: 1.5; }
.bill-price dd { flex: none; }
.bill-fig { justify-content: flex-end; }
.bill-fig .price-n { font-size: var(--t-h4); }
.bill-fig .price-from, .bill-fig .price-u { font-size: var(--t-sm); }

.bill-points { display: grid; gap: var(--s2); }
.bill-points li {
  position: relative; padding-left: 18px;
  font-size: var(--t-sm); color: var(--text-dim); line-height: 1.5;
}
.bill-points li::before { content: ""; position: absolute; left: 0; top: 0.55em; width: 7px; height: 7px; background: var(--mark); }
.bill-term {
  font-size: var(--t-sm); color: var(--text-dim); line-height: 1.5;
  padding: var(--s3) var(--s4);
  border-left: 3px solid var(--blue); background: var(--surface);
  border-radius: 0 var(--radius-chip) var(--radius-chip) 0;
}
.bill-term b, .bill-before b { color: var(--text); }

.bill-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.bill-chips li {
  font-size: var(--t-xs); font-weight: var(--w-mid); color: var(--text-2);
  padding: 5px 11px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-2); background: var(--surface);
}
.bill-sub { font-size: var(--t-sm); color: var(--text-dim); line-height: 1.5; }
.bill-before { font-size: var(--t-sm); color: var(--text-dim); line-height: 1.5; }

/* The four steps as a rail: a square per step, a line between, the word
   under. What a quote is, in one line. */
.bill-steps {
  display: grid; grid-template-columns: repeat(var(--n, 4), minmax(0, 1fr));
  gap: 0; counter-reset: bill;
  padding-top: var(--s2);
}
.bill-steps li {
  position: relative; counter-increment: bill;
  font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-2);
  padding-top: 22px;
}
.bill-steps li::before {
  content: ""; position: absolute; top: 0; left: 0; width: 12px; height: 12px;
  background: var(--blue);
}
.bill-steps li::after {
  content: ""; position: absolute; top: 5px; left: 16px; right: 4px; height: 2px;
  background: var(--line-2);
}
.bill-steps li:last-child::after { display: none; }
.bill-steps li:first-child::before { background: var(--navy); }
.bill-link { margin-top: auto; padding-top: var(--s2); }

/* Under 1152px the band's three columns are too narrow for a figure and a
   sentence side by side, so it becomes two columns with the term across
   the foot; and the two cards stack, because side by side at this width
   the shorter one was a third empty. */
@media (max-width: 72em) {
  .bill { grid-template-columns: minmax(0, 1fr); grid-template-areas: "monthly" "quoted" "hourly"; }
  .bill-col.bill-monthly {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    grid-template-areas: "head prices" "lead prices" "link prices" "term term" "note note";
    grid-template-rows: auto auto 1fr auto auto;
  }
}
@media (max-width: 40em) {
  .bill-col.bill-monthly { display: flex; }
  .bill-monthly .bill-link { margin-top: 0; }
}
@media (max-width: 620px) {
  .bill { gap: var(--s4); }
  .bill-col { padding: var(--s6); border-radius: var(--radius-card); }
  .bill-steps li { font-size: 0.75rem; }
}

/* ==========================================================================
   25. WHAT WE WORK WITH (/about/)

   Seven capabilities as tiles beside the display heading: a mark or an
   icon on a pale plate, a title, one line. Two columns; the WordPress and
   WooCommerce tile spans both, because it is most of the work and carries
   two marks. Not a logo wall: four software marks in their own colours at
   one fixed height, three at a glance, and the rest are the site's own
   line icons in the text colour with one blue pixel each.

   The tiles rise in sequence (data-rise, staggered by the reveal system),
   and each plate follows its tile by a beat. Hover is a border, a tint, a
   1px lift of the plate and a pixel appearing in the corner: nothing
   scales, nothing bounces, no mark ever moves on its own.
   ========================================================================== */
.cap {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4);
  margin-top: var(--s3);
}
.cap-wide { grid-column: 1 / -1; }
.cap-item {
  position: relative;
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s4);
  align-items: center;
  padding: var(--s4) var(--s5);
  background: var(--surface);
  border: 1px solid var(--surface-edge);
  border-radius: var(--radius-card);
  overflow: clip;
  transition: border-color var(--d-state) var(--ease), background-color var(--d-state) var(--ease);
}
.cap-plate {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-width: 52px; height: 52px; padding: 0 12px;
  background: var(--blue-film);
  border-radius: var(--radius-control);
  transition: transform var(--d-state) var(--ease), background-color var(--d-state) var(--ease);
}
.cap-wide .cap-plate { padding: 0 14px; }
/* the marks, one height, their own colours */
.cap-mark { display: block; height: 26px; width: 26px; }
.cap-mark-woocommerce { height: 19px; width: 76px; }
.cap-mark-php { height: 22px; width: 40px; }
.cap-wide .cap-mark-wordpress { height: 32px; width: 32px; }
.cap-wide .cap-mark-woocommerce { height: 23px; width: 92px; }
.cap-wide .cap-plate { gap: 12px; }
/* the icons: text-colour strokes, one blue element */
.cap-ic {
  display: block; width: 26px; height: 26px;
  fill: none; stroke: var(--text-2); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round;
}
.cap-ic .px { fill: var(--blue); stroke: none; }
.cap-ic .ln { stroke: var(--blue); }
.cap-body { display: grid; gap: 3px; min-width: 0; }
.cap-t { font-size: var(--t-body); font-weight: var(--w-bold); color: var(--text); line-height: 1.3; }
.cap-l { font-size: var(--t-xs); color: var(--text-dim); line-height: 1.45; }
.cap-wide .cap-t { font-size: var(--t-h4); letter-spacing: var(--tr-h4); }
.cap-wide .cap-l { font-size: var(--t-sm); }
/* the pixel in the corner, shown on hover */
.cap-px {
  position: absolute; top: 12px; right: 12px; width: 6px; height: 6px;
  background: var(--blue); opacity: 0;
  transform: translate(4px, -4px);
  transition: opacity var(--d-state) var(--ease), transform var(--d-state) var(--ease);
}
@media (hover: hover) {
  .cap-item:hover { border-color: var(--blue-lift); background: var(--paper-2); }
  .cap-item:hover .cap-plate { transform: translateY(-1px); background: var(--blue-film-2); }
  .cap-item:hover .cap-px { opacity: 1; transform: none; }
}
/* the plate follows its tile in */
@media (prefers-reduced-motion: no-preference) {
  .cap[data-rise] .cap-plate { transition: opacity var(--m-enter) var(--e-out), transform var(--m-enter) var(--e-out), background-color var(--d-state) var(--ease); }
  .cap[data-rise="wait"] .cap-plate { opacity: 0; transform: scale(0.85); }
  .cap[data-rise="in"] .cap-plate { transition-delay: calc(var(--m-step) * 2); }
  .cap[data-rise="in"] > :nth-child(2) .cap-plate { transition-delay: calc(var(--m-step) * 3); }
  .cap[data-rise="in"] > :nth-child(3) .cap-plate { transition-delay: calc(var(--m-step) * 4); }
  .cap[data-rise="in"] > :nth-child(4) .cap-plate { transition-delay: calc(var(--m-step) * 5); }
  .cap[data-rise="in"] > :nth-child(5) .cap-plate { transition-delay: calc(var(--m-step) * 6); }
  .cap[data-rise="in"] > :nth-child(6) .cap-plate { transition-delay: calc(var(--m-step) * 7); }
  .cap[data-rise="in"] > :nth-child(7) .cap-plate { transition-delay: calc(var(--m-step) * 8); }
  .cap[data-rise="in"] > :nth-child(7) { transition-delay: calc(var(--m-step) * 6); }
  /* the hover transition on the plate must not inherit the entrance delay */
  .cap[data-rise="in"] .cap-item:hover .cap-plate { transition-delay: 0ms; }
}
@media (max-width: 61.25em) {
  .cap { margin-top: var(--s7); }
}
@media (max-width: 40em) {
  .cap { grid-template-columns: minmax(0, 1fr); gap: var(--s3); }
  .cap-item { padding: var(--s4); }
  .cap-plate { min-width: 48px; height: 48px; padding: 0 10px; }
  .cap-wide .cap-t { font-size: var(--t-body); }
}
