/* ==========================================================================
   Tech4TIME — layout.css
   Page skeleton: sections, grids, the sticky site header and the site footer.
   Structural rules only; visual component styling lives in components.css.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Page shell
   -------------------------------------------------------------------------- */

.page {
  display: flex;
  flex-direction: column;
  min-height: 100svh;
}

.page__main {
  flex: 1;
}

/* --------------------------------------------------------------------------
   2. Sections
   -------------------------------------------------------------------------- */

.section {
  padding-block: var(--section-padding);
}

.section--tight {
  padding-block: calc(var(--section-padding) * 0.6);
}

.section--surface {
  background-color: var(--bg-surface);
}

/* Hairline silver rule between bands, echoing the clock hands. */
.section--ruled {
  border-block-start: var(--border-width) solid transparent;
  border-image: var(--silver-gradient-rule) 1;
}

.section__header {
  max-width: 48rem;
  margin-inline: auto;
  margin-block-end: var(--space-xl);
  text-align: center;
}

.section__eyebrow {
  display: block;
  margin-block-end: var(--space-2xs);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--accent-text);
}

.section__title {
  margin-block-end: var(--space-sm);
}

.section__lead {
  font-size: var(--text-md);
  color: var(--text-secondary);
}

/* Banner every inner page opens with: the page name in the silver gradient over
   a tagline. Shared rather than per-page, so all fourteen read as one family. */
.page-hero {
  padding-block: clamp(4rem, 10vw, 7.5rem) clamp(3.5rem, 8vw, 6rem);
  text-align: center;
  background-color: var(--bg-surface);
  border-block-end: var(--border-width) solid var(--border-subtle);
}

.page-hero__inner {
  max-width: 50rem;
  margin-inline: auto;
}

.page-hero__title {
  margin-block-end: var(--space-2xs);
  font-size: var(--text-4xl);
  font-weight: 800;
  background-image: var(--silver-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

.page-hero__subtitle {
  font-size: var(--text-lg);
  font-weight: 500;
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   3. Grid helpers
   Auto-fit grids so columns reflow fluidly instead of snapping at breakpoints.
   -------------------------------------------------------------------------- */

.grid {
  display: grid;
  gap: var(--space-md);
}

.grid--auto {
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
}

.grid--auto-sm {
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
}

.grid--auto-lg {
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
}

/* Two-up split that collapses to a single column below the tablet band. */
.grid--split {
  grid-template-columns: 1fr;
  gap: var(--space-xl);
}

@media (min-width: 64rem) {
  .grid--split {
    grid-template-columns: 1fr 1fr;
    align-items: center;
  }
}

/* --------------------------------------------------------------------------
   4. Site header
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  border-block-end: var(--border-width) solid var(--border-subtle);
}

/* The frosted background is on a pseudo-element rather than on .site-header
   itself, and that is not a stylistic choice.

   backdrop-filter makes an element the containing block for any
   position: fixed DESCENDANT. The mobile nav drawer is fixed and lives inside
   the header, so with the blur on .site-header its inset:0 resolved against
   the header's own box instead of the viewport: the drawer opened 120px tall
   instead of full height, and all six links fell outside it and could not be
   hit. Nothing looked broken in the markup, the attribute said data-open=true,
   and the transition ran. Moving the blur one level down leaves the drawer's
   containing block as the viewport, where it belongs.

   Negative z-index keeps it behind the header's own content while staying
   inside the header's stacking context, so it blurs the page scrolling under
   it and nothing else. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: color-mix(in srgb, var(--bg-base) 85%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

/* Browsers without color-mix get the solid base tone. */
@supports not (background-color: color-mix(in srgb, red 50%, transparent)) {
  .site-header::before {
    background-color: var(--bg-base);
  }
}

.site-header--scrolled {
  box-shadow: var(--shadow);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  min-height: var(--header-height);
}

/* The drawer is a positioned sibling with a higher z-index, so it would paint
   over the logo and — critically — over the hamburger, leaving no way to tap
   the menu closed again. Lifting the bar's own controls above it keeps the
   header usable while the drawer is open. */
.site-header__brand,
.site-header__actions {
  position: relative;
  z-index: calc(var(--z-overlay) + 1);
}

.site-header__brand {
  flex-shrink: 0;
  line-height: 0;
}

/* HEIGHT DRIVES THE SIZE, AND max-width IS THE GUARD ON WHAT THAT MULTIPLIES.
   The mark is sized by its height so lockups of different proportions all sit
   on the same baseline. That means the WIDTH it occupies is height x aspect
   ratio, and .site-header__brand above is flex-shrink: 0, so nothing downstream
   can take that width back.

   Which was fine while the logo was three committed files at 2.81:1, and
   stopped being fine when it became something an operator uploads. Measured
   with tools/check_responsive.py: at 320px the header holds a mark up to about
   8:1 and a 10:1 one pushes the page 56px sideways. A long wordmark with a
   tagline set beside it reaches that.

   So the width is capped and the picture is contained inside the cap rather
   than squashed to fit it. Today's mark is drawn at 79-113px, far inside the
   cap, so this changes nothing that ships and bounds everything that does not. */
.site-header__logo {
  width: auto;
  height: clamp(1.75rem, 1.4rem + 1.7vw, 2.5rem);
  max-width: min(45vw, 11.25rem);
  object-fit: contain;
  object-position: left center;
}

/* Mode-aware artwork. Two variants ship; CSS shows the one that reads against
   the active surface. This mirrors the theme-toggle icon rules, so it follows
   both the OS preference and an explicit data-theme choice — which a
   <picture media="(prefers-color-scheme: …)"> could not do.

   .theme-swap--light / --dark are the general-purpose pair for page content;
   the header and footer keep their own class names. */
.theme-swap--dark,
.site-header__logo-wrap--dark,
.site-footer__logo-wrap--dark {
  display: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-swap--light,
  :root:not([data-theme="light"]) .site-header__logo-wrap--light,
  :root:not([data-theme="light"]) .site-footer__logo-wrap--light {
    display: none;
  }
  :root:not([data-theme="light"]) .theme-swap--dark,
  :root:not([data-theme="light"]) .site-header__logo-wrap--dark,
  :root:not([data-theme="light"]) .site-footer__logo-wrap--dark {
    display: block;
  }
}

:root[data-theme="dark"] .theme-swap--light,
:root[data-theme="dark"] .site-header__logo-wrap--light,
:root[data-theme="dark"] .site-footer__logo-wrap--light {
  display: none;
}

:root[data-theme="dark"] .theme-swap--dark,
:root[data-theme="dark"] .site-header__logo-wrap--dark,
:root[data-theme="dark"] .site-footer__logo-wrap--dark {
  display: block;
}

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}

/* Desktop navigation ------------------------------------------------------ */

.site-nav__list {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-xs), 1.6vw, var(--space-md));
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Below the laptop band the header nav is not shown at all. Navigation there
   is the dock — a floating bar at the bottom of the viewport with a panel of
   sections above it, styled in section 10 of components.css.

   The header nav used to become a full-screen drawer here instead. Hiding it
   outright is what keeps exactly one navigation on screen at any width: the
   drawer needed a hamburger in the header to open it, and a second control
   that duplicates a nav already visible is how the hamburger ended up sitting
   beside the full desktop nav.

   The links stay in the markup rather than being removed, so the header nav
   is still there for anything reading the document rather than viewing it. */
@media (max-width: 63.999em) {
  .site-nav {
    display: none;
  }
}

/* Keep the last of the footer clear of the floating bar. --dock-height is
   declared on .dock in components.css; the fallback covers the case where
   that rule has not applied. */
@media (max-width: 63.999em) {
  .page {
    padding-block-end: calc(
      var(--dock-height, 4rem) + var(--space-lg) + env(safe-area-inset-bottom, 0px)
    );
  }
}

/* --------------------------------------------------------------------------
   5. Site footer
   -------------------------------------------------------------------------- */

.site-footer {
  background-color: var(--bg-surface);
  border-block-start: var(--border-width) solid var(--border-subtle);
  padding-block: var(--space-2xl) var(--space-lg);
}

.site-footer__main {
  display: grid;
  gap: var(--space-xl);
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  padding-block-end: var(--space-xl);
}

/* The brand column carries a logo and a paragraph, so it gets more room. */
.site-footer__brand {
  max-width: 24rem;
}

/* Capped for the reason the header's is, and against the same upload. The
   footer's column is narrower than the page, so 100% of it is the bound. */
.site-footer__logo {
  width: auto;
  height: 2.25rem;
  max-width: 100%;
  object-fit: contain;
  object-position: left center;
  margin-block-end: var(--space-sm);
}

.site-footer__tagline {
  margin-block-end: var(--space-2xs);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--accent-text);
}

.site-footer__description {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.site-footer__social {
  display: flex;
  gap: var(--space-2xs);
  margin: var(--space-md) 0 0;
  padding: 0;
  list-style: none;
}

.site-footer__section {
  min-width: 0;
}

.site-footer__heading {
  margin-block-end: var(--space-sm);
  font-size: var(--text-base);
  font-weight: 600;
}

.site-footer__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-footer__link {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-decoration: none;
}

.site-footer__link:hover {
  color: var(--accent-text);
  text-decoration: underline;
}

/* Contact column ---------------------------------------------------------- */

.site-footer__contact {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin: 0;
  /* <address> is italic by default; the semantics are wanted, the slant is not. */
  font-style: normal;
}

.contact-item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.contact-item__icon {
  flex-shrink: 0;
  width: 1rem;
  height: 1rem;
  margin-block-start: 0.3em;
  color: var(--accent-text);
}

.contact-item__label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-primary);
}

/* A second label inside the same item starts a new group (BD then Malaysia),
   so it needs breathing room above it. */
.contact-item__label ~ .contact-item__label,
.contact-item__note + .contact-item__label {
  margin-block-start: var(--space-2xs);
}

.contact-item__note {
  display: block;
  font-size: var(--text-xs);
  font-style: italic;
  color: var(--text-muted);
}

.contact-item a {
  color: inherit;
  text-decoration: none;
}

.contact-item a:hover {
  color: var(--accent-text);
  text-decoration: underline;
}

/* Footer bottom bar ------------------------------------------------------- */

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block-start: var(--space-lg);
  border-block-start: var(--border-width) solid var(--border-subtle);
}

.site-footer__copyright {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-xs);
}

@media (max-width: 47.999em) {
  .site-footer__bottom {
    flex-direction: column;
    text-align: center;
  }
}

/* --------------------------------------------------------------------------
   The circuit around the page title

   Six layers: a band across the top and the bottom, and a cluster emerging
   from each of the four corners. The drawing is the company's own artwork,
   extracted from it by tools/build_hero_circuit.py rather than interpreted,
   and the proportions it is placed at are measured off the same file.

   One set of geometry, declared once in the first layer and mirrored here —
   the top-right corner is the top-left one flipped, the bottom band is the top
   band turned over. What keeps that from reading as a mirror is the timing:
   every corner charge runs on its own number of seconds, so no two layers ever
   line up.

   Motion here is suppressed by the prefers-reduced-motion block in base.css,
   which leaves the traces as a still drawing. That works only because every
   animation below rests on its declared value: the charges end at
   stroke-dashoffset 0, which is also their base, and the nodes revert to the
   opacity declared on the rule. An animation that ended anywhere else would
   collapse to something nobody designed.
   -------------------------------------------------------------------------- */

.page-hero {
  position: relative;
  /* The layers are inset to the edges of this box; without this they would
     resolve against the page. */
  isolation: isolate;
  overflow: hidden;
}

.hero-circuit {
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Read by circuit.js, which paints the charges. Declared here so the colour
     lives with the rest of the drawing's and follows the theme by itself. */
  --charge-ink: var(--accent-text);

  /* EVERY GAP IS THE SAME GAP, AND A GRID IS WHAT MAKES THAT TRUE
     There are four channels in this composition: cluster to band on the left,
     band to cluster on the right, and the two vertical ones -- between the top
     and bottom bands, and between the top and bottom clusters down each edge.
     They were four different numbers arrived at four different ways, and the
     artwork's own two horizontal ones are not even equal to each other (5.60%
     against 4.80%). A person asked for one consistent channel and could not
     find it anywhere.

     Six absolutely positioned layers cannot express that: the horizontal gap
     came from a percentage of the WIDTH and the vertical from the band's own
     height, so the two only ever coincided by accident at one aspect ratio. A
     grid states it once. The rows are 1fr, so band height and cluster height
     are both (banner - gap) / 2 without arithmetic, and gap is the gap in both
     directions, equal BY CONSTRUCTION rather than by four numbers that have to
     be kept in step by hand.

     IT MUST BE A LENGTH AND NEVER A PERCENTAGE.
     row-gap resolves a percentage against the container's HEIGHT and column-gap
     against its WIDTH, so a percentage gap is unequal in pixels by definition
     -- which is the exact fault this exists to prevent. One token, so
     "adjustable" is one number.

     The band's column carries a floor. A near-square banner -- a phone with a
     title on three lines -- cannot hold a 2.95:1 composition with four equal
     channels; measured at 360x280 the band is left 65px. Something has to give,
     and it is the clusters rather than the band: where that floor binds they
     are squeezed and the vertical channel opens past the gap. That is a real
     limit of asking for four equal gaps in a box this shape, and it is measured
     rather than hidden. */
  --hc-gap: clamp(2rem, 8vw, 8rem);
  display: grid;
  /* THE BAND'S COLUMN TAKES WHAT IS LEFT, AND IT IS ALLOWED TO BE NARROW
     It carried a floor, so that a near-square banner could not squeeze it away.
     But a floor there is paid for by the CLUSTERS: the column steals their
     width, their height stays the row's, and "meet" then letterboxes the
     drawing inside a box of the wrong ratio -- which opens the vertical channel
     past the gap and breaks the one promise this layout exists to keep.
     Measured at 390x300 the vertical channel went to 100px against a 32px gap.

     So there is no floor. On the narrowest phones the band is a narrow window
     onto the artwork rather than a wide one -- at true scale, undistorted, just
     less of it -- and every channel stays equal. That is the right way round:
     the gaps were what was asked for. */
  grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
  grid-template-rows: 1fr 1fr;
  gap: var(--hc-gap);

  /* Inherited, so it covers every shape inside. Without it the artwork sits in
     front of the title for hit-testing and check_focus.py reports the focus
     ring as covered. */
  pointer-events: none;
}

.page-hero__inner {
  position: relative;
}

.hero-circuit__layer {
  display: block;
  /* A grid item's automatic minimum size is its content, which for an <svg>
     with a viewBox is that viewBox. Without these the band would refuse to be
     narrower than 21600px and blow the page sideways. */
  min-width: 0;
  min-height: 0;
  width: 100%;
  height: 100%;
}

/* THE CIRCUITS BRANCH OUT FROM THEIR OWN EDGE, ONCE
   Every trace in a corner starts on one of the two edges that meet there, and
   every trace in a band starts at the outer edge. So a clip expanding from that
   same origin uncovers each trace from its root outward, which is the growth
   the artwork is meant to have — and it costs six animated elements rather than
   the two hundred it would take to draw every trace on individually.

   The resting value is the fully revealed one, declared on the rule. That is
   load-bearing: base.css sets no animation-fill-mode, so under reduced motion
   the collapsed animation reverts to whatever is declared here. Had the rule
   said "hidden" and the keyframes said "revealed", asking for less motion would
   have emptied the band. */
.hero-circuit__layer--band-top,
.hero-circuit__layer--band-bottom {
  clip-path: inset(0 0 0 0);
  animation: hero-emerge-band 1.5s cubic-bezier(0.22, 0.7, 0.3, 1) both;
}

.hero-circuit__layer--corner-tl,
.hero-circuit__layer--corner-tr,
.hero-circuit__layer--corner-bl,
.hero-circuit__layer--corner-br {
  clip-path: circle(160% at 0 0);
  /* No stagger between the four: they are asked to emerge together. */
  animation: hero-emerge-corner 1.5s cubic-bezier(0.22, 0.7, 0.3, 1) both;
}

/* Each layer is clipped in its own coordinate space, which the mirrors carry
   round with them — so "from the top edge" and "from this corner" stay true for
   all six without a rule each. */
@keyframes hero-emerge-band {
  from {
    clip-path: inset(0 0 100% 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes hero-emerge-corner {
  from {
    clip-path: circle(0% at 0 0);
  }
  to {
    clip-path: circle(160% at 0 0);
  }
}

/* --- the bands, top and bottom ------------------------------------------- */

/* THE BAND IS THE MIDDLE COLUMN, AND THE GRID PUTS THE GAP EITHER SIDE OF IT
   This used to carry the composition itself: an inset derived from --hc-corner
   plus the artwork's 5.9%, and a width that was the arithmetic on it, because
   an absolutely positioned <svg> is a replaced element and takes its width from
   its own intrinsic size unless told otherwise -- set loose it measured 1,298px
   inside a 1,905px banner. All of that is now the grid's middle column, and the
   two channels are the grid's own column gap.

   The height goes with it. It was clamp(3.5rem, 31%, 6.5rem), tracking the
   artwork's 30.8% of the banner; the row is 1fr, so it is (banner - gap) / 2
   and equal to the cluster's height, which is what makes the vertical channels
   the same as the horizontal ones. */
.hero-circuit__layer--band-top,
.hero-circuit__layer--band-bottom {
  grid-column: 2;
  /* THE HEIGHT IS THE ROW'S, AND NOTHING ELSE'S.
     The base rule gives every layer height: 100% -- and a grid item's
     percentage resolves against its grid AREA in Blink but the whole grid
     CONTAINER in WebKit. Measured on an iPhone over LAN: the band's box
     came back 156x239 in a 414x239 hero, the row being 103 -- so slice
     magnified a 75-unit sliver of the center tile 2.1x and the bands
     rendered as sparse verticals with dots where Android renders the
     dense run. height: auto is NOT the answer either: an <svg> is a
     replaced element, so auto height falls back to width x intrinsic
     ratio and the band collapses to a 1px rule on every engine
     (test_motion caught it at 1-3px against a 103px cluster). The row is
     (banner - gap) / 2 and the container's height is definite, so -- like
     the clusters above -- the bands state it outright in cqh, inside the
     same gate. The width stays 100% of the middle column, an axis whose
     percentages no engine disputes. */
  /* (height set in the @supports block below, beside the clusters) */
  /* THE FADE IS NEARLY GONE, AND THE CHANNEL IS WHY
     It used to begin at 72%, to keep the title clear of a band that ran close
     to it. The grid's row gap does that now -- the title sits in a channel the
     same width as every other one -- so a long fade only does harm: it stops
     the band's ink short of its own box, which made the vertical channel
     measure 57px against 40px for the others at 500px. The artwork has no fade
     at all; its traces simply end at different depths. This keeps a hair of
     softening so the edge is not a rule across the banner, and no more. */
  -webkit-mask-image: linear-gradient(to bottom, #000 94%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 94%, transparent 100%);
}

.hero-circuit__layer--band-top {
  grid-row: 1;
}

/* The same drawing turned over, so the traces run out of the bottom edge
   rather than into it. The mask turns over with it. */
.hero-circuit__layer--band-bottom {
  grid-row: 2;
  transform: scaleY(-1);
}

/* --- the four corner clusters -------------------------------------------- */

.hero-circuit__layer--corner-tl,
.hero-circuit__layer--corner-tr,
.hero-circuit__layer--corner-bl,
.hero-circuit__layer--corner-br {
  /* THE HEIGHT COMES FROM THE ROW AND THE WIDTH FOLLOWS FROM THE RATIO
     This was width: var(--hc-corner), a share of the banner's WIDTH. That is
     what made the vertical channel down each edge a different number from the
     horizontal one either side of the band -- the two were measured against
     different axes and only agreed at one aspect ratio. Now the row is 1fr, so
     a cluster is exactly as tall as a band, and its width is whatever that
     height and this ratio give. The grid's auto column takes that width, and
     the gap beside it is the same gap as everywhere else.

     The ratio is the cluster's own: a 45 degree fan, very nearly square. It has
     to be stated, because a box of some other ratio would letterbox the drawing
     under "meet" and the slack would read as a wider channel on one side --
     which is the fault being fixed, arriving by a different route. */
  aspect-ratio: 200 / 215;
  width: auto;
  /* Fades on a diagonal away from its own corner. The gradient is anchored at
     0% 0%, which each mirror carries round to the corner it belongs to. */
  -webkit-mask-image: radial-gradient(130% 130% at 0% 0%, #000 40%, transparent 96%);
  mask-image: radial-gradient(130% 130% at 0% 0%, #000 40%, transparent 96%);
}

/* AND THE ROW IS NOT KNOWN WHEN THE COLUMN IS MEASURED
   A grid sizes its COLUMNS before its rows. The two outer columns are auto, so
   the grid must ask a cluster how wide it wants to be -- and the only answer
   above is "as wide as my row is tall, times my ratio", which is a row that
   has not been sized yet. Firefox and Chrome come back to it and get 96.9px on
   a 390px phone, to within a tenth of a pixel of the row height times 200/215.
   Nothing in the specification promises that.

   Safari does not. Reported from an iPhone: the clusters came out about twice
   their intended width, reaching into the middle of the banner and clipped by
   the hero's own overflow, and the 1fr band column between them was squeezed
   to nothing -- so the banner showed four oversized corner fans and no band at
   all. Two browser harnesses in this repository, both of them the ones that
   guess the way the design wants.

   So the guess is removed. The container's height is definite -- this box is
   inset: 0 against the banner -- and container-type: size makes it readable as
   a length, so a cluster states its own width and height outright and the auto
   column has a real number to take. Nothing is derived from anything that is
   still being decided.

   IT IS DECLARED ON THE CHILDREN AND NOT HERE. A container query unit used in
   the container's own declarations resolves against ITS container, one level
   further out -- so --hc-gap can live on .hero-circuit and this arithmetic
   cannot.

   Gated on @supports, and placed BEFORE the wide-screen block below so that
   block still wins above 1280px, where the cluster is sized by the viewport
   and was never circular. A browser without container queries gets exactly
   what it got before. */
@supports (container-type: size) {
  .hero-circuit {
    container-type: size;
  }

  .hero-circuit__layer--corner-tl,
  .hero-circuit__layer--corner-tr,
  .hero-circuit__layer--corner-bl,
  .hero-circuit__layer--corner-br {
    height: calc((100cqh - var(--hc-gap)) / 2);
    width: calc((100cqh - var(--hc-gap)) / 2 * 200 / 215);
  }

  /* THE BANDS STATE THE SAME ROW HEIGHT, FOR THE SAME WEBKIT REASON.
     Corners prove cqh resolves on WebKit (95.8x102.9 measured on an
     iPhone, exact to the tenth); the bands' height: 100% is what does
     not, resolving against the container instead of the row. So they get
     the same outright number -- which on Blink and Gecko equals the row
     to the pixel, keeping hero_gaps() identical, and on WebKit replaces
     a 239px box with the 103px row. Pre-cqh browsers keep height: 100%,
     exactly what they had before this gate existed. */
  .hero-circuit__layer--band-top,
  .hero-circuit__layer--band-bottom {
    height: calc((100cqh - var(--hc-gap)) / 2);
  }
}

/* THE CLUSTERS GROW ON A LARGE SCREEN, AND D IS WHAT PAYS FOR IT
   Sizing a cluster by the row is what makes the vertical channels equal the
   horizontal ones, and on a wide banner it also makes the cluster small: its
   share of the WIDTH follows the banner's shape rather than the artwork's, so
   it falls from 11.4% at 1024px -- which is the artwork's own proportion -- to
   7.0% at 1440 and 2.5% at 3840. The band ends up carrying the composition
   alone and the corners read as specks.

   So above 1280px the cluster is sized by the VIEWPORT instead, and is allowed
   to reach past its row. Three of the four channels are untouched: A and B are
   still the grid's column gap, and C is still the gap between the two bands,
   because the bands still fill their rows. Only D -- the channel down each
   edge, between the top cluster and the bottom one -- closes up. That is a
   deliberate trade and it is the artwork's own shape: there the clusters very
   nearly meet down the sides.

   1280px and not 1024px because at 1024 the row-sized cluster is already 11.4%
   of the width. Starting lower would make it SMALLER, which is the opposite of
   the point.

   THE CEILING IS THE BANNER'S HEIGHT, NOT TASTE.
   A cluster is very nearly square and the banner is about 332px tall at every
   desktop width, so two of them stacked down one edge cannot exceed that: above
   about 154px they meet in the middle. The 9rem cap leaves D at 22px. */
@media (min-width: 80rem) {
  .hero-circuit__layer--corner-tl,
  .hero-circuit__layer--corner-tr,
  .hero-circuit__layer--corner-bl,
  .hero-circuit__layer--corner-br {
    width: clamp(7rem, 9vw, 9rem);
    /* WITHOUT THIS THE CLUSTER GETS WIDER AND NOT TALLER
       .hero-circuit__layer sets height: 100%, which pins a grid item to its
       row and overrides the height aspect-ratio would otherwise derive. The box
       then grows sideways while the drawing inside it, fitted by "meet", stays
       the height of the row -- so the cluster is UNCHANGED and gains a strip of
       empty box on its inner edge. Measured: the horizontal channel read 145px
       against a 115px gap, which is the slack, and the vertical one never
       moved. */
    height: auto;
  }

  /* Sized by the viewport now rather than stretched to the row, so each has to
     be told which edge of its row to hug -- otherwise they would centre in it
     and drift away from the corner they grow out of. */
  .hero-circuit__layer--corner-tl,
  .hero-circuit__layer--corner-tr {
    align-self: start;
  }

  .hero-circuit__layer--corner-bl,
  .hero-circuit__layer--corner-br {
    align-self: end;
  }
}

/* The mirrors are unchanged: each cluster is turned to face its own corner, and
   that carries the masks and the emerge clip round with it. */
.hero-circuit__layer--corner-tl { grid-area: 1 / 1; }
.hero-circuit__layer--corner-tr { grid-area: 1 / 3; transform: scaleX(-1); }
.hero-circuit__layer--corner-bl { grid-area: 2 / 1; transform: scaleY(-1); }
.hero-circuit__layer--corner-br { grid-area: 2 / 3; transform: scale(-1, -1); }


/* --- the three static layers --------------------------------------------- */

.hero-circuit__wires {
  fill: none;
  stroke: var(--silver-accent-mid);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.3;
}

/* Pads and the small rectangles a board carries where a component lands. */
.hero-circuit__pads {
  fill: var(--silver-accent-mid);
  opacity: 0.34;
}

/* The vias: the pads a board leaves hollow. */
.hero-circuit__rings {
  fill: none;
  stroke: var(--silver-accent-mid);
  stroke-width: 1.5;
  stroke-linecap: round;
  opacity: 0.3;
}

/* A cluster and a band are drawn at different scales -- a cluster's viewBox is
   200 units across and a band's tile is 1,440 -- so one pen cannot serve both.
   Both are sized by the banner's HEIGHT now, so each scale varies only with the
   banner's shape rather than with the viewport's width: a band draws at 0.81 to
   1.25 and a cluster at 0.43 to 0.66. 1.6 and 2.5 hold the two between one and
   two device pixels across all of that, which is why the three tiers of pens
   that used to follow this are gone. */
.hero-circuit__layer--corner-tl .hero-circuit__wires,
.hero-circuit__layer--corner-tr .hero-circuit__wires,
.hero-circuit__layer--corner-bl .hero-circuit__wires,
.hero-circuit__layer--corner-br .hero-circuit__wires {
  stroke-width: 2.5;
  opacity: 0.38;
}

.hero-circuit__layer--corner-tl .hero-circuit__pads,
.hero-circuit__layer--corner-tr .hero-circuit__pads,
.hero-circuit__layer--corner-bl .hero-circuit__pads,
.hero-circuit__layer--corner-br .hero-circuit__pads {
  opacity: 0.44;
}

.hero-circuit__layer--corner-tl .hero-circuit__rings,
.hero-circuit__layer--corner-tr .hero-circuit__rings,
.hero-circuit__layer--corner-bl .hero-circuit__rings,
.hero-circuit__layer--corner-br .hero-circuit__rings {
  stroke-width: 2.2;
  opacity: 0.36;
}

/* --- the charge ---------------------------------------------------------- */

/* Every trace carries pathLength="100", so one dash length works for a 150-unit
   corner elbow and a 380-unit band bus alike: 22 units lit against 78 dark is
   one charge crossing, then a rest before the next. 100 is exactly one period,
   so the loop closes on itself with no visible jump.

   The class is on ONE <use> of ONE trace, never on a group of them. Animating
   an inherited property on a group makes the browser push the value down
   through every <use> shadow tree beneath it every frame, which is the fault
   this band is shaped around; tools/test_motion.py asserts the shape because
   the cost is not visible from a browser test. These 24 are the fallback --
   with scripting, circuit.js paints all 176 on a canvas instead. */
.hero-circuit__charge {
  fill: none;
  stroke: var(--accent-text);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 22 78;
  animation-name: hero-charge;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes hero-charge {
  from {
    stroke-dashoffset: 100;
  }
  to {
    stroke-dashoffset: 0;
  }
}

/* The band's charge used to ask 3.6 against the base 3, because
   preserveAspectRatio="none" squashed its box and a horizontal run came out at
   roughly six tenths of its stated width -- so the two halves of the drawing
   needed different pens to weigh the same on screen. The band is scaled
   uniformly now, exactly as the clusters always were, so one pen is right for
   both and the override is gone. */

/* Three durations, shared by all four clusters — and that sharing is measured,
   not lazy. Giving the twelve corner charges twelve distinct durations gives
   them twelve distinct computed styles, which Chrome can then share between no
   two elements: 55ms of style recalculation per second against 35ms for the
   same twenty-four charges on three. The clusters are mirror images anyway, so
   a shared phase reads as the board lighting symmetrically.

   They are the slow half of the drawing: the bands run their whole circuit in
   4s, three to five times faster, which is the difference in pace the artwork
   is meant to have — the bands are the current, the corners are the board it
   runs on.

   Adding a charge costs about 1.1ms of style recalculation per second, every
   second the page is open. Run tools/check_style_budget.py, and read the table
   in docs/10-development/frontend/motion.md, before raising the count. */
.hero-circuit__charge--c1 { animation-duration: 12s; }
.hero-circuit__charge--c2 { animation-duration: 13s; }
.hero-circuit__charge--c3 { animation-duration: 14s; }

/* Consecutive traces are dealt round-robin into the six groups, so no two
   neighbours share one. Reversing the odd groups is therefore what makes
   adjacent lines in a cluster run against each other. */
.hero-circuit__charge--back {
  animation-direction: reverse;
}

/* THE TWO BANDS ARE A MATCHED PAIR, AND THAT IS WHY THEY SHARE A DURATION
   Everywhere else here a shared duration is the fault being avoided. Along the
   top and bottom edges it is the requirement: the flow runs left to right
   across the top and right to left across the bottom, at one speed, so the two
   read as a single current going round the band rather than as two unrelated
   animations that happen to be nearby. One duration is what makes the speeds
   equal; the delays only stagger where each charge sits on its circuit. */
.hero-circuit__charge--band { animation-duration: 4s; }

.hero-circuit__charge--p1 { animation-delay: 0s; }
.hero-circuit__charge--p2 { animation-delay: -1s; }
.hero-circuit__charge--p3 { animation-delay: -2s; }
.hero-circuit__charge--p4 { animation-delay: -3s; }

/* The right half of each band is the left half mirrored, so a charge running
   forward along its path travels right to left there. Reversing it puts both
   halves back in step with each other — and because the delays are shared, the
   two halves reach the centre line together and meet there. */
.hero-circuit__charge--mirrored {
  animation-direction: reverse;
}

/* And the bottom band is the top one turned over, so the whole flow inverts:
   what ran left to right along the top runs right to left along the bottom. */
.hero-circuit__layer--band-bottom .hero-circuit__charge {
  animation-direction: reverse;
}

.hero-circuit__layer--band-bottom .hero-circuit__charge--mirrored {
  animation-direction: normal;
}

/* --- the charges, once a canvas has taken them over ----------------------- */

/* THE SVG CHARGES ARE THE FALLBACK, AND THEY ARE SWITCHED OFF LAST
   circuit.js adds this class only after its canvas is measured and drawing.
   Until then — and forever, without JavaScript — the CSS charges below run,
   so the band is never left with no charge at all. Once the canvas is up they
   are display:none, which cancels their animations outright: that is the
   point, because a charge on every one of 176 traces is affordable on a
   canvas and was not affordable as 176 style recalculations a frame.
   docs/10-development/frontend/motion.md */
.hero-circuit--canvas .hero-circuit__charges,
.hero-circuit--canvas .hero-circuit__nodes {
  display: none;
}

.hero-circuit__charge-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* Measured NEUTRAL, and kept anyway: 37ms of style recalculation per second
     against 40ms without, which is inside the noise. It is here because
     telling the browser that a leaf which repaints thirty times a second
     cannot affect anything outside itself is true and costs nothing, not
     because it was shown to buy anything.

     An earlier note here claimed it halved the cost. That was a measurement
     run without warm-up, where the uncontained case went first and paid for a
     cold cache. It did not survive being measured properly. */
  contain: layout paint style;
}

/* --- the junctions ------------------------------------------------------- */

/* opacity is declared here, not only in the keyframes: under reduced motion the
   animation is collapsed and the element reverts to this value, so this is the
   still drawing's brightness. */
.hero-circuit__node {
  fill: var(--silver-accent-mid);
  opacity: 0.3;
  transform-box: fill-box;
  transform-origin: center;
  animation-name: hero-pulse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

@keyframes hero-pulse {
  from {
    opacity: 0.16;
    scale: 0.85;
  }
  to {
    opacity: 0.5;
    scale: 1.15;
  }
}

.hero-circuit__node--a { animation-duration: 7s; }
.hero-circuit__node--b { animation-duration: 11s; }
.hero-circuit__node--c { animation-duration: 13s; }
.hero-circuit__node--d { animation-duration: 17s; }
.hero-circuit__node--e { animation-duration: 19s; }
.hero-circuit__node--f { animation-duration: 23s; }
.hero-circuit__node--g { animation-duration: 29s; }
.hero-circuit__node--h { animation-duration: 31s; }
.hero-circuit__node--i { animation-duration: 37s; }
.hero-circuit__node--j { animation-duration: 41s; }
.hero-circuit__node--k { animation-duration: 43s; }
.hero-circuit__node--l { animation-duration: 47s; }
.hero-circuit__node--m { animation-duration: 53s; }
.hero-circuit__node--n { animation-duration: 59s; }
.hero-circuit__node--o { animation-duration: 61s; }
.hero-circuit__node--p { animation-duration: 67s; }
.hero-circuit__node--q { animation-duration: 71s; }
.hero-circuit__node--r { animation-duration: 73s; }
.hero-circuit__node--s { animation-duration: 79s; }
.hero-circuit__node--t { animation-duration: 83s; }
.hero-circuit__node--u { animation-duration: 89s; }
.hero-circuit__node--v { animation-duration: 97s; }
.hero-circuit__node--w { animation-duration: 101s; }
.hero-circuit__node--x { animation-duration: 103s; }

/* --- the pen, against the scale it is drawn at --------------------------- */

/* An SVG stroke is scaled with its drawing, and there used to be a ladder of
   pens here because both drawings changed scale with the VIEWPORT'S WIDTH: the
   band ran from 0.85 at 1920px down to 0.33 at 768px, a cluster from 1.14 to
   0.46, and a line under a device pixel does not disappear -- it turns to haze.
   Three tiers of rules widened the pen as the drawing shrank.

   THE LADDER IS GONE, BECAUSE THE THING IT TRACKED IS GONE.
   Both are sized by the BANNER'S HEIGHT now: the grid's rows are 1fr, so a band
   and a cluster are each (banner - gap) / 2 tall, and the band's tiling means
   its width no longer touches its scale at all. Measured across every shape the
   banner takes, from a 360x240 phone to a 3840x332 desktop:

       band    0.81 .. 1.25      cluster   0.43 .. 0.66

   A range of about one and a half, where it used to be three and a half, and no
   longer correlated with viewport width -- so tiers keyed to viewport width
   were measuring the wrong variable and would have been actively wrong. One pen
   each covers it:

       band wires 1.6  ->  1.30 .. 2.00px        cluster wires 2.5  ->  1.08 .. 1.65px

   Both inside the one-to-two-device-pixel rule everywhere. The numbers are that
   arithmetic, not taste, and tools/test_motion.py asserts the rendered result
   rather than the arithmetic. */


