/* ============================================================================
   LATTICE LAYER — Ed Chen homepage
   ----------------------------------------------------------------------------
   Loaded last, after tokens.min.css and main.min.css, and scoped to the
   `ed-` prefix plus a small number of deliberate overrides. Nothing here
   restyles a component; it only opens the page up enough for the WebGL field
   to be visible through it, and adds the rail.

   THE CONTRAST CONTRACT
   Body copy on this site is rgba(255,255,255,.72). The field sits BEHIND
   section plates held at 0.80–0.84 alpha, and those alphas were not chosen by
   eye. Each one was set by rendering the page twice at the same scroll
   position — field on, field off — sampling the composited background inside
   the bounding box of every real paragraph, and comparing the two.

   Measured at 1440x900, worst case per section:
     works 10.1:1 · systems 10.2:1 · compliance 6.2:1 · leadership 9.8:1
     stance 18.9:1 · contact 10.2:1        (WCAG AA for body copy is 4.5:1)
   Contrast the field costs a paragraph: 0.00–0.13 in six of seven sections,
   1.09 worst case. In other words the field is very nearly free, and that is
   the whole point of the plates.

   The plates were opened up a second time after a coverage audit showed why
   the field was invisible below the fold: the section plate is not the only
   thing in the stack. Component cards sit on top of it at 0.88 and cover
   14-28% of the viewport, so under a card the field was reaching ~2% and in
   the open ~9%. Because the dense copy lives INSIDE those cards, the section
   plate could drop a long way without touching it. It now backs section
   headings and ledes only — larger type, far more headroom.
   ========================================================================== */

/* ── the field ──────────────────────────────────────────────────────────── */

/* The page's own void colour moves off <body> and onto a fixed plate, so the
   canvas has somewhere to live between the background and the content. */
html.has-lattice body { background: transparent; }

.ed-void,
#ed-gl,
.ed-veil {
  position: fixed;
  inset: 0;
  pointer-events: none;
  /* Hidden until the field is confirmed running. This is a failure mode, not
     a nicety: the veil is a deliberately lopsided gradient, and if the boot
     module 404s the class below is never added, standDown() never runs, and
     the page would ship a permanent dark wash down its left third with no
     canvas behind it to justify it. Verified by aborting the request for
     lattice-boot.js — before this rule the hero rendered visibly broken. */
  visibility: hidden;
}

html.has-lattice .ed-void,
html.has-lattice #ed-gl,
html.has-lattice .ed-veil { visibility: visible; }

.ed-void {
  z-index: -3;
  background: var(--bg-void, #060810);
}

#ed-gl {
  z-index: -2;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;                       /* choreography owns this from frame one */
  transition: opacity 620ms cubic-bezier(.25, .1, .25, 1);
}

/* Reading protection, weighted to the composition rather than to the centre.
   The homepage hero sets its type in a left column (.ix-hero) with the two
   live-system tiles to the right, so the dark is pulled left onto the name
   and subtitle and the right side is left open for the structure to show
   through the tiles. The horizontal pass guarantees the type column keeps its
   ground no matter which layout state the field happens to be holding.

   The vertical pass holds a shoulder across the top 17-32%, where the 10px
   eyebrow sits, and ramps up hard below 64%. Neither is decoration: the
   hero's four-cell signal ledger sits across the bottom of the viewport in
   9px and 13px type, and once the structure was scaled up to fill the frame
   it was measured at 4.0:1 there on a 1920 screen — under AA. Darkening the
   foot of the field fixed it without shrinking the structure, and reads as
   the lattice sinking into the page rather than being cropped by it. */
.ed-veil {
  z-index: -1;
  background:
    /* Weighted to the TYPE COLUMN, not to the viewport. The hero's eyebrow,
       name, role and subtitle all sit inside the left 53%; the structure's
       mass is to the right of it. So this holds hard to 52% and then releases
       fast — which is what lets the field run at full strength in the hero
       without the 10px eyebrow dropping to 3.1:1, measured. */
    linear-gradient(90deg,
      rgba(6, 8, 16, .88) 0%,
      rgba(6, 8, 16, .78) 30%,
      rgba(6, 8, 16, .52) 52%,
      rgba(6, 8, 16, .12) 72%,
      rgba(6, 8, 16, 0) 100%),
    radial-gradient(ellipse 42% 44% at 26% 44%,
      rgba(6, 8, 16, .66) 0%,
      rgba(6, 8, 16, .28) 58%,
      rgba(6, 8, 16, 0) 100%),
    linear-gradient(180deg,
      rgba(6, 8, 16, .52) 0%,
      rgba(6, 8, 16, .22) 17%,
      rgba(6, 8, 16, .10) 32%,
      rgba(6, 8, 16, .09) 64%,
      rgba(6, 8, 16, .50) 86%,
      rgba(6, 8, 16, .84) 100%);
}

/* ── letting the field through the document ─────────────────────────────────
   Every band that was fully opaque becomes near-opaque. See the contrast
   contract above for how these two numbers were arrived at. */
html.has-lattice .section--deep { background: rgba(12, 15, 26, .44); }

html.has-lattice .section--surface { background: rgba(19, 23, 32, .52); }

html.has-lattice .works-preview { background: rgba(12, 15, 26, .56); }

html.has-lattice .rec-band {
  background:
    linear-gradient(180deg, var(--accent-06), transparent 80%),
    rgba(12, 15, 26, .66);
}

/* Component cards are the second plate in the stack, and they are the ones
   carrying the dense copy — so they stay near-solid and only open by a hair.
   The measured effect of the pair is what matters: a paragraph inside a card
   sits on 0.56 x 0.86 ≈ 0.94 combined opacity, while the gaps BETWEEN cards
   sit on 0.56 alone. That difference is the whole trick — the structure reads
   through the negative space of the layout instead of through the text.

   Several of these are declared with !important in the page's own critical
   CSS, so the overrides have to match. The list came from walking all 25
   viewports of the document and collecting every background at >=0.5 alpha
   covering >=3% of a screen — not from reading the markup, which is how
   .compliance-system was missed on the first pass.

   .ent-video-card is deliberately NOT here. It is fully opaque because it is
   media, not a plate; a video thumbnail with the lattice showing through it
   would read as a broken image. */
html.has-lattice .jurisdiction,
html.has-lattice .compliance-system,
html.has-lattice .evidence,
html.has-lattice .dl-card,
html.has-lattice .research-synthesis,
html.has-lattice .bx-card,
html.has-lattice .ds-linkcard,
html.has-lattice body .subproject,
html.has-lattice body .rec-stat,
html.has-lattice body .ix-term,
html.has-lattice body .comp-tradex { background: rgba(10, 13, 24, .86) !important; }

/* ── hero type column ───────────────────────────────────────────────────────
   The eyebrow, role and subtitle are full-width block elements whose text only
   fills the left half, and they sit exactly where the structure's left edge
   crosses. Raising the field's intensity dropped the 10px gold eyebrow to
   3.1:1 — under AA — while the same field was still barely visible three
   sections further down, which is the wrong trade in both directions.

   A left-weighted ground under each line fixes the type without touching the
   veil, so the right half of the hero — where the structure actually is —
   stays as open as it now looks. The gradient dies by 72%, well before the
   sphere's mass, so nothing reads as a bar. */
html.has-lattice .ix-hero__eyebrow,
html.has-lattice .ix-hero__role,
html.has-lattice .ix-hero__subtitle { position: relative; }

html.has-lattice .ix-hero__eyebrow::before,
html.has-lattice .ix-hero__role::before,
html.has-lattice .ix-hero__subtitle::before {
  content: '';
  position: absolute;
  inset: -14px -20px -14px -30px;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(90deg,
    rgba(6, 8, 16, .93) 0%,
    rgba(6, 8, 16, .90) 42%,
    rgba(6, 8, 16, .66) 58%,
    rgba(6, 8, 16, .16) 68%,
    rgba(6, 8, 16, 0) 76%);
}

/* The hero's signal ledger is the one place the enlarged structure had to be
   answered locally rather than globally. Those four cells are transparent by
   design and set their labels in 9px gold at rgba(201,169,89,.75) — 5.3:1
   before any field exists, only 0.8 above AA. Measured against the scaled-up
   opening state it lost 0.90 of that and fell to 4.4:1 at 1453x825.
   A local gradient ground under the row returns it without dimming the
   structure anywhere else, which is the right trade: the fix belongs to the
   200px band that has the problem, not to the whole composition.

   It was strengthened again when the global veil was lightened on request.
   Lightening the veil alone dropped this row to 2.0:1 at 1280px — the veil
   was silently doing the ledger's protection for it. Moving that load here
   is what lets the rest of the field be as clear as it now is. */
html.has-lattice .ix-sig { position: relative; }

/* The ground alone was not enough — it bought 0.2. The binding constraint is
   the label colour itself, which starts at 5.3:1 with no field at all. Under
   the field it gets a little more weight. This is the only place this layer
   changes an existing colour, and it is a change of 0.20 alpha on a 9px
   label, scoped to pages where the field actually exists. */
html.has-lattice .ix-sig__k { color: rgba(201, 169, 89, .95); }

html.has-lattice .ix-sig::before {
  content: '';
  position: absolute;
  inset: -48px -34px -52px;
  z-index: -1;
  pointer-events: none;
  /* Starts fully transparent well above the hairline so there is no visible
     edge, and is already dense by the time it reaches the 9px labels. */
  /* Near-opaque by the time it reaches the labels, and that is the point.
     This row asks 9px GOLD type to sit on the brightest part of a GOLD field —
     gold on gold is not winnable with alpha. Measured at 1280x800 the labels
     land 39% down this gradient; at .82 there they read 3.7:1, under AA. The
     band is the bottom ~180px of an 800px hero, so closing it costs almost
     nothing visually — the structure still fills the 620px above it — and it
     buys the whole rest of the field the freedom to be as light as it is. */
  background: linear-gradient(180deg,
    rgba(6, 8, 16, 0) 0%,
    rgba(6, 8, 16, .58) 13%,
    rgba(6, 8, 16, .95) 27%,
    rgba(6, 8, 16, .99) 52%,
    rgba(6, 8, 16, 1) 100%);
}

/* The hero and the close are the two places the field is allowed to be the
   loudest thing on screen, so neither gets a plate at all. */
html.has-lattice .ix-hero,
html.has-lattice .contact { background: transparent; }

html.has-lattice .footer { background: rgba(6, 8, 16, .90); }

/* Content has to sit above the fixed layers without changing any stacking
   the components already rely on internally.

   The nav is deliberately NOT in this list. It ships as position:fixed with a
   high z-index, and including it here overrode that to position:relative —
   which silently un-stuck the header AND dropped it below <main> in paint
   order, making the language dropdown physically unclickable (elementFromPoint
   over the JA option returned the hero section). The fixed plates live at
   z-index -3/-2/-1, so a positioned nav is already above them and needs
   nothing from this rule. */
html.has-lattice main,
html.has-lattice .footer { position: relative; z-index: 1; }

/* ── scroll rail ────────────────────────────────────────────────────────── */
.ed-rail {
  position: fixed;
  top: 0;
  right: 0;
  width: 2px;
  height: 100vh;
  z-index: 90;
  background: rgba(255, 255, 255, .06);
  pointer-events: none;
}

.ed-rail__fill {
  width: 100%;
  height: 0;
  background: linear-gradient(180deg,
    rgba(201, 169, 89, .25),
    var(--accent, #c9a959));
}

/* ── section index ──────────────────────────────────────────────────────── */
.ed-sidx {
  position: fixed;
  top: 50%;
  right: 22px;
  transform: translateY(-50%);
  z-index: 91;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ed-sidx button {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 5px 0;
  background: none;
  border: 0;
  cursor: pointer;
  font-family: var(--font-body, Inter, sans-serif);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: .16em;
  color: rgba(255, 255, 255, .28);
  transition: color 260ms var(--ease, cubic-bezier(.25, .1, .25, 1));
}

.ed-sidx button i {
  display: block;
  width: 14px;
  height: 1px;
  background: currentColor;
  transition: width 300ms var(--ease, cubic-bezier(.25, .1, .25, 1));
}

/* The written label is the affordance, so it is revealed on intent — hover or
   keyboard focus — rather than sitting there as permanent chrome. */
.ed-sidx__l {
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  opacity: 0;
  text-transform: uppercase;
  transition:
    max-width 320ms var(--ease, cubic-bezier(.25, .1, .25, 1)),
    opacity 240ms var(--ease, cubic-bezier(.25, .1, .25, 1));
}

.ed-sidx button:hover,
.ed-sidx button:focus-visible { color: var(--accent, #c9a959); }

.ed-sidx button:hover .ed-sidx__l,
.ed-sidx button:focus-visible .ed-sidx__l { max-width: 110px; opacity: 1; }

.ed-sidx button:focus-visible {
  outline: 1px solid var(--accent-40, rgba(201, 169, 89, .4));
  outline-offset: 4px;
}

.ed-sidx button.is-on { color: rgba(255, 255, 255, .82); }
.ed-sidx button.is-on i { width: 26px; background: var(--accent, #c9a959); }

/* ── recruiter mode ─────────────────────────────────────────────────────────
   A recruiter is on this page to answer one question in ninety seconds. The
   field holds its opening state, drops to a whisper, and the rail — which
   exists for people who traverse a long document — gets out of the way. */
html.has-lattice body.mode-recruiter .ed-veil {
  background:
    linear-gradient(90deg, rgba(6, 8, 16, .90) 0%, rgba(6, 8, 16, .60) 60%,
      rgba(6, 8, 16, .40) 100%),
    linear-gradient(180deg, rgba(6, 8, 16, .68), rgba(6, 8, 16, .86));
}

/* And the plates close up — in Recruiter mode the copy is the only thing
   that matters and the field should not be competing for any of it. */
html.has-lattice body.mode-recruiter .section--deep,
html.has-lattice body.mode-recruiter .works-preview { background: rgba(12, 15, 26, .93); }
html.has-lattice body.mode-recruiter .section--surface { background: rgba(19, 23, 32, .94); }

body.mode-recruiter .ed-sidx { display: none; }

/* ── responsive ─────────────────────────────────────────────────────────────
   Under 1180px the rail would collide with content gutters; under 720px the
   boot module has already declined to start the renderer at all. */
@media (max-width: 1180px) {
  .ed-sidx { display: none; }
}

@media (max-width: 720px) {
  .ed-rail { display: none; }
}

/* ── reduced motion ─────────────────────────────────────────────────────────
   The engine already stops rotation, drift, ripples and the camera dolly. The
   remaining job here is to stop the field's own opacity from animating, so a
   reader who has asked for stillness gets a static structure rather than a
   crossfade that follows them down the page. */
@media (prefers-reduced-motion: reduce) {
  #ed-gl { transition: none; }
  .ed-sidx button i,
  .ed-sidx__l { transition: none; }
}
