import { style } from '@vanilla-extract/css';
import { color, toRem } from 'folds';
import { TABS_ROW_PX } from '../stream-header/geometry';

// Pager root. Sits inside the authed shell's row-flex slot
// (ClientLayout → Box grow=Yes), so `flex: 1 1 0` fills the slot
// horizontally; `align-items: stretch` on the parent fills vertically.
//
// `touch-action: pan-y` lets the browser keep doing native vertical
// scroll (DM list virtualizer, curtain pull-to-refresh) without us
// having to call preventDefault on every move — only the pager's own
// listener calls preventDefault, and only after axis-resolve commits
// to "horizontal".
//
// `SurfaceVariant.Container` backdrop — THE single static light-blue
// canvas of the owner's composition model (his words, Jun 2026):
// «есть чисто статический бекграунд светлосиний (#181a20 в тёмной
// теме), растянут поверх всего экрана; в шапке статичные вкладки —
// просто надписи на этом фоне; ниже — тёмная (#0d0e11) шторка чатов
// со скруглением вверху; шторки листаются как картинки в галерее,
// между ними ничего — пустой прозрачный геп, виден этот бекграунд;
// оттянул шторку до низа — виден этот же бекграунд». So this one
// backdrop intentionally shows through (a) the inter-pane gap during
// a swipe, (b) the deep-pull void when the curtain is dragged far
// down, and (c) any sub-pixel seam at rest. DO NOT darken it «to
// match the curtain» — that was tried and rejected on device: a dark
// gap reads as «чёрные полосы» between sheets, and the deep-pull
// void merges with the curtain into one undifferentiated dark slab.
//
// `color: Background.OnContainer` mirrors what the route-level
// `<PageRoot>` would have set via its `ContainerColor({ variant:
// 'Background' })` wrapper. We mount Direct/Channels/Bots directly
// here, bypassing PageRoot for the swipe-pager experience, so without
// this declaration the form labels rendered inside the per-pane
// StreamHeader curtain (`<Text size="L400">` for Username/Server/
// Options) had `color: inherit` cascading all the way up to `body`,
// which sets no color → browser default black. The labels became
// invisible against the dark form background only on native (where the
// pager activates). Desktop / web go through PageRoot and inherit the
// expected light tone for free.
export const pagerRoot = style({
  position: 'relative',
  flex: '1 1 0',
  minWidth: 0,
  minHeight: 0,
  height: '100%',
  overflow: 'hidden',
  touchAction: 'pan-y',
  backgroundColor: color.SurfaceVariant.Container,
  color: color.Background.OnContainer,
});

// Shared static tabs row painted BEHIND the strip in DOM order.
// Reserves the status-bar safe-area inset via padding-top so the
// segments + icons sit just below the system status bar, and so the
// backdrop colour extends through the inset zone (matching the per-pane
// PageNav's own `paddingTop: var(--vojo-safe-top)` so there's no
// visible band boundary at the inset edge).
//
// Curtain-overlay invariants (why no z-index here at rest):
//
//   The chats curtain must visually rise ABOVE this header when the
//   user pulls it up to the «pinned» snap — like a real blind sliding
//   over the segments rather than the segments moving. The curtain
//   lives inside each pane > stage with `z: 2` in stage's local
//   stacking context. The stage / pane stack inside the swipe `strip`,
//   which creates its own stacking context via `transform`. To let the
//   curtain visually surface above this static header we:
//
//     (a) leave both elements at `z-index: auto` in pagerRoot's
//         stacking context (this block has no `zIndex` AT REST), so
//         painting order falls back to DOM order. `pagerStaticHeader`
//         is rendered BEFORE `strip` in MobileTabsPager — so the
//         strip (and everything inside it that paints opaquely) paints
//         on top.
//
//     (b) tag the strip with `data-pager-pane="true"`. All per-pane
//         background paints (PageNav-inner surface, MobileSettings/
//         ChannelsWorkspace appBody, StreamHeader stage + header)
//         become transparent under that selector, so the static
//         header tabs show through every transparent layer of the
//         strip until the curtain — the only remaining opaque element
//         — covers them by being positioned at top: 0 (pinned snap).
//
//   Breaking (a) or (b) re-introduces the «paravozik» regression where
//   the tabs visually slide with the curtain. See git history for the
//   user-feedback trail.
//
// Conditional z-elevation (horseshoe-active override, suppressed by pin):
//
//   When a horseshoe sheet (Settings or workspace switcher) is
//   geometrically active — i.e. `expandedPx > 0`, which covers both
//   the in-flight drag and the committed-open state — the wrapping
//   container paints `VOJO_HORSESHOE_VOID_COLOR` (= #000 in dark
//   theme) across the entire pane so the carve at the sheet's top
//   reads as a dark seam. With the transparent strip stack from (b),
//   that void would bleed up through the safe-top + tabsRow zone,
//   turning the system-tray strip + tabs solid black.
//
//   `MobileTabsPagerHeader.tsx` bumps this element to a positive
//   `zIndex` (inline style, driven by `mobileHorseshoeActiveAtom`)
//   from the first frame of drag. Positive z beats the strip's
//   `z: auto` stacking context, putting the static header back on
//   top in the safe-top + tabsRow band — the void is contained to
//   the carve area, tabs stay visible. The horseshoe's `appBody`
//   flips back to opaque on the same signal so the void doesn't
//   bleed into the mascot/form band between the static header and
//   the curtain top either. The curtain pin gesture is gated off
//   in the same state (see `StreamHeader.gestureDisabled`) so no
//   pin can race the elevation flip.
//
//   Pinned-override: when the active pane's curtain is pinned, the
//   curtain itself sits at the top of the stage (z:2 inside the
//   strip's stacking ctx) and covers everything from `y = safe-top`
//   downward — including the tabsRow band. Above the curtain
//   (y=0..safe-top) the opaque appBody contains the void. Elevating
//   the static header in that state would visibly slice the pinned
//   curtain in the tabsRow band, popping tabs over what the user
//   explicitly pulled up to cover. So `MobileTabsPagerHeader`
//   suppresses elevation whenever `curtainPinnedByTabAtom[activeTab]`
//   is true — preserves the «pinned hides tabs» invariant across
//   sheet open/drag without re-introducing the void leak.
export const pagerStaticHeader = style({
  position: 'absolute',
  top: 0,
  left: 0,
  right: 0,
  paddingTop: 'var(--vojo-safe-top, 0px)',
  // The wrapped tabsRow has its own height of TABS_ROW_PX via the
  // stream-header recipe; we don't set a fixed height here so the
  // status-bar inset adds on top naturally.
  backgroundColor: color.SurfaceVariant.Container,
});

// Horizontal strip carrying all three panes side-by-side. Width &
// transform are computed inline in the JSX (they depend on tabs.length
// and visualIdx + visualDragPx, and the gap math couples to them), as
// is `counterTx` — the negated strip X handed to the strip-hosted
// refresh singleton for its counter-translation (see
// `pagerRefreshSingleton` below for the full contract, including why
// the chrome must stay INSIDE the strip).
//
// `gap: PANE_GAP_PX` is what makes the inter-pane void visible during
// a swipe — the pagerRoot's SurfaceVariant.Container colour shows
// through the gap, matching the static header tone exactly.
// NO `will-change: transform` here — deliberately (Jun 2026). The strip
// is ALREADY composited by its always-on inline `translate3d(...)` (a 3D
// transform is a direct compositing reason), so will-change added
// nothing for promotion — but it DID set cc's raster-even-if-not-drawn
// on the strip's content (cc TransformNode::will_change_transform),
// forcing the WebView to keep tiles for the entire ~3.2-screen-wide
// strip, including off-screen panes. With all three curtains pulled to
// the refresh snap and the user shuttling between tabs, that standing
// cost blew the tile budget (489 «tile memory limits exceeded» warnings
// in one 14s device session) — dropped curtain tiles read as flicker
// and the rings «просвечивали» through. Without the flag, off-screen
// tiles demote and evict under pressure; the commit-slide transition
// still runs on the compositor because the layer exists either way.
export const strip = style({
  display: 'flex',
  flexDirection: 'row',
  height: '100%',
});

// ── The pull-to-refresh chrome singleton's host ─────────────────────
//
// WHAT: the screen-static pull-to-refresh chrome (mascot card + radar
// rings), mounted ONCE as the strip's FIRST child
// (`PagerRefreshSingleton` in MobileTabsPager.tsx) and counter-
// translated against the strip's live translation. The strip's
// always-on inline `translate3d` makes it the containing block for
// absolute descendants, so `left: 0` is the STRIP origin — negating the
// strip's X lands the host at the screen origin for every active tab,
// with no per-pane offset needed.
//
// The counter-transform itself is INLINE (PagerRefreshSingleton.tsx):
// `translateX(counterTx) translateZ(0)` + the strip's transition string
// verbatim, both passed as props from MobileTabsPager so the two
// transforms are committed in the same React style flush and — during
// the 280ms commit-slide — both transition on the COMPOSITOR in
// lockstep. History (device-verified, Jun 2026): a registered
// `--vojo-strip-tx` @property transition was tried first; custom-
// property interpolation runs on the MAIN thread, and the `navigate()`
// route commit at slide start reliably froze it for the first frames of
// every swipe commit — the mascot rode the strip, then snapped back
// («дёргает в сторону свайпа»), and the per-frame main-thread re-raster
// pressured the tile budget under a double-dwell shuttle until curtain
// tiles dropped (rings «просвечивали» through). The translateZ(0)
// promotion is part of the fix: it moves the host's transform onto the
// compositor and stops the strip-layer re-raster; the layer is cheap —
// the host paints nothing, tiles materialise only for the card band
// (and the rings canvas is its own untiled TextureLayer — see
// RadarPulse.tsx).
//
// WHY INSIDE THE STRIP (the hoist postmortem — empirically established
// on device, Jun 2026): hosted OUTSIDE the strip (at the pager root)
// the WebView tile-memory budget blew at ~80 «tile memory limits
// exceeded» warnings/second while the rings animated — dropped tiles,
// curtain flicker, stuttering rings. Hosted inside: 0 warnings.
// In-strip hosting keeps the feature's promoted-layer set down to ONE
// rings canvas (an untiled TextureLayer — its ~7MB×2-3 buffers never
// enter the tile budget at all; see the RadarPulse.tsx postmortem),
// ONE video, and this host's empty container layer — instead of
// stacking un-cullable full-screen layers at the pager root. (Do NOT over-claim occlusion culling of the
// rings by the curtains — whole-layer contents-opaque is likely
// defeated by the curtain's rounded corners; the budget argument is
// parity with the empirically proven arrangement. See also the strip
// comment above: `will-change: transform` was removed from the strip
// because it forced raster-even-if-not-drawn across all ~3.2 screens of
// pane content and blew the budget when every curtain was parked at the
// refresh snap during a shuttle swipe.)
//
// PAINT ORDER: this host (absolute + transform ⇒ a z:auto stacking
// context) is the FIRST positioned z:auto participant of the strip's
// stacking context in tree order; every pane surface that must cover
// it is positioned-later-in-tree (the `pane` class below is
// position:relative; the horseshoe container is relative, its appBody
// is absolute with an always-on clip-path stacking context, the
// silhouette is absolute; the StreamHeader stage is relative) or
// positive-z (header z:1, curtain z:2). NOT plain «DOM order»: a
// non-positioned flex item would paint in the earlier in-flow phase
// BELOW this positioned host regardless of order. INVARIANT: any
// opaque paint inside a pane between backdrop and curtain must be
// positioned or a stacking context, or it will paint UNDER the rings.
//
// HARD RULES: never give this host a positive z-index (it would lift
// the chrome above the curtains), and never use `will-change` here
// (unlike plain translateZ it forces raster-even-if-not-drawn — the
// chrome would keep rastering behind the closed curtains).
//
// GEOMETRY: `top: 0` — NOT stagePulseLayer's `-safe-top` offset (that
// one is STAGE-relative; the stage starts at y = safe-top). This strip
// child's y-origin is already the screen top, since pagerRoot fills
// from y = 0. `stagePulseCanvas`'s safe-top-relative HEIGHT therefore
// yields the identical ring centre as in-stage, and the rings still
// spill into the status-bar zone.
//
// P1c DETERMINATION (recorded so it isn't re-litigated): the thin
// 0.26-opacity rings paint OVER the static tab glyphs. Putting the
// labels above the rings would require z-elevating the static header
// exactly while revealing — but the reveal flag is also true during
// every curtain drag, including the pin gesture, and an elevated
// header there paints tabs OVER the rising curtain (the documented
// pin-contract regression); a dwell-only elevation causes visible
// z-pops. Keep rings-over-labels.
export const pagerRefreshSingleton = style({
  position: 'absolute',
  top: 0,
  bottom: 0,
  left: 0,
  width: '100vw',
  overflow: 'hidden',
  pointerEvents: 'none',
});

// Positions the shared mascot card inside `pagerRefreshSingleton` at
// the exact screen Y the per-pane card occupied: below the safe-top
// inset plus the tabs row. The card itself (`stream-header/
// StreamHeader.css.ts::mascotCard`) brings its own height + centring +
// opaque backdrop. No promotion needed here: the HOST is the composited
// layer (see above) and repositions on the compositor; the card rasters
// once into the host's layer.
export const pagerMascotSlot = style({
  position: 'absolute',
  top: `calc(var(--vojo-safe-top, 0px) + ${toRem(TABS_ROW_PX)})`,
  left: 0,
  right: 0,
});

// Each pane is exactly one viewport wide. CRITICALLY `display: flex;
// flex-direction: row` so the nested Folds PageNav (which is a flex
// child with `flex-grow: 1` on mobile to override its 256px recipe
// width) expands to fill the pane. A column-flex parent here would
// leave PageNav at 256px — the bug that ate the previous attempt.
//
// No paddingTop here: the per-pane StreamHeader still renders its
// own tabs row (kept for the curtain's TABS_ROW_PX snap math, just
// painted invisible via `opacity: 0` — load-bearing because
// `visibility: hidden` would remove the row from hit-testing and
// the per-pane Segments need to capture taps at rest, see
// `StreamHeader.tsx` tabsRow rationale), and PageNav's inner column
// reserves the status-bar safe-area inset via its own
// `paddingTop: var(--vojo-safe-top)`. The static header overlay at
// the pager root simply paints OVER the same screen zone, so the
// underlying geometry stays identical to non-pager mode.
//
// `position: relative` (z-index:auto — deliberately NO z, so no
// stacking context and no layer): makes each pane a tree-order
// participant of the strip's positioned paint phase, painting AFTER
// the DOM-first refresh singleton. This hardens the singleton-below-
// panes ordering against any future non-positioned stacking-context
// wrapper (opacity/filter/contain) inside a pane, which would
// otherwise drop the pane's whole subtree — curtain included — into
// the in-flow phase below the host. See `pagerRefreshSingleton`.
export const pane = style({
  position: 'relative',
  display: 'flex',
  flexDirection: 'row',
  flexShrink: 0,
  width: '100vw',
  height: '100%',
  minWidth: 0,
});

// ── Navigation-stack host (swipe-to-go-back) ────────────────────────────
//
// On mobile + native, MobileTabsLayout renders the listing pager as a
// persistent, STATIC base layer and mounts a chat as a sliding OVERLAY on
// top (see `features/.../swipe-back`). `tabsHost` is the shared positioning +
// CSS-custom-property host: the overlay writes `--swipe-x` (px the chat card
// is dragged right) onto this element and toggles `data-swipe-dragging`. The
// base layer does NOT move or dim — dragging the chat card right just reveals
// the static screen underneath, like a shutter.
//
// `tabsHost` takes over the flex-slot sizing that `pagerRoot` carries when the
// pager is the only child — when the pager is the base layer it's absolute
// inset:0 inside this host instead.
export const tabsHost = style({
  position: 'relative',
  flex: '1 1 0',
  minWidth: 0,
  minHeight: 0,
  height: '100%',
  overflow: 'hidden',
});

// The base layer wraps the persistent (inert, while a chat is up) pager.
// `display:flex; column` so a child fills the layer's height the same way the
// authed shell's flex slot used to: the pager's `pagerRoot` carries its own
// height:100%, but the pager's invalid-space early-return renders a bare
// `<Outlet/>` (JoinBeforeNavigate) with no height source of its own — a flex
// column parent stretches it, matching the pre-`tabsHost` layout.
export const tabsBaseLayer = style({
  position: 'absolute',
  inset: 0,
  display: 'flex',
  flexDirection: 'column',
});
