// ────────────────────────────────────────────────────────────────────
// StreamHeader geometry — shared constants for the curtain layout.
//
// Mental model: the chats card is a curtain layered ABOVE the header
// (z-index higher). The curtain's `top` is the visible part of the
// header below the always-pinned tabs row. When the curtain is fully
// closed it sits flush under the tabs row (covering the mascot card +
// form area beneath). Dragging it DOWN reveals more of the header from
// underneath. Dragging UP raises the curtain back over the header.
//
// Snap stops (curtain.top, px):
//   pinned       = 0  (curtain sits flush at top of the stage, tabs row
//                      covered; the safe-top status-bar strip above the
//                      stage stays painted by the surrounding context —
//                      see «pinned visual contract» below)
//   closed       = TABS_ROW_PX
//   refresh      = TABS_ROW_PX + MASCOT_CARD_PX  (dancing-mascot pull-to-
//                  refresh card fully revealed; a MOMENTARY snap — entering
//                  it pokes /sync and auto-closes once sync returns)
//   form:*       = TABS_ROW_PX + formHeight  (NO breather — the search
//                  results clip flush at the curtain's top edge; the
//                  form's own height already reaches the curtain)
//
// Pinned visual contract: at `pinned` the curtain's top edge RESTS at
// y = safe-top in viewport coords (because the stage starts after the
// PageNav / appBody padding-top: var(--vojo-safe-top)). The system tray
// strip stays painted by appBody / PageNav-inner / MobileTabsPager's
// static header — all of which use `SurfaceVariant.Container` for that
// zone, so the colour is continuous across surfaces. The curtain MUST
// NOT REST inside the safe-top zone (otherwise system text is covered)
// and MUST NOT add internal padding-top (otherwise the chat list grows
// visually taller). This is a REST-state invariant: every commit
// destination (pinned = 0) honours it, while a live `closed-free` /
// `close-refresh` drag intentionally has NO upper clamp and may
// overshoot into the safe-top under the finger (the «drag up
// off-screen freely» feel) — the release always settles back to a
// legal snap. We deliberately do not add any padding inside the
// curtain to enforce the second half.
// ────────────────────────────────────────────────────────────────────

// Tabs row height. Always visible above the curtain.
export const TABS_ROW_PX = 64;

// Web-only tabs-row height override. Matches folds `<Header size="600">`
// = 3.375rem = 54 px, which is the height the right-side `PageHeader`
// in the room chat panel (see `components/page/Page.tsx::PageHeader`)
// renders at. The 1 px divider rule on web lives as `tabsRow.
// borderBottom` so — under the global `* { box-sizing: border-box }`
// reset — it lands at y=53→54 inside this 54 px box, exactly matching
// where PageHeader's `outlined: true` border-bottom paints on the
// right pane. The two panes thus share one visible header baseline.
//
// Native keeps `TABS_ROW_PX` because the pin-gesture travel
// (`PIN_TRAVEL_PX`) is anchored to it and shrinking it would change
// the curtain's snap geometry. Web has no pin gesture, so the override
// is applied through two coordinated levers:
//   1. CSS `[data-platform="web"] &` selectors on `tabsRow` (height
//      → `WEB_TABS_ROW_PX`, plus the divider as `borderBottom`).
//   2. TSX `platformOffset = WEB_TABS_ROW_PX - TABS_ROW_PX` (= -10)
//      added to `curtainTop` so the closed / refresh / form snaps all
//      ride the reduced tabs row without recomputing `snapTopPx`.
export const WEB_TABS_ROW_PX = 54;

// Dancing-mascot pull-to-refresh card. The curtain's `refresh` snap
// rests `MASCOT_CARD_PX` below the tabs row, revealing the card: the
// mascot above a short caption («Обновляем…»), stacked in a centred
// column.
//
// The mascot fits ENTIRELY inside the card (no clipping). The dance clip
// fills ~88% of its 384² frame (the radar rings reach near the edges, so
// there's little transparent margin to crop), so the `MASCOT_VIDEO_PX`
// square video box is kept SMALLER than the card and rendered
// `object-fit: contain`, centred — displayed mascot ≈ 0.88 ×
// MASCOT_VIDEO_PX (≈ 84px). `MASCOT_CARD_PX` budgets the box + caption +
// gaps; the frame's ~6% margin adds the breathing room. Painted in the
// header's `SurfaceVariant.Container` tone.
export const MASCOT_VIDEO_PX = 96;
export const MASCOT_CARD_PX = 124;

// Radar-pulse ring field — the square box (px) the concentric rings
// expand inside, centred on the mascot. 760 is the APPROVED look —
// byte-for-byte the original ring field — and is load-bearing twice
// over. DO NOT GROW IT:
//
//   (1) Speed/feel: a ring scales 0.05 → 1 over a fixed 4s, so the box
//       size IS the travel speed. An attempt to widen the sweep to
//       1600 made the rings (and especially the tap-echo rings)
//       visibly «вылетать» ~2× faster than the approved pace —
//       user-rejected on device.
//   (2) Memory: the field is one transparent 2D canvas (RadarPulse),
//       and the box's bottom edge sets the canvas height — the GPU
//       buffers (~7 MB × 2–3 at dpr 3) scale with it. The WebView
//       tile-memory budget around this feature has NO headroom — an
//       attempted hoist of the ring field out of the strip blew it
//       even at 760² (see the `pagerRefreshSingleton` postmortem in
//       mobile-tabs-pager/style.css.ts); a bigger box only digs the
//       hole deeper.
//
// «Rings across the whole screen / mascot doesn't ride the tab» is
// achieved by the HOST instead: in pager mode the chrome renders ONCE
// in a strip-hosted singleton (PagerRefreshSingleton) counter-
// translated against the strip's live translation (inline `counterTx`
// prop, compositor-transitioned in lockstep with the strip), so it
// stays screen-static during tab swipes and the rings spill across the
// inter-pane seam — while the field keeps the approved size and pace,
// and stays inside the strip where the tile budget is proven to hold.
export const PULSE_SPAN_PX = 760;

// Vertical centre of the radar pulse, measured from the TOP OF THE
// SCREEN minus the safe-top inset — i.e. the canvas height formula adds
// `var(--vojo-safe-top)` itself. = tabs row + the mascot card's top
// pad + half the video box, so the rings emanate from the mascot's
// centre. Encoded in `stagePulseCanvas`'s height (StreamHeader.css.ts);
// RadarPulse recovers the draw centre as
// height − (PULSE_SPAN_PX/2 + PULSE_EDGE_PX). Both hosts' layers start
// at the screen top, so one formula serves either.
export const PULSE_CENTER_Y_PX = TABS_ROW_PX + 53;

// Canvas headroom (px) below the largest ring's outer edge: half the
// full-scale 2px stroke plus 1px of anti-aliasing, so the ring never
// clips at the canvas's bottom edge.
export const PULSE_EDGE_PX = 2;

// Initial estimate for the search form's outer height. The actual
// height is measured at runtime via ResizeObserver and adapts to the
// available viewport so the form never overflows the chats card.
export const SEARCH_FORM_BASE_PX = 360;

// Curtain snap transition. Tuned tight for an in-app reveal —
// emphasized-decelerate territory.
export const CURTAIN_SNAP_MS = 280;
export const CURTAIN_SNAP_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)';

// Curtain card top-corner radius. Matches the composer card and the
// horseshoe surfaces elsewhere in the app.
export const CURTAIN_RADIUS_PX = 24;

// Curtain displacement (px) the user must drag DOWN from closed before
// release for the refresh to commit. Absolute (NOT a fraction of the
// card height) so the pull-to-refresh trigger distance stays a
// comfortable thumb-pull regardless of how tall the mascot card is.
// On the rubber-banded body surface (×RUBBER_BAND) the finger pull
// needed to reach this displacement is correspondingly longer.
export const REFRESH_COMMIT_PX = 88;

// Refresh dance timing. The mascot stays revealed for AT LEAST
// `REFRESH_MIN_MS` — two full turns of the 2.0s mascot loop — so the pull
// reads as a deliberate ≥2-cycle dance, not a flicker. Paired with the
// loop-boundary close (StreamHeader reads `video.currentTime`), it then ends
// on a clean cycle once /sync returns AND this minimum has elapsed.
// `REFRESH_MAX_MS` caps the dance so an offline client (sync never returns)
// can't strand the curtain open — it closes regardless after the cap.
export const REFRESH_MIN_MS = 4000;
export const REFRESH_MAX_MS = 8000;

// Touch gesture tuning. RUBBER_BAND dampens finger→curtain motion on
// the body surface so the refresh pull feels resistive (physically
// «heavier» than the handle's crisp 1:1). DIRECTION_DEAD_ZONE_PX is
// the finger travel before a drag resolves to up/down.
// ACTIVE_CLOSE_THRESHOLD_PX is the pull-up distance (raw finger px)
// required to close an active form OR dismiss the refresh card early.
export const RUBBER_BAND = 0.65;
export const DIRECTION_DEAD_ZONE_PX = 10;
export const ACTIVE_CLOSE_THRESHOLD_PX = 100;

// Total vertical CURTAIN travel for the closed ↔ pinned gesture.
// Equals the tabs row height because pinning lifts the curtain by
// exactly that distance (from y = TABS_ROW_PX down to y = 0 inside
// the stage).
export const PIN_TRAVEL_PX = TABS_ROW_PX;

// Commit threshold for pin / unpin. Tuned very high (≈95%) so the
// user must drag the curtain almost-all-the-way to the cap before
// release for the snap to flip. Anything shorter reads as accidental
// and springs back to the previous resting snap.
//
// On the handle the up direction is 1:1 with no upper clamp (the
// «closed-free» transition spans the full pin↔closed↔refresh range in
// one gesture and the curtain follows the finger off-screen freely);
// the committing curtain DISPLACEMENT is `PIN_COMMIT_THRESHOLD ×
// PIN_TRAVEL_PX` ≈ 61 px — essentially «drag the curtain across the
// full tabs-row height». On the body the same displacement is reached
// with a longer finger pull because the body path is rubber-banded
// (×0.65).
//
// `pinned-free` (handle-only) lower-bounds the live delta at 0 (no
// destination above pinned) and commits unpin at this threshold. The
// body never resolves to `pinned-free` — unpin stays a deliberate
// drag-handle pull.
export const PIN_COMMIT_THRESHOLD = 0.95;

// Drag-handle hit-zone at the top of the curtain. NATIVE-ONLY: the
// handle is rendered only when `isNativePlatform()` is true (see
// StreamHeader.tsx) — on web (desktop / Electron) the curtain has
// no interactive snap states, so the handle would be pure
// decoration and is omitted entirely.
//
// On native the handle is the AUTHORITATIVE gesture surface —
// closed-free / unpin / close-refresh / form-close all bind here with
// 1:1 finger ↔ curtain tracking, no matter whether the chat list
// inside the curtain is scrollable. See `useCurtainHandleGesture`
// for the full state machine.
//
// A parallel `useCurtainBodyGesture` bound to the curtain's body
// handles drag from anywhere on the card, but only when the inner
// chat list has no scrollable content AND the curtain isn't pinned
// (unpin is handle-only). Its dynamics are rubber-banded so the
// body drag reads as physically «heavier» than the handle's crisp
// pull.
//
// Size: 32 px tall — enough touch target to land on comfortably with
// a thumb (the visible grabber pill inside is much smaller, see
// `StreamHeader.css.ts::handleBar`). The list (or DirectEmpty / the
// equivalent placeholder) starts 32 px below the curtain's top edge
// on native; on web the list sits flush at the curtain's top.
export const HANDLE_HEIGHT_PX = 32;
