/* ─────────────────────────────────────────────────────────────
   Noveriq - feature previews

   Six of the features carry a preview: a small, cropped fragment of
   the real notch panel sitting in a softly tinted box, with a bold
   lead-in and a sentence underneath. The rest of the features stay
   as a compact text row below.

   ── what a fragment is ────────────────────────────────────────
   Not a screenshot and not the whole screen: only the part of the
   notch UI the feature is about, drawn in the DOM the same way the
   hero scene is. The brand marks, the pips and the status colours
   are the app's own, pulled from the tokens hero-demo.css publishes
   at :root, so there is exactly one copy of every value the app
   also owns.

   ── how it is sized ───────────────────────────────────────────
   The hero is authored in fixed design pixels and scaled with a
   transform, because it has to hold one screen's geometry. A crop
   has no geometry to preserve, so these are fluid instead: .fx-box
   is a container, and every size inside a fragment is in em off one
   container-relative font size. Nothing is transform-scaled, so
   nothing goes soft.

   ── motion ────────────────────────────────────────────────────
   Cards reveal on scroll (features-demo.js adds .is-in, staggered),
   and each fragment plays one beat when it lands: Allow is pressed,
   the tab hands over, an option is picked, the rings draw. Every
   beat runs once. Under prefers-reduced-motion everything is simply
   present, in its resolved state.

   Namespaced fx- to stay out of both styles.css and hero-demo.css.
   ───────────────────────────────────────────────────────────── */

.fx-grid, .fx-grid *, .fx-grid *::before, .fx-grid *::after,
.fx-more, .fx-more * { box-sizing: border-box; }

/* ── the grid of preview cards ────────────────────────────────── */

.fx-grid {
  display: grid;
  gap: 40px 24px;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

.fx-card {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* Hiding is opt-in, never the default. features-demo.js sets .is-revealing
   on the grid as its first act, so a page with no JS - or with a script
   that failed - shows every card, already resolved, instead of six blank
   boxes. Nothing here is content the reveal is allowed to withhold. */
.fx-grid.is-revealing .fx-card {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition: opacity .6s ease, transform .6s cubic-bezier(.22, 1, .28, 1);
}
.fx-grid.is-revealing .fx-card.is-in { opacity: 1; transform: none; }

.fx-copy {
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
  color: var(--slate);
}
.fx-copy b {
  font-weight: 600;
  color: var(--ink);
}

/* ── the tinted box ───────────────────────────────────────────── */
/* The tint is the product's own wallpaper, blurred right down and
   washed out: the same artwork that glows behind the hero screen, so
   a preview reads as a detail lifted out of that scene rather than a
   decoration invented for this section. */

.fx-box {
  container-type: inline-size;
  position: relative;
  /* The boxes are one height across a row, so the copy under them starts on
     one line. The aspect gives them that; the cap stops a wide two-column
     layout from turning the extra width into dead air, because the fragment
     inside stops growing once its type hits the 12px ceiling. */
  aspect-ratio: 3 / 2;
  max-height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 5.5%;
  overflow: hidden;
  border-radius: var(--r-card-lg);
  background: var(--paper);
  border: 1px solid var(--edge);
  isolation: isolate;
}
.fx-box::before {
  content: "";
  position: absolute;
  inset: -30%;
  background: var(--ct-wallpaper);
  filter: blur(46px) saturate(120%);
  opacity: .55;
  z-index: -2;
}
/* a white wash over the wallpaper, so six of these in a row stay a
   background and the black fragments keep all the contrast */
.fx-box::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, rgba(255, 255, 255, .58), rgba(255, 255, 255, .78));
  z-index: -1;
}

/* ── a fragment of the notch ──────────────────────────────────── */
/* One font size drives everything inside; every other size is in em
   off it, so a fragment fills its box at any card width without ever
   being scaled. */

.fx-frag {
  position: relative;
  width: 100%;
  font-family: var(--font-ui);
  /* Capped at 12px because the longest real strings in these fragments -
     the question, and "Running tests · 128 passed, 0 failed" - have to
     land whole. A crop that ellipsises its own subject explains nothing. */
  font-size: clamp(8.5px, 4.3cqw, 12px);
  line-height: 1.3;
  color: #fff;
  -webkit-font-smoothing: antialiased;
}

/* One crop here is a control cluster rather than a card - two buttons and a
   short menu - and at the shared type size it filled about a third of its
   box. A close-up that does not fill its frame reads as a rendering fault,
   not as a close-up, so that one is authored a step larger. */
.fx-frag-lg {
  font-size: clamp(10px, 5.6cqw, 15.5px);
  /* The scope menu is a popover and takes no space in flow, so the fragment
     reserves its height here. Without it the box centres the button pair
     alone and the menu hangs off the bottom edge into the clip. */
  padding-bottom: 5.4em;
}

/* The panel's own surface: black, with the app's card radius. */
.fx-panel {
  padding: .7em .8em;
  border-radius: 1.5em;
  background: #000;
  box-shadow: 0 1.2em 2.6em rgba(16, 15, 18, .3), 0 .3em .8em rgba(16, 15, 18, .16);
}
/* A crop of a control cluster rather than of a row: the panel hugs what it
   holds instead of stretching a couple of buttons across the whole width.
   Relative, because the scope menu hangs off it as a popover. */
.fx-panel-fit { position: relative; width: max-content; max-width: 100%; margin: 0 auto; }

/* one session's row inside the panel */
.fx-row {
  padding: .75em .9em;
  border-radius: 1.1em;
  background: rgba(255, 255, 255, .06);
  box-shadow: inset 0 0 0 1px transparent;
}
.fx-row + .fx-row { margin-top: .45em; }
.fx-row-perm { box-shadow: inset 0 0 0 1px rgba(255, 159, 46, .55); }
.fx-row-ask  { box-shadow: inset 0 0 0 1px rgba(89, 166, 255, .55); }

.fx-head { display: flex; align-items: flex-start; gap: .8em; }
.fx-main { flex: 1; min-width: 0; }
.fx-top  { display: flex; align-items: center; gap: .5em; }
.fx-spacer { flex: 1; }

.fx-project { font-size: 1.08em; font-weight: 600; letter-spacing: -.01em; color: #fff; }
.fx-elapsed { font-size: .86em; color: rgba(255, 255, 255, .45); white-space: nowrap; }

.fx-status {
  margin-top: .3em;
  font-family: var(--font-mono);
  font-size: .88em;
  color: rgba(255, 255, 255, .55);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fx-status-attention { color: var(--ct-attention); }
.fx-status-asking    { color: var(--ct-asking); }

.fx-detail { padding-left: 2.4em; margin-top: .55em; }

/* The brand mark and its pip are hero-demo.css's, re-sized: the mask,
   the tints and the pip palette are the app's roster, declared once. */
.fx-frag .ct-agent { --ct-agent-size: 1.8em; margin-top: .1em; }
.fx-frag .ct-agent-pip { box-shadow: 0 0 0 .13em #000; }

/* ── the risk callout ─────────────────────────────────────────── */

.fx-risk {
  display: flex;
  align-items: center;
  gap: .45em;
  font-size: .9em;
  font-weight: 500;
  color: var(--ct-high);
}
.fx-risk svg { width: 1em; height: 1em; flex: none; }

.fx-code {
  margin-top: .5em;
  padding: .6em .7em;
  border-radius: .6em;
  background: rgba(255, 255, 255, .07);
  font-family: var(--font-mono);
  font-size: .9em;
  color: rgba(255, 255, 255, .85);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── buttons ──────────────────────────────────────────────────── */

.fx-btns { display: flex; align-items: center; gap: .45em; margin-top: .6em; }
/* the scope crop opens on the buttons, with no row above them to clear */
.fx-panel > .fx-btns:first-child { margin-top: 0; }
.fx-btn {
  display: flex;
  align-items: center;
  gap: .35em;
  height: 2em;
  padding: 0 .8em;
  border-radius: 1em;
  font-size: .9em;
  font-weight: 600;
  white-space: nowrap;
  color: rgba(255, 255, 255, .85);
  background: rgba(255, 255, 255, .12);
}
.fx-btn svg { width: .9em; height: .9em; flex: none; }
.fx-btn-allow {
  color: rgba(0, 0, 0, .9);
  background: var(--ct-attention);
  padding-right: 0;
}
.fx-btn-allow .fx-split { width: 1px; height: 1.15em; margin-left: .65em; background: rgba(0, 0, 0, .2); }
.fx-btn-allow .fx-chev { display: flex; align-items: center; padding: 0 .6em; border-radius: 0 1em 1em 0; }
/* The plan's Approve has no scope menu behind it, so it is not a split
   button and gets its right padding back. */
.fx-btn-solid { padding-right: .8em; }

/* ── the plan card ────────────────────────────────────────────────
   The one card in the app taller than the notch it opens in, which is why
   it is the only fragment here with a scroller. The window is authored in
   em like everything else, so it holds the same four lines at any card
   width; the beat below moves the body under it. */

.fx-plan {
  position: relative;
  /* Sized to what is left in a 3:2 box once the head, the buttons and the
     panel's own padding have taken theirs - about three and a half lines,
     which is enough to read as a window onto something longer. */
  height: 4.8em;
  margin-top: .1em;
  padding: .5em .6em;
  overflow: hidden;
  border-radius: .6em;
  background: rgba(255, 255, 255, .07);
  /* The beat leaves the body part-scrolled, which means a half line against
     the top edge. Hard-clipped that reads as a rendering fault; faded, it
     reads as what it is - there is more of this above and below. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 1.1em, #000 calc(100% - 1.1em), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 1.1em, #000 calc(100% - 1.1em), transparent 100%);
}
.fx-plan-scroll { will-change: transform; }
.fx-plan-scroll b {
  display: block;
  margin: .5em 0 .3em;
  font-size: .88em;
  font-weight: 600;
  color: rgba(255, 255, 255, .92);
}
.fx-plan-scroll b:first-child { margin-top: 0; }
.fx-plan-scroll span {
  display: flex;
  gap: .5em;
  font-size: .86em;
  line-height: 1.45;
  color: rgba(255, 255, 255, .62);
}
.fx-plan-scroll i {
  flex: none;
  width: 1em;
  font-style: normal;
  color: rgba(255, 255, 255, .38);
  font-variant-numeric: tabular-nums;
}
/* The thumb is the app's: it says the body is longer than the window, which
   is the only reason the scroller is in this crop at all. Its height and its
   travel have to add up to the track or it runs off the bottom - track is
   4.8em less .5em of padding at each end, so 1.9 + 1.7 = 3.6 fits with a
   little to spare. */
.fx-plan-thumb {
  position: absolute;
  top: .5em;
  right: .3em;
  width: .22em;
  height: 1.9em;
  border-radius: .22em;
  background: rgba(255, 255, 255, .3);
}

/* ── the quota strip ──────────────────────────────────────────────
   Pinned under the sessions the way the app pins it: a week's quota
   describes the account, not any one run, so it cannot live in a row. */

.fx-usage {
  display: flex;
  align-items: center;
  gap: 1.1em;
  margin-top: .55em;
  padding: .55em .9em 0;
  border-top: 1px solid rgba(255, 255, 255, .1);
}
.fx-usage-account { font-size: .9em; font-weight: 600; color: rgba(255, 255, 255, .5); }
.fx-gauge { display: flex; align-items: center; gap: .5em; }
.fx-gauge-name { font-size: .86em; font-weight: 500; color: rgba(255, 255, 255, .62); }
.fx-gauge-track {
  width: 4.4em;
  height: .42em;
  border-radius: .21em;
  overflow: hidden;
  background: rgba(255, 255, 255, .14);
}
/* The value is the element's width; the beat only scales it, so the CSS
   never has to know the number and the resolved state is simply the width. */
.fx-gauge-fill {
  display: block;
  width: var(--fx-pct);
  height: 100%;
  border-radius: inherit;
  background: var(--ct-ok);
  transform-origin: left center;
}
.fx-gauge-warm { background: var(--ct-attention); }
.fx-gauge-num {
  font-size: .86em;
  font-weight: 500;
  color: rgba(255, 255, 255, .5);
  font-variant-numeric: tabular-nums;
}

/* ── two states of the collapsed bar ──────────────────────────────
   The privacy crop, and the only fragment here that draws the bar rather
   than what opens out of it - because the feature is not that the bar goes
   away, it is that it stops opening. Both states are drawn at once so the
   comparison survives with no motion at all, which matters for the one card
   somebody checks in a hurry before a screen share. */

/* The gap between the two states has to beat the gap inside one of them by
   a clear margin, or the card leaning out of the first bar reads as though
   it belonged to the second. */
.fx-states { display: grid; gap: 2.1em; }
.fx-state { display: grid; justify-items: center; gap: .4em; }
/* and the card tucks under its own bar, the way it leans out of the notch */
.fx-state .fx-leaf { margin-top: -.05em; }
.fx-state-label {
  font-size: .84em;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: rgba(16, 15, 18, .42);
}
.fx-state-on { color: var(--iris); }
.fx-state-note {
  font-family: var(--font-mono);
  font-size: .8em;
  color: rgba(16, 15, 18, .34);
}

/* the collapsed bar: the notch's own pill, at the width its marks need */
.fx-bar {
  display: flex;
  align-items: center;
  gap: .55em;
  height: 2.4em;
  padding: 0 1em;
  border-radius: 1.2em;
  background: #000;
  box-shadow: 0 .5em 1.2em rgba(16, 15, 18, .26);
}
.fx-bar .ct-agent { --ct-agent-size: 1.25em; }
.fx-bar .ct-agent-pip { box-shadow: 0 0 0 .12em #000; }

/* what leans out of it: a card, cropped to its first line */
.fx-leaf {
  display: flex;
  align-items: center;
  gap: .55em;
  max-width: 100%;
  padding: .5em .8em;
  border-radius: .9em;
  background: #000;
  box-shadow: inset 0 0 0 1px rgba(255, 159, 46, .55), 0 .6em 1.4em rgba(16, 15, 18, .3);
}
.fx-leaf .ct-agent { --ct-agent-size: 1.3em; }
.fx-leaf .ct-agent-pip { box-shadow: 0 0 0 .12em #000; }
.fx-leaf-text {
  font-family: var(--font-mono);
  font-size: .86em;
  color: var(--ct-attention);
  white-space: nowrap;
}

/* ── the Allow scope menu ─────────────────────────────────────── */
/* Labels are SessionRowView's own: the split button is "once", the menu
   carries the two durable scopes.

   It hangs off the panel rather than sitting inside it, which is both what
   the app does - a menu is a popover over everything, not a row in a card -
   and what stops the crop being mostly empty: in flow, the menu is wider
   than the button pair above it, so the panel took the menu's width and
   left an L of black with nothing in it. */

.fx-menu {
  position: absolute;
  top: calc(100% + .5em);
  right: 0;
  width: 12.5em;
  margin: 0;
  padding: .3em;
  border-radius: .85em;
  background: rgba(38, 38, 42, .98);
  box-shadow: 0 1em 2.4em rgba(0, 0, 0, .5), inset 0 0 0 1px rgba(255, 255, 255, .09);
}
.fx-menu div {
  display: flex;
  align-items: center;
  gap: .5em;
  height: 1.9em;
  padding: 0 .6em;
  border-radius: .55em;
  font-size: .92em;
  color: rgba(255, 255, 255, .9);
  white-space: nowrap;
}
.fx-menu i { width: .35em; height: .35em; border-radius: 50%; background: var(--ct-attention); flex: none; }

/* ── question options ─────────────────────────────────────────── */

.fx-opt {
  display: flex;
  align-items: center;
  gap: .55em;
  padding: .5em .6em;
  border-radius: .7em;
  background: rgba(255, 255, 255, .10);
  box-shadow: inset 0 0 0 1px transparent;
}
.fx-opt + .fx-opt { margin-top: .35em; }
.fx-opt-no {
  width: 1.35em;
  height: 1.35em;
  border-radius: 50%;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: .85em;
  font-weight: 700;
  color: rgba(255, 255, 255, .8);
  background: rgba(255, 255, 255, .15);
}
.fx-opt-label {
  font-size: .92em;
  color: rgba(255, 255, 255, .9);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* the option that was picked - the beat below animates into exactly this */
.fx-opt-picked { background: rgba(89, 166, 255, .16); box-shadow: inset 0 0 0 1px rgba(89, 166, 255, .6); }
.fx-opt-picked .fx-opt-no { background: var(--ct-asking); color: rgba(0, 0, 0, .85); }

/* ── the terminal tab strip ───────────────────────────────────── */
/* Under the notch fragment, so the pair says "click here, land there".
   Glass over the wallpaper, exactly as the hero's terminal is. */

.fx-term {
  margin-top: .9em;
  border-radius: .7em;
  overflow: hidden;
  background: rgba(22, 20, 30, .82);
  box-shadow: 0 .8em 1.8em rgba(16, 15, 18, .26), inset 0 1px 0 rgba(255, 255, 255, .12);
}
.fx-tabs { display: flex; gap: 1px; padding: .35em .35em 0; }
.fx-tab {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 1.9em;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: .45em .45em 0 0;
  font-size: .8em;
  color: rgba(255, 255, 255, .45);
  background: transparent;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 .5em;
}
.fx-tab::after {
  content: "";
  position: absolute;
  left: .5em; right: .5em; bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--ct-high);
  opacity: 0;
}
.fx-tab-on { background: #1d2129; color: rgba(255, 255, 255, .95); }
.fx-tab-on::after { opacity: 1; }
.fx-term-body {
  height: 2.6em;
  padding: .5em .7em;
  background: #0d1015;
  font-family: var(--font-mono);
  font-size: .82em;
  color: rgba(238, 235, 250, .78);
  white-space: nowrap;
  overflow: hidden;
}

/* ── the context ring ─────────────────────────────────────────── */
/* Bands are ContextUsage.level: normal under 70%, high under 90%,
   critical at or above it. Colours are ContextIndicator's. */

.fx-ring { display: flex; align-items: center; gap: .35em; }
.fx-ring svg { width: 1.15em; height: 1.15em; display: block; }
.fx-ring circle { fill: none; stroke-width: 3; }
.fx-ring-bg { stroke: rgba(255, 255, 255, .14); }
.fx-ring-fg { stroke: var(--ct-ok); stroke-linecap: round; }
.fx-ring-num {
  font-size: .86em;
  font-weight: 500;
  color: rgba(255, 255, 255, .5);
  font-variant-numeric: tabular-nums;
}
.fx-ring-high .fx-ring-fg { stroke: var(--ct-high); }
.fx-ring-high .fx-ring-num { color: var(--ct-high); }
.fx-ring-crit .fx-ring-fg { stroke: var(--ct-crit); }
.fx-ring-crit .fx-ring-num { color: var(--ct-crit); }

/* Three whole rows at once is the tallest fragment here, and it has to be
   whole rows: a session's card carries its status line, and a preview that
   dropped it would be showing a row the app never draws. The type comes
   down a step instead. */
.fx-frag-dense { font-size: .88em; }

/* ── the pointer ──────────────────────────────────────────────── */

.fx-cursor {
  position: absolute;
  width: 1.15em;
  height: 1.6em;
  opacity: 0;
  pointer-events: none;
  z-index: 5;
  filter: drop-shadow(0 .1em .2em rgba(0, 0, 0, .55));
}
.fx-cursor svg { width: 100%; height: 100%; display: block; }

/* ── the beats ────────────────────────────────────────────────── */
/* Each fires once, on the card landing. `both` holds the end state, so
   a fragment that has played reads as a finished interaction rather
   than snapping back to its opening frame. */

.fx-card.is-in .fx-beat-press  { animation: fx-press 2.4s .5s both; }
@keyframes fx-press {
  0%, 30%   { transform: none; box-shadow: none; }
  38%       { transform: scale(.94); box-shadow: 0 0 0 .3em rgba(255, 159, 46, .3); }
  46%, 100% { transform: none; box-shadow: none; }
}

.fx-card.is-in .fx-beat-cursor-allow { animation: fx-cursor-allow 2.4s .2s both; }
@keyframes fx-cursor-allow {
  0%        { opacity: 0; transform: translate3d(4.5em, 3.2em, 0); }
  18%       { opacity: 1; transform: translate3d(4.5em, 3.2em, 0); }
  40%, 78%  { opacity: 1; transform: translate3d(0, 0, 0); }
  100%      { opacity: 0; transform: translate3d(.4em, .5em, 0); }
}

/* The flag arriving. The command lands first and the callout drops in above
   it, pushing it down - which is the order it happens in, and the only thing
   that card has to say. Animating margin rather than height keeps it off the
   layout of everything around it. */
.fx-card.is-in .fx-beat-risk { animation: fx-risk 2.2s .6s both; }
@keyframes fx-risk {
  0%, 26%  { opacity: 0; transform: translate3d(0, -.5em, 0); margin-bottom: -1.5em; }
  46%      { opacity: 1; transform: none; margin-bottom: 0; }
  56%      { transform: translate3d(0, -.12em, 0); }
  64%, 100%{ opacity: 1; transform: none; margin-bottom: 0; }
}

/* The scope menu. The chevron is the half of the split button that opens it,
   so it lights first and the menu unrolls from directly under it. No pointer
   on this one: the approve card already spends that device, and two drawn
   cursors in one grid read as a motif rather than as an explanation. */
.fx-card.is-in .fx-beat-chev { animation: fx-chev 2.4s .5s both; }
@keyframes fx-chev {
  0%, 22%   { background: transparent; }
  34%, 58%  { background: rgba(0, 0, 0, .16); }
  70%, 100% { background: transparent; }
}
.fx-card.is-in .fx-beat-menu { animation: fx-menu-open 2.4s .5s both; }
@keyframes fx-menu-open {
  0%, 30%   { opacity: 0; transform: translate3d(0, -.5em, 0) scaleY(.72); }
  52%, 100% { opacity: 1; transform: none; }
}
.fx-menu { transform-origin: top center; }

/* The plan moving under its own window. Slow, and it stops short of the end:
   a body that scrolled to its last line would be saying the plan fits, which
   is the opposite of the point. */
.fx-card.is-in .fx-beat-plan  { animation: fx-plan-scroll 4.4s 1s both; }
.fx-card.is-in .fx-beat-thumb { animation: fx-plan-thumb  4.4s 1s both; }
@keyframes fx-plan-scroll {
  0%, 12%   { transform: none; }
  74%, 100% { transform: translate3d(0, -3.5em, 0); }
}
@keyframes fx-plan-thumb {
  0%, 12%   { transform: none; }
  74%, 100% { transform: translate3d(0, 1.7em, 0); }
}

/* The gauges fill to the width already on the element, so the number and the
   bar can never disagree. */
.fx-card.is-in .fx-beat-gauge { animation: fx-gauge-fill 1.5s .45s cubic-bezier(.22, 1, .28, 1) both; }
@keyframes fx-gauge-fill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* The card leaning out of the bar - the thing privacy mode stops. */
.fx-card.is-in .fx-beat-lean { animation: fx-lean 2.4s .5s both; }
@keyframes fx-lean {
  0%, 18%   { opacity: 0; transform: translate3d(0, -1.4em, 0) scale(.92); }
  46%, 100% { opacity: 1; transform: none; }
}

.fx-card.is-in .fx-beat-pick    { animation: fx-pick 2.2s .55s both; }
.fx-card.is-in .fx-beat-pick-no { animation: fx-pick-no 2.2s .55s both; }
@keyframes fx-pick {
  0%, 40%   { background: rgba(255, 255, 255, .10); box-shadow: inset 0 0 0 1px rgba(89, 166, 255, 0); }
  55%, 100% { background: rgba(89, 166, 255, .16);  box-shadow: inset 0 0 0 1px rgba(89, 166, 255, .6); }
}
@keyframes fx-pick-no {
  0%, 40%   { background: rgba(255, 255, 255, .15); color: rgba(255, 255, 255, .8); }
  55%, 100% { background: var(--ct-asking); color: rgba(0, 0, 0, .85); }
}

.fx-card.is-in .fx-beat-tab-off  { animation: fx-tab-off 2.4s .5s both; }
.fx-card.is-in .fx-beat-tab-on   { animation: fx-tab-on  2.4s .5s both; }
.fx-card.is-in .fx-beat-pane-off { animation: fx-fade-out 2.4s .5s both; }
.fx-card.is-in .fx-beat-pane-on  { animation: fx-fade-in  2.4s .5s both; }
@keyframes fx-tab-off {
  0%, 40%   { background: #1d2129; color: rgba(255, 255, 255, .95); }
  55%, 100% { background: rgba(29, 33, 41, 0); color: rgba(255, 255, 255, .45); }
}
@keyframes fx-tab-on {
  0%, 40%   { background: rgba(29, 33, 41, 0); color: rgba(255, 255, 255, .45); }
  55%, 100% { background: #1d2129; color: rgba(255, 255, 255, .95); }
}
@keyframes fx-fade-out { 0%, 40% { opacity: 1; } 52%, 100% { opacity: 0; } }
@keyframes fx-fade-in  { 0%, 44% { opacity: 0; } 56%, 100% { opacity: 1; } }
/* the underline rides with its tab */
.fx-card.is-in .fx-beat-tab-off::after { animation: fx-fade-out 2.4s .5s both; }
.fx-card.is-in .fx-beat-tab-on::after  { animation: fx-fade-in  2.4s .5s both; }

/* The second pane is stacked over the first, so the strip keeps one
   height. Resolved is "the tab you jumped to", which is pane two. */
.fx-term-body { position: relative; }
.fx-pane { position: absolute; inset: 0; padding: .5em .7em; opacity: 0; }
.fx-pane-on { opacity: 1; }

/* The rings draw to their value. The dash array is the circle's full
   circumference (r=7, so 43.98) and the offset is what is left of it,
   set per ring in the markup as --fx-dash. Resolved is the resting
   state; only a revealing grid winds them back to empty first. */
.fx-ring-fg { stroke-dasharray: 43.98; stroke-dashoffset: var(--fx-dash); }
.fx-grid.is-revealing .fx-ring-fg { stroke-dashoffset: 43.98; }
.fx-card.is-in .fx-ring-fg { animation: fx-draw 1.1s .35s cubic-bezier(.22, 1, .28, 1) both; }
@keyframes fx-draw {
  from { stroke-dashoffset: 43.98; }
  to   { stroke-dashoffset: var(--fx-dash); }
}

/* ── the remaining features ───────────────────────────────────── */
/* Everything the previews do not cover. No card and no box - just a mark,
   a line and a sentence - so it reads as the rest of the same list rather
   than as a second wall of tiles.

   Every item carries a mark now. Twelve headings with nothing beside them,
   directly under nine illustrated cards, read as the page running out of
   pictures; the chip is the argument section's, so the two text blocks on
   this page are marked the same way and the product's own black surface
   turns up in both. */

.fx-more {
  margin-top: 72px;
  padding-top: 36px;
  border-top: 1px solid var(--edge);
  display: grid;
  gap: 34px 40px;
  /* the same track as the previews above, so the two blocks line up
     column for column instead of reading as separate grids */
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.fx-more h3 {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -.015em;
  margin-bottom: 6px;
}
.fx-more p {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--slate);
}

/* The mark stacks above the heading rather than sitting beside it. Beside
   it - which is what the argument section does - costs 50px of every
   280px column, and these paragraphs are twice as long as that section's. */
.fx-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin-bottom: 12px;
  border-radius: 10px;
  background: #000;
  color: rgba(255, 255, 255, .78);
  box-shadow: 0 4px 12px rgba(16, 15, 18, .18);
}
.fx-mark svg { width: 19px; height: 19px; display: block; }
/* Colour only where the app itself colours the thing: the nudge ladder is
   amber in the notch, typing back is the asking blue, and the local
   guarantee takes the healthy green. Everything else stays white, or the
   row turns into a paint chart. */
.fx-mark-ok   { color: var(--ct-ok); }
.fx-mark-warn { color: var(--ct-attention); }
.fx-mark-done { color: var(--ct-done); }

/* The app's own idle sprite, still running, at chip size. The sheet is six
   64px frames and the app draws a 58x46 window into it, dropped 18px, at
   1:1. Everything below is that same geometry times 0.4125 - the window,
   the sheet, the drop and the travel - so the walk still lands on whole
   frames. Change the factor and every number here changes with it; change
   one of them on its own and the cat tears. */
.fx-mark-cat {
  width: 24px;                    /* 58  x .4125 */
  height: 19px;                   /* 46  x .4125 */
  background-image: url("assets/characters/cat-idle.png");
  background-size: 158.4px 26.4px;/* 384 x 64, x .4125 */
  background-position: 0 -7.43px; /* -18 x .4125 */
  background-repeat: no-repeat;
  image-rendering: pixelated;
  animation: fx-cat 1s steps(6) infinite;
}
@keyframes fx-cat { from { background-position-x: 0; } to { background-position-x: -158.4px; } }

/* ── narrow ───────────────────────────────────────────────────── */

@media (max-width: 640px) {
  .fx-grid { gap: 32px; }
  /* One card across. A fixed aspect at full width is nearly all empty
     box, so here the box takes the height its fragment needs and the
     floor keeps the shortest of them from looking squashed. */
  .fx-box { aspect-ratio: auto; max-height: none; min-height: 190px; padding: 22px; }
  .fx-more { margin-top: 56px; gap: 24px; }
}

/* ── reduced motion ───────────────────────────────────────────── */
/* No reveal, no beats: every fragment is simply present, already in
   its resolved state. features-demo.js does not observe at all in this
   mode, so nothing here has to fight a class it might add. */

@media (prefers-reduced-motion: reduce) {
  .fx-grid.is-revealing .fx-card { opacity: 1; transform: none; transition: none; }
  .fx-card .fx-cursor { display: none; }
  .fx-grid .fx-ring-fg { stroke-dashoffset: var(--fx-dash); }
  .fx-card * { animation: none !important; }
  /* The sprite loops forever and lives outside .fx-card, so the blanket
     rule above never reaches it. It holds its first frame instead of
     disappearing: the line is about the character, so the character stays. */
  .fx-mark-cat { animation: none !important; }
  /* The plan never scrolls here, so there is no half line at either edge
     for the fade to explain - it would only be dimming the heading. */
  .fx-plan { -webkit-mask-image: none; mask-image: none; }
}
