/* usernode-native v1 — native.css
 *
 * The visual half of the Usernode native-feel UI kit. Centrally hosted
 * (like usernode-bridge.js) at:
 *
 *   https://<USERNODE_DOMAIN>/usernode-native/v1/native.css
 *
 * Canonical source: social-vibecoding/public/usernode-native/v1/native.css.
 * Never vendor per app; load the hosted URL together with native.js.
 *
 * THEMING CONTRACT — apps re-theme by overriding the --un-* custom
 * properties below (on :root, .dark, or any wrapper element); never by
 * fighting selector specificity or forking this file. Every color/radius
 * in the kit routes through these variables. Physics, thresholds and
 * gesture geometry are deliberately NOT themeable — native feel stays
 * uniform across differently-branded apps. The variable list is part of
 * the frozen /v1/ contract: additions are fine, renames/removals require
 * /v2/.
 *
 * Platform skins hang off html.un-ios / html.un-android / html.un-desktop
 * (set by native.js). Dark mode follows the platform idiom: a `.dark`
 * class on <html> (same as app.css / the Tailwind config).
 */

:root {
  /* Accent (defaults: the platform violet, per public/css/app.css) */
  --un-accent: #7c3aed;
  --un-accent-contrast: #ffffff;

  /* Switches */
  --un-switch-track-off: #d4d4e4;
  --un-switch-thumb: #ffffff;

  /* Swipe-action buttons */
  --un-action-danger: #dc2626;
  --un-action-neutral: #6e6e8a;
  --un-action-text: #ffffff;

  /* Kit-drawn chrome (PTR spinner puck) */
  --un-surface: #f4f4f5;

  /* Hairlines, muted text, grouped-list / sheet / nav-bar surfaces */
  --un-hairline: rgba(60, 60, 67, 0.29);
  --un-muted: rgba(60, 60, 67, 0.6);
  --un-group-bg: #ffffff;
  --un-sheet-bg: #f4f4f7;
  --un-navbar-bg: rgba(249, 249, 251, 0.82);
  --un-backdrop: rgba(0, 0, 0, 0.4);

  /* Anchored-popover surface. Deliberately its OWN token rather than
     --un-group-bg: a grouped-list card sits IN the page and is often
     themed as a translucent tint over it, whereas a popover floats over
     arbitrary content with no backdrop behind it and must be opaque or
     the page shows straight through the menu (#847). Defaults to the
     sheet surface — the kit's other floating-overlay family
     (sheet/modal/alert), opaque by contract in both modes — so dark
     mode and app-level re-themes inherit correctly with no second
     override (same pattern as --un-zoom-bg above). */
  --un-popover-bg: var(--un-sheet-bg);

  /* Side panel / drawer (unNative.presentPanel). Surface defaults to the
     sheet's — same "overlay chrome" family — and inherits dark mode
     through it, but is a separate token so an app can split the two.
     The width is the drawer's own geometry knob: apps override the
     variable (or pass opts.width) rather than fighting the selector. */
  --un-panel-bg: var(--un-sheet-bg);
  --un-panel-width: min(20rem, 86vw);

  /* Zoom transition overlay surface ('zoom-in'/'zoom-out'): the moving
     screen gets this opaque background for the animation's duration so
     the grid beneath can't bleed through a transparent screen mid-zoom.
     Dark mode inherits via --un-sheet-bg. */
  --un-zoom-bg: var(--un-sheet-bg);

  /* Toast (surface is dark in BOTH modes — the iOS HUD idiom — so the
     action color defaults to the light accent rather than --un-accent) */
  --un-toast-bg: rgba(28, 28, 42, 0.92);
  --un-toast-text: #ffffff;
  --un-toast-action: #a78bfa;

  /* Radii */
  --un-radius: 0.5rem;
  --un-radius-full: 9999px;
  --un-radius-card: 12px;

  /* Motion — linear() spring curves pre-computed from the SAME integrator
     parameters native.js uses (mass/tension/friction: default 1/170/26,
     stiff 1/250/30, gentle 1/120/24), so CSS and JS motion are one family.
     The cubic-bezier values are the fallback for pre-linear() engines. */
  --un-ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
  --un-ease-spring-stiff: cubic-bezier(0.3, 1, 0.4, 1);
  --un-ease-spring-gentle: cubic-bezier(0.16, 1, 0.3, 1);
  --un-dur-spring: 725ms;
  --un-dur-spring-stiff: 532ms;
  --un-dur-spring-gentle: 1006ms;
  /* One fade clock for dialog cards AND the full-page dim (#1566). */
  --un-dialog-fade-duration: 180ms;
}

@supports (animation-timing-function: linear(0, 1)) {
  :root {
    --un-ease-spring: linear(0, 0.0838, 0.2508, 0.4201, 0.5677, 0.6861, 0.7786, 0.8446, 0.8922, 0.9259, 0.95, 0.9661, 0.9771, 0.9846, 0.9898, 0.9932, 0.9955, 0.997, 0.9981, 0.9987, 0.9992);
    --un-ease-spring-stiff: linear(0, 0.0728, 0.2141, 0.3757, 0.5192, 0.6446, 0.7438, 0.8168, 0.8731, 0.9123, 0.9412, 0.9612, 0.9744, 0.9837, 0.9896, 0.9936, 0.9961, 0.9977, 0.9987, 0.9993, 0.9996);
    --un-ease-spring-gentle: linear(0, 0.1045, 0.2929, 0.4691, 0.6124, 0.7233, 0.8029, 0.8603, 0.9013, 0.9309, 0.9513, 0.9657, 0.9761, 0.9832, 0.9882, 0.9917, 0.9942, 0.9959, 0.9971, 0.998, 0.9986);
  }
}

.dark {
  --un-accent: #a78bfa;
  --un-accent-contrast: #1a1a30;
  --un-switch-track-off: #2e2e50;
  --un-switch-thumb: #eeeef6;
  --un-action-danger: #ef4444;
  --un-action-neutral: #4e4e6a;
  --un-action-text: #ffffff;
  --un-surface: #1a1a30;
  --un-hairline: rgba(120, 120, 150, 0.4);
  --un-muted: rgba(235, 235, 245, 0.6);
  --un-group-bg: #222240;
  --un-sheet-bg: #1e1e38;
  --un-navbar-bg: rgba(18, 18, 32, 0.78);
  --un-backdrop: rgba(0, 0, 0, 0.55);
  --un-toast-bg: rgba(58, 58, 82, 0.94);
  --un-toast-text: #f4f4f8;
  --un-toast-action: #c4b5fd;
}

/* ── Touch polish ──────────────────────────────────────────────────────
   Applies globally when the stylesheet loads: kill the grey tap flash,
   give interactive elements an immediate pressed state instead. The
   pressed transform applies instantly on press (transition only eases
   the release), so feedback has zero latency. */

html {
  -webkit-tap-highlight-color: transparent;
}

button,
[role="button"],
.un-pressable {
  transition: transform 160ms var(--un-ease-spring-stiff), filter 160ms ease;
}

button:active,
[role="button"]:active,
.un-pressable:active {
  transform: scale(0.97);
  filter: brightness(0.92);
  transition: none; /* engage instantly; the release eases back */
}

.dark button:active,
.dark [role="button"]:active,
.dark .un-pressable:active {
  filter: brightness(1.15);
}

/* Never squash a row mid-swipe or a form field. */
input:active,
select:active,
textarea:active,
.un-swipe-row:active,
.un-swipe:active {
  transform: none;
  filter: none;
}

/* ≥44px hit area for small icon buttons without changing layout.
 *
 * The ::after pseudo-element only needs SOME containing block, so the
 * `position: relative` fallback ships at ZERO specificity via :where().
 * That means the helper never overrides the element's own positioning —
 * an app-side `absolute` / `fixed` / `sticky` (Tailwind's `.absolute`,
 * an inline style, anything) wins automatically and supplies the
 * containing block itself, regardless of stylesheet insertion order.
 * Static elements behave exactly as before. So the helper is safe on an
 * overlay control (clear-search "✕" inside a field, a close button
 * pinned to a card corner) with no wrapper element. */
:where(.un-touch-target) {
  position: relative;
}
.un-touch-target::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, 44px);
  height: max(100%, 44px);
  transform: translate(-50%, -50%);
}

/* ── Switches ──────────────────────────────────────────────────────────
   <input type="checkbox" class="un-switch"> — pure CSS, no JS, no extra
   markup. Base = iOS skin (51×31 pill, white thumb); html.un-android
   swaps to the Material 3 track/thumb. Thumb motion rides the stiff
   spring curve at a native-toggle duration. */

.un-switch {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  width: 51px;
  height: 31px;
  margin: 0;
  border: none;
  border-radius: var(--un-radius-full);
  background: var(--un-switch-track-off);
  cursor: pointer;
  vertical-align: middle;
  transition: background-color 200ms ease;
}
.un-switch::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 27px;
  height: 27px;
  border-radius: var(--un-radius-full);
  background: var(--un-switch-thumb);
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12), 0 1px 1px rgba(0, 0, 0, 0.08);
  transition: transform 260ms var(--un-ease-spring-stiff), width 260ms var(--un-ease-spring-stiff), height 260ms var(--un-ease-spring-stiff), background-color 200ms ease;
}
.un-switch:checked {
  background: var(--un-accent);
}
.un-switch:checked::before {
  transform: translateX(20px);
}
/* UISwitch press behavior: the thumb stretches while held (the leading
   edge holds still) and settles back on release via the spring above. */
.un-switch:active::before {
  width: 34px;
}
.un-switch:checked:active::before {
  transform: translateX(13px);
}
.un-switch:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.un-switch:focus-visible {
  outline: 2px solid var(--un-accent);
  outline-offset: 2px;
}
.un-switch:active {
  transform: none; /* the global pressed scale would distort the track */
}

/* Material 3 skin */
html.un-android .un-switch {
  width: 52px;
  height: 32px;
  background: transparent;
  border: 2px solid var(--un-switch-track-off);
}
html.un-android .un-switch::before {
  top: 50%;
  left: 6px;
  width: 16px;
  height: 16px;
  margin-top: -8px;
  background: var(--un-switch-track-off);
  box-shadow: none;
}
html.un-android .un-switch:checked {
  background: var(--un-accent);
  border-color: var(--un-accent);
}
html.un-android .un-switch:checked::before {
  transform: translateX(18px);
  width: 24px;
  height: 24px;
  margin-top: -12px;
  background: var(--un-accent-contrast);
}
/* The iOS press-stretch doesn't apply to the Material skin. */
html.un-android .un-switch:active::before {
  width: 16px;
}
html.un-android .un-switch:checked:active::before {
  width: 24px;
  transform: translateX(18px);
}

/* ── Swipe-to-act rows ─────────────────────────────────────────────────
   Structure built by unNative.attachSwipeActions(): the original row is
   wrapped in .un-swipe with a ride-along action tray anchored off its
   right edge. Row AND tray translate via inline transform (JS-owned, 1:1
   with the finger); this file only styles the chrome. */

.un-swipe {
  position: relative;
  overflow: hidden;
}
.un-swipe.un-collapsing {
  transition: height 300ms var(--un-ease-spring-stiff);
}
.un-swipe-row {
  position: relative;
  z-index: 1;
  touch-action: pan-y; /* horizontal drags are ours; vertical stays scroll */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.un-swipe.un-dragging .un-swipe-row {
  cursor: grabbing;
}
/* Ride-along tray: anchored just past the row's right edge and translated
   in lockstep with it by JS, so nothing is ever painted BEHIND the row —
   rounded corners, margins and translucent rows stay clean. At rest it
   sits entirely outside the wrap's overflow:hidden. JS stretches its
   width past the natural size during a full-swipe overdrag. */
.un-swipe-tray {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 100%;
  display: flex;
  align-items: stretch;
  will-change: transform;
}
.un-swipe-action {
  min-width: 80px;
  padding: 0 16px;
  border: none;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--un-action-text);
  background: var(--un-action-neutral);
  cursor: pointer;
  transition: flex-grow 220ms var(--un-ease-spring-stiff);
  flex-grow: 1;
}
.un-swipe-action.un-destructive {
  background: var(--un-action-danger);
}
/* Full-swipe preview: past the commit point the destructive button grows
   to fill the tray (a reversible, lightweight cue — nothing fires until
   the finger lifts). */
.un-swipe.un-commit-armed .un-swipe-action.un-destructive {
  flex-grow: 8;
}
.un-swipe.un-committing .un-swipe-action {
  pointer-events: none;
}

/* ── Pull-to-refresh ───────────────────────────────────────────────────
   Puck injected by unNative.attachPullToRefresh(); scale/opacity/rotation
   are driven per-frame by pull progress (never popping at a threshold).

   The puck lives inside .un-ptr-layer, an inert clip box whose TOP EDGE is
   the anchor line — the app header's bottom (opts.topEl / opts.top), or by
   default the scroller's own top edge (element mode) / the safe-area inset
   (window mode). Two things keep the header clean: the layer's overflow
   clip hides the retracted puck (which parks at -40px) instead of letting
   it ride up over the header, and the layer sits BENEATH the header in
   stacking order — level 0 and first in tree order in element mode, below
   .un-navbar's z-index:100 in window mode. So the puck is revealed by the
   content sliding away rather than painted on top of the chrome.

   The 34px box below is mirrored by PTR_PUCK in native.js, which derives
   the whole travel pose from it (park, hold, per-px travel) so the puck
   rests FULLY below the anchor line with equal space above and below —
   issue #1526. The two copies are cross-checked by a test in
   tests/native-kit.test.js; change them together. */

.un-ptr-layer {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
/* Window-scroller mode: nothing in flow to anchor to, so pin to the
   viewport at the safe-area inset unless the caller anchors it lower.
   z-index stays under .un-navbar (100) — the puck goes behind a fixed
   header, never over it. */
.un-ptr-layer.un-ptr-layer-fixed {
  position: fixed;
  top: var(--un-safe-inset-top, env(safe-area-inset-top, 0px));
  z-index: 90;
}

.un-ptr-puck {
  position: absolute;
  top: 0;
  left: 50%;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--un-radius-full);
  background: var(--un-surface);
  color: var(--un-accent);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
}
.un-ptr-puck svg {
  width: 18px;
  height: 18px;
}
.un-ptr-puck.un-armed {
  color: var(--un-accent);
}
.un-ptr-puck.un-refreshing svg {
  animation: un-ptr-spin 800ms linear infinite;
}
@keyframes un-ptr-spin {
  to { transform: rotate(360deg); }
}

/* ── Drag-to-reorder ───────────────────────────────────────────────────
   Wired by unNative.attachReorder(): the lifted item's translateY is
   JS-owned (1:1 with the finger, spring settle on drop); this file styles
   the elevation, the handle, and the drop indicator. The indicator is a
   body-level FIXED overlay bar — never a row box-shadow, which clips
   against a group card's overflow:hidden at the first/last row. */

.un-reorder-handle {
  cursor: grab;
  touch-action: none; /* a drag from the handle is never a scroll */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  color: var(--un-muted);
}
.un-reordering,
.un-reordering .un-reorder-handle {
  cursor: grabbing;
}
.un-reorder-lifting {
  position: relative;
  z-index: 5; /* above sibling rows and the group hairlines (z-index 2) */
  scale: 1.02;
  border-radius: var(--un-radius); /* reads as a floating card once outside its group */
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);
  transition: scale 200ms var(--un-ease-spring-stiff), box-shadow 200ms ease;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  will-change: transform;
}
.un-reorder-indicator {
  position: fixed;
  z-index: 9985;
  height: 3px;
  margin-top: -1.5px;
  border-radius: 2px;
  background: var(--un-accent);
  pointer-events: none;
}
.un-reorder-indicator::before {
  content: "";
  position: absolute;
  left: -6px;
  top: 50%;
  width: 9px;
  height: 9px;
  margin-top: -4.5px;
  border-radius: var(--un-radius-full);
  border: 2.5px solid var(--un-accent);
  box-sizing: border-box;
}

/* Grid (displacement) reorder — attachReorder({ grid: true }). The
   ghost is the fixed-position clone floating under the pointer; the
   slot is the real item left in flow as a dashed drop target while
   siblings FLIP aside (homescreen idiom). */
.un-reorder-ghost {
  position: fixed;
  z-index: 9985;
  margin: 0;
  pointer-events: none; /* invisible to the slot's elementFromPoint hit-test */
  scale: 1.04;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.3);
  border-radius: var(--un-radius);
  will-change: transform;
}
.un-reorder-ghost-drop {
  scale: 1;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  transition: scale 180ms ease, box-shadow 180ms ease;
}
.un-reorder-slot {
  /* !important: the slot must win over the host's own hover/background
     rules for the duration of the gesture (the pointer often sits over
     the slot, so :hover states would otherwise repaint it). Fallback
     first for engines without color-mix. */
  border: 1px dashed var(--un-accent) !important;
  border-color: color-mix(in srgb, var(--un-accent) 55%, transparent) !important;
  background: color-mix(in srgb, var(--un-accent) 7%, transparent) !important;
  border-radius: var(--un-radius);
  box-shadow: none !important;
}
.un-reorder-slot > * {
  visibility: hidden;
}

/* ── Inset-grouped lists ───────────────────────────────────────────────
   Pure CSS: rounded grouped cards, uppercase section headers, and inset
   hairline separators painted on the STATIC row container (never on the
   translating swipe row), so they hold still during swipes. Compatible
   with attachSwipeActions: the kit's .un-swipe wrap is the container. */

.un-group {
  background: var(--un-group-bg);
  border-radius: var(--un-radius-card);
  overflow: hidden;
}
.un-group-header {
  padding: 0 16px 7px;
  font-size: 0.8125rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--un-muted);
}
.un-group-row {
  position: relative;
}
.un-group-row + .un-group-row::after,
.un-group > .un-swipe + .un-swipe::after,
.un-group > .un-swipe + .un-group-row::after,
.un-group > .un-group-row + .un-swipe::after {
  content: "";
  position: absolute;
  top: 0;
  left: 16px;
  right: 0;
  height: 1px;
  background: var(--un-hairline);
  z-index: 2;
  pointer-events: none;
}

/* ── Overlay backdrop (sheets, action sheets, alerts) ──────────────────
   Opacity is JS-driven for gesture-coupled surfaces (rides the sheet's
   position 1:1); the alert uses the fade modifier instead. */

.un-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9990;
  background: var(--un-backdrop);
  opacity: 0;
  will-change: opacity;
}
.un-backdrop.un-backdrop-fade {
  transition: opacity var(--un-dialog-fade-duration) ease;
}

/* ── Bottom sheet ──────────────────────────────────────────────────────
   Presented by unNative.presentSheet(); translateY is JS-owned (spring
   in, 1:1 drag, momentum dismiss) — no CSS transition on transform, on
   purpose. `bottom` carries the keyboard inset (see the tracker in
   native.js): the box position rides above the keyboard while the
   transform keeps its spring/drag semantics untouched. */

.un-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--un-kb-inset, 0px);
  z-index: 9991;
  background: var(--un-sheet-bg);
  border-radius: 14px 14px 0 0;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.22);
  padding-bottom: var(--un-safe-inset-bottom, env(safe-area-inset-bottom, 0px));
  transform: translateY(100%);
  transition: bottom 250ms ease-out;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  will-change: transform;
}
/* Overscroll surface extension (issue #789). The sheet's translateY goes
   NEGATIVE on three paths — the elastic give above rest during a drag
   (rubberband(raw, 32) in native.js), the entrance spring's ~1px
   overshoot past rest, and a post-present content SHRINK (remeasuredSheetY
   renders newHeight - oldHeight, which can be hundreds of px) — and each
   one lifts the sheet's bottom edge off the screen edge, exposing the
   dimmed backdrop over the page below it. This pseudo-element continues
   the sheet surface downward so whatever the bounce uncovers is more
   sheet. It is ABSOLUTELY POSITIONED on purpose: out of flow, so it can
   never perturb sheet.offsetHeight — the value that seeds the entrance
   spring, the backdrop opacity denominator, the dismissal travel and the
   release decision, and that watchHeight's ResizeObserver watches. Which
   is also why `.un-sheet` must never gain `overflow: hidden`.
   pointer-events: none keeps a tap in the strip below the sheet (present
   whenever the keyboard inset lifts it) landing on the backdrop, i.e.
   still dismissing, exactly as before. */
.un-sheet::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 100vh;
  background: var(--un-sheet-bg);
  border-radius: 0;
  box-shadow: none;
  pointer-events: none;
}
/* Keyboard up: the home-indicator safe area sits behind the keyboard —
   don't stack its padding on top of the inset. */
html.un-kb .un-sheet {
  padding-bottom: 0;
}
.un-sheet-grabber {
  width: 36px;
  height: 5px;
  margin: 8px auto;
  border-radius: var(--un-radius-full);
  background: var(--un-hairline);
}
.un-sheet-body {
  padding: 4px 16px 20px;
}

/* ── Side panel / drawer ───────────────────────────────────────────────
   Presented by unNative.presentPanel(). translateX is JS-owned (spring in,
   spring out) — no CSS transition on transform, same rule as .un-sheet.

   NOT a gesture surface: unlike the sheet there is no drag-to-dismiss and
   no grabber, so this block deliberately sets NO `touch-action` and NO
   `user-select` — scrolling and text selection inside the drawer are
   whatever the page normally does. Adding `touch-action` here would only
   take capability away.

   The keyboard is absorbed as body padding rather than by moving the box
   (as the sheet does with `bottom`): a full-height panel has nowhere to
   ride up to. */

.un-panel {
  position: fixed;
  top: 0;
  bottom: 0;
  z-index: 9991;
  display: flex;
  flex-direction: column;
  width: var(--un-panel-width);
  max-width: 100vw;
  background: var(--un-panel-bg);
  padding-top: var(--un-safe-inset-top, env(safe-area-inset-top, 0px));
  will-change: transform;
}
.un-panel[data-un-side="right"] {
  right: 0;
  /* Landscape notch on the trailing edge must not clip the rows. */
  padding-right: var(--un-safe-inset-right, env(safe-area-inset-right, 0px));
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.22);
  transform: translateX(100%);
}
.un-panel[data-un-side="left"] {
  left: 0;
  padding-left: var(--un-safe-inset-left, env(safe-area-inset-left, 0px));
  box-shadow: 8px 0 32px rgba(0, 0, 0, 0.22);
  transform: translateX(-100%);
}
/* Overshoot surface extension, the .un-sheet::after story on the x axis
   (issue #789). Still needed with no drag gesture: the `sheet` preset is
   underdamped, so the ENTRANCE spring crosses the rest position before
   settling — the panel's displacement goes slightly negative on every
   open, lifting it off its screen edge and exposing the dimmed backdrop
   over the page below. This pseudo-element continues the surface outward
   so whatever the bounce uncovers is more panel. ABSOLUTELY POSITIONED on
   purpose: out of flow, so it can never perturb panel.offsetWidth — the
   value that seeds the entrance spring, the backdrop opacity denominator
   and the exit travel, and that watchSize's ResizeObserver watches. Which
   is also why `.un-panel` must never gain `overflow: hidden`.
   pointer-events: none keeps taps outside the panel landing on the
   backdrop. */
.un-panel::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100vw;
  background: var(--un-panel-bg);
  pointer-events: none;
}
.un-panel[data-un-side="right"]::after {
  left: 100%;
}
.un-panel[data-un-side="left"]::after {
  right: 100%;
}
/* The scroller. Deliberately no horizontal padding: full-bleed rows are
   the drawer idiom, and content that wants insets brings its own. */
.un-panel-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding-bottom: calc(var(--un-safe-inset-bottom, env(safe-area-inset-bottom, 0px)) + var(--un-kb-inset, 0px));
}
/* Keyboard up: the home-indicator safe area sits behind the keyboard —
   don't stack its padding on top of the inset. */
html.un-kb .un-panel-body {
  padding-bottom: var(--un-kb-inset, 0px);
}

/* ── Action sheet ──────────────────────────────────────────────────────
   unNative.actionSheet(): main card of hairline-separated actions plus a
   separate Cancel card. translateY is JS-owned (spring up/down). */

.un-action-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--un-kb-inset, 0px);
  z-index: 9991;
  padding: 8px 8px calc(8px + var(--un-safe-inset-bottom, env(safe-area-inset-bottom, 0px)));
  transform: translateY(100%);
  transition: bottom 250ms ease-out;
  will-change: transform;
}
html.un-kb .un-action-sheet {
  padding-bottom: 8px;
}
.un-action-card {
  border-radius: 14px;
  overflow: hidden;
  background: var(--un-sheet-bg);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}
.un-action-card + .un-action-card {
  margin-top: 8px;
}
.un-action-title {
  padding: 12px 16px;
  text-align: center;
  font-size: 0.8125rem;
  color: var(--un-muted);
}
.un-action-btn {
  display: block;
  width: 100%;
  padding: 16px;
  border: none;
  background: transparent;
  font: inherit;
  font-size: 1.0625rem;
  text-align: center;
  color: var(--un-accent);
  cursor: pointer;
}
.un-action-title + .un-action-btn,
.un-action-btn + .un-action-btn {
  border-top: 1px solid var(--un-hairline);
}
/* A group label inside the stack, as opposed to .un-action-title on its own,
   which is the sheet's one centred caption. Left-aligned and heavier so it
   reads as a heading over the rows beneath it rather than as another caption,
   and it takes a hairline above when it follows a row — the rule the rows
   already draw between themselves, carried over the boundary so a group
   visibly starts. */
.un-action-title.un-action-section {
  text-align: left;
  font-weight: 600;
  padding-bottom: 8px;
}
.un-action-btn + .un-action-title.un-action-section {
  border-top: 1px solid var(--un-hairline);
}
.un-action-btn.un-destructive {
  color: var(--un-action-danger);
}
.un-action-btn.un-action-cancel {
  font-weight: 600;
}

/* ── Menu-row icons ────────────────────────────────────────────────────
   Rows given `icon` / `iconEl` draw the glyph to the left of the label.
   The class lands on EVERY row of a menu where at least one row has an
   icon, so the rows that don't get an empty spacer and all the labels
   still start at the same x — a menu with the icons only half applied
   reads as a bug rather than a style.

   The icon inherits `currentColor`, which is what makes a destructive
   row's icon red and a disabled row's icon dim with no rule of its own.
   The sheet centres the icon+label pair (iOS action sheets centre their
   labels); the popover leaves it ranged left. */
.un-action-btn.un-has-icon,
.un-popover-item.un-has-icon {
  display: flex;
  align-items: center;
  gap: 10px;
}
.un-action-btn.un-has-icon {
  justify-content: center;
}
.un-item-icon {
  flex: none;
  width: 1.15em;
  height: 1.15em;
  opacity: 0.75;
}
.un-item-icon-empty {
  opacity: 0;
}
/* The label WRAPS — it does not truncate. A menu row before icons existed
   was a bare text node in a block button, so a long label has always run to
   a second line, and an ellipsis here would newly hide the end of every long
   row on the platform. `min-width: 0` is what lets it wrap inside the flex
   row instead of forcing the icon out. */
.un-item-label {
  min-width: 0;
}

/* ── Anchored popover / dropdown menu ──────────────────────────────────
   unNative.popover(): the desktop menu idiom. No backdrop and no
   entrance animation on purpose (menus are high-frequency UI — see the
   fidelity rules). Sits one layer above sheets/modals so a popover can
   open from inside one. position/left/top are JS-owned (the pure
   placePopover math); max-height keeps a long menu scrolling internally
   instead of overflowing the viewport.

   The surface is an OPAQUE token of its own (--un-popover-bg): there is
   no backdrop behind a popover, so anything translucent here lets the
   content underneath read through the menu rows (#847). */

.un-popover {
  position: fixed;
  z-index: 9992;
  min-width: 11rem;
  max-width: 20rem;
  max-height: calc(100vh - 16px);
  overflow-y: auto;
  padding: 4px 0;
  background: var(--un-popover-bg);
  border: 1px solid var(--un-hairline);
  border-radius: var(--un-radius-card);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18), 0 2px 6px rgba(0, 0, 0, 0.08);
  text-align: left;
  outline: none;
}
/* A black shadow all but vanishes against a dark page — lift the menu
   off the background harder so the floating surface still reads. */
.dark .un-popover {
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.35);
}
.un-popover-header {
  padding: 8px 14px 9px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--un-hairline);
}
.un-popover-title {
  font-size: 0.875rem;
  font-weight: 600;
}
.un-popover-item {
  display: block;
  width: 100%;
  padding: 8px 14px;
  border: none;
  background: transparent;
  font: inherit;
  font-size: 0.8125rem;
  text-align: left;
  color: inherit;
  cursor: pointer;
}
.un-popover-item:hover:not(:disabled),
.un-popover-item:focus-visible {
  background: var(--un-surface);
}
.un-popover-item:disabled {
  opacity: 0.5;
  cursor: default;
}
.un-popover-item.un-destructive {
  color: var(--un-action-danger);
}

/* ── Alert dialog ──────────────────────────────────────────────────────
   unNative.alert(): 270px centered card, centered title, optional inset
   field, hairline Cancel|OK row. Fade + scale-down entrance (alerts
   don't spring from an edge — this transition is intentional). */

.un-alert {
  position: fixed;
  left: 50%;
  /* Centered in the strip above the on-screen keyboard (the field
     autofocuses, so the keyboard is usually up). */
  top: calc(50% - var(--un-kb-inset, 0px) / 2);
  z-index: 9992;
  width: 270px;
  border-radius: 14px;
  overflow: hidden;
  background: var(--un-sheet-bg);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  text-align: center;
  transform: translate(-50%, -50%) scale(1.12);
  opacity: 0;
  transition: transform 260ms var(--un-ease-spring), opacity var(--un-dialog-fade-duration) ease, top 250ms ease-out;
  will-change: transform, opacity;
}
.un-alert.un-in {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
}
.un-alert-title {
  padding: 18px 16px 4px;
  font-size: 1.0625rem;
  font-weight: 600;
}
.un-alert-message {
  padding: 2px 16px 4px;
  font-size: 0.8125rem;
  color: var(--un-muted);
}
.un-alert-field {
  display: block;
  width: calc(100% - 32px);
  margin: 10px 16px 2px;
  padding: 6px 8px;
  font: inherit;
  font-size: 0.8125rem;
  border: 1px solid var(--un-hairline);
  border-radius: 6px;
  background: rgba(127, 127, 138, 0.08);
  color: inherit;
}
.un-alert-buttons {
  display: flex;
  margin-top: 14px;
  border-top: 1px solid var(--un-hairline);
}
.un-alert-buttons.un-stacked {
  flex-direction: column;
}
.un-alert-btn {
  flex: 1;
  padding: 12px 8px;
  border: none;
  background: transparent;
  font: inherit;
  font-size: 1.0625rem;
  color: var(--un-accent);
  cursor: pointer;
}
.un-alert-btn + .un-alert-btn {
  border-left: 1px solid var(--un-hairline);
}
.un-alert-buttons.un-stacked .un-alert-btn + .un-alert-btn {
  border-left: none;
  border-top: 1px solid var(--un-hairline);
}
.un-alert-btn.un-cancel {
  font-weight: 600;
}
.un-alert-btn.un-destructive {
  color: var(--un-action-danger);
}

/* ── Centered modal ────────────────────────────────────────────────────
   unNative.presentModal(): arbitrary content in a centered card over the
   shared .un-backdrop.un-backdrop-fade. Fade + subtle scale-settle
   entrance (the alert's motion family, gentler scale for the larger
   surface); tall content scrolls inside the card. */

.un-modal {
  position: fixed;
  left: 50%;
  /* Centered in the strip above the on-screen keyboard; on iOS 100dvh
     ignores the (overlaying) keyboard, so the inset is subtracted from
     the max-height too. Both are 0 on resize-mode Android / desktop. */
  top: calc(50% - var(--un-kb-inset, 0px) / 2);
  z-index: 9992;
  width: min(480px, calc(100vw - 32px));
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px - var(--un-safe-inset-top, env(safe-area-inset-top, 0px)) - var(--un-safe-inset-bottom, env(safe-area-inset-bottom, 0px)) - var(--un-kb-inset, 0px));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  padding: 20px 16px;
  border-radius: 14px;
  background: var(--un-sheet-bg);
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.3);
  transform: translate(-50%, -50%) scale(1.04);
  opacity: 0;
  transition: transform 260ms var(--un-ease-spring), opacity var(--un-dialog-fade-duration) ease, top 250ms ease-out;
  will-change: transform, opacity;
  outline: none;
}
.un-modal.un-in {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
}

/* ── Keyboard avoidance for content scrollers ──────────────────────────
   unNative.attachKeyboardAvoidance() adds un-kb-avoid to the app's
   fixed-shell content scroller. Keyboard clearance is CONTENT PADDING on
   the scroller (never a resized layout): it must appear INSTANTLY on
   open — else reveals clamp against half-grown padding and re-adjust
   (stutter) — but ease ~150ms on close, so the browser's automatic
   scrollTop clamp rides the ease instead of snapping. While the keyboard
   is up the padding REPLACES the scroller's own bottom padding (the
   sheet precedent above: safe-area padding sits behind the keyboard and
   must not stack on the inset). */

.un-kb-avoid {
  transition: padding-bottom 150ms ease-out;
}
html.un-kb .un-kb-avoid {
  padding-bottom: var(--un-kb-inset, 0px);
  transition: none;
}

/* ── Toast ─────────────────────────────────────────────────────────────
   unNative.toast(): bottom capsule HUD on iOS/desktop, Material snackbar
   on Android. Safe-area aware; pointer-events stay off except on the
   optional action button so it never steals taps from content below. */

.un-toast {
  position: fixed;
  left: 50%;
  bottom: calc(16px + var(--un-safe-inset-bottom, env(safe-area-inset-bottom, 0px)));
  z-index: 9995; /* above sheets/alerts: feedback fired from overlays shows */
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: min(85vw, 480px);
  padding: 10px 18px;
  border-radius: var(--un-radius-full);
  background: var(--un-toast-bg);
  color: var(--un-toast-text);
  font-size: 0.875rem;
  line-height: 1.4;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%) translateY(12px);
  transition: opacity 180ms ease, transform 260ms var(--un-ease-spring-stiff);
  will-change: transform, opacity;
}
.un-toast.un-in {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.un-toast-msg {
  min-width: 0;
  overflow-wrap: anywhere;
}
.un-toast-action {
  pointer-events: auto;
  flex-shrink: 0;
  margin: -6px -8px;
  padding: 6px 8px;
  border: none;
  background: transparent;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--un-toast-action);
  cursor: pointer;
}

/* Material snackbar skin: near-full-width rectangle, left-aligned. */
html.un-android .un-toast {
  left: 16px;
  right: 16px;
  max-width: none;
  justify-content: space-between;
  border-radius: var(--un-radius);
  transform: translateY(12px);
}
html.un-android .un-toast.un-in {
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .un-toast {
    transition: opacity 180ms ease;
  }
  .un-modal,
  .un-alert {
    transition: opacity var(--un-dialog-fade-duration) ease;
  }
  /* Keyboard-inset moves still apply, just without animation. */
  .un-sheet,
  .un-action-sheet,
  .un-panel {
    transition: none;
  }
}

/* ── Nav bars ──────────────────────────────────────────────────────────
   .un-navbar: fixed blurred compact bar; unNative.attachNavBar() toggles
   .un-scrolled (hairline appears) and, with a large title, .un-collapsed
   (compact title fades in as the large title fades out). */

.un-navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: var(--un-safe-inset-top, env(safe-area-inset-top, 0px)) 12px 0;
  background: var(--un-navbar-bg);
  backdrop-filter: blur(20px) saturate(1.8);
  -webkit-backdrop-filter: blur(20px) saturate(1.8);
  border-bottom: 1px solid transparent;
  transition: border-color 200ms ease;
}
.un-navbar.un-scrolled {
  border-bottom-color: var(--un-hairline);
}
.un-navbar-title {
  flex: 1;
  text-align: center;
  font-size: 1.0625rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.un-navbar.un-has-large .un-navbar-title {
  opacity: 0;
  transition: opacity 180ms ease;
}
.un-navbar.un-has-large.un-collapsed .un-navbar-title {
  opacity: 1;
}
.un-navbar-back {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 8px 4px;
  border: none;
  background: transparent;
  font: inherit;
  font-size: 1.0625rem;
  color: var(--un-accent);
  cursor: pointer;
}
.un-navbar-back::before {
  content: "‹";
  font-size: 1.75rem;
  line-height: 1;
  margin-top: -3px;
}
/* Large title block: lives in the page flow under the bar; JS fades and
   scales it with scroll (transform-origin keeps the left edge pinned). */
.un-navbar-large {
  padding: 6px 16px 8px;
  font-size: 2.125rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  transform-origin: left center;
  will-change: transform, opacity;
}

/* ── Page transitions ──────────────────────────────────────────────────
   Driven by unNative.transition(fn, {type:'push'|'pop'}) which sets
   data-un-vt on <html> for the duration. iOS: slide + parallax + dim.
   Android: Material shared-axis fade-through. High-frequency UI (tabs,
   menus, panels) must use type:'none' — no keyframes exist for it on
   purpose. */

html[data-un-vt]::view-transition-old(root),
html[data-un-vt]::view-transition-new(root) {
  animation-duration: 420ms;
  animation-timing-function: var(--un-ease-spring);
}

/* iOS push: new screen slides in from the right over the old, which
   parallaxes left and dims. */
html.un-ios[data-un-vt="push"]::view-transition-new(root) {
  animation-name: un-vt-slide-in-right;
  z-index: 2;
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.2);
}
html.un-ios[data-un-vt="push"]::view-transition-old(root) {
  animation-name: un-vt-parallax-out-left;
}
/* iOS pop: old screen slides away right, previous parallaxes back in. */
html.un-ios[data-un-vt="pop"]::view-transition-old(root) {
  animation-name: un-vt-slide-out-right;
  z-index: 2;
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.2);
}
html.un-ios[data-un-vt="pop"]::view-transition-new(root) {
  animation-name: un-vt-parallax-in-left;
}

@keyframes un-vt-slide-in-right {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}
@keyframes un-vt-slide-out-right {
  from { transform: translateX(0); }
  to { transform: translateX(100%); }
}
@keyframes un-vt-parallax-out-left {
  from { transform: translateX(0); filter: brightness(1); }
  to { transform: translateX(-30%); filter: brightness(0.82); }
}
@keyframes un-vt-parallax-in-left {
  from { transform: translateX(-30%); filter: brightness(0.82); }
  to { transform: translateX(0); filter: brightness(1); }
}

/* Android (and desktop fallback): Material shared-axis fade-through —
   quicker, no slide theatrics. */
html.un-android[data-un-vt]::view-transition-old(root),
html.un-android[data-un-vt]::view-transition-new(root),
html.un-desktop[data-un-vt]::view-transition-old(root),
html.un-desktop[data-un-vt]::view-transition-new(root) {
  animation-duration: 240ms;
}
html.un-android[data-un-vt="push"]::view-transition-old(root),
html.un-desktop[data-un-vt="push"]::view-transition-old(root) {
  animation-name: un-vt-fade-out;
}
html.un-android[data-un-vt="push"]::view-transition-new(root),
html.un-desktop[data-un-vt="push"]::view-transition-new(root) {
  animation-name: un-vt-fade-up-in;
}
html.un-android[data-un-vt="pop"]::view-transition-old(root),
html.un-desktop[data-un-vt="pop"]::view-transition-old(root) {
  animation-name: un-vt-fade-down-out;
}
html.un-android[data-un-vt="pop"]::view-transition-new(root),
html.un-desktop[data-un-vt="pop"]::view-transition-new(root) {
  animation-name: un-vt-fade-in;
}

/* THE TWO HALVES ARE SEQUENCED, AND THAT IS WHAT MAKES IT A FADE-THROUGH
   RATHER THAN A CROSS-FADE (#1920).

   Both keyframe pairs used to run opacity across the WHOLE duration — old
   1 -> 0 and new 0 -> 1, simultaneously. The iOS lane gets away with the
   equivalent because its outgoing screen is opaque, z-index 2 and slides
   off over the incoming one. Here nothing is stacked and nothing is
   opaque, so at the midpoint both snapshots sat at ~0.5 and painted on top
   of each other. Measured with a red outgoing screen over a blue incoming
   one, sampled mid-animation: t=40ms #f8232a, t=120ms #bf3f7f, t=200ms
   #4d22d4 — a continuous blend, i.e. both screens visible the whole way.
   On the app directory that reads as two grids of app chips ghosting
   through one another, which is what #1920 reports on the back
   navigation.

   Material's shared-axis fade-through — which the block above already
   names — does NOT cross-fade. The outgoing surface leaves over roughly
   the first third, and only then does the incoming one arrive, so the
   handoff passes through the container's own ground instead of through a
   mixture of the two surfaces. Same measurement after sequencing:
   t=40ms #ff7979, t=80ms #fff3f3, t=120ms #c4c4ff — red, then the ground,
   then blue, and never a red/blue mix.

   35% is the crossover for both directions. The translateY halves keep
   their full duration: motion may overlap, only OPACITY may not, and
   moving a surface that is already transparent costs nothing. */

@keyframes un-vt-fade-out {
  from { opacity: 1; }
  35%, to { opacity: 0; }
}
@keyframes un-vt-fade-in {
  from, 35% { opacity: 0; }
  to { opacity: 1; }
}
@keyframes un-vt-fade-up-in {
  from { opacity: 0; transform: translateY(10px); }
  35% { opacity: 0; }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes un-vt-fade-down-out {
  from { opacity: 1; transform: translateY(0); }
  35% { opacity: 0; }
  to { opacity: 0; transform: translateY(10px); }
}

@media (prefers-reduced-motion: reduce) {
  html[data-un-vt]::view-transition-old(root),
  html[data-un-vt]::view-transition-new(root) {
    animation: none;
  }
}

/* ── Safe areas ────────────────────────────────────────────────────────
   Opt-in helpers for fixed bars on pages that set viewport-fit=cover.
   Inside the Flutter shell env() reports whatever the WebView exposes —
   if the shell already insets the WebView these resolve to 0 and are
   no-ops, so they are safe in both hosts.

   EVERY safe-area value in this stylesheet is written
   `var(--un-safe-inset-X, env(safe-area-inset-X, 0px))` (issue #970), not
   bare env(). Reason: env() resolves to 0px inside a cross-origin iframe,
   so in a Usernode app frame — where most kit consumers run — every one
   of these rules was silently inert, and the platform shell had to reserve
   the home-indicator strip on the app's behalf (which is what cut apps off
   at a phone's rounded bottom edge). The hosted bridge now receives the
   real insets from the shell and publishes them as those custom properties
   on <html>, so these rules finally do their job inside an app. The env()
   fallback keeps them exact in a top-level document — the platform shell
   itself, or an app opened standalone — where nothing sets the property.

   The @supports condition below stays bare env() on purpose: it is a
   syntax-support probe, and wrapping it would test the wrong thing. */

@supports (padding: env(safe-area-inset-top)) {
  .un-safe-top {
    padding-top: calc(var(--un-safe-inset-top, env(safe-area-inset-top, 0px))) !important;
  }
  .un-safe-top-extend {
    padding-top: calc(0.75rem + var(--un-safe-inset-top, env(safe-area-inset-top, 0px))) !important;
  }
  .un-safe-bottom {
    padding-bottom: calc(var(--un-safe-inset-bottom, env(safe-area-inset-bottom, 0px))) !important;
  }
  .un-safe-bottom-extend {
    padding-bottom: calc(0.75rem + var(--un-safe-inset-bottom, env(safe-area-inset-bottom, 0px))) !important;
  }
  .un-safe-x {
    padding-left: calc(var(--un-safe-inset-left, env(safe-area-inset-left, 0px))) !important;
    padding-right: calc(var(--un-safe-inset-right, env(safe-area-inset-right, 0px))) !important;
  }
}

/* ── Spring tuner (?un-tune=1, demo/tuning aid) ───────────────────────── */

.un-tuner {
  position: fixed;
  right: 8px;
  bottom: 8px;
  z-index: 99999;
  width: 240px;
  padding: 10px 12px;
  border-radius: var(--un-radius);
  background: rgba(10, 10, 20, 0.88);
  color: #e7edf7;
  font: 11px/1.6 ui-monospace, monospace;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.un-tuner label {
  display: block;
}
.un-tuner input[type="range"] {
  width: 100%;
  display: block;
  margin: 0 0 6px;
}
