/* ═══════════════════════════════════════════════════════════════════════════
   hub-interact.css — the interaction layer's skin for the games hub.
   ---------------------------------------------------------------------------
   THIS FILE DOES NOT IMPLEMENT DARK MODE EITHER.
   index.html remaps the neural.css token names under [data-theme="dark"] and
   hub-theme.css adds the mineral set on the same hook. This is the third layer
   on that ONE hook and it introduces no fourth: every value below either
   consumes an existing token or declares a new one twice, in the two blocks
   the page already keeps in step — the [data-theme="dark"] attribute block and
   the prefers-color-scheme duplicate that makes the theme correct before any
   JavaScript runs.

   WHAT IT ADDS
     1. --hub-quiet-ink   the one new colour decision, and it is a contrast fix
     2. a galaxy wash on #atmos for the dark theme (paint, not pixels — it is a
        CSS background on the canvas ELEMENT, so it costs nothing per frame)
     3. mineral depth on the game cards and the current rank tier
     4. the reveal state driven by js/hub-interact.js
     5. the reduced-motion switch for exactly the properties this file added

   WHAT IT DOES NOT RE-DECIDE. index.html:293 repairs .note and .muted,
   hub-theme.css:174 repairs the current rung's .t-xp, and index.html:208 already
   rejected a hover LIFT on the cards in favour of a growing underline. All three
   are correct, all three are somebody else's call, and none is overridden here.
   A stylesheet loaded third is in a position to quietly win arguments it was
   never part of; the discipline is to add, not to relitigate.

   MATERIAL LANGUAGE, and what each theme is trying to be
     DARK is a night sky: the indigo/violet wash below sits over hub-theme.css's
     amethyst sky, with the starfield hub-atmos.js draws inside it. Depth cues
     lean on --gem-amethyst and --gem-geode: a dark rind with a crystal
     interior.
     LIGHT is a precision laboratory: warm white and ivory (--cream family),
     mineral neutrals, GRAPHITE for the rim (--ink at 10%, not a new grey), and
     one warm --gem-opal ember low in the box. There is no wash on the light
     canvas at all — paper, hairlines and a restrained shadow are the material.

   SPECIFICITY IS DELIBERATELY HIGH IN PLACES.
   Several rules below carry a redundant-looking `> li >` or an id. That is not
   sloppiness: index.html's inline <style> loads AFTER any <link> in <head>, so
   a rule here that merely ties on specificity would lose to it depending on
   where the <link> is placed. The extra compound makes every override below win
   from either position. Do not "tidy" them down — and read the reduced-motion
   note at the foot of the file before you do, because winning the cascade is
   also what makes it possible to win an argument you did not mean to enter.

   MEASURED CONTRAST, light theme, sRGB, computed not eyeballed:
     #78716C on #F3F1EA = 4.24:1   FAILS SC 1.4.3   ← the surface this fixes
     #78716C on #FFFDF8 = 4.72:1   passes
     #44403C on #F3F1EA = 9.09:1   the replacement
   Dark theme measures clean on the same elements (#A29A91 on #231F1D = 5.89:1),
   which is why the fix is scoped to the light token set and dark keeps the
   quieter ink it was designed with.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── tokens ────────────────────────────────────────────────────────────────
   THREE BLOCKS, KEPT IN STEP, in the order index.html established: base, then
   the attribute override, then the OS duplicate. The OS block must stay LAST —
   `:root[data-theme='dark']` and `:root:not([data-theme='light'])` have equal
   specificity, so the later one wins, and that is what lets the header toggle
   override the operating system in both directions.                          */

:root {
  /* The quiet ink, for ONE remaining case. index.html:293 already repairs .note
     and .muted, and hub-theme.css:174 repairs the current rung's .t-xp — both
     to --ink-2, in both themes, and neither is re-litigated here. What is left
     is the card HOVER state: index.html:201 darkens the cell to --cream-2, and
     .cg-step (10.5px) and .cg-k (9.5px) are --ink-3, which measures 4.72:1 on
     the resting --hub-surface and 4.24:1 on that hover surface. So pointing at
     a card is what pushes its eyebrow under the floor.
     --ink-2 is the existing token that clears it; no fifth ink is invented. */
  --hub-quiet-ink: var(--ink-2);

  /* Depth, named for the JOB rather than the hue, so the dark values can be a
     different material without the selector lying about what it does.
     --lab-* is this file's own namespace on purpose: --gem-* belongs to
     hub-theme.css, and a fourth file quietly adding names to another file's set
     is how two stylesheets end up owning one token. */
  --lab-rim:   color-mix(in srgb, var(--ink) 10%, transparent);       /* graphite */
  --lab-fire:  color-mix(in srgb, var(--gem-opal) 30%, transparent);  /* the ember */
  --lab-facet: color-mix(in srgb, var(--gem-quartz) 60%, transparent);/* the lit edge */
  --geode-core: color-mix(in srgb, var(--gem-amethyst) 30%, transparent);
}

:root[data-theme='dark'] {
  --hub-quiet-ink: var(--ink-3);   /* 5.89:1 on the dark card — already passing */
  --lab-rim:   color-mix(in srgb, var(--gem-amethyst) 26%, transparent);
  --lab-fire:  color-mix(in srgb, var(--gem-opal) 42%, transparent);
  --lab-facet: color-mix(in srgb, var(--gem-quartz) 30%, transparent);
  --geode-core: color-mix(in srgb, var(--gem-amethyst) 55%, transparent);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --hub-quiet-ink: var(--ink-3);
    --lab-rim:   color-mix(in srgb, var(--gem-amethyst) 26%, transparent);
    --lab-fire:  color-mix(in srgb, var(--gem-opal) 42%, transparent);
    --lab-facet: color-mix(in srgb, var(--gem-quartz) 30%, transparent);
    --geode-core: color-mix(in srgb, var(--gem-amethyst) 55%, transparent);
  }
}

/* ── the galaxy ────────────────────────────────────────────────────────────
   Deep indigo and violet, as a BACKGROUND ON THE CANVAS ELEMENT rather than as
   pixels in the bitmap. The canvas draws over its own CSS background, so this
   composites for free and costs exactly zero per frame — a full-viewport
   gradient repainted at 30fps was the alternative and it is the single most
   expensive thing this layer could have done.

   THE TWO CLOUD CENTRES — 12% 18% and 78% 62% — ARE DUPLICATED as the NEBULA
   constant in js/hub-atmos.js, which clusters roughly a third of the stars
   inside them. Keep the two in step; if they drift, the stars sit beside the
   cloud instead of in it and the depth reads as an accident.

   Alphas are 16/12/10% because they layer over hub-theme.css's amethyst sky,
   which is already doing most of the work. This is the near field, not the sky.
                                                                              */
#atmos { background-image: none; }   /* light: the paper is the material */

:root[data-theme='dark'] #atmos {
  background-image:
    radial-gradient(46vmax 34vmax at 12% 18%, rgba(76, 61, 148, .16), transparent 66%),
    radial-gradient(40vmax 30vmax at 78% 62%, rgba(122, 74, 148, .12), transparent 64%),
    radial-gradient(72vmax 42vmax at 50% 116%, rgba(46, 48, 120, .10), transparent 70%);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) #atmos {
    background-image:
      radial-gradient(46vmax 34vmax at 12% 18%, rgba(76, 61, 148, .16), transparent 66%),
      radial-gradient(40vmax 30vmax at 78% 62%, rgba(122, 74, 148, .12), transparent 64%),
      radial-gradient(72vmax 42vmax at 50% 116%, rgba(46, 48, 120, .10), transparent 70%);
  }
}

/* ── the scroll reveal ─────────────────────────────────────────────────────
   The class is added by js/hub-interact.js, per GRID, and only to grids that
   start below the fold — so nothing a reader can already see is ever hidden,
   and no grid ends up half-driven by this and half by the CSS layer. Without
   that module, or without IntersectionObserver, these selectors match nothing
   and the page is the page.

   THE OTHER REVEAL. index.html:517 animates `.reveal > *` on an
   `animation-timeline: view()` timeline and hands over with one rule,
   `.reveal > *.ncg-reveal{animation:none}` — an animation would otherwise beat
   this transition in the cascade and the JS layer would silently lose. Both
   layers exist on purpose: view() timelines are not universal, and an engine
   without them skips that @supports block entirely and gets nothing.

   THIS LAYER USES OPACITY AND THAT ONE DOES NOT, deliberately. A CSS scroll
   timeline can mis-fire in an engine nobody tested, and transform-only means
   the worst case is text 18px from where it belongs rather than text stuck
   invisible. This state is only ever applied by script that has already proved
   IntersectionObserver exists, is removed again if the observer never delivers
   (hub-interact.js arms a failsafe for exactly that), and carries the three
   escape hatches below.                                                       */
/* THE TRANSITION IS DECLARED ON THE ARRIVING STATE ONLY, and that is not a
   style preference — it is the fix for a defect found by measuring this.
   A transition is read from the state being transitioned TO. With the
   transition on the base rule, adding .ncg-reveal to nineteen elements at load
   started nineteen half-second FADE-OUTS: the elements animated from the
   opacity 1 they were already rendering at down to 0, and only then waited to
   be revealed. Nobody sees it (everything marked is below the fold) but the
   page pays for it. Declared here, adding the class hides instantly and only
   the arrival animates. */
.ncg-reveal {
  opacity: 0;
  transform: translateY(14px);
}
.ncg-reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity .5s ease, transform .5s cubic-bezier(.2, .9, .3, 1.1);
}

/* Stagger inside each group. :nth-child restarts per parent, which is why
   hub-interact.js selects by group rather than flattening — four cards step
   against their own neighbours, not against the twelve cells above them. */
.ncg-reveal.is-in:nth-child(2) { transition-delay: .06s; }
.ncg-reveal.is-in:nth-child(3) { transition-delay: .12s; }
.ncg-reveal.is-in:nth-child(4) { transition-delay: .18s; }
.ncg-reveal.is-in:nth-child(n+5) { transition-delay: .24s; }

/* Escape hatches. Anything that can put a reader inside a not-yet-revealed
   element must reveal it instantly: tabbing into it, an in-page #anchor landing
   on it, or a browser find-in-page moving focus. Each is its OWN rule on
   purpose — a selector list is discarded whole if any part of it is unknown to
   the engine, so grouping :has() with the others would take them all down on a
   browser that lacks it. */
.ncg-reveal:focus-within { opacity: 1; transform: none; transition: none; }
.ncg-reveal:target { opacity: 1; transform: none; transition: none; }
.ncg-reveal:has(:target) { opacity: 1; transform: none; transition: none; }
@media print { .ncg-reveal { opacity: 1 !important; transform: none !important; } }

/* ── mineral depth · the game cards ────────────────────────────────────────
   A dark rind, a lit top facet and one warm ember low in the box — the whole
   geode in three inset shadows and nothing else.

   THIS IS THE INTERIOR; index.html:215 ALREADY OWNS THE EDGE. That rule grows a
   2px crimson underline from the left on the same two states, having rejected a
   translateY lift because .cardgrid's 1px gaps ARE its hairlines and the
   container is overflow:hidden, so a lifted cell opens a gap under itself and
   covers the line above. Nothing here re-opens that: the underline is the edge
   cue, this is the depth behind it, and neither touches layout.

   Inset box-shadow is the instrument, and the alternatives were rejected:
     · a ::before overlay would be a POSITIONED descendant, so it paints above
       the anchor's inline text and lays a film over the words. Fixing that
       needs a stacking context and a z-index on every child.
     · animating background-image does not animate: it pops.
   An inset shadow paints over the background and under the content, transitions
   smoothly, needs no extra element, and cannot take a click.

   FOCUS GETS THE SAME TREATMENT AS HOVER — the rule index.html:197 sets out for
   its own cues, followed here. A depth cue only a mouse can reach turns a
   navigation affordance into a mouse-only affordance.

   The `> li >` compound is what wins this over index.html:196's `.cardgrid a`
   transition. See the reduced-motion note at the foot of this file for why that
   makes the switch-off below mandatory rather than tidy.                      */
.cardgrid > li > a {
  transition:
    background .2s ease,
    box-shadow .24s ease,
    color .2s ease;
}
.cardgrid > li > a:hover,
.cardgrid > li > a:focus-visible {
  box-shadow:
    inset 0 1px 0 0 var(--lab-facet),
    inset 0 0 0 1px var(--lab-rim),
    inset 0 -30px 40px -34px var(--lab-fire);
}

/* CONTRAST, and this one is a real defect rather than a decoration. It is a
   HOVER-AND-FOCUS-STATE failure, which is why the page's other contrast repairs
   did not reach it: at rest .cg-step (10.5px) and .cg-k (9.5px) are --ink-3 on
   --hub-surface and measure 4.72:1, which passes. index.html:201 moves the cell
   to --cream-2 on hover and focus, and the identical token measures 4.24:1
   there — under the 4.5:1 floor. Pointing at a card is what breaks it.
   .cg-chip is deliberately absent: index.html:243 already sets it to --ink-2.
   --hub-quiet-ink is --ink-2 in light (9.09:1) and stays --ink-3 in dark, where
   the same pair measures 5.89:1 and needs no help. */
.cardgrid > li > a:hover .cg-step,
.cardgrid > li > a:focus-visible .cg-step,
.cardgrid > li > a:hover .cg-k,
.cardgrid > li > a:focus-visible .cg-k {
  color: var(--hub-quiet-ink);
}

/* The accent cell is a solid --hub-accent plate, so quartz and graphite have
   nothing to sit on. Its geode is warmer: a gold rind and a stronger opal
   ember, which also keeps the flagship distinguishable from the other three at
   the moment of interaction rather than only at rest. */
.cardgrid > li.is-accent > a:hover,
.cardgrid > li.is-accent > a:focus-visible {
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, .16),
    inset 0 0 0 1px color-mix(in srgb, var(--gem-geode) 30%, transparent),
    inset 0 -34px 46px -34px color-mix(in srgb, var(--gem-opal) 52%, transparent);
}
/* The plate's own copy is set at rgba(250,249,246,.88) by index.html:250 and is
   not --ink-3, so the contrast rule above must not reach it. Restated rather
   than excluded with :not() because the value has to match index.html exactly;
   if that number moves, this is the one place that has to move with it. */
.cardgrid > li.is-accent > a:hover .cg-step,
.cardgrid > li.is-accent > a:focus-visible .cg-step,
.cardgrid > li.is-accent > a:hover .cg-k,
.cardgrid > li.is-accent > a:focus-visible .cg-k {
  color: rgba(250, 249, 246, .88);
}

/* ── mineral depth · the rank ladder ───────────────────────────────────────
   THIS REPLACES hub-theme.css:163's box-shadow rather than adding to it —
   box-shadow does not merge across rules, so that rule's gold rind is restated
   as the first layer here and the amethyst core and ember are added under it.
   Change one, check the other.

   THE SELECTOR PAIR IS COPIED FROM hub-theme.css:156 ON PURPOSE, including the
   order. hub.js:324 writes aria-current="step" — the correct ARIA value for an
   item in a sequence — so a rule matching [aria-current='true'] never fires.
   Two rules that are supposed to paint the same element must select it the same
   way; if that pair ever changes, both files change together.

   Only the CURRENT tier gets the geode. Giving all eight a crystal interior
   would make the marker mean nothing, which is the same reason hub-theme.css
   spends --gem-geode on exactly one element.

   The current rung's .t-xp contrast is NOT repaired here: hub-theme.css:174
   already takes it to --ink-2 in both themes, and re-deciding another file's
   accessibility fix from a third file is how a value ends up with two owners. */
#rank-ladder > li[aria-current='step'],
#rank-ladder > li.is-current {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--gem-geode) 26%, transparent),
    inset 0 20px 30px -26px var(--geode-core),
    inset 0 -14px 24px -20px var(--lab-fire);
}

/* Earned medals get a cut edge rather than a flat disc: one lit rim at the top,
   one ember at the bottom. hub-theme.css:178 already gives them a faceted
   gradient; this is the depth that gradient implies. */
#rank-ladder > li.is-earned .medal {
  box-shadow:
    inset 0 1px 0 var(--lab-facet),
    inset 0 -4px 7px -5px color-mix(in srgb, var(--gem-opal) 55%, transparent);
}

/* ── reduced motion ────────────────────────────────────────────────────────
   TWO BLOCKS, KEPT IN STEP, for the same reason the token blocks above are: the
   media query is the operating system's answer, and [data-motion="reduce"] is
   the player's. index.html carries the same pair and already covers everything
   it owns — the launch veil, .cardgrid a, .cg-go .chev, .rank-bar i,
   scroll-behavior and its own .reveal > * animation. THIS BLOCK ONLY COVERS
   WHAT THIS FILE ADDED, which is the correct division and also the necessary
   one, for a reason that is easy to miss:

     index.html's OS-query rule is `.cardgrid a{transition:none}` at (0,1,1).
     The transition rule higher up in THIS file is `.cardgrid > li > a` at
     (0,1,2) and beats it. Without the line below, adding a box-shadow
     transition here would have QUIETLY RE-ENABLED card motion for exactly the
     users who asked for none. The extra compound that makes the depth cue win
     the cascade is the same compound that makes it dangerous, so it has to be
     switched off at matching specificity.

   The reveal is not shortened, it is removed. hub-interact.js also strips the
   class off the DOM, so this is the belt to that braces — and it is the half
   that works if the module is present but a11y.js failed to load.            */
@media (prefers-reduced-motion: reduce) {
  .ncg-reveal { opacity: 1; transform: none; }
  .ncg-reveal.is-in { transition: none; }
  .cardgrid > li > a { transition: none; }
}

:root[data-motion='reduce'] .ncg-reveal { opacity: 1; transform: none; }
:root[data-motion='reduce'] .ncg-reveal.is-in { transition: none; }
:root[data-motion='reduce'] .cardgrid > li > a { transition: none; }
