/* Self-hosted (not the Google Fonts CSS API) so the font bytes start
   downloading the moment the HTML's own <link rel="preload"> is seen,
   instead of waiting on a first round-trip to fetch fonts.googleapis.com's
   CSS before the browser even learns the font file's URL — that extra
   hop is most of why the fallback font was visible for a beat before
   Patrick Hand swapped in. One subset, not the 3-way unicode-range split
   Google's own stylesheet served — every visible EN string on this site
   is plain Latin (Thai falls back to a system font regardless of subset,
   Patrick Hand has no Thai glyphs at all), so the other two were dead
   weight, and collapsing to one @font-face removes unicode-range
   matching across multiple same-family rules as a whole axis of
   per-browser-engine risk. local() tried first: if the visitor's OS
   already has a font actually installed under this exact name, that's
   used immediately, no network fetch and no swap/flash at all — the
   downloaded woff2 only comes into play for everyone else. */
@font-face {
  font-family: 'Patrick Hand';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  /* ?v= must match the <link rel="preload"> in every HTML file's own
     reference to this same font — a preload is only "used" (not a
     wasted duplicate fetch) if its URL matches the real fetch byte for
     byte, including the query string. See CACHE_VERSION comment near
     the top of app.js for why this exists and how to bump it. */
  src: local('Patrick Hand'), url('/fonts/patrick-hand-latin.woff2?v=20260827') format('woff2');
}

:root {
  --bg-deep: #0A0E1A;
  --bg-card: #141B2D;
  --bg-surface: #1A2340;
  --bg-elevated: #1F2B4A;
  /* The app's own card gradient (SurfaceStyleExt.verticalGradient in
     hand_drawn_edge_overlay.dart — top -> bottom, stops 0/0.5/1), used
     wherever a card-shaped panel currently sits flat on --bg-card alone. */
  --bg-card-gradient: linear-gradient(180deg, #0E1322 0%, #141B2D 50%, #1D2C5A 100%);

  --text-primary: #E8EAF6;
  --text-secondary: #9FA8DA;
  --text-muted: #5C6BC0;

  --accent-primary: #7C4DFF;
  --accent-secondary: #00E5FF;
  --accent-gold: #FFD54F;
  --accent-danger: #FF5252;
  --accent-soul: #FF4FD8;

  --accent-fire: #FF6B35;
  --accent-water: #5A6BD8;
  --accent-earth: #7BC67E;
  --accent-air: #B8E4FF;
  --accent-void: #B388FF;

  /* Myru's own accent + bubble ink, exact values from the app's
     _SpeechBubble / SketchBorder (shared/widgets/myru/myru_overlay.dart). */
  --myru-accent: #FF2ED2;
  --myru-ink: #F2EFE4;
  --myru-bubble-bg: #12082E;

  /* Same pink as the wave-nav's own active-tab sketch border and canvas
     bulge (_kNavSketchActive — see .wave-tab.active and nav-wave.js's
     STROKE_COLOR). A DIFFERENT pink from --myru-accent above — Myru has
     her own signature colour, this is the site chrome's, and the share
     UI reads as chrome (same family as the nav) rather than as Myru. */
  --nav-pink: #E85CFF;

  /* Crown (violet) down to root (red), the site's own brand spectrum,
     used for primary buttons. Not an app color, a Notself one. */
  --chakra-gradient: linear-gradient(
    100deg,
    #7C4DFF 0%,
    #5B4FCF 18%,
    #3B82F6 36%,
    #4CAF50 54%,
    #FFC94D 70%,
    #FF8C42 85%,
    #E5484D 100%
  );

  --font-hand: 'Patrick Hand', 'Comic Sans MS', cursive;

  --radius-lg: 24px;
  --radius-md: 16px;
  --radius-sm: 12px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html { scroll-behavior: smooth; }

body {
  margin: 0;
  /* Fallback only, in case #sky-canvas fails: same flat bgDeep the app
     scaffold uses (shared/theme/app_theme.dart). The sky canvas paints
     over this once JS runs, see initSky() in app.js. */
  background: var(--bg-deep);
  color: var(--text-primary);
  font-family: var(--font-hand);
  font-size: 19px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* The sky cycles through a bright daytime phase behind sections with no
     opaque card underneath; same fix the app's own onboarding sky page
     uses for its overlaid text (features/habits/onboarding_pages.dart,
     Shadow(color: Colors.black54, blurRadius: 6)). text-shadow inherits,
     so this covers every heading and paragraph without repeating it. */
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
}

/* ---------- Sky background (features/habits/habit_time_sky.dart's
   onboarding day/night picker, simplified into an ambient backdrop: the
   same sky palette, hills, and a sun/moon riding opposite ends of one
   slowly turning wheel). ---------- */
#sky-canvas {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: -1;
  display: block;
}

/* ---------- CRT overlay (shared/widgets/crt_overlay.dart, default
   CRTSettings: scanlines .65, vignette .50) applied globally like the app.
   Scaled down from the in-app defaults since this sits over dense reading
   text rather than game art. ---------- */
.crt-overlay {
  position: fixed;
  inset: 0;
  z-index: 999;
  pointer-events: none;
}
.crt-scanlines {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.5) 0px,
    rgba(0, 0, 0, 0.5) 1px,
    transparent 1px,
    transparent 3px
  );
  mix-blend-mode: multiply;
  opacity: 0.22;
}
.crt-bright-lines {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.5) 0px,
    rgba(255, 255, 255, 0.5) 1px,
    transparent 1px,
    transparent 6px
  );
  mix-blend-mode: screen;
  opacity: 0.02;
}
.crt-vignette {
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle at top left, rgba(0, 0, 0, 0.10) 0%, transparent 60%),
    radial-gradient(circle at top right, rgba(0, 0, 0, 0.10) 0%, transparent 60%),
    radial-gradient(circle at bottom left, rgba(0, 0, 0, 0.10) 0%, transparent 60%),
    radial-gradient(circle at bottom right, rgba(0, 0, 0, 0.10) 0%, transparent 60%),
    radial-gradient(ellipse at center, transparent 0%, transparent 58%, rgba(0, 0, 0, 0.14) 84%, rgba(0, 0, 0, 0.28) 100%);
}
.crt-edge-glow {
  position: absolute;
  inset: 1px;
  border: 1px solid rgba(0, 255, 136, 0.05);
}
.crt-beam {
  position: absolute;
  left: 0;
  right: 0;
  height: 140px;
  background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.05), transparent);
  mix-blend-mode: screen;
  animation: crt-beam-sweep 12.82s linear infinite;
}
@keyframes crt-beam-sweep {
  /* Bottom to top, matching the app's phosphor beam (crt_overlay.dart,
     kCrtBeamPeriodMs = 12820, "one full travel, bottom to top"). */
  0% { top: 100%; }
  100% { top: -140px; }
}
@media (prefers-reduced-motion: reduce) {
  .crt-beam { animation: none; display: none; }
}

img { max-width: 100%; display: block; }
button, input, textarea { font-family: var(--font-hand); }

.wrap { width: 100%; max-width: 900px; margin: 0 auto; padding: 0 24px; }
a { color: inherit; text-decoration: none; }

h1, h2 { font-weight: 400; line-height: 1.15; margin: 0 0 0.5em; }
h1 { font-size: clamp(2.2rem, 6vw, 3.4rem); }
h2 { font-size: clamp(1.6rem, 4vw, 2.1rem); }
.center { text-align: center; margin-left: auto; margin-right: auto; }

.section { padding: 56px 0; }

/* ---------- Buttons ---------- */
.btn {
  font-family: var(--font-hand);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 24px;
  border-radius: 999px;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
  font-size: 1.15rem;
}
.btn:hover { transform: translateY(-1px); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.btn-primary {
  background: linear-gradient(135deg, var(--accent-primary), #5b32d6);
  color: #fff;
  box-shadow: 0 8px 24px rgba(124, 77, 255, 0.35);
}
.btn-ghost {
  background: transparent;
  color: var(--text-secondary);
  border-color: rgba(159, 168, 218, 0.35);
}
.btn-ghost:hover { border-color: var(--accent-secondary); color: var(--accent-secondary); }
.btn-lg { padding: 14px 32px; font-size: 1.3rem; }
.btn-small { padding: 7px 16px; font-size: 0.95rem; }

/* ---------- Header ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  /* No background on this outer element: it used to sit behind
     .wave-nav too, so the whole 82px bar read as one flat dark
     rectangle instead of a transparent bar with only the wave's own
     pendant shape opaque. The tint now lives on .header-bar alone;
     .wave-nav stays see-through except for whatever nav-wave.js paints. */
}
.header-bar {
  /* --header-bg is written every frame by initSky() in app.js, tracking
     the same day/night cycle as the sky behind it: dark navy at night,
     fading up toward near white at solar noon. The literal value here
     is only the pre-JS/no-JS fallback. */
  background: var(--header-bg, rgba(10, 14, 26, 0.78));
  backdrop-filter: blur(10px);
}
.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 12px;
  padding-bottom: 12px;
}
.brand { display: flex; align-items: center; gap: 8px; font-size: 1.3rem; }
.header-right { display: flex; align-items: center; gap: 10px; }
.lang-toggle {
  display: flex;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 999px;
  padding: 3px;
  gap: 2px;
}
.lang-btn {
  font-family: var(--font-hand);
  font-size: 0.9rem;
  padding: 5px 11px;
  border-radius: 999px;
  border: none;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
}
.lang-btn[aria-pressed="true"] {
  background: var(--myru-accent);
  color: #fff;
}
.header-right .btn-primary {
  background: linear-gradient(135deg, var(--myru-accent), #C91FB0);
  box-shadow: 0 8px 24px rgba(255, 46, 210, 0.35);
}
.brand-word {
  -webkit-text-stroke: 0.4px rgba(0, 0, 0, 0.75);
  text-shadow: 0 0 0.5px rgba(0, 0, 0, 0.55);
}
.brand-mark { width: 30px; height: 30px; object-fit: contain; }

/* Readability boost for white text that sits directly on the open
   background (sky/hills) rather than inside a card or button: body's
   own soft blur text-shadow alone isn't enough once the sky cycles
   into its bright daytime phase. Applied explicitly per element rather
   than globally, so it doesn't dull the crisp look of text that's
   already sitting on a solid card.
   NOT -webkit-text-stroke (like .brand-word uses): that draws its
   outline around each glyph independently, and Thai's stacked vowel/
   tone marks sit close enough above/below their base consonant that
   the stroke reads as smeared, doubled text once it's added. This
   page has Thai body copy; .brand-word never does. Four small solid
   offsets fake the same thin-border look without touching glyph
   outlines, so stacked marks render clean in both languages. */
.text-outline {
  text-shadow:
    -0.6px -0.6px 0 rgba(0, 0, 0, 0.75),
    0.6px -0.6px 0 rgba(0, 0, 0, 0.75),
    -0.6px 0.6px 0 rgba(0, 0, 0, 0.75),
    0.6px 0.6px 0 rgba(0, 0, 0, 0.75),
    0 0 0.5px rgba(0, 0, 0, 0.55);
}

/* ---------- "Coming soon" overlay ----------
   Wraps a not-yet-live control (signup email, donate) so it's visibly
   inert rather than something a visitor can fill in and submit into
   the void. The real inputs/buttons underneath also carry `disabled`
   in the markup, both for assistive tech and as a second layer in case
   this CSS somehow doesn't load; pointer-events here is the layer that
   also blocks the merely-cosmetic bits (picking a donation chip) that
   `disabled` alone wouldn't reach. */
.coming-soon { position: relative; }
.coming-soon > *:not(.coming-soon-badge) {
  opacity: 0.4;
  filter: grayscale(0.5);
  pointer-events: none;
}
.coming-soon-badge {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.coming-soon-pill {
  background: var(--bg-elevated);
  color: var(--text-primary);
  font-family: var(--font-hand);
  font-size: 1.1rem;
  padding: 10px 26px;
  border-radius: 999px;
  /* --tilt is set per-element in app.js (initComingSoonTilt), a fresh
     random left/right lean on every load; this fixed value is only the
     pre-JS fallback. */
  transform: rotate(var(--tilt, -6deg));
  transition: transform 0.3s cubic-bezier(.34, 1.56, .64, 1);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.45);
  /* .coming-soon-badge (its parent) is pointer-events:none so clicks pass
     through to the dimmed real controls underneath; this opts back in so
     tapping the pill itself (to flip its lean) still works. */
  pointer-events: auto;
  cursor: pointer;
}

/* ---------- Wave nav ----------
   Inverted twin of feelfami's app bottom tab bar (main.dart's
   _NavBarWaveBackground / _NavBarWavePainter): the same single-gaussian
   swell (5px baseline, 96px peak, sigma 38-ish, super-gaussian exponent
   4), but flipped so the baseline sits at the TOP of the bar and the
   peak hangs DOWN under whichever of the 3 tabs is active, instead of
   rising UP from a bottom baseline under one of 5. Drawn in nav-wave.js,
   which duplicates sketch-border.js's own two-layer wobble noise so the
   curve's stroke reads as hand-drawn like everything else here.
   96px, not the app's own 82px: a thicker stroke and the active pill's
   own rounded corners need real clearance from the header edge above
   them, or the pill's hand-drawn wobble reads as clipped against it. */
.wave-nav { position: relative; height: 96px; }
.wave-nav-inner { position: relative; height: 100%; }
.wave-nav-canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* Auto, not none: the tab pills sit above this in z-order and cover
     their own hit areas, so this only ever receives events in the gaps
     around them, where the bulge itself is visible. That's what lets
     pulling down on the bulge summon Myru (see initWaveNavPull). */
  pointer-events: auto;
  cursor: grab;
}
.wave-nav-tabs {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  justify-content: space-around;
  align-items: flex-start;
  /* Clearance from the header edge above, so the active pill's own
     rounded corners (and their hand-drawn wobble) have room to round
     out instead of butting right up against it. */
  padding-top: 16px;
}
.wave-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 64px;
  height: 40px;
  margin-top: 2px;
  padding: 0 14px;
  border-radius: 12px;
  color: var(--text-secondary);
  font-size: 0.95rem;
  text-decoration: none;
  /* Solid, not transparent: this pill sits IN FRONT of the wave's own
     canvas-drawn bulge (and that bulge's hand-drawn stroke), so it
     can't let the sky or the curve show through and read as "overlap". */
  background: var(--bg-card);
  transition: height 0.2s cubic-bezier(.2, .8, .2, 1.6),
              margin-top 0.2s cubic-bezier(.2, .8, .2, 1.6),
              padding-top 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.wave-tab:hover { color: var(--text-primary); }
.wave-tab.active:hover { color: #E85CFF; }
.wave-tab.active {
  height: 64px;
  margin-top: 0;
  padding-top: 16px;
  align-items: flex-start;
  background: var(--bg-elevated);
  /* Same pink as this tab's own sketch border / the wave's stroke
     (_kNavSketchActive in the app), so the selected label reads as
     part of the same accent, not just a lighter grey. */
  color: #E85CFF;
  font-size: 1.02rem;
}
@media (max-width: 480px) {
  .wave-nav { height: 84px; }
  .wave-nav-tabs { padding-top: 12px; }
  .wave-tab { min-width: 48px; font-size: 0.85rem; padding: 0 8px; }
  .wave-tab.active { height: 56px; padding-top: 12px; font-size: 0.92rem; }

  /* EN/TH and "Get early access" read as tall pills on a narrow phone
     header — a couple px less vertical padding on each keeps the sticky
     header bar (this row plus the wave-nav right under it) from eating
     so much of a short viewport. */
  .lang-btn { padding: 3px 10px; }
  .header-right .btn-small { padding: 5px 14px; font-size: 0.88rem; }
}

/* ---------- Sketch border ---------- */
.sketch { position: relative; }
/* .sketch-own-position is an escape hatch: a direct child of a sketch
   container that needs its OWN position value (absolute, for its own
   left/top or transform-based layout) opts out here, instead of this
   rule silently overwriting it (bit twice already: .myru-drag and
   .self-label both need position:absolute and both got forced to
   position:relative by this rule before it excluded them). */
.sketch > *:not(svg.sketch-svg):not(.sketch-own-position) { position: relative; z-index: 1; }

/* ---------- Hero ---------- */
.hero { padding: 56px 0 24px; }
.hero-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
}
.hero-copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  width: 100%;
  max-width: 640px;
}
.hero-copy .hangout-wrap { width: 100%; }

/* Hidden until summoned via the mystery button; see .myru-peek below for
   the idle hint that she's there at all. Screen-fixed and JS-positioned
   (left/top) once revealed, the same "floats over the viewport and
   wanders on her own" behavior as the app's Myru overlay, rather than
   living inside the hero section and scrolling away with the page. */
.hero-myru {
  display: none;
  flex-direction: column;
  align-items: center;
  position: fixed;
  z-index: 998;
  left: 0;
  top: 0;
  pointer-events: none;
  /* Same anticipation-overshoot curve as _kMyruGlide in myru_overlay.dart
     (Cubic(0.45,-0.28,0.28,1.32)): a small dip backwards before she sets
     off, then a slight overshoot past the landing spot before settling,
     instead of a flat ease. Duration is set per-hop in JS (see
     initMyruWander's glideTo), scaled to how far she's travelling. */
  transition: left 1.8s cubic-bezier(0.45, -0.28, 0.28, 1.32), top 1.8s cubic-bezier(0.45, -0.28, 0.28, 1.32);
}
.hero-myru.revealed { display: flex; }
.hero-myru .myru-bubble,
.hero-myru .myru-figure { pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .hero-myru { transition: none; }
}
.myru-bubble {
  position: relative;
  font-size: 1.2rem;
  background: rgba(18, 8, 46, 0.92);
  color: var(--myru-ink);
  border: 1.2px solid rgba(255, 46, 210, 0.7);
  border-radius: 12px;
  padding: 8px 12px;
  margin-bottom: 10px;
  max-width: 240px;
  min-height: 1.6em;
  text-align: center;
  box-shadow: 0 0 12px rgba(255, 46, 210, 0.25);
}
.myru-bubble .diagram { display: flex; align-items: center; justify-content: center; gap: 6px; flex-wrap: wrap; font-size: 1rem; }
.myru-bubble .diagram .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.myru-bubble.pop { animation: pop-in 0.35s ease; }

/* Word-by-word reveal (myru_overlay.dart _BeatText): each word is laid out
   from the start (reserving its space so the bubble never resizes/jumps
   as words arrive) but painted invisible until its turn. The word
   currently "being said" gets the app's pink stroke behind its own ink
   fill, -webkit-text-stroke draws exactly that stroke-then-fill in one
   declaration, no separate stacked text needed the way Flutter required. */
.myru-beat { opacity: 0; }
.myru-beat.shown { opacity: 1; }
.myru-beat.current {
  -webkit-text-stroke: 1.1px var(--myru-accent);
  text-stroke: 1.1px var(--myru-accent);
}
.myru-bubble-extra {
  display: inline-block;
  opacity: 0;
  transition: opacity 250ms ease;
}
.myru-bubble-extra.shown { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .myru-beat { opacity: 1; }
  .myru-bubble-extra { opacity: 1; transition: none; }
}
@keyframes pop-in {
  0% { transform: scale(0.85); opacity: 0.4; }
  60% { transform: scale(1.05); }
  100% { transform: scale(1); opacity: 1; }
}

/* Exact port of myruBodySize() (myru_models.dart): 210px at scale 1.0 on
   a 420 reference short-side, adapting HALF the device-size difference,
   clamped to a 0.72-1.55 factor. Reduces to 105px + 0.25 * shortSide
   before the outer clamp. Using min(vw,vh) as the web's "device short
   side" analogue. */
.myru-figure {
  position: relative;
  width: clamp(151.2px, calc(105px + 0.25 * min(100vw, 100vh)), 325.5px);
  height: clamp(151.2px, calc(105px + 0.25 * min(100vw, 100vh)), 325.5px);
  border-radius: 50%;
  border: none;
  background: radial-gradient(circle at 50% 40%, rgba(255, 46, 210, 0.12), transparent 70%);
  padding: 0;
  cursor: grab;
  touch-action: none;
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
  animation: myru-float 5s ease-in-out infinite;
}
.myru-figure.grabbing { cursor: grabbing; animation-play-state: paused; }
@keyframes myru-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-8px); }
}

/* Wander glide deformation: a squash-anticipation crouch, then squash-and-
   stretch ALONG the travel direction, then a small overshoot settle, ported from _MyruMotionRig's velocity-driven strain (rotate to align
   with travel, scale, rotate back, so the stretch is axis-aligned rather
   than a fixed shape) plus a "gather before you go" crouch of our own,
   same animation-principle spirit as the app's bounceBody squash-hop.
   --glide-angle/--glide-lean are set per-hop by initMyruWander(). */
.myru-drag.gliding {
  animation: myru-glide-pulse var(--glide-ms, 1800ms) cubic-bezier(0.45, -0.28, 0.28, 1.32) 1;
}
@keyframes myru-glide-pulse {
  0%   { transform: scale(1, 1); }
  6%   { transform: scale(0.91, 1.08); }
  38%  { transform: rotate(var(--glide-lean, 0deg)) rotate(var(--glide-angle, 0deg)) scale(1.14, 0.9) rotate(calc(-1 * var(--glide-angle, 0deg))); }
  72%  { transform: rotate(calc(var(--glide-lean, 0deg) * -0.4)) rotate(var(--glide-angle, 0deg)) scale(0.95, 1.05) rotate(calc(-1 * var(--glide-angle, 0deg))); }
  100% { transform: scale(1, 1); }
}
@media (prefers-reduced-motion: reduce) {
  .myru-drag.gliding { animation: none; }
}

/* Pulled by the pointer (rubber-banded translate): everything below this
   moves together, same as the app's grab handling in myru_overlay.dart.
   Explicit width/height rather than relying on inset:0 to stretch it,
   since the generic ".sketch > *" rule below forces this to
   position:relative (higher specificity than a plain class), and inset
   only stretches absolute/fixed boxes. */
.myru-drag {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Spins + stretches with the body (drag angular component + squash-and-
   stretch); the face lives OUTSIDE this so it always stays level, exactly
   like shared/widgets/myru/myru_body.dart's own layer split. */
.myru-spin {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transition: filter 1150ms ease, opacity 1150ms linear;
}
.myru-figure img {
  position: relative;
  z-index: 1;
  width: 72%;
  aspect-ratio: 1 / 1;
  margin: 14% auto;
  display: block;
  filter: drop-shadow(0 12px 20px rgba(0, 0, 0, 0.4));
  -webkit-user-drag: none;
  user-select: none;
  pointer-events: none;
}
.myru-arrows {
  position: absolute;
  z-index: 2;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Each arrow breathes independently while she's being spun: a lively,
   staggered pulse along its OWN radial direction (rotate to align, scale,
   rotate back), the same "arrows have their own life" feel as the app's
   propeller/extension animation. */
.myru-figure.grabbing .myru-arrow {
  animation: myru-arrow-pulse 0.55s ease-in-out infinite;
}
.myru-figure.grabbing .myru-arrow:nth-child(2) { animation-delay: 0.07s; }
.myru-figure.grabbing .myru-arrow:nth-child(3) { animation-delay: 0.14s; }
.myru-figure.grabbing .myru-arrow:nth-child(4) { animation-delay: 0.21s; }
.myru-figure.grabbing .myru-arrow:nth-child(5) { animation-delay: 0.28s; }
@keyframes myru-arrow-pulse {
  0%, 100% { transform: rotate(var(--adeg)) scale(1, 1) rotate(calc(-1 * var(--adeg))); }
  50% { transform: rotate(var(--adeg)) scale(1.28, 1) rotate(calc(-1 * var(--adeg))); }
}
.myru-figure svg.myru-face {
  position: absolute !important;
  z-index: 2;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  transition: opacity 1150ms linear, filter 1150ms ease, transform 500ms ease;
}
/* fill/stroke transition alongside d, not just d alone: the shape morph
   already eases over 620ms, but fill/stroke used to snap instantly on
   the .surprised class toggle, and the tongue's opacity snapped
   independently of both. Any frame caught mid-morph could show the
   in-between path shape with the wrong fill/tongue state paired to it,
   e.g. the white lip outline mid-open with the black interior not yet
   applied and the tongue already at full opacity. */
.myru-face .mouth { transition: d 620ms ease-in-out, fill 620ms ease-in-out, stroke 620ms ease-in-out; }
/* Same fixed cycle as the app's blink (myru_face.dart): 2800ms open,
   80ms close, 60ms hold shut, 110ms open, total 3050ms. */
.myru-face .eye {
  fill: var(--myru-ink);
  transform-box: fill-box;
  transform-origin: center;
  animation: myru-blink 3050ms ease-in-out infinite;
}
@keyframes myru-blink {
  0%, 91.80% { transform: scaleY(1); }
  94.43%, 96.39% { transform: scaleY(0.12); }
  100% { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .myru-face .eye { animation: none; }
}
.myru-face .mouth {
  fill: none;
  stroke: var(--myru-ink);
  stroke-width: 0.6;
  stroke-linecap: round;
}

/* MyruEmotion.surprised: bigger, wide-open eyes (no blink while it holds)
   and the mouth switches from a stroked curve to a small filled "O".
   Triggered when she's freshly summoned (see applyLaunchKick in app.js)
   or hauled out to ~95% of her pull tether and kept there (see
   updateStartle), same as the app. */
.myru-figure.surprised .myru-face .eye {
  animation: none;
  transition: transform 120ms ease, cx 120ms ease;
  transform: scale(1.4);
}
/* 2.5% further apart than the resting 6.8-unit gap (cx 46.6/53.4),
   surprise only, the `cx` SVG geometry property (not transform, which
   is already carrying the scale above and would otherwise have to
   compose with it) repositions each eye in its own untransformed
   coordinate space, so this is independent of the scale regardless of
   transform order. */
.myru-figure.surprised .myru-face circle.eye:first-of-type { cx: 46.515px; }
.myru-figure.surprised .myru-face circle.eye:last-of-type { cx: 53.485px; }
/* An open mouth in the app is a DARK interior (the same near-black the
   speech bubble uses) with the ink-colored lip line stroked on top of
   it, not a solid light fill. A light fill with no outline is exactly
   what read as a pale floating blob instead of an open mouth. */
.myru-figure.surprised .myru-face .mouth {
  fill: var(--myru-bubble-bg);
  stroke: var(--myru-ink);
  stroke-width: 0.5;
}
/* kMyruTonguePink (myru_face.dart), hidden except while surprised, same
   as the app only painting a tongue once the mouth is actually open. No
   opacity transition: the tongue's clip path is fixed geometry that
   doesn't track the mouth's animated `d`, so fading it in/out let a
   half-opacity tongue show through the still-mostly-closed mouth shape
   mid-transition, reading as a pink beard. Snapping it with the mouth's
   fill/stroke switch instead avoids that mismatched in-between frame. */
.myru-face .tongue {
  fill: #F740F2;
  opacity: 0;
}
.myru-figure.surprised .myru-face .tongue {
  opacity: 1;
}
.myru-aura {
  position: absolute;
  inset: -10%;
  border-radius: 50%;
  z-index: 0;
  opacity: 0.6;
  filter: blur(6px);
}
.myru-aura.pulse { animation: aura-pulse 1.4s ease; }
@keyframes aura-pulse {
  0% { transform: scale(0.7); opacity: 0.9; }
  100% { transform: scale(1.25); opacity: 0.35; }
}
.myru-figure.bounce { animation: myru-float 5s ease-in-out infinite, myru-bounce 0.5s ease; }
@keyframes myru-bounce {
  0% { transform: scale(1); }
  35% { transform: scale(0.9, 1.1); }
  60% { transform: scale(1.08, 0.94); }
  100% { transform: scale(1); }
}

/* Launch: the same "spat out of the crest" pop the app's nav bar gives
   Myru on summon (main_nav_shade.dart's bulge-launch-tube), replayed here
   as an elastic scale/rise instead of an actual bulging bar. */
.myru-figure.launch { animation: myru-launch 700ms cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes myru-launch {
  0% { transform: scale(0.15) translateY(60px); opacity: 0; }
  55% { transform: scale(1.12) translateY(-8px); opacity: 1; }
  75% { transform: scale(0.94) translateY(4px); }
  100% { transform: scale(1) translateY(0); }
}

/* The goodbye. In the app this is ONE wrapper (_withDissolve) around the
   whole composited body INCLUDING the face, she swells, thins and blurs
   out as one coherent thing, not two layers fading on different curves.
   growth/opacity are exactly linear in time (matches kMyruDissolveGrowth
   0.55 / a plain 1-t fade); blur is held back on a squared ramp (ease-in
   approximates t²: negligible at first, arriving late), kMyruDissolveBlur
   4.5, kMyruDissolveMs 1150. Declared here (not on the base rule) because
   the base .myru-spin transform is driven raw, per animation-frame, by
   drag/spin/calm-spin, giving it a 1.15s transition there would smear
   every ordinary spin into a laggy blur. */
.myru-figure.dissolving .myru-spin,
.myru-figure.dissolving svg.myru-face {
  transition: opacity 1150ms linear, filter 1150ms ease-in, transform 1150ms linear;
  opacity: 0;
  filter: blur(4.5px);
  transform: scale(1.55);
}
/* She can't be interacted with while leaving or gone, same as the app
   removing her gesture detector once nothing is visible to grab. Coming
   back only happens through a real re-summon (the mystery button), never
   on a timer, see initMyru's resummon()/isGone() in app.js. */
.myru-figure.dissolving { pointer-events: none; }
/* The relieved exhale the app leaves her on for the whole exit: eyes
   nearly shut (kMyruEmotion.relieved's lid factor 0.22), settled quickly
   rather than easing shut over the full fade. The mouth's own `d`
   transition (below) handles the matching relieved mouth shape. */
.myru-figure.dissolving .myru-face .eye {
  animation: none;
  transition: transform 320ms ease;
  transform: scaleY(0.22);
}
/* Scattering arrows: each one flies out along its OWN direction and
   fades, staggered, instead of the whole arrow layer scaling as one flat
   sheet, matching the app's _ScatteredArrowsPainter (myru_overlay.dart). */
.myru-figure.dissolving .myru-arrow {
  animation: myru-arrow-scatter 1150ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
.myru-figure.dissolving .myru-arrow:nth-child(2) { animation-delay: 30ms; }
.myru-figure.dissolving .myru-arrow:nth-child(3) { animation-delay: 60ms; }
.myru-figure.dissolving .myru-arrow:nth-child(4) { animation-delay: 90ms; }
.myru-figure.dissolving .myru-arrow:nth-child(5) { animation-delay: 120ms; }
/* Thrown outward along the arrow's own direction (reach), then gravity
   pulls it down an extra bit past that (fall), same 6.0:5.0 reach:fall
   ratio as kMyruDissolveArrowReach/Fall (myru_models.dart), traced as a
   3-stop arc instead of a straight radial line. */
@keyframes myru-arrow-scatter {
  0% { transform: translate(0, 0) scale(1); opacity: 1; }
  55% {
    transform: translate(calc(var(--ax) * 46px), calc(var(--ay) * 46px - 6px)) scale(1.4);
    opacity: 1;
  }
  100% {
    transform: translate(calc(var(--ax) * 46px), calc(var(--ay) * 46px + 38px)) scale(1.7);
    opacity: 0;
  }
}
.myru-hint {
  color: var(--text-muted);
  margin-top: 10px;
  font-size: 1rem;
  transition: opacity 0.35s ease;
}
/* The hint lives outside .myru-figure, so the dissolve class on the figure
   never reached it, she would vanish and "drag to spin" would keep talking
   about a character that is no longer there. */
.myru-hint.is-gone { opacity: 0; pointer-events: none; }

/* ---------- Demo ---------- */
.demo-section { padding-top: 24px; }
.demo-panel {
  position: relative;
  background: var(--bg-card-gradient);
  border-radius: var(--radius-lg);
  /* Extra top padding so the protruding tab (see .draw-feeling-tab) has
     room to poke out above the content without overlapping "1. draw it". */
  padding: 44px 28px 28px;
  display: flex;
  flex-direction: column;
  gap: 24px;
  margin-bottom: 20px;
}
/* Draw-it in its own column on the left, feel+element stacked in a wider
   column right next to it, more compact than everything stacked in one
   column, and a tall canvas next to two short chip rows uses the box's
   height evenly instead of leaving the chip column mostly empty air.
   Wraps to stacked on anything too narrow for both side by side. */
.demo-columns {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  /* stretch, not flex-start: the feel/element column needs the SAME
     height as the (now much taller) draw column so its own send-out
     button (margin-top: auto below) has somewhere to sink to. */
  align-items: stretch;
  justify-content: center;
}
.demo-step-draw { flex: 0 0 auto; }
.demo-step-feel { flex: 1 1 240px; min-width: 220px; padding-top: 4px; }
/* Parks at the bottom of the feel/element column rather than adding a
   separate full-width row below both columns, that column is usually
   short next to the tall draw canvas, so there's room to spare down
   there without growing the panel any taller. Falls back to sitting
   right after the chips, in flow, whenever that spare room isn't there
   (a narrow stacked layout, or a long chip row wrapping). */
.demo-step-send { margin-top: auto; padding-top: 10px; }
/* The protruding tab: sits half in, half out of the panel's own top
   edge, and, because it carries .sketch-own-position, opting it out of
   the ".sketch > *" rule that would otherwise pin it to z-index 1, an
   explicit z-index here puts it above the sketch-border SVG (z-index 0)
   instead of behind it. */
.draw-feeling-tab {
  position: absolute;
  top: -20px;
  left: 28px;
  z-index: 2;
  font-family: var(--font-hand);
  font-size: 1.15rem;
  color: #fff;
  background: linear-gradient(135deg, var(--accent-primary), #5b32d6);
  padding: 12px 26px;
  border-radius: 999px;
  box-shadow: 0 8px 20px rgba(124, 77, 255, 0.4);
}
.demo-step { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.step-label { margin: 0; color: var(--accent-secondary); font-size: 1.05rem; }
.step-label-count { color: var(--text-muted); font-size: 0.85rem; }
/* Flashed by flashFeelingCap() in app.js when a pick over the feeling
   cap gets rejected — three quick red pulses on the (n/3) count itself,
   the same spot the user was just looking at. */
.step-label-count.flash-cap { animation: feeling-cap-flash 0.6s ease-in-out; }
@keyframes feeling-cap-flash {
  0%, 100% { color: var(--text-muted); }
  20%, 60% { color: var(--accent-danger); }
  40%, 80% { color: var(--text-muted); }
}
/* No overflow:hidden here: this element carries its own sketch-border
   wobble (data-sketch), which needs to bulge slightly OUTSIDE its own
   box. Corner-rounding the canvas content lives on #draw-canvas itself
   instead, so the two clips don't fight each other.
   Portrait, not square, a tall canvas nudges people toward drawing an
   upright (humanoid) figure rather than a stray blob shape, the way a
   square canvas doesn't push either way.
   Width-only 10% wider (height unchanged, aspect ratio NOT preserved):
   characters already render wider than tall once they're small on the
   energy field, so a squarer canvas matches that better than scaling
   the whole 9:16 box up would have. */
.draw-wrap {
  background: #0d1220;
  border-radius: var(--radius-sm);
  /* 24% bigger than clamp(140px, 26vw, 200px), then 10% wider, then
     another 3% wider / 2% taller, 255x408 off the 248x400 attributes
     on #draw-canvas simplifies exactly to a 5:8 ratio. */
  width: clamp(196.73px, 36.52vw, 281.19px);
  aspect-ratio: 5 / 8;
  /* A bit of room to its right in this column that a plain centered
     draw-wrap wasn't using, nudged over into it. */
  margin-left: 14px;
}
/* "1. draw it" sits right above the canvas, centered on the COLUMN by
   default, same rightward nudge as .draw-wrap so it stays centered
   over the canvas instead of the canvas drifting out from under it. */
.demo-step-draw .step-label {
  margin-left: 14px;
}
#draw-canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-sm);
  overflow: hidden;
  touch-action: none;
  cursor: crosshair;
}

.chip-row { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: min(340px, 100%); }
/* 8% smaller across the board (font/padding/icon/swatch) than the
   original 1.05rem / 7px 14px / 20px / 12px.
   Sizing is font-size + padding, not transform:scale, a transform
   wouldn't touch layout, so neighbors would just sit there while the
   picked chip visually overlapped them instead of the row actually
   making room for it. Icon/swatch use em units so they scale along
   with whichever font-size a given state lands on, picked or not,
   without needing their own separate rules per state. */
.chip {
  font-family: var(--font-hand);
  font-size: 0.966rem;
  background: var(--bg-elevated);
  /* No CSS border any more, each chip carries its own hand-drawn
     sketch-border SVG instead (see buildChips() in app.js), same as
     everything else on this site. Element chips pass their own
     element's color through data-sketch-color; emotion chips fall back
     to the shared default accent. */
  border: none;
  border-radius: 999px;
  padding: 6.4px 12.9px;
  color: var(--text-primary);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5.5px;
  transition: font-size 240ms cubic-bezier(0.34, 1.56, 0.64, 1),
    padding 240ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.chip[aria-checked="true"] {
  /* Tints toward the SAME color as this chip's own sketch border
     (--chip-tint, set per-chip in buildChips()) rather than one fixed
     cyan on everything, an element chip picks up its own element's
     color, an emotion chip picks up its own — matching whatever its
     border is already doing. Much closer to the full border colour than
     before (was 16%, barely a tint) to match the app's own selected-chip
     fill, which sits close to its border colour rather than just hinting
     at it. */
  background: color-mix(in srgb, var(--chip-tint, var(--accent-primary)) 55%, var(--bg-elevated));
  /* 13% bigger than the base 0.966rem / 6.4px 12.9px, the pick gets to
     look like the fun, obvious choice, not just a background tint. */
  font-size: 1.092rem;
  padding: 7.23px 14.58px;
}
/* Once something in the row is actually picked, everything else steps
   back to make room for it, a little smaller than the resting size. */
.chip-row:has(.chip[aria-checked="true"]) .chip:not([aria-checked="true"]) {
  font-size: 0.87rem;
  padding: 5.9px 11.9px;
}
.chip .swatch { width: 0.71em; height: 0.71em; border-radius: 50%; display: inline-block; }
.chip-icon { width: 1.19em; height: 1.19em; object-fit: contain; }

/* The teaser chip at the end of each row: greyed out, not a real pick, clicking it doesn't select anything, it just has Myru mention the rest
   live in the app. */
.chip-locked {
  color: var(--text-muted);
  background: rgba(255, 255, 255, 0.04);
  opacity: 0.65;
}
.chip-locked:hover { opacity: 0.9; }

.demo-step-send {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  justify-content: center;
}
.demo-hint { color: var(--text-muted); font-size: 0.95rem; margin: 0; }

/* Step 3's name field — same dark chip-like surface as the emotion
   chips next to it, not a browser-default text input. */
.name-input {
  font-family: var(--font-hand);
  font-size: 1.05rem;
  text-align: center;
  background: var(--bg-elevated);
  border: none;
  border-radius: 14px;
  padding: 9px 16px;
  color: var(--text-primary);
  width: min(220px, 100%);
}
.name-input::placeholder { color: var(--text-muted); }
.name-input:focus { outline: 2px solid var(--accent-secondary); outline-offset: 1px; }

/* Same nav-pink "share" accent as the big share/import button under the
   hero disclaimer, so every entry point into sharing reads as the same
   action wherever it is — and the same pink the wave-nav's own active
   tab already uses, not Myru's separate signature colour. */
.demo-share-btn {
  background: linear-gradient(135deg, var(--nav-pink), #B026C4);
  color: #fff;
  box-shadow: 0 8px 20px rgba(232, 92, 255, 0.3);
}
/* .btn's own display:inline-flex is an author-stylesheet rule at the
   same specificity as [hidden]'s UA-stylesheet display:none, and author
   styles always win that tie — same fix as .share-sheet-import-btn[hidden]. */
.demo-share-btn[hidden] { display: none; }

/* No overflow:hidden on the sketch-bordered element itself, same reason
   as .draw-wrap: it needs its wobble border to bulge past its own box.
   The canvas gets its own rounded clip layer instead. Nameplates
   (.self-label) are appended as direct children of THIS element, so
   they now float freely above the panel rather than being cut off when
   a character's energy pushes it near the top edge. */
.hangout-wrap {
  position: relative;
  border-radius: var(--radius-lg);
  /* Roughly 2x the original clamp(160px, 30vw, 220px): bumps can now
     both rise and dip, so the box needs the extra headroom/legroom to
     keep them from running off the top or bottom edge. */
  height: clamp(320px, 60vw, 440px);
}
.hangout-canvas-clip {
  position: absolute;
  inset: 0;
  /* 86% transparent base, the sky behind the page shows through the top
     half (above the terrain line). The canvas itself paints a near-opaque
     fill from the terrain line down to the bottom (see draw() in app.js),
     so the "floor" half still reads solid on top of this. */
  background: rgba(26, 35, 64, 0.14);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
#hangout-canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; }

/* Tap-a-bar callout, sits directly under .hangout-wrap. --arrow-left
   (0-100%, set by app.js from the tapped bar's own x) positions both the
   box's horizontal anchor and the little pointer triangle so it visibly
   points back up at the bar that was tapped; --chakra-color tints the
   pointer and the name to match that bar's own fixed color. */
.chakra-info {
  position: relative;
  margin-top: 10px;
  padding: 10px 14px 12px;
  border-radius: var(--radius-md);
  background: var(--bg-elevated);
  border: 1.5px solid color-mix(in srgb, var(--chakra-color, var(--accent-primary)) 55%, transparent);
  max-width: 100%;
}
.chakra-info::before {
  content: '';
  position: absolute;
  top: -7px;
  left: var(--arrow-left, 50%);
  width: 12px;
  height: 12px;
  background: var(--bg-elevated);
  border-left: 1.5px solid color-mix(in srgb, var(--chakra-color, var(--accent-primary)) 55%, transparent);
  border-top: 1.5px solid color-mix(in srgb, var(--chakra-color, var(--accent-primary)) 55%, transparent);
  /* Rotation folded into the same transform as the horizontal centering
     instead of the standalone `rotate` property — Samsung Internet (and
     other older/lagging Chromium forks) render this element as a plain
     unrotated, unbordered-looking square when `rotate` isn't supported,
     even though color-mix() itself works fine (the tooltip box's own
     border, right above, uses the same color-mix and renders correctly
     everywhere) — `transform` has been universally supported for over a
     decade, so folding the 45° turn into it removes the one part of this
     rule that was actually the weak link. */
  transform: translateX(-50%) rotate(45deg);
  /* Above .sketch-svg's own z-index:0 (see sketch-border.js), so this
     stays the crisp, clean point the wobble border's own top-edge notch
     traces alongside rather than something the SVG line paints over. */
  z-index: 1;
}
.chakra-info-name {
  margin: 0 0 2px;
  font-weight: 700;
  color: var(--chakra-color, var(--accent-primary));
}
.chakra-info-desc {
  margin: 0;
  font-size: 0.92rem;
  color: var(--text-muted);
}
.energy-disclaimer {
  margin: 10px 0 0;
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--text-muted);
  text-align: center;
}

/* ---------- Share/import (app.js's initShareSheet, card_share.dart's
   CardShareMenuButton ported to a modal) ---------- */
.share-import-btn {
  display: flex;
  margin: 16px auto 0;
  background: linear-gradient(135deg, var(--nav-pink), #B026C4);
  box-shadow: 0 8px 24px rgba(232, 92, 255, 0.35);
}

.share-sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(6, 8, 16, 0.62);
  backdrop-filter: blur(3px);
  padding: 0;
}
/* [hidden]'s UA-stylesheet display:none loses to the class rule above on
   specificity alone — without this the sheet (and its poster-wrap/empty
   sibling below) would render open on first paint. */
.share-sheet-backdrop[hidden] { display: none; }
@media (min-width: 640px) {
  .share-sheet-backdrop { align-items: center; padding: 24px; }
}

.share-sheet {
  position: relative;
  width: 100%;
  max-width: 420px;
  max-height: 92vh;
  overflow-y: auto;
  background: var(--bg-card-gradient);
  padding: 22px 20px 24px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
@media (min-width: 640px) {
  .share-sheet { border-radius: var(--radius-lg); }
}

.share-sheet-close {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-secondary);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}
.share-sheet-close:hover { color: var(--text-primary); }

.share-sheet-title {
  margin: 0 32px 14px 0;
  font-size: 1.2rem;
  text-align: center;
}

.share-sheet-poster-wrap {
  display: flex;
  justify-content: center;
  margin-bottom: 16px;
}
.share-sheet-poster-wrap[hidden] { display: none; }
.share-sheet-poster-wrap canvas {
  width: 100%;
  max-width: 300px;
  height: auto;
  border-radius: var(--radius-md);
}

.share-sheet-empty {
  text-align: center;
  color: var(--text-muted);
  font-size: 0.95rem;
  margin: 4px 0 18px;
}

.share-sheet-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.share-sheet-actions[hidden] { display: none; }
.share-sheet-actions .btn { width: 100%; }
/* The GIF now carries the same character data the file does (see
   drawCharacterDataPatch/NotselfGif.decode) — the file export is now a
   reliability fallback for a resized/re-hosted GIF losing its patch, not
   the primary path, so it reads as the smaller, secondary option here. */
.share-sheet-actions .btn-file-small {
  align-self: center;
  width: auto;
  padding: 6px 14px;
  font-size: 0.6rem;
}

.share-sheet-import-btn {
  width: 100%;
  margin-top: 14px;
}
/* .btn's own display:inline-flex is an author-stylesheet rule at the
   same specificity as [hidden]'s UA-stylesheet display:none, and author
   styles always win that tie — so a hidden .btn stayed visible without
   this, same root cause as .share-sheet-backdrop[hidden] above. */
.share-sheet-import-btn[hidden] { display: none; }

.share-sheet-status {
  min-height: 1.3em;
  margin: 12px 0 0;
  text-align: center;
  font-size: 0.85rem;
  color: var(--text-secondary);
}

/* ---------- Back-button guard (initBackGuard) ---------- */
.back-guard-backdrop {
  position: fixed;
  inset: 0;
  z-index: 220;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(6, 8, 16, 0.62);
  backdrop-filter: blur(3px);
  padding: 24px;
}
.back-guard-backdrop[hidden] { display: none; }

.back-guard {
  position: relative;
  width: 100%;
  max-width: 360px;
  background: var(--bg-card-gradient);
  padding: 26px 22px 22px;
  border-radius: var(--radius-lg);
  text-align: center;
}

.back-guard-title {
  margin: 0 0 18px;
  font-size: 1.05rem;
  color: var(--accent-danger);
}

.back-guard .share-import-btn { width: 100%; }

.back-guard-leave {
  display: block;
  width: 100%;
  margin: 12px 0 0;
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: var(--font-hand);
  font-size: 0.9rem;
  text-decoration: underline;
  cursor: pointer;
}
.back-guard-leave:hover { color: var(--text-secondary); }
.share-sheet-status.is-error { color: var(--accent-danger); }
.share-sheet-status.is-ok { color: var(--accent-earth); }

/* A user-created self reads as tappable (the two seeded demo selves
   don't open the share sheet, see initHangoutDrag's isDrawn check). */
.hangout-canvas.has-drawn-selves { cursor: pointer; }

/* Nameplate above each self: level-of-detail matches the app's own
   character label in hangout_screen.dart, a SketchBorder name pill sitting
   right above a thin energy bar (see EnergyRateBar in hangout_energy.dart). */
.self-label {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, -100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  pointer-events: none;
}
.self-name {
  background: rgba(20, 27, 45, 0.65);
  border-radius: 8px;
  padding: 2px 6px;
  font-size: 0.62rem;
  letter-spacing: 0.02em;
  line-height: 1.3;
  /* Not nowrap: the two seeded hero characters carry much longer joke
     names than the short generated "Adjective Element" ones (see
     selfDisplayName in app.js), so this wraps instead of running the
     pill off the edge of the small hero canvas. A short name just never
     gets tall enough to wrap, so this is a no-op for those. */
  max-width: 100px;
  white-space: normal;
  text-align: center;
}
.self-energy-row {
  display: flex;
  align-items: center;
  gap: 4px;
}
.self-energy-track {
  width: 36px;
  height: 4px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 4px;
  overflow: hidden;
}
.self-energy-fill {
  height: 100%;
  border-radius: 4px;
  transition: width 150ms linear, background-color 300ms ease;
}
.self-energy-num {
  font-size: 0.55rem;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.75);
  line-height: 1;
}

@media (max-width: 480px) {
  body { font-size: 17px; }
  .wrap { padding: 0 16px; }
  .demo-panel { padding: 40px 18px 20px; gap: 20px; }
  .draw-feeling-tab { left: 18px; padding: 9px 18px; font-size: 1rem; }
  .signup-inner { padding: 24px 18px; }
  h1 { font-size: clamp(1.8rem, 9vw, 2.6rem); }
  .btn-lg { padding: 12px 24px; font-size: 1.1rem; }
}

/* ---------- Identity forms ---------- */
.signup-inner {
  background: var(--bg-card-gradient);
  border-radius: var(--radius-lg);
  padding: 40px;
  text-align: center;
}
.identity-form { display: flex; flex-direction: column; gap: 14px; align-items: center; }
.oauth-row { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
.oauth-slot { min-height: 40px; }
.divider {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--text-muted);
  font-size: 1rem;
  width: 100%;
  max-width: 320px;
}
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: rgba(159, 168, 218, 0.2); }
.email-row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.email-row input,
.identity-form textarea {
  background: var(--bg-elevated);
  border: 1px solid rgba(159, 168, 218, 0.25);
  border-radius: 12px;
  padding: 12px 14px;
  color: var(--text-primary);
  font-family: var(--font-hand);
  font-size: 1.1rem;
  min-width: 220px;
}
.email-row input:focus,
.identity-form textarea:focus { outline: none; border-color: var(--accent-secondary); }
.identity-form textarea { width: 100%; max-width: 480px; min-height: 80px; resize: vertical; }

.form-status { margin: 0; font-size: 1rem; min-height: 1.2em; color: var(--text-secondary); }
.form-status.is-ok { color: var(--accent-earth); }
.form-status.is-error { color: var(--accent-danger); }

.apple-btn {
  background: #000; color: #fff; border: 1px solid #333; border-radius: 999px;
  padding: 9px 18px; font-family: var(--font-hand); cursor: pointer; height: 40px;
}
.apple-btn:hover { background: #111; }

/* ---------- Comments ---------- */
.comment-locked {
  max-width: 480px;
  margin: 0 auto 32px;
  background: var(--bg-surface);
  border-radius: var(--radius-md);
  padding: 20px;
  text-align: center;
  color: var(--text-secondary);
}
.comment-form[hidden] { display: none; }
.comment-form { max-width: 560px; margin: 0 auto 40px; }
.comment-form-actions { display: flex; align-items: center; gap: 16px; }
.comment-list { max-width: 560px; margin: 0 auto; display: grid; gap: 14px; }
.comment-item {
  background: var(--bg-surface);
  border: 1px solid rgba(159, 168, 218, 0.14);
  border-radius: var(--radius-sm);
  padding: 16px 18px;
}
.comment-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
.comment-name { color: var(--accent-secondary); font-size: 1.05rem; }
.comment-head time { color: var(--text-muted); font-size: 0.9rem; }
.comment-body { margin: 0 0 8px; color: var(--text-primary); white-space: pre-wrap; word-break: break-word; }
.comment-empty { text-align: center; color: var(--text-muted); }

.comment-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.reaction-btn {
  background: var(--bg-elevated);
  border: 1px solid rgba(159, 168, 218, 0.2);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 0.95rem;
  color: var(--text-secondary);
  cursor: pointer;
  display: inline-flex;
  gap: 5px;
  align-items: center;
}
.reaction-btn:hover { border-color: var(--accent-secondary); }
.reaction-btn img { width: 16px; height: 16px; object-fit: contain; }
.reaction-btn.mine { border-color: var(--accent-soul); color: var(--text-primary); }
.reply-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-family: var(--font-hand);
  font-size: 0.95rem;
  text-decoration: underline;
}
.reply-list { margin-top: 12px; padding-left: 18px; border-left: 2px solid rgba(159, 168, 218, 0.15); display: grid; gap: 10px; }
.reply-form { display: none; margin-top: 10px; gap: 8px; }
.reply-form.open { display: flex; }
.reply-form textarea {
  flex: 1;
  min-height: 46px;
  background: var(--bg-elevated);
  border: 1px solid rgba(159, 168, 218, 0.25);
  border-radius: 10px;
  padding: 8px 10px;
  color: var(--text-primary);
  font-family: var(--font-hand);
}

/* ---------- Support / donate ---------- */
.support-inner {
  background: var(--bg-card-gradient);
  border-radius: var(--radius-lg);
  padding: 32px;
  text-align: center;
  max-width: 480px;
  margin: 0 auto;
}
.support-note { color: var(--text-secondary); margin: 0 0 16px; }
#donate-chips { margin-bottom: 14px; }
#donate-custom {
  width: 110px;
  background: var(--bg-elevated);
  border: 1px solid rgba(159, 168, 218, 0.25);
  border-radius: 12px;
  padding: 12px 14px;
  color: var(--text-primary);
  font-family: var(--font-hand);
  font-size: 1.1rem;
}
#donate-custom:focus { outline: none; border-color: var(--accent-secondary); }

/* ---------- About page ---------- */
.about-hero { padding-bottom: 8px; text-align: center; }
.about-lead {
  max-width: 640px;
  margin: 0 auto;
  color: var(--text-primary);
  font-size: 1.15rem;
}
.about-more { margin: 10px auto 0; max-width: 640px; text-align: center; }
.about-more-toggle {
  background: none;
  border: none;
  color: var(--accent-secondary);
  font-family: var(--font-hand);
  font-size: 1rem;
  text-decoration: underline;
  cursor: pointer;
  padding: 4px 0;
}
.about-more-toggle:hover { color: var(--text-primary); }
.about-more-content {
  margin-top: 12px;
  text-align: left;
  color: var(--text-primary);
  font-size: 0.98rem;
}
/* Visual-only collapse (see initAboutMore in app.js) — max-height clip
   instead of the `hidden` attribute or display:none, so the full text
   still exists in the rendered page for anything reading the page's raw
   text content (AI browsing tools, search crawlers) even while a human
   visitor only sees it after clicking "...more". Generous max-height
   covers the real content's height with room to spare; overflow:hidden
   is what actually keeps it visually clipped to nothing at 0. */
.about-more-content.is-collapsed {
  max-height: 0;
  overflow: hidden;
  margin-top: 0;
}
.about-more-content:not(.is-collapsed) { max-height: 4000px; }
/* The EN body is one unbroken paragraph (kept verbatim, not ours to
   reformat); the TH translation uses blank-line paragraph breaks, so
   this renders those instead of collapsing them like normal text flow
   would, a no-op for EN, which has none. */
.about-more-content p { white-space: pre-line; }
.about-more-credits { color: var(--text-primary); font-size: 0.9rem; }
.sources-list {
  margin: 14px 0 0;
  padding-left: 1.4em;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 0.9rem;
  color: var(--text-primary);
}
.sources-list li { padding-left: 4px; }
.sources-list a { color: var(--accent-secondary); text-decoration: underline; }
.sources-list a:hover { color: var(--text-primary); }
.about-processes { margin: 20px auto 0; }
.about-cta {
  background: var(--bg-card-gradient);
  border-radius: var(--radius-lg);
  padding: 32px;
  text-align: center;
  max-width: 480px;
  margin: 0 auto;
}
.about-cta p { color: var(--text-primary); }
.about-cta .btn { margin: 6px; }

/* ---------- Myru, peeking from behind the mystery button ---------- */
.myru-peek {
  position: fixed;
  left: 30px;
  bottom: 52px;
  z-index: 999;
  width: 34px;
  height: 34px;
  object-fit: contain;
  pointer-events: none;
  opacity: 0;
  transform: translateY(22px) scale(0.5) rotate(-8deg);
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.55));
}
.myru-peek.peeking { animation: myru-peek-bob 2.4s ease-in-out; }
@keyframes myru-peek-bob {
  0% { opacity: 0; transform: translateY(22px) scale(0.5) rotate(-8deg); }
  28% { opacity: 1; transform: translateY(-10px) scale(1) rotate(4deg); }
  70% { opacity: 1; transform: translateY(-10px) scale(1) rotate(-4deg); }
  100% { opacity: 0; transform: translateY(22px) scale(0.5) rotate(-8deg); }
}
@media (prefers-reduced-motion: reduce) {
  .myru-peek.peeking { animation: none; }
}

/* ---------- Mystery button ---------- */
.mystery-btn {
  position: fixed;
  left: 18px;
  bottom: 18px;
  z-index: 1000;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: rgba(18, 8, 46, 0.92);
  color: var(--myru-ink);
  border: 1.2px solid rgba(255, 46, 210, 0.7);
  font-family: var(--font-hand);
  font-size: 1.6rem;
  cursor: pointer;
  box-shadow: 0 0 14px rgba(255, 46, 210, 0.28);
}
.mystery-btn:hover { box-shadow: 0 0 20px rgba(255, 46, 210, 0.45); }

/* The get-early-access link Myru's bubble carries for the mystery-button
   message, same button classes, just small enough for the bubble's own
   max-width and a little breathing room above it. */
/* Same pink gradient as the header's own "Get early access" button
   (.header-right .btn-primary) — this used to fall through to .btn-
   primary's plain default (purple), so Myru's own early-access link
   didn't match the one at the top of the page. */
.myru-bubble-cta {
  display: inline-block;
  margin-top: 8px;
  background: linear-gradient(135deg, var(--myru-accent), #C91FB0);
  box-shadow: 0 8px 24px rgba(255, 46, 210, 0.35);
}

/* ---------- Footer ---------- */
.site-footer {
  border-top: 1px solid rgba(159, 168, 218, 0.12);
  padding: 28px 0 40px;
  color: var(--text-muted);
  font-size: 1rem;
  text-align: center;
}


/* ---------- Discord CTA ---------- */
.discord-cta {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px dashed rgba(159, 168, 218, 0.28);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.discord-blurb {
  margin: 0;
  color: var(--text-secondary);
  font-size: 1rem;
}
.btn-discord {
  background: #5865F2;
  color: #fff;
  box-shadow: 0 8px 24px rgba(88, 101, 242, 0.32);
}
.btn-discord:hover { background: #4a56d6; }
.discord-cta.is-highlight {
  transform: scale(1.03);
}
.discord-cta.is-highlight .discord-blurb { color: var(--accent-earth); }
.footer-link { color: var(--accent-secondary); text-decoration: none; }
.footer-link:hover { text-decoration: underline; }

/* ---------- Signup counters ---------- */
.signup-stats {
  margin: -6px 0 16px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: 8px;
  color: var(--text-secondary);
  font-size: 1rem;
}
.signup-stats strong {
  color: var(--accent-secondary);
  font-weight: 400;
  font-size: 1.15rem;
}
.stat-sep { opacity: 0.4; }

/* ---------- Email verification / comment ownership ---------- */
.verify-block { margin-top: 14px; display: flex; flex-direction: column; gap: 10px; align-items: center; }
.comment-identity { margin: 0 0 10px; color: var(--text-muted); font-size: 0.95rem; text-align: center; }
.comment-item.is-deleted { opacity: 0.55; }
.comment-item.is-deleted .comment-body { font-style: italic; color: var(--text-muted); }
.delete-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: var(--font-hand);
  font-size: 0.95rem;
  cursor: pointer;
  padding: 4px 6px;
}
.delete-btn:hover { color: var(--accent-danger); }
.delete-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.edit-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: var(--font-hand);
  font-size: 0.95rem;
  cursor: pointer;
  padding: 4px 6px;
}
.edit-btn:hover { color: var(--accent-secondary); }
.edit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.comment-edited { color: var(--text-muted); font-size: 0.85rem; }
/* [hidden] must win over the flex display below, or the browser's own
   [hidden] { display: none } (a user-agent rule) loses to this class
   selector (an author rule always beats a UA one at equal specificity)
   and the form stays visible despite the hidden attribute being set. */
.edit-form[hidden] { display: none; }
.edit-form { display: flex; flex-direction: column; gap: 8px; margin: 0 0 8px; }
.edit-form textarea {
  min-height: 60px;
  background: var(--bg-elevated);
  border: 1px solid rgba(159, 168, 218, 0.25);
  border-radius: 10px;
  padding: 8px 10px;
  color: var(--text-primary);
  font-family: var(--font-hand);
  resize: vertical;
}
.edit-form-actions { display: flex; gap: 8px; }
