/* ============================================================================
   app-shell.css — the two pieces of chrome that must be IDENTICAL on
   index.html, extra.html and pro.html: the desktop Home/Extra/Pro switch and
   the phone bottom bar's travelling indicator.
   Loaded by all three pages. Deliberately the first shared stylesheet on this
   site: everything else is duplicated per page, and the whole point of these
   two controls is that a visitor meets the same thing wherever they are.

   Self-sufficient by design. Every token is read with a fallback
   (var(--ink,#E8EFE9)) because the three pages do not define the same set —
   extra.html and pro.html carry their own trimmed palettes.
   ============================================================================ */

:root{
  --as-ink:#E8EFE9;
  --as-line:rgba(232,239,233,.13);
  --as-panel:rgba(232,239,233,.05);
  --as-mono:ui-monospace,"Cascadia Mono","SF Mono",Consolas,"Liberation Mono",monospace;
}

/* The three legacy mode buttons stay in the DOM so index.html's existing JS keeps its
   handles, but the switch is what the visitor sees. */
.asRetired{display:none !important;}

/* ============================ THE MODE SWITCH ============================
   Three destinations, one control, same place on every page. It replaces the
   old arrangement of three separate buttons whose labels changed meaning by
   context (#extraBtn / #modeBtn / #goProBtn) — where "Pro" could mean either
   "go to the pro product" or "leave pro", depending on where you already were.
   A segmented control cannot have that problem: the current page is the lit
   segment, and every segment is a destination. */
.asSwitch{
  position:relative;display:inline-flex;align-items:stretch;
  padding:3px;border-radius:100px;
  background:var(--hero-panel,var(--as-panel));
  border:1px solid var(--hero-line,var(--as-line));
  /* Sits above the moving pill's glow so the labels stay legible. */
  isolation:isolate;
}
.asSwitch__pill{
  position:absolute;top:3px;left:0;height:calc(100% - 6px);
  border-radius:100px;pointer-events:none;z-index:0;
  background:rgba(232,239,233,.13);
  border:1px solid rgba(232,239,233,.26);
  box-shadow:0 0 12px rgba(232,239,233,.10);
}
.asSwitch__pill.asNoTrans{transition:none;}
.asSeg{
  position:relative;z-index:1;
  background:none;border:0;cursor:pointer;font-family:inherit;
  font-size:12.5px;font-weight:600;letter-spacing:.01em;line-height:1;
  padding:7px 14px;border-radius:100px;white-space:nowrap;
  color:rgba(232,239,233,.55);
  transition:color .18s ease;
  -webkit-tap-highlight-color:transparent;
}
.asSeg:hover{color:rgba(232,239,233,.85);}
.asSeg[aria-current="page"]{color:var(--ink,var(--as-ink));}
.asSeg:focus-visible{outline:2px solid var(--ink,var(--as-ink));outline-offset:2px;border-radius:100px;}
/* A locked destination still shows — knowing Pro exists is the point — but says so. */
.asSeg[data-locked="1"]::after{
  content:"";display:inline-block;width:5px;height:5px;margin-left:6px;
  border-radius:50%;background:var(--mid-lite,#EFBE58);vertical-align:middle;opacity:.75;
}
@media (max-width:760px){
  /* On a phone the switch lives in the Daugiau screen, where there is room for it to be
     read rather than squeezed into a header beside the language toggle. */
  .asSeg{font-size:13px;padding:9px 16px;}
}

/* ===================== THE BOTTOM BAR'S TRAVELLING INDICATOR =====================
   The capsule that marks the active tab. It does not slide as a rigid shape: on
   the way over it STRETCHES to span the gap it is crossing and contracts again
   at the destination, which is what makes the movement read as one connected
   gesture rather than a box being teleported.

   NEUTRAL ON PURPOSE. The reference this was taken from gives every tab its own
   hue; this bar cannot. Colour on this site means PRICE — cheap, mid, dear — and
   a red selection ring on an expensive evening would read as an alarm rather
   than as "you are here". The motion is copied; the colour language is not.

   The width is animated rather than scaleX'd: scaling a bordered capsule
   horizontally thins its vertical strokes and smears the round ends, and at this
   size that is visible. It is one absolutely-positioned element, so an animated
   width reflows nothing but itself. */
/* nav.mTabs, not .mTabs: index.html sets `position:static` on the bar inside its own
   ≤760px block (deliberately — the bar is in flow, not over the scroller), that rule is
   later in the cascade, and a media query adds no specificity. The tag selector wins by
   one without touching what the static was there to guarantee: `relative` is still in
   flow, it only gives the absolutely-positioned capsule a containing block to measure
   against. Without it the capsule resolves against the viewport and never moves. */
nav.mTabs{position:relative;}
.mTabInd{
  position:absolute;top:5px;left:0;z-index:0;
  width:46px;height:32px;border-radius:100px;
  border:1.5px solid rgba(232,239,233,.85);
  background:rgba(232,239,233,.05);
  box-shadow:0 0 14px rgba(232,239,233,.22),
             0 0 4px  rgba(232,239,233,.30),
             inset 0 0 10px rgba(232,239,233,.10);
  pointer-events:none;
  /* Hidden until the first measurement lands, so it can never flash at x=0. */
  opacity:0;
}
.mTabInd.asOn{opacity:1;transition:opacity .2s ease;}
/* The tab's own content must sit above the capsule. */
.mTabs .mTab{position:relative;z-index:1;}

@media (prefers-reduced-motion:reduce){
  /* The indicator still moves — it has to, it is the only thing saying which tab is
     live — but it goes straight there with no stretch and no travel. */
  .mTabInd{transition:none;}
}

/* ============================ REFRACTIVE GLASS ============================
   From the technique in the reference clip: blur() only AVERAGES the pixels
   behind a panel, which is why a blurred div still reads as a flat rectangle.
   Real glass BENDS what is behind it, so the convincing version displaces the
   backdrop through noise — feTurbulence into feDisplacementMap — and then adds
   a one-pixel lit edge along the top, which is what sells the thickness.

   THREE DELIBERATE LIMITS ON WHERE THIS IS USED, all of them load-bearing:

   1. Only where something INTERESTING sits behind it. Refraction over a flat
      dark ground is invisible and still costs a full filtered repaint of the
      backdrop every frame. On this site that means the phone's cards and the
      tab bar, which sit over the starfield and the state gradient — not the
      desktop's full-screen bands, which are the most expensive possible target
      and have nothing behind them to bend.
   2. The blur is the BASE declaration and the refraction is layered on top in
      @supports. url() inside backdrop-filter is a Chromium feature today;
      Safari — which is most of this site's phone traffic — does not honour it.
      Written the other way round, an unsupported url() would invalidate the
      whole declaration and Safari would lose the frosting altogether.
   3. Gated behind .asFx on <html>, which app-shell.js sets only when the
      visitor has not asked for reduced motion. No JS, no refraction, still
      frosted.

   Note this REOPENS a decision from the 2026-08-18 design-system pass, which
   replaced every backdrop-filter panel with flat --card + --rule. That removal
   was right for the desktop bands and stays. What comes back is narrower: the
   phone surfaces that already went translucent on purpose so the background
   could show through. */
.asGlass{
  background:rgba(20,27,23,.52);
  -webkit-backdrop-filter:saturate(160%) blur(14px);
  backdrop-filter:saturate(160%) blur(14px);
  /* The lit edge. One pixel, white, low alpha, top only — the thing that stops
     it being a div. */
  box-shadow:inset 0 1px 0 rgba(255,255,255,.34);
}
@supports ((backdrop-filter:url(#asGlassLG)) or (-webkit-backdrop-filter:url(#asGlassLG))){
  html.asFx .asGlass{
    -webkit-backdrop-filter:url(#asGlassLG) saturate(160%) blur(9px);
    backdrop-filter:url(#asGlassLG) saturate(160%) blur(9px);
  }
}
/* A quieter grade for large surfaces: same lit edge, weaker displacement cost. */
.asGlass--calm{
  -webkit-backdrop-filter:saturate(140%) blur(10px);
  backdrop-filter:saturate(140%) blur(10px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22);
}

/* ======================= THE AMBIENT FIELD (portable) =======================
   The starfield and the three drifting colour fields, lifted out of index.html
   so extra.html and pro.html can carry the same background. Deliberately a NEW
   class (.asAmbient, not .ambient): index.html's original is entangled with its
   scroll gauge, its story bands and a pile of z-index rules that took several
   passes to settle, and it works. Nothing is gained by making it share this.

   THE COLOUR CHAIN IS THE WHOLE TRICK. The three pages do not share a palette:
   index has --cheap/--mid/--high, pro has the same NAMES holding index's -lite
   values, and extra calls them --green/--amber/--coral. A blob filled from an
   undefined custom property is an invalid declaration and is dropped silently,
   so a naive port renders three invisible divs. Each fill therefore walks all
   three vocabularies before falling back to a literal. */
.asAmbient{
  --as-cheap:var(--cheap,var(--green,#35A46E));
  --as-mid:var(--mid,var(--amber,#C99732));
  --as-high:var(--high,var(--coral,#D65B40));
  position:fixed;inset:-20vmax;z-index:0;pointer-events:none;overflow:hidden;
  /* saturate() under 1 is the "different tone" — the fields read as weather
     behind the page rather than as a second, larger copy of the price colour
     competing with the verdict that is supposed to carry it. */
  filter:hue-rotate(var(--amb-hue,0deg)) saturate(var(--as-amb-sat,.78));
  transition:filter 1.4s cubic-bezier(.22,1,.36,1);
}
.asAmbient i{
  position:absolute;border-radius:50%;
  width:70vmax;height:70vmax;
  filter:blur(90px);mix-blend-mode:screen;
  /* --as-amb-k is the per-page dimmer. See the [data-app-page] rules below. */
  opacity:calc(var(--as-ambO,.28) * var(--as-amb-k,1));
  will-change:transform;
  transform:translate3d(calc((var(--amb-x,.5) - .5) * var(--lean,6vmax)),calc((var(--amb-y,.5) - .5) * var(--lean,6vmax)),0);
  transition:transform 2.2s cubic-bezier(.22,1,.36,1),opacity 1.4s cubic-bezier(.22,1,.36,1);
}
.asAmbient .a1{left:-10vmax;top:5vmax;--lean:9vmax;
  background:radial-gradient(closest-side,var(--as-high) 0%,transparent 70%);animation:asDrift1 38s ease-in-out infinite alternate;}
.asAmbient .a2{right:-14vmax;top:22vmax;--lean:-7vmax;
  background:radial-gradient(closest-side,var(--as-mid) 0%,transparent 70%);animation:asDrift2 47s ease-in-out infinite alternate;}
.asAmbient .a3{left:20vmax;bottom:-18vmax;--lean:5vmax;
  background:radial-gradient(closest-side,var(--as-cheap) 0%,transparent 70%);animation:asDrift3 55s ease-in-out infinite alternate;}

/* THE STATE MIX. The blob HUES never change; what leans with the price is the
   opacity. The current state's own field leads and the other two drop to a
   trace, so the background says something instead of showing all three states
   at once. Both target pages set body[data-state] themselves (extra in
   renderHero, pro in renderNow) — but BOTH can also fail to set it at all when
   the price feed is empty, so the .28 fallback above has to be a sane neutral
   on its own rather than a value that only makes sense once a state arrives. */
body[data-state="high"]  .asAmbient .a1{--as-ambO:.34;}
body[data-state="high"]  .asAmbient .a2{--as-ambO:.13;}
body[data-state="high"]  .asAmbient .a3{--as-ambO:.07;}
body[data-state="mid"]   .asAmbient .a1{--as-ambO:.10;}
body[data-state="mid"]   .asAmbient .a2{--as-ambO:.36;}
body[data-state="mid"]   .asAmbient .a3{--as-ambO:.07;}
body[data-state="cheap"] .asAmbient .a1{--as-ambO:.07;}
body[data-state="cheap"] .asAmbient .a2{--as-ambO:.12;}
body[data-state="cheap"] .asAmbient .a3{--as-ambO:.34;}

/* Drift with `translate`, never margins. index.html learned this the expensive
   way: animating margins re-lays-out every frame of a 38-second loop and each
   one is recorded as a layout shift — measured at CLS 0.418 and still climbing,
   against a 0.1 threshold. It was not late content, it was the background
   breathing. `translate` composites and is excluded from CLS by definition, and
   it does not collide with the `transform` already carrying the cursor lean. */
@keyframes asDrift1{from{translate:0 0}to{translate:12vmax 16vmax}}
@keyframes asDrift2{from{translate:0 0}to{translate:-10vmax -14vmax}}
@keyframes asDrift3{from{translate:0 0}to{translate:-16vmax -12vmax}}

.asAmbient .asStars{
  position:absolute;inset:0;width:100%;height:100%;
  opacity:calc(.55 * var(--as-amb-k,1));
  transform:translate3d(calc((var(--amb-x,.5) - .5) * -1.6vmax),calc((var(--amb-y,.5) - .5) * -1.6vmax),0);
  transition:transform 1.6s cubic-bezier(.22,1,.36,1);
  animation:none;filter:none;border-radius:0;margin:0;
}
.asAmbient .asCursor{
  position:absolute;left:0;top:0;width:44vmax;height:44vmax;border-radius:50%;
  margin:-22vmax 0 0 -22vmax;
  background:radial-gradient(closest-side,var(--accent,var(--as-cheap)) 0%,transparent 70%);
  opacity:calc(.15 * var(--as-amb-k,1));filter:blur(36px);mix-blend-mode:screen;
  /* The pointer is in VIEWPORT pixels but this element's origin is .asAmbient's
     corner, and the layer is inset -20vmax so the blur can bleed off-screen.
     Without the +20vmax the light sits a fifth of a screen up and to the left of
     the hand, which reads as a lamp following at a distance. */
  transform:translate3d(calc(var(--cur-x,50vw) + 20vmax),calc(var(--cur-y,50vh) + 20vmax),0);
  transition:transform .22s cubic-bezier(.2,.8,.3,1),opacity .6s ease;
  animation:none;
}
.asAmbient .asCursor.idle{opacity:0;}

/* PER-PAGE STRENGTH. extra.html is a television dashboard read across a room and
   its own ground already carries a state-tinted radial glow; at full strength the
   fields fought it and washed the figures. Dimmed hard there, per the brief.
   app-shell.js writes data-app-page on <html>. */
html[data-app-page="extra"] .asAmbient{--as-amb-k:.42;--as-amb-sat:.62;}
html[data-app-page="pro"]   .asAmbient{--as-amb-k:.85;}

/* Content must be lifted above the layer or the fixed ambient paints over it —
   neither page has a root-level stacking context of its own. */
html[data-app-page="extra"] .kiosk,
html[data-app-page="extra"] section.ten,
html[data-app-page="pro"] .wrap{position:relative;z-index:1;}

@media (prefers-reduced-motion:reduce){
  .asAmbient i{animation:none;transition:none;}
  .asAmbient{transition:none;}
  .asAmbient .asCursor{display:none;}
}

/* ===================== THE PHONE APP SHELL (portable) =====================
   The five-screen phone app: a fixed compact page, a scrolling middle, and the
   bottom tab bar. Lifted out of index.html so extra.html and pro.html get the
   SAME bar rather than a third and fourth hand-copy — the divergence this site
   keeps paying for (four independent forecast panels, and a fix reaches one).

   The mechanism is one rule per tab that HIDES every tagged section not
   belonging to it. Nothing is ever forced visible, so [hidden], .hidden and every
   JS reveal keep their exact current meaning inside a screen — which matters
   enormously here, because extra.html gates five of its panels on the hidden
   attribute and pro.html gates two more. */
.mTabs{display:none;}
.asShellOnly{display:none;}

@media (max-width:760px){
  html[data-app-page="extra"] body,
  html[data-app-page="pro"] body{
    height:100dvh;overflow:hidden;display:flex;flex-direction:column;margin:0;
  }
  /* The page stops scrolling and an inner element starts, so the bar can be an
     ordinary in-flow sibling instead of a fixed overlay that content slides under. */
  .asScroll{
    flex:1 1 auto;min-height:0;overflow-y:auto;
    overscroll-behavior-y:contain;-webkit-overflow-scrolling:touch;
  }
  .asShellOnly{display:revert;}

  body[data-mtab="now"]      [data-mscreen]:not([data-mscreen~="now"]),
  body[data-mtab="chart"]    [data-mscreen]:not([data-mscreen~="chart"]),
  body[data-mtab="forecast"] [data-mscreen]:not([data-mscreen~="forecast"]),
  body[data-mtab="market"]   [data-mscreen]:not([data-mscreen~="market"]),
  body[data-mtab="more"]     [data-mscreen]:not([data-mscreen~="more"]){display:none !important;}

  /* --- the bar --- */
  .mTabs{
    display:grid;grid-template-columns:repeat(5,1fr);
    position:static;flex:0 0 auto;z-index:70;
    padding:6px 4px calc(6px + env(safe-area-inset-bottom));
    border-top:1px solid var(--mtabLine,rgba(255,255,255,.09));
    transition:background .9s ease,border-color .9s ease;
  }
  /* Element+class (0,1,1) so it outranks .asGlass (0,1,0): the bar keeps its own
     state-tinted ground and .asGlass contributes only the blur and the lit edge.
     Written the other way round, every page's bar would be the same fixed
     green-grey and would read as wrong over extra.html's brown ground. */
  nav.mTabs{background:var(--mtabBg,rgba(13,18,16,.82));}
  /* The bar leans with the price like the rest of the page, but only just: it sits
     under the thumb for a whole session and a saturated bar would compete with the
     verdict, which is the thing that is supposed to carry colour. A lean, not a paint. */
  body[data-state="cheap"] .mTabs{--mtabBg:rgba(11,23,17,.86);--mtabLine:rgba(95,211,148,.20);}
  body[data-state="mid"]   .mTabs{--mtabBg:rgba(24,19,8,.86); --mtabLine:rgba(239,190,88,.22);}
  body[data-state="high"]  .mTabs{--mtabBg:rgba(26,14,10,.86);--mtabLine:rgba(245,132,101,.22);}

  .mTab{
    display:flex;flex-direction:column;align-items:center;gap:3px;
    background:none;border:0;cursor:pointer;
    font-family:inherit;font-size:10.5px;font-weight:600;letter-spacing:.02em;
    color:var(--ink-faint,rgba(232,239,233,.45));
    padding:5px 2px 4px;border-radius:12px;min-height:48px;
    -webkit-tap-highlight-color:transparent;
    transition:color .18s ease;
  }
  .mTab svg{width:23px;height:23px;display:block;}
  /* Active = ink, never the accent: the accent tracks the price state, and a red
     "selected" glyph on an expensive evening reads as an alert, not a selection. */
  .mTab[aria-current="true"]{color:var(--ink,var(--as-ink));}
  .mTab:focus-visible{outline:2px solid var(--cheap-lite,#5FD394);outline-offset:2px;}

  /* --- per-page release of the desktop shape --- */
  /* THE blocker on extra.html: .kiosk declares min-width:1500px and nothing ever
     resets it, so a 375px phone renders a 1500px box and scrolls sideways. Until
     this is undone no other responsive work on that page is even visible. */
  html[data-app-page="extra"] .kiosk{
    min-width:0;width:auto;height:auto;min-height:0;overflow:visible;display:block;
  }
  html[data-app-page="extra"] .wrap{padding:0 12px;}
  html[data-app-page="extra"] .grid{display:block;}
  /* The columns group cards that belong to DIFFERENT screens, so they must stop
     being boxes and let their children be tagged individually. */
  html[data-app-page="extra"] .col{display:contents;}
  html[data-app-page="extra"] .tenIn{padding:0 12px;}
  html[data-app-page="extra"] .top{flex-wrap:wrap;row-gap:8px;}

  html[data-app-page="pro"] .wrap{padding:0 12px 18px;}
  html[data-app-page="pro"] .g2,
  html[data-app-page="pro"] .g3{grid-template-columns:1fr;}
  html[data-app-page="pro"] .top{flex-wrap:wrap;row-gap:8px;}
  /* The two vh-derived chart heights are the ones that misbehave first on a phone. */
  html[data-app-page="pro"] #tenLead svg{height:clamp(170px,26vh,260px);}
  /* pro.html is really two pages: the entitled dashboard (#app) and the sign-in gate
     (#gate) shown when the payload is not entitled. The gate has no tagged sections
     at all, so every tab would show the same single screen — a five-tab bar that
     visibly does nothing. Hide it there. If :has() is unsupported the rule is simply
     dropped and the bar reappears, which is the behaviour without this rule anyway. */
  html[data-app-page="pro"] body:has(#gate:not(.hidden)) .mTabs{display:none;}
}

/* ======================= WAITING IS A DESIGNED MOMENT =======================
   Reserve the space BEFORE it fills. Late content that arrives into a collapsed
   box does not just look untidy — it moves whatever the reader was about to
   touch, and on a phone that means the tap lands on the wrong thing.

   These are the containers measured as jumping on extra.html: an empty grid, an
   empty bar row and a one-line paragraph that becomes eight. Each is given the
   height its filled state actually occupies, so the arrival is a paint, not a
   reflow. The values are the rendered heights, not guesses.

   Note what is NOT done here: nothing is given a spinner. A spinner says only
   "something is happening"; a held space that then fills says the same thing and
   costs no attention. index.html's #genCard already had this right — it keeps its
   content laid out under `visibility:hidden` rather than removing it — and this
   generalises that instinct to the places that lacked it. */
/* Measured, not guessed: #zGrid fills to 100px and #genCols to 116px, so those are
   the heights held. An earlier pass reserved 132px for #zGrid and made it WORSE —
   the box then shrank by 32px when the data landed, trading a grow for a collapse.
   Under-reserving slightly is the safer error of the two; over-reserving guarantees
   a shift in the opposite direction. */
html[data-app-page="extra"] #zGrid:empty{min-height:100px;}
html[data-app-page="extra"] #genCols:empty{min-height:116px;}
/* #flowRows is deliberately NOT reserved. Its card is `hidden` until flows.php
   answers, so the row box is not what moves — the whole card appearing is, and a
   min-height on its contents cannot fix that. Doing it properly means holding the
   card's own space (or keeping it visible with a real empty state), which needs the
   flows endpoint stubbed to measure honestly. Left alone rather than shipped on a
   guessed number. */
/* One line while empty is what makes a five-to-eight line paragraph shove the
   card below it down the column. Hold a realistic block instead. */
html[data-app-page="extra"] .insBody{min-height:5.2em;}

/* The shimmer, for the one case a held space is not enough: a surface that will
   stay empty for seconds and needs to say it is working rather than broken.
   Opt-in per element, and it stops dead under reduced-motion — a permanently
   sweeping gradient is exactly the kind of ambient movement that setting exists
   to switch off. */
.asSkel{
  position:relative;overflow:hidden;
  background:rgba(232,239,233,.045);border-radius:8px;
}
.asSkel::after{
  content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent 0%,rgba(232,239,233,.08) 50%,transparent 100%);
  animation:asSkelSweep 1.35s ease-in-out infinite;
}
@keyframes asSkelSweep{to{transform:translateX(100%);}}
@media (prefers-reduced-motion:reduce){
  .asSkel::after{animation:none;opacity:.5;}
}
