// `|| {}` guards against _ds_bundle.js failing to load in production (a
// real network failure, not reproducible on localhost): destructuring
// straight off `undefined` would throw here and abort the rest of this
// file's top-level code (see Shell.jsx for the full mechanism this fixes).
const { Kicker, Card, Button, Input, Textarea, Select, Checkbox, RadioGroup, Icon, Rule, Dialog, StatBlock } = window.INKStudioDesignSystem_0136e3 || {};

// Mutually exclusive ranges (a SAR 12,000 project satisfies exactly one),
// deliberately separate from the package "starting from" prices — a
// Budget Band is client budget information, not a package selector. The
// values below are the real state (used for budgetIndex and passed to
// ForestScene) and stay identical in both languages so switching
// language never resets/loses the selection.
const BUDGET_OPTIONS = ["Under SAR 1,000", "SAR 1,000 – 4,999", "SAR 5,000 – 9,999", "SAR 10,000 – 14,999", "SAR 15,000+", "Not sure yet"];
const BUDGET_LABELS_AR = {
  "Under SAR 1,000": "أقل من 1,000 ر.س",
  "SAR 1,000 – 4,999": "1,000 – 4,999 ر.س",
  "SAR 5,000 – 9,999": "5,000 – 9,999 ر.س",
  "SAR 10,000 – 14,999": "10,000 – 14,999 ر.س",
  "SAR 15,000+": "15,000 ر.س فأكثر",
  "Not sure yet": "غير متأكد بعد",
};
function budgetSelectOptions(ar) {
  return BUDGET_OPTIONS.map((v) => ({ value: v, label: ar ? BUDGET_LABELS_AR[v] : v }));
}
const TIMING_LABELS_AR = { "This week": "هذا الأسبوع", "Within a month": "خلال شهر", "Later": "لاحقًا" };
function timingOptions(ar) {
  return ["This week", "Within a month", "Later"].map((v) => ({ value: v, label: ar ? TIMING_LABELS_AR[v] : v }));
}
// Builds the WhatsApp hand-off message from a captured form snapshot.
// Budget/timing values are stored as fixed EN keys regardless of display
// language (see BUDGET_OPTIONS/timingOptions above), so the AR label maps
// already used for the on-page Select/RadioGroup are reused here verbatim
// rather than re-typing another copy of the same translations.
function buildWhatsAppMessage(ar, s) {
  const serviceLabel = (key) => { const svc = SERVICE_LIST.find((x) => x.key === key); return svc ? (ar ? svc.ar : svc.en) : key; };
  const budgetLabel = ar ? (BUDGET_LABELS_AR[s.budget] || s.budget) : s.budget;
  const timingLabel = ar ? (TIMING_LABELS_AR[s.when] || s.when) : s.when;
  const lines = ar ? ["مرحبًا INK Studio،", "", "أرغب في بدء مشروع.", "", `الاسم: ${s.name}`] : ["Hello INK Studio,", "", "I'd like to start a project.", "", `Name: ${s.name}`];
  if (s.organisation) lines.push(ar ? `الجهة: ${s.organisation}` : `Organisation: ${s.organisation}`);
  if (s.email) lines.push(ar ? `البريد الإلكتروني: ${s.email}` : `Email: ${s.email}`);
  if (s.whatsapp) lines.push(ar ? `واتساب: ${s.whatsapp}` : `WhatsApp: ${s.whatsapp}`);
  if (s.services.length) {
    lines.push("", ar ? "الخدمات:" : "Services:", s.services.map(serviceLabel).join(ar ? "، " : ", "));
  }
  lines.push("", ar ? `الميزانية: ${budgetLabel}` : `Budget: ${budgetLabel}`, ar ? `الموعد: ${timingLabel}` : `Timing: ${timingLabel}`);
  if (s.brief) lines.push("", ar ? "الفكرة:" : "The idea:", s.brief);
  lines.push("", ar ? "— أُرسلت عبر inkstudio.sa" : "— Sent from inkstudio.sa");
  return lines.join("\n");
}

// The DS Input/Textarea/Select components' own `required` prop only paints
// the red label asterisk — it's deliberately stripped before reaching the
// native element (see components/forms/Input.jsx), so it never enabled
// real browser constraint validation here, on ANY field. Name and What You
// Need are the only fields that can actually be left blank, so this checks
// those explicitly at submit time; Budget/Timing already always carry a
// selected value via their own state defaults ("Not sure yet" / "Within a
// month"), so no separate emptiness check is needed for them.
function isValidEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }

const TIMING_SPRING = {
  "This week": { k: 1.32, c: 0.82 },
  "Within a month": { k: 1, c: 1 },
  "Later": { k: 0.7, c: 1.28 },
};
const BASE_K = 120;
const BASE_C = 14;
const MAX_TILT = 3.2; // degrees, pointer proximity
const FOCUS_TILT = 1.7; // degrees, additional bias toward the focused field
const IMPULSE = 0.55; // deg/s kick per keystroke

function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

// One damped-spring step (semi-implicit Euler, mass = 1). Slightly
// underdamped by design across the whole file — everything should
// settle with one small, tasteful overshoot, never a snap and never a
// bounce.
function springStep(state, target, k, c, dt) {
  const accel = (target - state.pos) * k - state.vel * c;
  state.vel += accel * dt;
  state.pos += state.vel * dt;
}

// A field's own wrapper — lets the specific active input read as
// fractionally elevated off the sheet, independent of the sheet's own
// tilt. Tiny, discrete, CSS-eased; no physics needed at this scale.
function FieldLift({ id, focusedField, reduced, children }) {
  const active = id === focusedField;
  return (
    <div
      style={{
        transform: active && !reduced ? "translateY(-2px)" : "translateY(0)",
        filter: active && !reduced ? "drop-shadow(0 4px 8px rgba(20,17,15,0.10))" : "none",
        transition: "transform 320ms cubic-bezier(0.34,1.56,0.64,1), filter 320ms ease",
      }}
    >
      {children}
    </div>
  );
}

// ============================================================
// THE QUIET FOREST
// The Contact section's own environment, not an illustration placed
// on top of it: one full-bleed painted world (mist, rows of trees,
// rain, a wet floor) spanning the entire section edge to edge, from
// just under the header to the bottom of the section, continuing
// underneath the form's column as well as the text column. Detail and
// contrast are heaviest on the left, where the creature lives, and
// fall away toward the right so the form — which sits on its own
// opaque card regardless — always reads against a calm backdrop.
// Pure decoration: aria-hidden, pointer-events: none, absolutely
// positioned to fill its parent section so it never intercepts a
// click. Idle-only for now — no field reactions yet.
// ============================================================
const FOREST_W = 1600;
const FOREST_H = 900;
const FOREST_GROUND = 760;

// A tapered trunk silhouette from a center x, half-widths at base/top,
// and a height — the same hand-authored curve shape as before, just
// parametrized so a whole row of trees can be laid out across the now
// much wider canvas without hand-plotting every point.
function trunkPath(x, baseW, topW, h, groundY) {
  const topY = groundY - h;
  const wob = Math.min(9, baseW * 0.85);
  const n = (v) => Math.round(v * 10) / 10;
  return `M ${n(x - baseW)},${groundY} C ${n(x - baseW + wob)},${n(groundY - h * 0.4)} ${n(x - topW - wob)},${n(topY + h * 0.3)} ${n(x - topW)},${n(topY)} L ${n(x + topW)},${n(topY)} C ${n(x + topW + wob)},${n(topY + h * 0.3)} ${n(x + baseW - wob)},${n(groundY - h * 0.4)} ${n(x + baseW)},${groundY} Z`;
}

// Detail/contrast falls off left → right so the environment reads as
// calm and simple wherever the form sits, without ever fading to
// nothing (this is edge-to-edge, not a vignette).
function densityAt(x) {
  const t = clamp((x - 640) / 760, 0, 1);
  return 1 - t * 0.64;
}

function rainField(n, spacingX, xOffset, baseLen, lenVar, slant, spanW) {
  return Array.from({ length: n }, (_, i) => {
    const x = (i * spacingX + xOffset) % spanW;
    const y = (i * 53 + xOffset * 3) % 340;
    const len = baseLen + (i % 3) * lenVar;
    return { x, y, len, x2: x + slant, k: densityAt(x) };
  });
}

const DIST_TREES = [
  { x: 60, h: 520, baseW: 7, topW: 2.2 }, { x: 210, h: 580, baseW: 8, topW: 2.5 },
  { x: 380, h: 500, baseW: 6.5, topW: 2 }, { x: 560, h: 610, baseW: 8, topW: 2.5 },
  { x: 760, h: 470, baseW: 6, topW: 2 }, { x: 950, h: 540, baseW: 7, topW: 2.2 },
  { x: 1140, h: 460, baseW: 6, topW: 2 }, { x: 1330, h: 510, baseW: 6.5, topW: 2.2 },
  { x: 1520, h: 440, baseW: 5.5, topW: 1.8 },
];
const MID_TREES = [
  { x: 140, h: 640, baseW: 13, topW: 4.2 }, { x: 400, h: 700, baseW: 15, topW: 5 },
  { x: 660, h: 600, baseW: 12, topW: 4 }, { x: 900, h: 630, baseW: 11, topW: 3.6 },
  { x: 1160, h: 560, baseW: 10, topW: 3.2 }, { x: 1420, h: 580, baseW: 9.5, topW: 3 },
];
const FRONT_TREES = [
  { x: -10, h: 780, baseW: 32, topW: 11 }, { x: 130, h: 660, baseW: 20, topW: 8 },
  { x: 1610, h: 720, baseW: 28, topW: 10 },
];

// ----- service hats -----
// A fixed canonical stacking order — independent of the order services
// were actually clicked in, so the stack always looks the same for a
// given set of selections. Index 0 sits lowest (right on the head),
// higher indices stack upward.
const HAT_ORDER = ["media", "motion", "print", "custom3d"];
// The form fields whose keystrokes count as "the visitor is telling him
// something" for the reading/clipboard reaction — the budget select and
// the checkboxes/radios fire their own focus/keydown events too, but
// they're not prose, so they're deliberately excluded.
const TEXT_FIELDS = ["n", "o", "e", "p", "brief"];
// Full equip/remove sequence: dip (bow forward, arm reaches, ears fold)
// -> settle (hat added/removed, small bounce) -> raise (everything
// returns) -> total ~800ms, comfortably in the "readable, not rushed"
// 0.7-1.0s range.
const HAT_PHASE_MS = { dip: 300, settle: 220, raise: 300 };
const HAT_ROT = [-6, 7, -5, 6];
const HAT_DX = [0, -5, 6, -3];
const HAT_STEP = 44;

// Each hat is drawn roughly twice the linear size of the first pass
// (~70-80 wide, ~48-56 tall) so it properly sits around the head rather
// than floating above it as a small colored blob, and each one carries a
// small, unmistakable identifying mark in the site's own brand colors —
// not the same shape recolored four times. Still the same flat
// illustrated language as the creature (a vertical dark-to-light
// gradient body plus a translucent highlight), never a pasted-on icon.
function renderHat(id) {
  switch (id) {
    case "media":
      // Dark cinematography cap — a flat, wide-brimmed director's cap
      // with a small cobalt-blue viewfinder + REC dot, unmistakably
      // "media" against the near-black body.
      return (
        <>
          <path d="M -38,4 C -38,-24 -18,-40 0,-40 C 18,-40 38,-24 38,4 Z" fill="url(#hatMediaGrad)" />
          <ellipse cx="-13" cy="-27" rx="11" ry="5.5" fill="#fff" opacity="0.1" />
          <rect x="-47" y="0" width="94" height="11" rx="5.5" fill="#0a0a0b" />
          <rect x="-10" y="-26" width="20" height="14" rx="3" fill="none" stroke="#3a56e8" strokeWidth="2.4" opacity="0.95" />
          <circle cx="14" cy="-19" r="3.4" fill="#3a56e8" opacity="0.95" />
        </>
      );
    case "motion":
      // Creative animator beanie — vermilion/orange, domed and slightly
      // slouched, with the site's own circle + square + triangle motion
      // vocabulary picked out in white across the crown.
      return (
        <>
          <path d="M -36,6 C -36,-28 -16,-48 0,-48 C 16,-48 36,-28 36,6 Z" fill="url(#hatMotionGrad)" />
          <rect x="-36" y="2" width="72" height="10" rx="5" fill="#8a2c10" opacity="0.6" />
          <circle cx="-15" cy="-22" r="4.6" fill="none" stroke="#fff" strokeWidth="2" opacity="0.85" />
          <rect x="-5" y="-31" width="9.5" height="9.5" fill="none" stroke="#fff" strokeWidth="2" opacity="0.85" transform="rotate(10 -0.2 -26.2)" />
          <path d="M 11,-14 L 18,-26 L 25,-14 Z" fill="none" stroke="#fff" strokeWidth="2" opacity="0.85" strokeLinejoin="round" />
        </>
      );
    case "print":
      // Warm workshop/press cap — saffron, with a folded crease (a
      // physical, made-thing detail) and a small print-registration
      // crosshair mark, the classic press symbol.
      return (
        <>
          <path d="M -36,4 C -36,-26 -16,-40 0,-40 C 16,-40 36,-26 36,4 Z" fill="url(#hatPrintGrad)" />
          <path d="M -22,-13 C -11,-19 11,-19 22,-13" stroke="#7a5310" strokeWidth="2.2" fill="none" opacity="0.4" strokeLinecap="round" />
          <rect x="-36" y="0" width="72" height="10" rx="5" fill="#8a5a12" opacity="0.55" />
          {/* two small overlapping rounded cards/stickers — reads as
              "print & merchandise" (stickers, cards, printed pieces)
              rather than a generic print-registration mark */}
          <g transform="translate(0,-22)">
            <rect x="-8" y="-9" width="11" height="14" rx="2.4" fill="none" stroke="#6b4a10" strokeWidth="1.6" opacity="0.6" transform="rotate(-13 -2.5 -2)" />
            <rect x="-3" y="-6" width="11" height="14" rx="2.4" fill="none" stroke="#6b4a10" strokeWidth="1.8" opacity="0.8" transform="rotate(11 2.5 1)" />
          </g>
        </>
      );
    case "custom3d":
      // Green technical/fabrication cap — a slightly angular brim (less
      // rounded than the others, reads as "hard", technical) with a
      // small wireframe cube picked out on the crown.
      return (
        <>
          <path d="M -35,4 L -29,-30 C -14,-40 14,-40 29,-30 L 35,4 Z" fill="url(#hatCustomGrad)" />
          <rect x="-35" y="0" width="70" height="10" rx="5" fill="#122e1c" opacity="0.55" />
          <g transform="translate(0,-22)">
            <path d="M -10,-5 L 0,-10 L 10,-5 L 10,6 L 0,11 L -10,6 Z" fill="none" stroke="#eafff0" strokeWidth="1.7" opacity="0.9" />
            <path d="M -10,-5 L 0,0 L 10,-5 M 0,0 L 0,11" fill="none" stroke="#eafff0" strokeWidth="1.7" opacity="0.9" />
          </g>
        </>
      );
    default:
      return null;
  }
}

// ============================================================
// THE SUCCESS SKY
// A lightweight, illustrated atmosphere layer stacked directly above
// the forest (see its "cinema-world" wrapper in ForestScene), revealed
// only once that wrapper translates down during the success ascent.
// Colored as a direct continuation of the forest's own existing
// `forestSky`/`forestFloor` gradients (muted teal-grey) rather than an
// unrelated stock blue, so the two genuinely read as one continuous
// world rather than a cut to a different illustration.
//
// Clouds are drawn as one hand-authored SVG silhouette (an irregular,
// lobed puff — not a circle) reused at several positions/scales/depths,
// each rendered twice: a larger, heavily-blurred pass underneath for a
// hazy outer edge, and a smaller, lightly-blurred pass on top for a
// denser core — so a single cloud reads as soft-edged AND unevenly
// dense, rather than one uniform blurred blob. Position, the one-time
// ascend reveal, and the slow idle drift each live on their own nested
// group (outer -> ascend -> drift) so none of the three ever has to
// share a `transform` with another — the same split this file always
// uses to keep an attribute and a CSS transition/animation from
// fighting over the same property.
// ============================================================
// Deliberately NOT 1600x900 — the forest scene's own SVG below uses that
// exact viewBox, and some of this file's test tooling selects "the scene
// SVG" by matching that viewBox string uniquely. A different (still
// ~16:9) size keeps this an unambiguously separate element.
const SKY_W = 1600;
const SKY_H = 901;
const CLOUD_PUFF_D = "M18,86 C2,86 -2,58 18,52 C12,28 46,14 66,30 C74,8 112,4 128,26 C152,14 180,28 172,50 C194,48 198,76 172,82 C178,100 148,108 128,94 C112,110 76,108 64,92 C42,104 14,98 18,86 Z";

function skyCloud(key, { x, y, scale, opacity, dist, driftAmp, driftDur, driftDelay }, ascending, reduced) {
  return (
    <g key={key} transform={`translate(${x},${y}) scale(${scale})`} opacity={opacity}>
      <g style={{ transform: ascending ? `translateY(${-dist}px)` : "translateY(0)", transition: reduced ? "none" : "transform 1900ms cubic-bezier(0.4,0,0.2,1) 1150ms" }}>
        <g style={reduced ? undefined : { "--cloud-drift-amp": `${driftAmp}px`, animation: `cloud-drift ${driftDur}ms ease-in-out ${driftDelay}ms infinite` }}>
          <use href="#cloudPuff" filter="url(#cloudBlurHazy)" opacity="0.5" />
          <use href="#cloudPuff" transform="translate(16,9) scale(0.68)" filter="url(#cloudBlurSoft)" opacity="0.92" />
        </g>
      </g>
    </g>
  );
}

const SKY_CLOUDS = [
  { x: 130, y: 150, scale: 0.85, opacity: 0.34, dist: 16, driftAmp: 7, driftDur: 46000, driftDelay: -2000 },
  { x: 1180, y: 220, scale: 0.95, opacity: 0.3, dist: 14, driftAmp: 6, driftDur: 41000, driftDelay: -18000 },
  { x: 520, y: 420, scale: 1.35, opacity: 0.42, dist: 32, driftAmp: 10, driftDur: 33000, driftDelay: -9000 },
  { x: -70, y: 490, scale: 1.15, opacity: 0.36, dist: 28, driftAmp: 9, driftDur: 36000, driftDelay: -24000 },
  { x: 900, y: 640, scale: 1.85, opacity: 0.5, dist: 50, driftAmp: 14, driftDur: 26000, driftDelay: -5000 },
  { x: 210, y: 700, scale: 1.55, opacity: 0.44, dist: 46, driftAmp: 12, driftDur: 29000, driftDelay: -15000 },
];

// A separate, PORTRAIT-oriented canvas + cloud spread for mobile's `compact`
// sky — SKY_CLOUDS above is authored for the landscape 1600x901 desktop
// canvas and clusters every cloud in one horizontal band, which is exactly
// wrong for a tall phone screen. These spread top-to-bottom instead (some
// high in the empty space above where the message eventually appears, some
// low below it, a few bleeding in from the sides), so the whole vertical
// sweep of the viewport carries clouds rather than just the band around the
// text.
const MOBILE_SKY_W = 400;
const MOBILE_SKY_H = 866;
// The visible-viewport-worth of clouds above, kept byte-for-byte identical
// to preserve the top composition exactly (explicitly asked to stay as-is).
const MOBILE_SKY_CLOUDS = [
  { x: 70, y: 70, scale: 0.6, opacity: 0.3, dist: 10, driftAmp: 6, driftDur: 42000, driftDelay: -3000 },
  { x: 300, y: 130, scale: 0.75, opacity: 0.34, dist: 14, driftAmp: 7, driftDur: 38000, driftDelay: -16000 },
  { x: -30, y: 230, scale: 0.85, opacity: 0.3, dist: 16, driftAmp: 8, driftDur: 44000, driftDelay: -9000 },
  { x: 340, y: 300, scale: 0.65, opacity: 0.28, dist: 12, driftAmp: 6, driftDur: 40000, driftDelay: -22000 },
  { x: 120, y: 400, scale: 1.05, opacity: 0.4, dist: 22, driftAmp: 9, driftDur: 34000, driftDelay: -6000 },
  { x: -50, y: 520, scale: 0.9, opacity: 0.34, dist: 18, driftAmp: 8, driftDur: 37000, driftDelay: -27000 },
  { x: 310, y: 580, scale: 1.15, opacity: 0.42, dist: 24, driftAmp: 10, driftDur: 31000, driftDelay: -12000 },
  { x: 90, y: 680, scale: 0.95, opacity: 0.36, dist: 20, driftAmp: 9, driftDur: 35000, driftDelay: -19000 },
  { x: 350, y: 760, scale: 0.7, opacity: 0.3, dist: 14, driftAmp: 7, driftDur: 39000, driftDelay: -4000 },
  { x: 180, y: 810, scale: 1.3, opacity: 0.46, dist: 28, driftAmp: 11, driftDur: 28000, driftDelay: -14000 },
];
// The mobile pass praised the TOP of this composition but found the BOTTOM
// cut off abruptly — the "compact" viewBox above was sized to exactly one
// screen's worth (866 ~= a typical portrait viewport at this width), so its
// bottom edge coincides almost exactly with the visible viewport's own
// bottom edge, and whatever real vh/chrome variance exists exposes that
// `overflow:hidden` box's hard edge against the plain gradient beneath.
// Rather than touch MOBILE_SKY_H/MOBILE_SKY_CLOUDS above (which would
// rescale and reposition the already-correct top), this extends the SAME
// viewBox proportionally (so the top's own scale/placement is byte-for-byte
// unchanged) with an extra band of clouds purely below y=866, then masks
// only that extra band down to transparent — a natural-looking continuation
// that fades to nothing well past where a real device's viewport ends,
// instead of a visible seam.
const MOBILE_SKY_EXTRA_H = 480;
// The bottom fix above left the TOP with the exact same tight, unbuffered
// boundary it always had (deliberately, since it read as "already good"
// at the time) — so once the bottom's seam was gone, the top's own milder
// version of the identical bug (a cloud or two brushing the box's hard
// edge, and zero margin before the plain pale gradient above it) became
// the one that stood out. Same technique, mirrored: a buffer of extra
// canvas ABOVE y=0 (negative viewBox coordinates), its own small cluster
// of clouds, masked out at the far end — never touching MOBILE_SKY_H or
// MOBILE_SKY_CLOUDS, so the tuned, already-correct core still lands on
// the exact same pixels on a normal device.
const MOBILE_SKY_TOP_EXTRA_H = 260;
const MOBILE_SKY_H_FULL = MOBILE_SKY_TOP_EXTRA_H + MOBILE_SKY_H + MOBILE_SKY_EXTRA_H;
const MOBILE_SKY_CLOUDS_TOP_EXTRA = [
  { x: 210, y: -55, scale: 0.65, opacity: 0.26, dist: 12, driftAmp: 6, driftDur: 41000, driftDelay: -10000 },
  { x: 360, y: -125, scale: 0.55, opacity: 0.2, dist: 10, driftAmp: 5, driftDur: 45000, driftDelay: -18000 },
  { x: 30, y: -190, scale: 0.5, opacity: 0.16, dist: 8, driftAmp: 5, driftDur: 47000, driftDelay: -6000 },
];
const MOBILE_SKY_CLOUDS_EXTRA = [
  { x: 250, y: 900, scale: 1.1, opacity: 0.38, dist: 22, driftAmp: 10, driftDur: 33000, driftDelay: -7000 },
  { x: 40, y: 980, scale: 0.85, opacity: 0.3, dist: 16, driftAmp: 8, driftDur: 40000, driftDelay: -21000 },
  { x: 330, y: 1060, scale: 0.7, opacity: 0.24, dist: 12, driftAmp: 7, driftDur: 44000, driftDelay: -13000 },
  { x: 150, y: 1140, scale: 0.6, opacity: 0.18, dist: 10, driftAmp: 6, driftDur: 46000, driftDelay: -2000 },
  { x: 280, y: 1220, scale: 0.5, opacity: 0.12, dist: 8, driftAmp: 5, driftDur: 48000, driftDelay: -25000 },
];

// `compact`: MobileAscentSky's containing block is the WHOLE scrollable
// Contact section (thousands of px tall on a single-column mobile layout),
// not one screen's worth like ForestScene's desktop container — the "the
// camera rises by moving the world down by exactly one viewport-height"
// trick above assumes a roughly viewport-sized box. Forcing the desktop
// SVG's 1600x901 viewBox to fill a ~390x1800 box via `slice` (cover) picked
// the height-driven scale, blowing the effective width out to ~3100px and
// center-cropping all but a ~200px sliver of it — every cloud in
// SKY_CLOUDS sits outside that sliver, so mobile's sky rendered with zero
// visible clouds even though they were being drawn.
// A `position: sticky` layer was tried next (pin the clouds to the current
// 100vh window as the page scrolls through this huge panel) but Chromium
// (confirmed live) does not keep sticky positioning working through an
// ANCESTOR that has its own `transform` — and the ascend reveal directly
// above this is exactly such a transform (translateY 0 -> 100%), so the
// "sticky" layer just silently fell back to its static position instead.
// The actual fix needs no scroll-tracking at all: the visitor is always
// auto-scrolled (see Contact()'s own effect) to land with THIS PANEL'S OWN
// MIDPOINT at the viewport's vertical center — true on every device height,
// since both the scroll target and this panel are computed from the same
// section. So a fixed 100vh-tall band, centered on that same 50% mark via
// plain top/transform (not scroll position), lands exactly on the visible
// window every time. MOBILE_SKY_CLOUDS' own portrait spread then fills that
// full band top-to-bottom, rather than clustering around the message.
function SuccessSky({ ascending, reduced, compact }) {
  return (
    <div aria-hidden="true" style={{ position: "absolute", left: 0, right: 0, bottom: "100%", height: "100%", overflow: "hidden" }}>
      <style>{`
        @keyframes cloud-drift {
          0%, 100% { transform: translateX(calc(var(--cloud-drift-amp) * -1)); }
          50% { transform: translateX(var(--cloud-drift-amp)); }
        }
      `}</style>
      <div style={{ position: "absolute", inset: 0, background: "linear-gradient(to bottom, #f7f9f6 0%, #e7f0ea 32%, #c7dad2 62%, #8fa89d 100%)" }} />
      {compact ? (
        // The core (MOBILE_SKY_H, unchanged) still lands on exactly the
        // same screen pixels as before: the box's top edge shifts UP from
        // the old fixed `calc(50% - 50vh)` position by the top buffer's
        // own vh-equivalent, so the core's own start (now offset
        // MOBILE_SKY_TOP_EXTRA_H into the taller canvas) still meets the
        // real viewport top at the same place. Total height grows by both
        // buffers' combined proportion of the core.
        <div
          style={{
            position: "absolute", left: 0, right: 0,
            top: `calc(50% - ${50 + (MOBILE_SKY_TOP_EXTRA_H / MOBILE_SKY_H) * 100}vh)`,
            height: `calc(${MOBILE_SKY_H_FULL / MOBILE_SKY_H} * 100vh)`,
            overflow: "hidden",
            WebkitMaskImage: `linear-gradient(to bottom, transparent 0%, black ${(MOBILE_SKY_TOP_EXTRA_H / MOBILE_SKY_H_FULL) * 100}%, black ${((MOBILE_SKY_TOP_EXTRA_H + MOBILE_SKY_H) / MOBILE_SKY_H_FULL) * 100}%, transparent 100%)`,
            maskImage: `linear-gradient(to bottom, transparent 0%, black ${(MOBILE_SKY_TOP_EXTRA_H / MOBILE_SKY_H_FULL) * 100}%, black ${((MOBILE_SKY_TOP_EXTRA_H + MOBILE_SKY_H) / MOBILE_SKY_H_FULL) * 100}%, transparent 100%)`,
          }}
        >
          <div style={{ position: "absolute", left: "58%", top: `${((MOBILE_SKY_TOP_EXTRA_H + 0.08 * MOBILE_SKY_H) / MOBILE_SKY_H_FULL) * 100}%`, width: 300, height: 200, borderRadius: "50%", background: "radial-gradient(circle, rgba(240,201,150,0.22), transparent 70%)", filter: "blur(6px)" }} />
          <svg viewBox={`0 ${-MOBILE_SKY_TOP_EXTRA_H} ${MOBILE_SKY_W} ${MOBILE_SKY_H_FULL}`} preserveAspectRatio="xMidYMid slice" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
            <defs>
              <path id="cloudPuff" d={CLOUD_PUFF_D} fill="#ffffff" />
              <filter id="cloudBlurHazy" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="11" /></filter>
              <filter id="cloudBlurSoft" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="3.5" /></filter>
            </defs>
            {MOBILE_SKY_CLOUDS_TOP_EXTRA.map((c, i) => skyCloud(`topExtra${i}`, c, ascending, reduced))}
            {MOBILE_SKY_CLOUDS.map((c, i) => skyCloud(i, c, ascending, reduced))}
            {MOBILE_SKY_CLOUDS_EXTRA.map((c, i) => skyCloud(`extra${i}`, c, ascending, reduced))}
          </svg>
        </div>
      ) : (
        <>
          {/* a very soft, restrained warm glow — the only hint of INK's own
              palette up here, deliberately faint */}
          <div style={{ position: "absolute", left: "58%", top: "10%", width: 340, height: 220, borderRadius: "50%", background: "radial-gradient(circle, rgba(240,201,150,0.22), transparent 70%)", filter: "blur(6px)" }} />
          <svg viewBox={`0 0 ${SKY_W} ${SKY_H}`} preserveAspectRatio="xMidYMid slice" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
            <defs>
              <path id="cloudPuff" d={CLOUD_PUFF_D} fill="#ffffff" />
              <filter id="cloudBlurHazy" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="11" /></filter>
              <filter id="cloudBlurSoft" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="3.5" /></filter>
            </defs>
            {SKY_CLOUDS.map((c, i) => skyCloud(i, c, ascending, reduced))}
          </svg>
        </>
      )}
      {/* faint mist bridging into the forest canopy just below */}
      <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, height: "24%", background: "linear-gradient(to bottom, transparent, rgba(150,176,166,0.5))", filter: "blur(3px)" }} />
    </div>
  );
}

// The mobile submission background: just the ascending sky reveal, none of
// ForestScene's creature/rain/hat-reaction machinery. Two reasons this is a
// separate component rather than ForestScene rendering less on narrow:
// (1) the beaver/forest composition is intentionally dropped from mobile
// entirely, on desktop's own two-column real estate it sits beside the
// form, but mobile's single stacked column leaves it either cropped by the
// scene's own aspect-ratio handling or hidden behind the form card, so it
// never reads as the intended "reacting character" there anyway; (2)
// performance — ForestScene's hat/typing/timing/budget reactions all take
// svc/when/budget/typingPulse/focusedField as props, so on mobile it was
// re-rendering that entire illustration (dozens of gradients, filters and
// keyframed elements) on every keystroke in the form, a primary contributor
// to the reported iPhone typing stutter. This component takes only
// success/reduced, so it never re-renders while the visitor is typing.
// Restrained mobile-only background decoration for Contact's normal (form)
// state — the beaver/forest is intentionally gone from mobile (see
// MobileAscentSky above), but that left the intro/contact-details/form
// stack reading as too visually empty. Reuses the same decorative
// vocabulary as Home/Studio (BG_SHAPES/BG_COLOR, the sitewide .ink-blob
// technique) rather than inventing anything new. Deliberately takes ONLY
// success/reduced as props — exactly the same constraint MobileAscentSky
// documents above — so, like that component, it never re-renders on
// focusedField/typingPulse/svc/budget changes and can't reintroduce the
// keystroke-stutter this page already had to fix once. Fades out once the
// send-off begins so it never competes with the ascending sky/clouds.
function ContactMobileDecor({ success, reduced }) {
  return (
    <div
      aria-hidden="true"
      style={{
        position: "absolute", inset: 0, zIndex: 0, overflow: "hidden", pointerEvents: "none",
        opacity: success ? 0 : 1,
        transition: reduced ? "none" : "opacity 500ms ease",
      }}
    >
      {/* The intro block above the form (heading, contact-details rows,
          reply-time stats) is short but densely packed with its own fixed
          left-hand icon column, measured against actual layout rather than
          guessed: it runs roughly 0-46% of this section's height, with its
          own icon+label rows landing around 16-44%. This round adds more
          decoration through that band too (per this task's explicit ask to
          decorate the intro/details/stats area, not just the form below
          it) by keeping every new mark small, close to the true edge (well
          left of that icon column's own x-position) and offset from the
          rows' own y-positions rather than landing on them, so they read
          as separate background marks rather than a second, confusing
          icon beside a real one. */}
      {[
        { size: 140, left: "-9%", top: "1%", color: "var(--cobalt-600)", op: 0.045 },
        { size: 100, right: "-8%", top: "18%", color: "var(--vermilion-500)", op: 0.04 },
        { size: 95, left: "-9%", top: "34%", color: "var(--saffron-500)", op: 0.04 },
        { size: 130, right: "-8%", top: "52%", color: "var(--saffron-500)", op: 0.045 },
        { size: 110, left: "-8%", top: "66%", color: "var(--vermilion-500)", op: 0.04 },
        { size: 120, right: "-9%", top: "80%", color: "var(--pine-500)", op: 0.04 },
        { size: 130, left: "-9%", bottom: "1%", color: "var(--cobalt-600)", op: 0.04 },
      ].map((b, i) => (
        <div key={`b${i}`} className="ink-blob" style={{ position: "absolute", width: b.size, height: b.size, left: b.left, right: b.right, top: b.top, bottom: b.bottom, background: b.color, opacity: b.op, animation: reduced ? "none" : `ink-blob-morph 9s var(--ease-standard) infinite, ink-drift ${15 + i * 3}s var(--ease-standard) infinite ${-i * 4}s` }} />
      ))}
      {[
        { type: "tote", top: "3%", side: "right", dx: 8, size: 28 },
        { type: "badge", top: "12%", side: "left", dx: 6, size: 20 },
        { type: "mCircle", top: "23%", side: "right", dx: 6, size: 20 },
        { type: "tag", top: "30%", side: "left", dx: 6, size: 20 },
        { type: "sticker", top: "40%", side: "right", dx: 8, size: 22 },
        { type: "camera", top: "58%", side: "left", dx: 6, size: 26 },
        { type: "mic", top: "70%", side: "right", dx: 6, size: 24 },
        { type: "notebook", top: "77%", side: "left", dx: 8, size: 24 },
        { type: "cube", top: "84%", side: "left", dx: 8, size: 26 },
        { type: "printer", top: "89%", side: "right", dx: 6, size: 24 },
        { type: "light", top: "96%", side: "right", dx: 8, size: 24 },
      ].map((m, i) => {
        const Shape = (window.BG_SHAPES || {})[m.type];
        if (!Shape) return null;
        return (
          <div key={`i${i}`} style={{ position: "absolute", width: m.size, height: m.size, top: m.top, [m.side]: m.dx, opacity: 0.13 }}>
            <Shape color={(window.BG_COLOR || {})[m.type] || "var(--ink-400)"} />
          </div>
        );
      })}
    </div>
  );
}

function MobileAscentSky({ success, reduced }) {
  const ascending = success && !reduced;
  return (
    <div aria-hidden="true" style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 0, overflow: "hidden" }}>
      <div
        style={{
          position: "absolute", inset: 0,
          transform: ascending ? "translateY(100%)" : "translateY(0)",
          // Much faster than the old 1150ms-delay/1700ms-duration pair —
          // that pacing matched the paper plane's old ~3600ms flight; now
          // that mobile's flight is ~1.2s (see PaperPlane's narrow-only
          // timing), the sky needs to be most of the way revealed by the
          // time the plane actually vanishes, not still waiting to start.
          transition: reduced ? "none" : "transform 950ms cubic-bezier(0.65,0,0.32,1) 450ms",
        }}
      >
        <SuccessSky ascending={ascending} reduced={reduced} compact />
      </div>
    </div>
  );
}

function ForestScene({ ar, reduced, narrow, sendPhase, success, svc, when, focusedField, typingPulse, budget }) {
  const [looking, setLooking] = React.useState(false);

  // ---- service hats: a small state machine, not independent CSS loops ----
  // `settledHats` is the single source of rendered truth (insertion-order
  // array of hat ids currently shown, fully resting). `animState` is the
  // one equip/remove sequence currently playing, if any. Whenever it's
  // idle (null), this reconciles the next difference between the actual
  // checkbox state (`svc`) and what's rendered, processing hats in the
  // same fixed canonical order they stack in — so rapid clicking never
  // starts two sequences at once, it just queues: each finished sequence
  // re-triggers this effect, which re-reads the latest `svc` and keeps
  // going until the two agree. That's what keeps the beaver from ever
  // ending up with a duplicated, missing, or stuck hat.
  const [settledHats, setSettledHats] = React.useState([]);
  const [hatAnim, setHatAnim] = React.useState(null); // { hat, kind: 'equip'|'remove', phase: 'dip'|'settle'|'raise' }

  React.useEffect(() => {
    if (!reduced) return;
    setSettledHats(HAT_ORDER.filter((h) => svc && svc[h]));
  }, [reduced, svc]);

  // Job dispatcher — only decides WHAT to animate next and flips hatAnim
  // to its first phase. Deliberately does no timer work itself: setting
  // state synchronously inside an effect that also lists that state as a
  // dependency would immediately re-run the effect (React re-renders,
  // sees hatAnim change), and that re-run's cleanup would cancel a timer
  // this same invocation had just armed. Splitting "start the job" from
  // "advance its phases" (below) avoids that self-cancellation.
  React.useEffect(() => {
    if (reduced) return;
    if (hatAnim) return;
    const toEquip = HAT_ORDER.find((h) => svc && svc[h] && !settledHats.includes(h));
    const toRemove = !toEquip ? HAT_ORDER.find((h) => !(svc && svc[h]) && settledHats.includes(h)) : null;
    const hat = toEquip || toRemove;
    if (!hat) return;
    setHatAnim({ hat, kind: toEquip ? "equip" : "remove", phase: "dip" });
  }, [svc, settledHats, hatAnim, reduced]);

  // Phase advancer — owns exactly one timer per phase, armed fresh each
  // time hatAnim itself changes, so its cleanup always cancels the right
  // (and only the right) pending step.
  React.useEffect(() => {
    if (reduced || !hatAnim) return;
    if (hatAnim.phase === "dip") {
      const t = setTimeout(() => {
        setSettledHats((cur) => (hatAnim.kind === "equip" ? [...cur, hatAnim.hat] : cur.filter((h) => h !== hatAnim.hat)));
        setHatAnim((cur) => (cur && cur.phase === "dip" ? { ...cur, phase: "settle" } : cur));
      }, HAT_PHASE_MS.dip);
      return () => clearTimeout(t);
    }
    if (hatAnim.phase === "settle") {
      const t = setTimeout(() => {
        setHatAnim((cur) => (cur && cur.phase === "settle" ? { ...cur, phase: "raise" } : cur));
      }, HAT_PHASE_MS.settle);
      return () => clearTimeout(t);
    }
    if (hatAnim.phase === "raise") {
      const t = setTimeout(() => setHatAnim(null), HAT_PHASE_MS.raise);
      return () => clearTimeout(t);
    }
  }, [hatAnim, reduced]);

  const hatStack = HAT_ORDER.filter((h) => settledHats.includes(h));
  const bowing = !reduced && hatAnim && (hatAnim.phase === "dip" || hatAnim.phase === "settle");
  const pawReaching = !reduced && hatAnim && (hatAnim.phase === "dip" || hatAnim.phase === "settle");
  const hatActive = !!hatAnim;

  // ---- reading: the visitor is actively typing --------------------
  // A held state, not a one-shot: it comes up the moment a real
  // keystroke lands in a text field and stays up for as long as the
  // visitor keeps typing (in the same field or a different one),
  // dropping only after a real pause. `typingPulse` ticks on every
  // keydown anywhere in the form (including the budget select, the
  // checkboxes, the radios), so it's gated here to keystrokes that
  // landed while a genuine text field had focus.
  const [isReading, setIsReading] = React.useState(false);
  const lastTypingPulse = React.useRef(typingPulse);
  const readingHideTimer = React.useRef(null);
  React.useEffect(() => {
    if (typingPulse === lastTypingPulse.current) return;
    lastTypingPulse.current = typingPulse;
    if (!TEXT_FIELDS.includes(focusedField)) return;
    setIsReading(true);
    if (readingHideTimer.current) clearTimeout(readingHideTimer.current);
    readingHideTimer.current = setTimeout(() => setIsReading(false), 1400);
  }, [typingPulse, focusedField]);
  React.useEffect(() => () => { if (readingHideTimer.current) clearTimeout(readingHideTimer.current); }, []);

  // The clipboard itself stays mounted a beat longer than `isReading`
  // — exactly as long as the arm's own lower transition takes — so it
  // travels back down attached to the paw and only vanishes once the
  // paw has actually tucked back in against the body, reading as
  // "put away" rather than a fade hanging in mid-air. Suppressed
  // outright whenever a hat animation currently owns the arm.
  const [noteMounted, setNoteMounted] = React.useState(false);
  const noteHideTimer = React.useRef(null);
  React.useEffect(() => {
    if (isReading) {
      if (noteHideTimer.current) { clearTimeout(noteHideTimer.current); noteHideTimer.current = null; }
      setNoteMounted(true);
      return;
    }
    noteHideTimer.current = setTimeout(() => setNoteMounted(false), HAT_PHASE_MS.dip);
    return () => { if (noteHideTimer.current) clearTimeout(noteHideTimer.current); };
  }, [isReading]);
  const showNote = noteMounted && !hatActive;

  // ---- timing reactions --------------------------------------------
  // "Within a month": a brief one-shot watch-check, the same dip/settle/
  // raise-style job used for hats — raise the arm, hold a beat, lower —
  // triggered only on a real change into this option (never on mount,
  // since it's the field's own default), and only when nothing higher
  // in the priority chain (a hat job) already owns the arm.
  const TIMING_CHECK_MS = { raise: 300, hold: 500, lower: 300 };
  const [timingAnim, setTimingAnim] = React.useState(null); // { phase: 'raise'|'hold'|'lower' }
  const prevWhen = React.useRef(when);
  React.useEffect(() => {
    if (when === "Within a month" && prevWhen.current !== "Within a month" && !reduced) {
      setTimingAnim({ phase: "raise" });
    }
    if (when !== "Within a month" && timingAnim) setTimingAnim(null);
    prevWhen.current = when;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [when, reduced]);
  React.useEffect(() => {
    if (hatAnim && timingAnim) setTimingAnim(null);
    if (hatAnim && budgetActive) setBudgetActive(false);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [hatAnim]);
  React.useEffect(() => {
    if (!timingAnim) return;
    if (timingAnim.phase === "raise") {
      const t = setTimeout(() => setTimingAnim((c) => (c && c.phase === "raise" ? { phase: "hold" } : c)), TIMING_CHECK_MS.raise);
      return () => clearTimeout(t);
    }
    if (timingAnim.phase === "hold") {
      const t = setTimeout(() => setTimingAnim((c) => (c && c.phase === "hold" ? { phase: "lower" } : c)), TIMING_CHECK_MS.hold);
      return () => clearTimeout(t);
    }
    if (timingAnim.phase === "lower") {
      const t = setTimeout(() => setTimingAnim(null), TIMING_CHECK_MS.lower);
      return () => clearTimeout(t);
    }
  }, [timingAnim]);
  const timingChecking = !!timingAnim && !hatActive;

  // ---- budget reaction: reuses the reading/notebook mechanism almost
  // exactly, just triggered by a budget-band change instead of a
  // keystroke in a text field. `budgetActive` comes up the moment the
  // selection actually changes (never on mount) and stays up until a
  // real pause — the identical 1400ms window `isReading` uses — reset
  // on every further change the same way continued typing resets it.
  const [budgetActive, setBudgetActive] = React.useState(false);
  const prevBudget = React.useRef(budget);
  const budgetHideTimer = React.useRef(null);
  React.useEffect(() => {
    if (budget === prevBudget.current) return;
    prevBudget.current = budget;
    setBudgetActive(true);
    if (budgetHideTimer.current) clearTimeout(budgetHideTimer.current);
    budgetHideTimer.current = setTimeout(() => setBudgetActive(false), 1400);
  }, [budget]);
  React.useEffect(() => () => { if (budgetHideTimer.current) clearTimeout(budgetHideTimer.current); }, []);
  const budgetChecking = budgetActive && !hatActive && !isReading && !timingChecking;

  // The calculator itself stays mounted a beat longer than
  // `budgetChecking`, same idiom as the note above, so it rides the
  // arm's own lower transition back down and disappears only once
  // tucked against the body.
  const [calcMounted, setCalcMounted] = React.useState(false);
  const calcHideTimer = React.useRef(null);
  React.useEffect(() => {
    if (budgetChecking) {
      if (calcHideTimer.current) { clearTimeout(calcHideTimer.current); calcHideTimer.current = null; }
      setCalcMounted(true);
      return;
    }
    calcHideTimer.current = setTimeout(() => setCalcMounted(false), HAT_PHASE_MS.dip);
    return () => { if (calcHideTimer.current) clearTimeout(calcHideTimer.current); };
  }, [budgetChecking]);
  const showCalc = calcMounted && !hatActive;

  // "Later": a sustained, ambient sleepy mood for as long as it's the
  // selected option — not a pulse — that simply steps aside for
  // anything higher in the priority chain and resumes on its own the
  // moment that clears, since it's just a plain derived condition
  // rather than its own job/timer.
  const sleepy = when === "Later" && !hatActive && !isReading && !timingChecking && !budgetChecking;

  // One combined, mutually-exclusive mode drives the arm/head targets
  // below — never several independent transforms fighting for the same
  // joint. Priority: submission success > hat equip/remove > reading >
  // timing reaction > budget reaction > idle. Success sits above
  // EVERYTHING else, unconditionally — it's a one-way, one-shot state
  // (the form is gone and nothing else can be typed/toggled once it's
  // true), so it never needs to negotiate with the other flags the way
  // they negotiate with each other.
  const mode = success ? "success" : hatActive ? "hat" : isReading ? "reading" : timingChecking ? "timingcheck" : budgetChecking ? "budgetcheck" : sleepy ? "sleepy" : "idle";

  // The single shoulder rotation target for whichever mode currently
  // owns the arm — reusing the exact same rotating group and rest/reach
  // convention as the hat reach (rest=4deg, negative sweeps down through
  // the front/outer side and up, same as the hat reach) so every mode
  // articulates the identical limb rather than a separate one. The
  // salute lands between the reading reach (-50) and the full overhead
  // hat reach (-135) — high enough that the existing paw shape reads as
  // "raised to the side of the head" rather than draped across the
  // chest or planted on the crown.
  const armAngle = success
    ? -104
    : hatActive
    ? (pawReaching ? -135 : 4)
    : mode === "reading" ? -50
    : mode === "timingcheck" ? (timingAnim.phase === "lower" ? 4 : -50)
    : mode === "budgetcheck" ? -50
    : 4;
  // Shared by both the (masked) arm-shape group and the (unmasked)
  // watch/clipboard group below — the same object reference guarantees
  // they rotate in exact lockstep rather than two copies drifting apart.
  const armRotateStyle = {
    transform: `rotate(${armAngle}deg)`,
    transformOrigin: "0px 0px",
    willChange: "transform",
    transition: reduced ? "none" : `transform ${HAT_PHASE_MS.dip}ms cubic-bezier(0.4,0,0.2,1)`,
  };

  // The single head translate+rotate target — same pivot/idiom as the
  // existing hat "bow", just a shallower tilt for the quieter reactions,
  // and a slower, heavier easing for the sleepy droop so it reads as
  // languid rather than a snap into position.
  const HEAD_POSE = {
    hat: bowing ? { tx: 4, ty: 7, rot: 15 } : { tx: 0, ty: 0, rot: 0 },
    reading: { tx: 2, ty: 4, rot: 8 },
    timingcheck: { tx: 3, ty: 5, rot: 11 },
    budgetcheck: { tx: 2, ty: 4, rot: 8 },
    sleepy: { tx: 3, ty: 9, rot: 9 },
    // Watching the plane leave: the opposite direction from every other
    // pose here — negative rot/ty tip the head UP and back rather than
    // down into a bow, so it reads as following something departing
    // overhead rather than looking at something held in the paws.
    success: { tx: -3, ty: -9, rot: -15 },
    idle: { tx: 0, ty: 0, rot: 0 },
  };
  const headPose = HEAD_POSE[mode] || HEAD_POSE.idle;
  // Watching the plane leave is the one pose that must NOT snap into
  // place like the others — it needs to read as one continuous,
  // decelerating look-up that spans roughly the same window as the
  // plane's own fold+launch, so the beaver visibly keeps tracking it
  // as it climbs rather than glancing up once and holding. A single
  // long CSS transition (not a multi-step keyframe animation) is
  // enough: it's already one smooth interpolation from wherever the
  // head currently is to the success target, just stretched out.
  const headMs = mode === "success" ? 2500 : mode === "sleepy" ? 900 : HAT_PHASE_MS.dip;
  const headEase = mode === "success" ? "cubic-bezier(0.22,1,0.36,1)" : "cubic-bezier(0.4,0,0.2,1)";

  // Occasional head turn toward the form — slow, infrequent, held
  // briefly, never a metronome.
  React.useEffect(() => {
    if (reduced || narrow) return;
    let showTimer, hideTimer;
    const scheduleNext = () => {
      showTimer = setTimeout(() => {
        setLooking(true);
        hideTimer = setTimeout(() => { setLooking(false); scheduleNext(); }, 2000);
      }, 9000 + Math.random() * 7000);
    };
    scheduleNext();
    return () => { clearTimeout(showTimer); clearTimeout(hideTimer); };
  }, [reduced, narrow]);

  // Rain hitting the creature's own back/head — each entry lands as a
  // small dot, which then disappears as a downward streak forms in its
  // place and slides away. Delay/duration are randomized once per mount
  // so hits never feel synchronized or mechanically repeated.
  const backDroplets = React.useMemo(() => (
    [
      { x: -20, y: -132 }, { x: 32, y: -150 }, { x: -46, y: -92 }, { x: 12, y: -68 },
    ].map((p) => ({
      ...p,
      dur: 4200 + Math.random() * 3600,
      delay: Math.random() * 6,
    }))
  ), []);

  // Each layer's drop count * spacing now comfortably exceeds FOREST_W so
  // the modulo wrap in rainField actually covers the full width — the
  // previous counts left real gaps (rainNear used to wrap at half the
  // canvas width, which is why the foreground rain only ever appeared on
  // the left). Per-layer stroke width/opacity/fall speed (set where these
  // are rendered below) are unchanged, so the far→near depth ordering —
  // thinner/dimmer/slower in back, bigger/brighter/faster up front —
  // still reads the same; only the horizontal coverage changed.
  const rainFar = rainField(48, 34, 6, 28, 9, -7, FOREST_W);
  const rainMid = rainField(35, 46, 18, 42, 11, -10, FOREST_W);
  const rainNear = rainField(21, 78, 30, 64, 15, -14, FOREST_W);

  // Very occasional larger, softer droplets passing close to the
  // "camera" — a handful of individually-timed streaks (long, randomized
  // durations/delays, most of each cycle invisible) rather than a fourth
  // constant layer, so they read as a rare event, not more rain.
  const rainClose = React.useMemo(() => (
    [260, 640, 1080, 1360].map((x, i) => ({
      x, y: 40 + (i % 2) * 90,
      dur: 9000 + Math.random() * 6000,
      delay: Math.random() * 10,
    }))
  ), []);

  // Vertical read, top to bottom of the FULL section: near-invisible
  // (but not zero — the canopy genuinely reaches up under the header),
  // staying watermark-faint through the text column's height, then
  // ramping to fully solid once past it, where the real environment —
  // trees in full, water, the creature — lives. This is the same
  // "stay out of the way of readable text" curve proven on the smaller
  // version, just re-based against the now full-height container.
  const fadeV = "linear-gradient(to bottom, rgba(0,0,0,0.045) 0%, rgba(0,0,0,0.07) 34%, rgba(0,0,0,0.09) 58%, rgba(0,0,0,0.5) 71%, #000 84%, #000 100%)";

  // The camera "rises" by moving the whole ground-level world DOWN by
  // exactly one viewport-height while a same-height sky layer, stacked
  // directly above it (bottom:100%, so it starts just off the top edge,
  // already hidden), rides down into view on the same transform. One
  // wrapper, one transform, no separate parallax math needed for the
  // reveal itself — everything above/below just changes which part of
  // this taller world sits inside the fixed, overflow:hidden viewport.
  const ascending = success && !reduced;
  return (
    <div
      aria-hidden="true"
      style={{
        position: "absolute", inset: 0,
        pointerEvents: "none", zIndex: 0,
        overflow: "hidden",
      }}
    >
      <div
        style={{
          position: "absolute", inset: 0,
          transform: ascending ? "translateY(100%)" : "translateY(0)",
          transition: reduced ? "none" : "transform 1700ms cubic-bezier(0.65,0,0.32,1) 1150ms",
        }}
      >
        <SuccessSky ascending={ascending} reduced={reduced} />
        <div style={{ position: "absolute", inset: 0, WebkitMaskImage: fadeV, maskImage: fadeV }}>
      <style>{`
        @keyframes forest-breathe { 0% { transform: translateY(0) scaleY(1); } 45% { transform: translateY(-3px) scaleY(1.012); } 100% { transform: translateY(0) scaleY(1); } }
        @keyframes forest-sway { 0%,100% { transform: rotate(4deg); } 50% { transform: rotate(4.6deg); } }
        @keyframes forest-ear-sway { 0%,100% { transform: rotate(0deg); } 50% { transform: rotate(-2.4deg); } }
        @keyframes forest-tail-sway { 0%,100% { transform: rotate(0deg); } 50% { transform: rotate(3deg); } }
        @keyframes forest-mist-drift-1 { 0% { transform: translateX(-22px); } 50% { transform: translateX(26px); } 100% { transform: translateX(-22px); } }
        @keyframes forest-mist-drift-2 { 0% { transform: translateX(16px); } 50% { transform: translateX(-28px); } 100% { transform: translateX(16px); } }
        @keyframes forest-parallax-far { 0%,100% { transform: translateX(0); } 50% { transform: translateX(-9px); } }
        @keyframes forest-rain-fall { from { transform: translateY(0); } to { transform: translateY(340px); } }
        @keyframes forest-splash { 0% { transform: scale(0.2); opacity: 0.55; } 70% { opacity: 0.12; } 100% { transform: scale(1.3); opacity: 0; } }
        @keyframes forest-crown { 0% { opacity: 0.6; transform: scale(0.6); } 35% { opacity: 0; transform: scale(1.15); } 100% { opacity: 0; transform: scale(1.15); } }
        @keyframes forest-wet-glint { 0%,100% { opacity: 0.12; } 50% { opacity: 0.38; } }
        @keyframes forest-water-shimmer { 0%,100% { transform: translateX(0) scaleX(1); } 50% { transform: translateX(24px) scaleX(1.015); } }
        @keyframes forest-droplet-dot { 0%, 66% { opacity: 0; transform: scale(0.25); } 68% { opacity: 0.45; transform: scale(0.85); } 74% { opacity: 0.2; transform: scale(0.6); } 79%, 100% { opacity: 0; transform: scale(0.4); } }
        @keyframes forest-droplet-streak { 0%, 66% { opacity: 0; transform: scaleY(0.15) translateY(0px); } 69% { opacity: 0.24; transform: scaleY(0.4) translateY(1px); } 78% { opacity: 0.36; transform: scaleY(0.9) translateY(7px); } 90% { opacity: 0.18; transform: scaleY(1.15) translateY(12px); } 100% { opacity: 0; transform: scaleY(1.35) translateY(18px); } }
        @keyframes forest-rain-close { 0%, 84% { opacity: 0; transform: translateY(-30px) scaleY(0.8); } 88% { opacity: 0.5; transform: translateY(90px) scaleY(1); } 95% { opacity: 0.22; transform: translateY(210px) scaleY(1.1); } 100% { opacity: 0; transform: translateY(320px) scaleY(1.2); } }
        @keyframes hat-bounce-in { 0% { transform: scale(0.5) translateY(-6px); opacity: 0; } 60% { transform: scale(1.08) translateY(1px); opacity: 1; } 100% { transform: scale(1) translateY(0); opacity: 1; } }
        @keyframes reaction-pop-in { 0% { transform: scale(0.4); opacity: 0; } 65% { transform: scale(1.06); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
        @keyframes forest-sleepy-z { 0% { opacity: 0; transform: translateY(0) scale(0.7); } 18% { opacity: 0.9; transform: translateY(-3px) scale(0.9); } 62% { opacity: 0.55; transform: translateY(-15px) scale(1); } 100% { opacity: 0; transform: translateY(-27px) scale(1.05); } }
        @keyframes forest-calc-symbol { 0% { opacity: 0; transform: translateY(0) scale(0.6); } 20% { opacity: 0.9; transform: translateY(-4px) scale(0.95); } 70% { opacity: 0.55; transform: translateY(-16px) scale(1); } 100% { opacity: 0; transform: translateY(-26px) scale(1.05); } }
      `}</style>
      <svg viewBox={`0 0 ${FOREST_W} ${FOREST_H}`} preserveAspectRatio="xMinYMax slice" style={{ width: "100%", height: "100%", display: "block" }}>
        <defs>
          <linearGradient id="forestSky" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="#e7ece9" stopOpacity="0" />
            <stop offset="55%" stopColor="#bdd0cb" stopOpacity="0.6" />
            <stop offset="100%" stopColor="#7d9691" stopOpacity="0.75" />
          </linearGradient>
          <linearGradient id="forestFloor" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="#4c625c" />
            <stop offset="55%" stopColor="#2c3e3a" />
            <stop offset="100%" stopColor="#182422" />
          </linearGradient>
          <linearGradient id="trunkDistant" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%" stopColor="#9fb3ac" />
            <stop offset="100%" stopColor="#b7c7c1" />
          </linearGradient>
          <linearGradient id="trunkMid" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%" stopColor="#526158" />
            <stop offset="100%" stopColor="#71847a" />
          </linearGradient>
          <linearGradient id="trunkNear" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%" stopColor="#1c2825" />
            <stop offset="100%" stopColor="#33443d" />
          </linearGradient>
          <linearGradient id="creatureBody" gradientUnits="userSpaceOnUse" x1="0" y1="-190" x2="0" y2="20">
            <stop offset="0%" stopColor="#241712" />
            <stop offset="100%" stopColor="#5c3f30" />
          </linearGradient>
          <radialGradient id="creatureRim" cx="18%" cy="10%" r="45%">
            <stop offset="0%" stopColor="#bcd8d0" stopOpacity="0.32" />
            <stop offset="100%" stopColor="#bcd8d0" stopOpacity="0" />
          </radialGradient>
          <radialGradient id="creatureBounce" cx="50%" cy="98%" r="42%">
            <stop offset="0%" stopColor="#8fc3b8" stopOpacity="0.22" />
            <stop offset="100%" stopColor="#8fc3b8" stopOpacity="0" />
          </radialGradient>
          <radialGradient id="contactShadow" cx="50%" cy="50%" r="50%">
            <stop offset="0%" stopColor="#0d1614" stopOpacity="0.55" />
            <stop offset="100%" stopColor="#0d1614" stopOpacity="0" />
          </radialGradient>
          <linearGradient id="hatMediaGrad" x1="0" y1="-40" x2="0" y2="4">
            <stop offset="0%" stopColor="#0a0a0c" /><stop offset="100%" stopColor="#26262b" />
          </linearGradient>
          <linearGradient id="hatMotionGrad" x1="0" y1="-48" x2="0" y2="6">
            <stop offset="0%" stopColor="#E23A18" /><stop offset="100%" stopColor="#f47a4a" />
          </linearGradient>
          <linearGradient id="hatPrintGrad" x1="0" y1="-40" x2="0" y2="4">
            <stop offset="0%" stopColor="#D8991C" /><stop offset="100%" stopColor="#F0B32C" />
          </linearGradient>
          <linearGradient id="hatCustomGrad" x1="0" y1="-40" x2="0" y2="4">
            <stop offset="0%" stopColor="#166147" /><stop offset="100%" stopColor="#1E7A55" />
          </linearGradient>
          <filter id="forestBlurSoft" x="-50%" y="-50%" width="200%" height="200%">
            <feGaussianBlur stdDeviation="7" />
          </filter>
          <linearGradient id="forestTopFadeG" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="#fff" stopOpacity="0" />
            <stop offset="40%" stopColor="#fff" stopOpacity="0.7" />
            <stop offset="100%" stopColor="#fff" stopOpacity="1" />
          </linearGradient>
          <mask id="forestTopFade">
            <rect x="0" y="0" width={FOREST_W} height={FOREST_H} fill="url(#forestTopFadeG)" />
          </mask>
          {/* real occlusion for the ears — not opacity. White = visible,
              black = hidden. The head's own silhouette (same path as the
              head shape itself) plus a block extending down from its
              base means anything that folds down far enough — the ears,
              during a bow — is genuinely clipped away behind the head/
              neck rather than fading out. Lives in the same rotating
              group as the head and ears, so it rotates rigidly with them
              and always stays aligned to wherever the head currently is. */}
          <mask id="earOcclusionMask" maskUnits="userSpaceOnUse" x="-300" y="-320" width="600" height="500">
            <rect x="-300" y="-320" width="600" height="500" fill="#fff" />
            <path d="M -46,-108 C -50,-140 -30,-168 4,-172 C 34,-176 62,-160 70,-132 C 74,-116 70,-100 56,-92 C 34,-104 6,-108 -20,-102 C -34,-99 -42,-103 -46,-108 Z" fill="#000" />
            <rect x="-52" y="-108" width="134" height="140" fill="#000" />
          </mask>
          {/* same real-occlusion idiom, sized for the arm's shoulder
              root: a small disk centered on the shoulder pivot (measured
              against the body's own silhouette to confirm the body's
              true edge clears this radius from the pivot in every
              direction the arm rotates through — using the full body
              shape here instead ate deep into the arm's own taper,
              since the torso is far wider at this height than it looks). */}
          <mask id="armShoulderMask" maskUnits="userSpaceOnUse" x="-300" y="-320" width="600" height="500">
            <rect x="-300" y="-320" width="600" height="500" fill="#fff" />
            <circle cx="48" cy="-90" r="26" fill="#000" />
          </mask>
        </defs>

        <g transform={ar ? `translate(${FOREST_W},0) scale(-1,1)` : undefined}>
          <g mask="url(#forestTopFade)">
            <rect x="0" y="0" width={FOREST_W} height={FOREST_GROUND} fill="url(#forestSky)" />

            {/* distant trunks, softened by atmospheric perspective — a slow
                whole-layer drift stands in for parallax as the eye travels
                the scene, the "tiny ambient movement in distant layers". */}
            <g style={reduced ? undefined : { animation: "forest-parallax-far 22000ms ease-in-out infinite" }}>
              <g filter="url(#forestBlurSoft)">
                {DIST_TREES.map((t, i) => (
                  <path key={i} d={trunkPath(t.x, t.baseW, t.topW, t.h, FOREST_GROUND)} fill="url(#trunkDistant)" opacity={0.55 * densityAt(t.x)} />
                ))}
                {/* very low-contrast undergrowth between the distant
                    trunks — implies the forest keeps going back into the
                    fog rather than stopping at the last visible tree;
                    same blur and drift as the trunks above so it never
                    reads as pasted on */}
                {[
                  { x: 130, w: 46, h: 26 }, { x: 470, w: 58, h: 30 },
                  { x: 860, w: 50, h: 24 }, { x: 1250, w: 54, h: 28 },
                ].map((b, i) => (
                  <ellipse key={i} cx={b.x} cy={FOREST_GROUND - b.h * 0.4} rx={b.w} ry={b.h} fill="url(#trunkDistant)" opacity={0.16 * densityAt(b.x)} />
                ))}
              </g>
            </g>

            {/* rain — far layer, behind the midground trees */}
            <g style={reduced ? undefined : { animation: "forest-rain-fall 950ms linear infinite" }}>
              {[0, -340].map((dy) => (
                <g key={dy} transform={`translate(0,${dy})`}>
                  {rainFar.map((r, i) => (
                    <line key={i} x1={r.x} y1={r.y} x2={r.x2} y2={r.y + r.len} stroke="#eef4f1" strokeWidth="1.3" strokeLinecap="round" opacity={0.4 * r.k} />
                  ))}
                </g>
              ))}
            </g>

            <g style={reduced ? undefined : { animation: "forest-mist-drift-1 13000ms ease-in-out infinite" }}>
              <ellipse cx="180" cy="330" rx="180" ry="52" fill="#e7efe9" opacity="0.28" filter="url(#forestBlurSoft)" />
              <ellipse cx="520" cy="270" rx="160" ry="46" fill="#e7efe9" opacity="0.22" filter="url(#forestBlurSoft)" />
              <ellipse cx="980" cy="300" rx="200" ry="50" fill="#e7efe9" opacity="0.16" filter="url(#forestBlurSoft)" />
              <ellipse cx="1380" cy="260" rx="170" ry="44" fill="#e7efe9" opacity="0.12" filter="url(#forestBlurSoft)" />
            </g>

            {/* midground trunks */}
            <g>
              {MID_TREES.map((t, i) => (
                <path key={i} d={trunkPath(t.x, t.baseW, t.topW, t.h, FOREST_GROUND + 20)} fill="url(#trunkMid)" opacity={0.9 * densityAt(t.x)} />
              ))}
            </g>

            {/* rain — midground */}
            <g style={reduced ? undefined : { animation: "forest-rain-fall 680ms linear infinite" }}>
              {[0, -340].map((dy) => (
                <g key={dy} transform={`translate(0,${dy})`}>
                  {rainMid.map((r, i) => (
                    <line key={i} x1={r.x} y1={r.y} x2={r.x2} y2={r.y + r.len} stroke="#eef4f1" strokeWidth="1.8" strokeLinecap="round" opacity={0.5 * r.k} />
                  ))}
                </g>
              ))}
            </g>

            <g style={reduced ? undefined : { animation: "forest-mist-drift-2 15000ms ease-in-out infinite" }}>
              <ellipse cx="340" cy="520" rx="230" ry="56" fill="#dcebe4" opacity="0.2" filter="url(#forestBlurSoft)" />
              <ellipse cx="1050" cy="500" rx="240" ry="54" fill="#dcebe4" opacity="0.13" filter="url(#forestBlurSoft)" />
            </g>
          </g>

          {/* wet forest floor — spans the full width, under the form too */}
          <path d={`M 0,${FOREST_GROUND} C 220,${FOREST_GROUND - 12} 460,${FOREST_GROUND + 14} 700,${FOREST_GROUND - 2} C 940,${FOREST_GROUND - 16} 1180,${FOREST_GROUND + 10} 1420,${FOREST_GROUND - 6} C 1500,${FOREST_GROUND - 10} 1560,${FOREST_GROUND - 4} ${FOREST_W},${FOREST_GROUND - 8} L ${FOREST_W},${FOREST_H} L 0,${FOREST_H} Z`} fill="url(#forestFloor)" />
          <g style={reduced ? undefined : { animation: "forest-water-shimmer 9000ms ease-in-out infinite" }}>
            <ellipse cx="340" cy={FOREST_GROUND + 30} rx="260" ry="15" fill="#9fc9bd" opacity="0.14" filter="url(#forestBlurSoft)" style={reduced ? undefined : { animation: "forest-wet-glint 6200ms ease-in-out infinite" }} />
            <ellipse cx="1000" cy={FOREST_GROUND + 40} rx="240" ry="13" fill="#9fc9bd" opacity="0.08" filter="url(#forestBlurSoft)" />
          </g>
          <path d={`M 40,${FOREST_GROUND + 60} C 160,${FOREST_GROUND + 52} 260,${FOREST_GROUND + 64} 380,${FOREST_GROUND + 54}`} stroke="#eef7f2" strokeWidth="2" fill="none" opacity="0.14" style={reduced ? undefined : { animation: "forest-water-shimmer 7400ms ease-in-out infinite" }} />
          <path d={`M 760,${FOREST_GROUND + 90} C 900,${FOREST_GROUND + 82} 1020,${FOREST_GROUND + 94} 1160,${FOREST_GROUND + 84}`} stroke="#eef7f2" strokeWidth="1.6" fill="none" opacity="0.08" style={reduced ? undefined : { animation: "forest-water-shimmer 8600ms ease-in-out infinite reverse" }} />

          {/* damp earth/mud patches — soft, blurred, never competing with
              the floor's own base tone, just breaking it up */}
          <ellipse cx="250" cy={FOREST_GROUND + 95} rx="72" ry="15" fill="#3a2c22" opacity="0.16" filter="url(#forestBlurSoft)" />
          <ellipse cx="600" cy={FOREST_GROUND + 130} rx="60" ry="12" fill="#3a2c22" opacity="0.12" filter="url(#forestBlurSoft)" />
          <ellipse cx="920" cy={FOREST_GROUND + 68} rx="90" ry="16" fill="#3a2c22" opacity="0.1" filter="url(#forestBlurSoft)" />
          <ellipse cx="1240" cy={FOREST_GROUND + 100} rx="70" ry="13" fill="#3a2c22" opacity="0.08" filter="url(#forestBlurSoft)" />

          {/* a further shallow-water reflection, same restrained language
              as the two above */}
          <ellipse cx="610" cy={FOREST_GROUND + 118} rx="130" ry="9" fill="#b7d6cb" opacity="0.1" filter="url(#forestBlurSoft)" style={reduced ? undefined : { animation: "forest-wet-glint 7000ms ease-in-out infinite" }} />

          {/* grass tufts breaking up the wet floor — thin curved blades,
              a touch brighter than the floor, never a lawn. Layered for
              depth: fainter/smaller clusters sit farther back (closer to
              the treeline), medium ones sit around the creature, and a
              few larger, more defined ones sit down in the near
              foreground — the same restrained vocabulary throughout,
              just scaled and faded by how far back each one reads. */}
          {[
            // far — small, faint, near the treeline
            { x: 150, y: 34, s: 0.5, o: 0.22 }, { x: 430, y: 40, s: 0.46, o: 0.18 },
            { x: 780, y: 30, s: 0.52, o: 0.2 }, { x: 1180, y: 38, s: 0.48, o: 0.17 },
            { x: 1460, y: 32, s: 0.44, o: 0.16 },
            // mid — around the creature and open ground
            { x: 210, y: 66, s: 1, o: 0.4 }, { x: 470, y: 66, s: 0.82, o: 0.36 },
            { x: 700, y: 66, s: 0.94, o: 0.38 }, { x: 1080, y: 66, s: 1.05, o: 0.4 },
            { x: 1340, y: 66, s: 0.78, o: 0.34 },
            // near — larger, more defined, down in the foreground
            { x: 90, y: 128, s: 1.35, o: 0.5 }, { x: 340, y: 148, s: 1.5, o: 0.52 },
            { x: 620, y: 132, s: 1.3, o: 0.48 }, { x: 1220, y: 150, s: 1.42, o: 0.5 },
          ].map((g, i) => (
            <g key={i} transform={`translate(${g.x},${FOREST_GROUND + g.y}) scale(${g.s})`} opacity={g.o * densityAt(g.x)}>
              <path d="M -6,0 C -8,-16 -4,-24 0,-30" stroke="#5e7a5a" strokeWidth="2" fill="none" strokeLinecap="round" />
              <path d="M 0,0 C -1,-18 2,-26 5,-32" stroke="#6f8f68" strokeWidth="2" fill="none" strokeLinecap="round" />
              <path d="M 6,0 C 7,-14 10,-20 14,-26" stroke="#5e7a5a" strokeWidth="1.6" fill="none" strokeLinecap="round" />
            </g>
          ))}

          {/* the creature — moved into the open gap just before the
              x=660 midground trunk, noticeably closer to the form than
              its old x=400 while still staying clear of the form panel
              itself (measured: the card's left edge falls around
              viewBox x=703, so anything past ~650 starts sliding under
              it), with a small constant body/head lean (forest-sway's
              new baseline + the head's own resting rotation) so it
              visibly favors that side even though we only ever see its
              back */}
          {/* On narrow, the ForestScene svg's own "xMinYMax slice"
              preserveAspectRatio scales up hugely to cover a mobile
              section's much taller (stacked single-column) aspect ratio.
              Two consequences the desktop x=560/y=808 position never had
              to deal with: (1) it crops everything past roughly viewBox
              x=200 off the right edge entirely, so the desktop position
              was simply invisible; (2) because this same section is now
              ONE tall stacked column instead of two side-by-side ones, the
              opaque form card covers nearly the section's full width for
              most of its height — a fixed viewBox Y is a FRACTION of the
              total section height regardless of the section's actual
              pixel height (slice scales uniformly), and the original
              y=808 sits at 808/900 ≈ 90% down, which is deep inside the
              form card on every phone. Moving it to a small enough
              fraction instead (y=280, ≈31%) keeps it inside the intro/
              contact-details area, which — unlike the form — has no
              opaque card behind it, so the creature reads as a background
              figure behind that text rather than vanishing behind a card. */}
          <g transform={narrow ? "translate(96,280) scale(0.5)" : "translate(560,808) scale(0.74)"}>
            {/* soft cast shadow, biased right and diffused as though the
                overcast ambient light comes from the left — grounds the
                creature without ever reading as a hard, dramatic shadow */}
            <ellipse cx="30" cy="9" rx="98" ry="17" fill="url(#contactShadow)" opacity="0.55" filter="url(#forestBlurSoft)" />
            <ellipse cx="8" cy="6" rx="76" ry="15" fill="url(#contactShadow)" opacity="0.4" />
            <g style={reduced ? undefined : { animation: "forest-sway 9200ms ease-in-out infinite", transformOrigin: "0px 0px" }}>
              <g style={reduced ? undefined : { animation: "forest-breathe 5200ms ease-in-out infinite", transformOrigin: "0px 0px" }}>
                {/* tail — a proper broad, flat beaver paddle (not the old
                    small round tail): wide, faintly tapered oval trailing
                    low behind the haunches, with a few scale-like texture
                    lines across its flattened surface. */}
                <g style={reduced ? undefined : { animation: "forest-tail-sway 4200ms ease-in-out infinite", transformOrigin: "-58px -4px" }}>
                  <path d="M -58,-4 C -80,-18 -118,-20 -146,-8 C -166,1 -168,18 -150,26 C -122,38 -84,34 -60,18 C -50,11 -50,4 -58,-4 Z" fill="url(#creatureBody)" opacity="0.97" />
                  <path d="M -58,-4 C -80,-18 -118,-20 -146,-8 C -166,1 -168,18 -150,26 C -122,38 -84,34 -60,18 C -50,11 -50,4 -58,-4 Z" fill="url(#creatureRim)" opacity="0.45" />
                  <path d="M -140,4 C -130,8 -110,10 -92,6" stroke="#1c130f" strokeWidth="1.6" opacity="0.25" fill="none" strokeLinecap="round" />
                  <path d="M -150,14 C -136,19 -110,21 -86,15" stroke="#1c130f" strokeWidth="1.6" opacity="0.22" fill="none" strokeLinecap="round" />
                  <path d="M -138,22 C -122,26 -100,27 -82,22" stroke="#1c130f" strokeWidth="1.4" opacity="0.18" fill="none" strokeLinecap="round" />
                </g>
                {/* body — a big rounded back/haunches mass, no facial features anywhere on it */}
                <path d="M -74,0 C -98,-36 -96,-88 -66,-118 C -50,-134 -26,-142 0,-142 C 26,-142 50,-134 66,-118 C 84,-100 86,-70 78,-40 C 74,-20 68,-4 54,6 C 30,20 -20,20 -46,10 C -60,4 -68,2 -74,0 Z" fill="url(#creatureBody)" />
                <path d="M -74,0 C -98,-36 -96,-88 -66,-118 C -50,-134 -26,-142 0,-142 C 26,-142 50,-134 66,-118 C 84,-100 86,-70 78,-40 C 74,-20 68,-4 54,6 C 30,20 -20,20 -46,10 C -60,4 -68,2 -74,0 Z" fill="url(#creatureRim)" />
                <path d="M -74,0 C -98,-36 -96,-88 -66,-118 C -50,-134 -26,-142 0,-142 C 26,-142 50,-134 66,-118 C 84,-100 86,-70 78,-40 C 74,-20 68,-4 54,6 C 30,20 -20,20 -46,10 C -60,4 -68,2 -74,0 Z" fill="url(#creatureBounce)" />
                {/* subtle fur texture on the back, never a facial mark */}
                <path d="M -44,-100 C -22,-112 10,-114 34,-102" stroke="#241812" strokeWidth="2" opacity="0.2" fill="none" strokeLinecap="round" />
                <path d="M -34,-56 C -8,-66 20,-64 44,-50" stroke="#241812" strokeWidth="2" opacity="0.18" fill="none" strokeLinecap="round" />
                {/* a touch more anatomical intent at the rump/tail junction */}
                <path d="M -66,-14 C -60,-2 -54,8 -44,14" stroke="#241812" strokeWidth="1.8" opacity="0.16" fill="none" strokeLinecap="round" />

                {/* head — a rounded bump fused onto the shoulders (no neck), split from the body only so it can turn independently; the shared gradient space keeps the seam invisible. Deliberately featureless — this is the back of the head, not a face. Rests at a slight turn (matching the body's own lean) and pulls further round on the occasional "looking" pulse — both read as watching the form beside it.

                    Equip/remove "bow": a real rigid rotation around a
                    pivot at the base of the skull (roughly where head
                    meets shoulders), not a scale — the head keeps its
                    exact proportions throughout, it just tips forward
                    and its center genuinely shifts down and across as a
                    rotated rigid body would. Nested outside the existing
                    looking-rotation group so the two transforms never
                    fight (same pattern used for the ear-perk nesting
                    below).

                    Beyond the hat bow, the same pivot now also carries
                    the quieter reactions — a shallow tilt toward the
                    clipboard while reading, toward the watch during a
                    timing check, and a gentle sustained droop while
                    sleepy — picked by the single `headPose` for
                    whichever mode currently owns it, never several of
                    these fighting at once. */}
                <g style={{ transform: `translate(${headPose.tx}px,${headPose.ty}px) rotate(${headPose.rot}deg)`, transformOrigin: "6px -100px", transition: reduced ? "none" : `transform ${headMs}ms ${headEase}` }}>
                <g style={{ transform: `rotate(${looking && !reduced ? 10 : 3}deg)`, transformOrigin: "6px -118px", transition: reduced ? "none" : "transform 1700ms cubic-bezier(0.45,0,0.2,1)" }}>
                  <path d="M -46,-108 C -50,-140 -30,-168 4,-172 C 34,-176 62,-160 70,-132 C 74,-116 70,-100 56,-92 C 34,-104 6,-108 -20,-102 C -34,-99 -42,-103 -46,-108 Z" fill="url(#creatureBody)" />
                  <path d="M -46,-108 C -50,-140 -30,-168 4,-172 C 34,-176 62,-160 70,-132 C 74,-116 70,-100 56,-92 C 34,-104 6,-108 -20,-102 C -34,-99 -42,-103 -46,-108 Z" fill="url(#creatureRim)" opacity="0.7" />

                  {/* ears, seen from behind — fully opaque at all times;
                      they never fade. While bowing, each one folds down
                      hard around its own base (a much larger rotation
                      than the tiny idle sway/perk) so its tip swings
                      down into the head's own silhouette — the shared
                      mask below then genuinely clips away whatever part
                      of the ear overlaps that silhouette, the same way
                      the body would occlude it in reality. That's real
                      layering, not a fade. */}
                  <g mask="url(#earOcclusionMask)">
                    {/* pushed outward and a little lower than the raw
                        path coordinates (a static offset, not animated)
                        so a hat this much bigger doesn't bury them under
                        its own brim at rest — they now sit beside it,
                        clear to fold down and reappear as designed. */}
                    <g transform="translate(32,16)">
                    <g style={{ transform: `rotate(${looking && !reduced ? 8 : 0}deg)`, transformOrigin: "18px -168px", transition: reduced ? "none" : "transform 900ms cubic-bezier(0.34,1.4,0.64,1)" }}>
                      <g style={{ transform: bowing ? "rotate(128deg)" : "rotate(0deg)", transformOrigin: "18px -168px", transition: reduced ? "none" : `transform ${HAT_PHASE_MS.dip}ms cubic-bezier(0.4,0,0.2,1)` }}>
                        <g style={reduced ? undefined : { animation: "forest-ear-sway 3400ms ease-in-out infinite", transformOrigin: "18px -168px" }}>
                          <path d="M 10,-168 C 5,-188 18,-204 33,-197 C 37,-184 30,-168 17,-161 Z" fill="url(#creatureBody)" />
                        </g>
                      </g>
                    </g>
                    </g>
                    <g transform="translate(-15,15)">
                    <g style={{ transform: bowing ? "rotate(-128deg)" : "rotate(0deg)", transformOrigin: "-26px -160px", transition: reduced ? "none" : `transform ${HAT_PHASE_MS.dip}ms cubic-bezier(0.4,0,0.2,1)` }}>
                      <g style={reduced ? undefined : { animation: "forest-ear-sway 3800ms ease-in-out infinite reverse", transformOrigin: "-26px -160px" }}>
                        <path d="M -32,-160 C -35,-176 -24,-188 -13,-182 C -11,-171 -18,-160 -28,-155 Z" fill="url(#creatureBody)" />
                      </g>
                    </g>
                    </g>
                  </g>

                  {/* a small glint of rain on the crown, well clear of anything that could read as a face */}
                  <ellipse cx="20" cy="-166" rx="8" ry="4" fill="#cfe8de" opacity="0.1" style={reduced ? undefined : { animation: "forest-wet-glint 4800ms ease-in-out infinite" }} />

                  {/* the hat stack — one hat per selected service, always
                      stacked in the same fixed order regardless of click
                      order, each with its own small rotation/offset so a
                      full stack of four reads as charmingly precarious
                      rather than mechanical, and its identifying detail
                      stays visible even when several are piled on.
                      Whichever hat is being equipped or removed changes
                      at the "settle" phase (the moment the head is
                      lowest and the paw is closest); every hat's own
                      position/rotation is transitioned, so the rest of
                      the stack visibly reflows up or down rather than
                      snapping when one is added or taken away. */}
                  {hatStack.map((h, i) => (
                    <g
                      key={h}
                      style={{
                        transform: `translate(${7 + HAT_DX[i]}px, ${-178 - i * HAT_STEP}px) rotate(${HAT_ROT[i]}deg)`,
                        transition: reduced ? "none" : "transform 320ms cubic-bezier(0.34,1.2,0.64,1)",
                      }}
                    >
                      <g style={reduced ? undefined : { animation: "hat-bounce-in 420ms cubic-bezier(0.34,1.56,0.64,1)" }}>
                        {renderHat(h)}
                      </g>
                    </g>
                  ))}
                </g>
                </g>

                {/* the arm — a single continuous soft limb, always
                    present in the resting silhouette (never spawns): no
                    elbow, no second joint, no rigid segment meeting
                    another at a corner — just one tapered, curving
                    flipper-like shape hinged at the shoulder. The curve
                    itself is baked into the shape (it bows from the
                    shoulder around toward the body's hidden front/belly
                    side, since we view the creature mostly from behind
                    and its front faces the same way as the form beside
                    it), not produced by a second rotating piece — a
                    positive bow toward the visible back/spine there
                    would read as the beaver scratching itself, which is
                    the opposite of what we want. Equip/remove swings
                    this one shape as a rigid whole around the shoulder
                    pivot, sweeping through the same front/outer side it
                    already bows toward (negative rotation here, which
                    sweeps down through the front/outer edge and up,
                    rather than back across the spine) so the paw arcs
                    up to the hat and settles back the same way — a
                    single soft reach, not a hinge unfolding.

                    Real occlusion at the shoulder root, same idiom as the
                    ear mask above — but a small purpose-built disk around
                    the shoulder pivot, NOT the full body silhouette: the
                    body's actual outline reaches much further out at this
                    height than it looks, so masking with the true body
                    shape ate deep into the taper itself. A disk just big
                    enough to swallow the small hidden extension below
                    (and comfortably smaller than the body's real
                    clearance around this pivot at every angle the arm
                    swings through) hides only the shoulder root, leaving
                    the whole approved taper and paw untouched. */}
                {/* The arm's own shape and its shoulder-occlusion mask
                    live in one rotating group; the watch/clipboard live
                    in a second, sibling rotating group using the exact
                    same transform/transition (armRotateStyle below), so
                    the two are always in perfect visual sync. They are
                    kept OUT of the masked group deliberately: Chromium
                    fails to paint content dynamically mounted inside an
                    already-masked subtree here (confirmed by removing
                    the mask attribute live — the watch appears instantly
                    with it gone), even though it sits well outside the
                    mask's own hole geometrically. Splitting the two
                    groups sidesteps that renderer bug entirely rather
                    than working around it. */}
                {/* the reading paper — a single, simple sheet the
                    beaver holds up to check while typing: a plain
                    rounded rectangle, large enough on its own to read
                    as "he's holding a list," with a few faint lines
                    standing in for writing. Anchored near the paw end
                    of the arm and counter-rotating against the limb's
                    own current angle so the sheet itself stays upright
                    and stable no matter how the arm is posed, while
                    still travelling wherever the paw goes. Its near
                    corner sits at this group's origin (right where the
                    paw grips it) and it rises up and to the right from
                    there, toward his face. Rendered in its own sibling
                    group, sharing armRotateStyle, placed BEFORE the
                    arm's own masked group below — so the real,
                    already-existing arm/paw paints on top of the
                    paper's near corner and grips it, with no separate
                    fake paw shape needed. Mounted a beat past
                    `isReading` itself (see `showNote`) so it rides the
                    arm all the way back down and disappears only once
                    tucked in against the body. */}
                <g transform="translate(48,-90)">
                  <g style={armRotateStyle}>
                    {showNote ? (
                      <g transform="translate(28,74)">
                        <g style={{ transform: `rotate(${-armAngle}deg)`, transition: reduced ? "none" : `transform ${HAT_PHASE_MS.dip}ms cubic-bezier(0.4,0,0.2,1)` }}>
                          <g style={{ opacity: reduced ? 1 : undefined, animation: reduced ? undefined : "reaction-pop-in 220ms cubic-bezier(0.34,1.56,0.64,1) forwards" }}>
                            {/* the sheet — one plain rounded rectangle, tilted a moderate, believable amount, rising up-and-right from the grip point toward his face. Nudged further left (in this already screen-stable frame) than a plain rotate(28) would put it, so more of its near corner sits under where the real paw is. */}
                            <g transform="translate(-18,0) rotate(28)">
                              <rect x="-4" y="-46" width="32" height="44" rx="4" fill="#f7f1e3" stroke="#c3ab74" strokeWidth="1.6" />
                              <line x1="1" y1="-37" x2="21" y2="-37" stroke="#b8a374" strokeWidth="1.6" strokeLinecap="round" opacity="0.55" />
                              <line x1="1" y1="-29" x2="19" y2="-29" stroke="#b8a374" strokeWidth="1.6" strokeLinecap="round" opacity="0.55" />
                              <line x1="1" y1="-21" x2="17" y2="-21" stroke="#b8a374" strokeWidth="1.6" strokeLinecap="round" opacity="0.55" />
                              <line x1="1" y1="-13" x2="13" y2="-13" stroke="#b8a374" strokeWidth="1.6" strokeLinecap="round" opacity="0.55" />
                            </g>
                          </g>
                        </g>
                      </g>
                    ) : null}
                  </g>
                </g>

                {/* the calculator — reuses the exact same anchor, tilt,
                    counter-rotation, and pop-in as the reading paper
                    above (same approximate dimensions too), just with
                    the object and effect swapped: a small, simple
                    calculator — dark body, a few green screen/button
                    details, no need for readable keys at this size —
                    instead of a page of writing, and a brief float of
                    operator symbols instead of nothing. Mounted a beat
                    past `budgetChecking` itself (see `showCalc`) so it
                    rides the arm back down and disappears only once
                    tucked against the body, exactly like the
                    notebook. */}
                <g transform="translate(48,-90)">
                  <g style={armRotateStyle}>
                    {showCalc ? (
                      <g transform="translate(28,74)">
                        <g style={{ transform: `rotate(${-armAngle}deg)`, transition: reduced ? "none" : `transform ${HAT_PHASE_MS.dip}ms cubic-bezier(0.4,0,0.2,1)` }}>
                          <g style={{ opacity: reduced ? 1 : undefined, animation: reduced ? undefined : "reaction-pop-in 220ms cubic-bezier(0.34,1.56,0.64,1) forwards" }}>
                            {/* the body — identical footprint to the reading paper's sheet, tilted and positioned the same way */}
                            <g transform="translate(-18,0) rotate(28)">
                              <rect x="-4" y="-46" width="32" height="44" rx="4" fill="#1c1c1c" stroke="#3a3a3a" strokeWidth="1.6" />
                              {/* screen — a subtle dark-green display, no digits needed at this size */}
                              <rect x="-1" y="-43" width="26" height="9" rx="1.5" fill="#0d2818" stroke="#1f4a34" strokeWidth="1" />
                              {/* a simple 3x3 grid standing in for buttons, plain green, no labels */}
                              <rect x="-1" y="-30" width="6" height="5" rx="1.2" fill="#2e7d4f" />
                              <rect x="8" y="-30" width="6" height="5" rx="1.2" fill="#2e7d4f" />
                              <rect x="17" y="-30" width="6" height="5" rx="1.2" fill="#2e7d4f" />
                              <rect x="-1" y="-21" width="6" height="5" rx="1.2" fill="#2e7d4f" opacity="0.85" />
                              <rect x="8" y="-21" width="6" height="5" rx="1.2" fill="#2e7d4f" opacity="0.85" />
                              <rect x="17" y="-21" width="6" height="5" rx="1.2" fill="#2e7d4f" opacity="0.85" />
                              <rect x="-1" y="-12" width="6" height="5" rx="1.2" fill="#2e7d4f" opacity="0.7" />
                              <rect x="8" y="-12" width="6" height="5" rx="1.2" fill="#2e7d4f" opacity="0.7" />
                              <rect x="17" y="-12" width="6" height="5" rx="1.2" fill="#2e7d4f" opacity="0.7" />
                            </g>
                            {/* the calculation effect — a brief, one-shot float of operator symbols rising and fading from the calculator's own screen, same visual idiom as the sleepy Z's but tied to this single reaction rather than a sustained ambient state */}
                            {!reduced ? (
                              <g aria-hidden="true">
                                {[{ x: 6, y: -50, size: 13, delay: 250, ch: "+" }, { x: 18, y: -60, size: 14, delay: 480, ch: "×" }, { x: 4, y: -68, size: 12, delay: 700, ch: "−" }].map((s, i) => (
                                  <text
                                    key={i}
                                    x={s.x} y={s.y}
                                    fontSize={s.size}
                                    fontWeight="700"
                                    fill="#3fae72"
                                    opacity="0"
                                    style={{ animation: `forest-calc-symbol 900ms ease-out ${s.delay}ms forwards`, fontFamily: "var(--font-display, sans-serif)" }}
                                  >{s.ch}</text>
                                ))}
                              </g>
                            ) : null}
                          </g>
                        </g>
                      </g>
                    ) : null}
                  </g>
                </g>

                <g mask="url(#armShoulderMask)">
                <g transform="translate(48,-90)">
                  <g style={armRotateStyle}>
                    {/* identical visible taper/paw geometry to the
                        previous pass (unchanged), but the old rounded top
                        cap at y=-5 is replaced with a short extension
                        curving back toward the pivot, so there is real
                        body-colored geometry for the mask above to
                        occlude instead of a bare rotation origin with a
                        visible seam right where the torso should swallow
                        it. Restored to its original, short proportion —
                        an earlier pass tried lengthening this path so
                        the watch would sit farther out, but the watch's
                        real problem turned out to be an unrelated CSS
                        bug (see the watch group below), so the arm's
                        own geometry didn't need to change at all; only
                        the watch's own anchor stays at its
                        already-correct, already-approved position. */}
                    <path d="M -13,-16 C -17,-11 -17,-8 -15,-5 C -18,15 -13,36 -2,52 C 5,63 12,68 16,74 C 19,80 26,86 34,83 C 41,80 40,70 33,61 C 24,49 14,32 11,14 C 9,4 11,-6 15,-5 C 17,-8 17,-11 13,-16 C 9,-21 -9,-21 -13,-16 Z" fill="url(#creatureBody)" />
                    <path d="M -13,-16 C -17,-11 -17,-8 -15,-5 C -18,15 -13,36 -2,52 C 5,63 12,68 16,74 C 19,80 26,86 34,83 C 41,80 40,70 33,61 C 24,49 14,32 11,14 C 9,4 11,-6 15,-5 C 17,-8 17,-11 13,-16 C 9,-21 -9,-21 -13,-16 Z" fill="url(#creatureRim)" opacity="0.35" />
                    {/* a soft, faint flow line along the limb's own curve — suggests form, never a hard joint crease */}
                    <path d="M -8,10 C -4,28 6,44 15,58 C 20,66 26,72 31,77" stroke="#241812" strokeWidth="1.6" opacity="0.1" fill="none" strokeLinecap="round" />
                    {/* tiny claw ticks folded into the paw's own rounded edge, subtle and short */}
                    <path d="M 20,78 L 17,83" stroke="#241812" strokeWidth="1.3" opacity="0.28" fill="none" strokeLinecap="round" />
                    <path d="M 27,82 L 26,88" stroke="#241812" strokeWidth="1.3" opacity="0.28" fill="none" strokeLinecap="round" />
                    <path d="M 34,80 L 36,85" stroke="#241812" strokeWidth="1.3" opacity="0.28" fill="none" strokeLinecap="round" />
                  </g>
                </g>
                </g>

                <g transform="translate(48,-90)">
                  <g style={armRotateStyle}>
                    {/* the wristwatch — same design, size, colors, and
                        timing as before, anchored exactly at the paw
                        bulge's own outermost point, pulled back a bit
                        farther along the same limb axis (from the
                        exact tip (34,83) to (29,71), ~85% of the way
                        out instead of sitting right on the end) so it
                        reads as worn on the wrist/forearm rather than
                        perched at the very tip. Parented inside the
                        arm's own rotating group, so it inherits every
                        bit of the arm's rotation automatically.
                        Mounted only for the timing-check reaction:
                        absent from the DOM the rest of the time, so it
                        comes up exactly with the raise and is gone the
                        instant the arm returns to rest.

                        IMPORTANT: the position (translate) and the
                        pop-in scale animation are on two SEPARATE
                        nested groups, not one. A single element can't
                        carry both an SVG `transform` attribute and a
                        CSS `animation` that touches `transform` — the
                        CSS value always wins outright and the
                        attribute is silently dropped, which is exactly
                        what was pinning this watch to the arm's own
                        rotation origin no matter what translate value
                        was set here. Splitting them keeps the anchor
                        attribute safe on the outer group while the
                        inner one is free to animate its own scale. */}
                    {timingChecking ? (
                      <g transform="translate(21,61)">
                      <g style={{ transform: `rotate(${-armAngle}deg)`, transformOrigin: "0px 0px", transition: reduced ? "none" : `transform ${HAT_PHASE_MS.dip}ms cubic-bezier(0.4,0,0.2,1)` }}>
                      <g style={{ opacity: reduced ? 1 : undefined, animation: reduced ? undefined : "reaction-pop-in 220ms cubic-bezier(0.34,1.56,0.64,1) forwards" }}>
                        {/* strap: a solid dark band running vertically top-to-bottom across the wrist, underneath the case */}
                        <rect x="-4.5" y="-16" width="9" height="32" rx="3.5" fill="#3d2b1a" stroke="#241812" strokeWidth="1" />
                        {/* case: ivory face, muted-gold rim, unmistakably a watch */}
                        <circle cx="0" cy="0" r="13" fill="#f7f1e3" stroke="#b8923f" strokeWidth="3.2" />
                        {/* simple hands + center dot */}
                        <path d="M 0,-0.6 L 0,-8.4" stroke="#2a2016" strokeWidth="1.9" strokeLinecap="round" />
                        <path d="M 0,-0.6 L 6.4,2.6" stroke="#2a2016" strokeWidth="1.9" strokeLinecap="round" />
                        <circle cx="0" cy="0" r="1.5" fill="#2a2016" />
                      </g>
                      </g>
                      </g>
                    ) : null}

                  </g>
                </g>

                {/* sleepy "Z"s — sits outside the head's own rotating
                    frame (fixed near its resting crown position) so they
                    just drift and fade on their own rather than
                    compounding with the head's droop rotation. Three,
                    staggered so they appear one at a time in a loop, for
                    as long as "Later" is the active mode; the whole
                    group unmounts the instant it isn't, so nothing is
                    ever left mid-fade. */}
                {mode === "sleepy" && !reduced ? (
                  <g aria-hidden="true">
                    {[{ x: 18, y: -195, size: 26, delay: 0 }, { x: 36, y: -218, size: 34, delay: 700 }, { x: 58, y: -246, size: 42, delay: 1400 }].map((z, i) => (
                      <text
                        key={i}
                        x={z.x} y={z.y}
                        fontSize={z.size}
                        fontWeight="700"
                        fill="#1d2b73"
                        opacity="0"
                        style={{ animation: `forest-sleepy-z 2200ms ease-in-out ${z.delay}ms infinite`, fontFamily: "var(--font-display, sans-serif)" }}
                      >Z</text>
                    ))}
                  </g>
                ) : null}

                {/* rain droplets on the creature's own back/head — one
                    continuous motion per hit, not a dot-pause-streak: the
                    impact point and the downward streak overlap and
                    cross-fade immediately (no held/static moment), toned
                    to match the creature's own wet highlight color rather
                    than a bright white fleck, so it reads as part of the
                    existing wet-fur sheen. Both start at opacity 0 in
                    their base style (not just in the keyframes) so
                    nothing is visible before each one's randomized delay
                    has elapsed — there is no frozen state on first paint. */}
                {reduced ? null : backDroplets.map((dr, i) => (
                  <g key={i}>
                    <circle
                      cx={dr.x} cy={dr.y} r="1.4" fill="#cfe8de"
                      style={{ opacity: 0, transformOrigin: `${dr.x}px ${dr.y}px`, animation: `forest-droplet-dot ${dr.dur}ms ease-in-out ${dr.delay}s infinite` }}
                    />
                    <line
                      x1={dr.x} y1={dr.y} x2={dr.x} y2={dr.y + 14} stroke="#cfe8de" strokeWidth="1.1" strokeLinecap="round"
                      style={{ opacity: 0, transformOrigin: `${dr.x}px ${dr.y}px`, animation: `forest-droplet-streak ${dr.dur}ms ease-in-out ${dr.delay}s infinite` }}
                    />
                  </g>
                ))}

                {/* stubby feet */}
                <ellipse cx="-44" cy="18" rx="16" ry="8" fill="#2a1d17" opacity="0.5" />
                <ellipse cx="32" cy="20" rx="18" ry="8" fill="#2a1d17" opacity="0.5" />
              </g>
            </g>
          </g>

          {/* foreground trunks — framing at the outer edges, thinning
              toward the right so the form's side of the scene stays calm */}
          {FRONT_TREES.map((t, i) => (
            <path key={i} d={trunkPath(t.x, t.baseW, t.topW, t.h, FOREST_H)} fill="url(#trunkNear)" opacity={0.94 * densityAt(t.x)} />
          ))}

          {/* restrained bark texture on the two far-left trunks — thin
              tonal ridges that taper with the trunk's own curve, just
              enough tonal variation to read as bark rather than a flat
              silhouette; kept subtle and consistent with the existing
              illustrated style */}
          {FRONT_TREES.slice(0, 2).map((t, i) => {
            const topY = FOREST_H - t.h;
            return (
              <g key={i} opacity={0.94 * densityAt(t.x)}>
                <path d={`M ${t.x - t.baseW * 0.42},${FOREST_H} C ${t.x - t.baseW * 0.28},${FOREST_H - t.h * 0.4} ${t.x - t.topW * 0.5},${topY + t.h * 0.3} ${t.x - t.topW * 0.4},${topY + 22}`} stroke="#0e1613" strokeWidth="2" fill="none" strokeLinecap="round" opacity="0.28" />
                <path d={`M ${t.x + t.baseW * 0.08},${FOREST_H} C ${t.x + t.baseW * 0.02},${FOREST_H - t.h * 0.45} ${t.x + t.topW * 0.12},${topY + t.h * 0.35} ${t.x + t.topW * 0.08},${topY + 26}`} stroke="#0e1613" strokeWidth="1.6" fill="none" strokeLinecap="round" opacity="0.2" />
                <path d={`M ${t.x + t.baseW * 0.48},${FOREST_H} C ${t.x + t.baseW * 0.34},${FOREST_H - t.h * 0.5} ${t.x + t.topW * 0.55},${topY + t.h * 0.4} ${t.x + t.topW * 0.48},${topY + 30}`} stroke="#587065" strokeWidth="1.4" fill="none" strokeLinecap="round" opacity="0.22" />
              </g>
            );
          })}

          {/* rain impacts on the shallow wet floor — real ripple pairs
              (an outer ring plus a faster-fading inner one) rather than a
              single flat circle, each with its own radius, duration and
              delay so they never read as the same mark repeating; roughly
              a third also get a brief "crown" of tiny splash droplets at
              the moment of impact. Sparser toward the right, same as the
              rest of the floor detail. Rings are flattened ellipses, not
              circles — the floor is a horizontal plane seen in
              perspective, so a ripple on it reads as a squashed oval, not
              a circle facing the camera. The uniform CSS scale() in
              forest-splash/forest-crown expands each ring outward while
              preserving that same flatness. */}
          {[
            { x: 160, y: FOREST_GROUND + 90, r: 6, dur: 2400, d: 0, crown: true },
            { x: 460, y: FOREST_GROUND + 118, r: 5, dur: 2100, d: 0.8, crown: false },
            { x: 300, y: FOREST_GROUND + 150, r: 7, dur: 2700, d: 1.5, crown: true },
            { x: 880, y: FOREST_GROUND + 100, r: 4.5, dur: 1900, d: 1.1, crown: false },
            { x: 60, y: FOREST_GROUND + 130, r: 5.5, dur: 2300, d: 0.4, crown: false },
            { x: 620, y: FOREST_GROUND + 70, r: 6.5, dur: 2600, d: 1.9, crown: true },
            { x: 1020, y: FOREST_GROUND + 140, r: 4, dur: 2000, d: 0.2, crown: false },
            { x: 720, y: FOREST_GROUND + 160, r: 5, dur: 2200, d: 2.2, crown: false },
            { x: 1180, y: FOREST_GROUND + 90, r: 4.2, dur: 1800, d: 1.6, crown: false },
            { x: 380, y: FOREST_GROUND + 60, r: 5.8, dur: 2500, d: 0.6, crown: true },
          ].map((s, i) => {
            const squash = 0.4; // flatten the ring to lie on the ground plane
            return (
            <g key={i} opacity={0.42 * densityAt(s.x)}>
              <g style={reduced ? undefined : { transformOrigin: `${s.x}px ${s.y}px`, animation: `forest-splash ${s.dur}ms ease-out ${s.d}s infinite` }}>
                <ellipse cx={s.x} cy={s.y} rx={s.r} ry={s.r * squash} fill="none" stroke="#dcece5" strokeWidth="1.4" />
                <ellipse cx={s.x} cy={s.y} rx={s.r * 0.55} ry={s.r * 0.55 * squash} fill="none" stroke="#dcece5" strokeWidth="1" opacity="0.6" />
              </g>
              {s.crown ? (
                <g style={reduced ? undefined : { transformOrigin: `${s.x}px ${s.y}px`, animation: `forest-crown ${s.dur}ms ease-out ${s.d}s infinite` }}>
                  {[0, 1, 2, 3].map((k) => {
                    const ang = (k / 4) * Math.PI * 2 + s.x;
                    const x1 = s.x + Math.cos(ang) * s.r * 0.7, y1 = s.y + Math.sin(ang) * s.r * 0.7 * squash;
                    const x2 = s.x + Math.cos(ang) * s.r * 1.35, y2 = s.y + Math.sin(ang) * s.r * 1.35 * squash;
                    return <line key={k} x1={x1} y1={y1} x2={x2} y2={y2} stroke="#eef7f2" strokeWidth="1" opacity="0.55" strokeLinecap="round" />;
                  })}
                </g>
              ) : null}
            </g>
            );
          })}

          {/* rain — foreground, in front of the creature, spanning the
              full width now rather than just the left half */}
          <g style={reduced ? undefined : { animation: "forest-rain-fall 520ms linear infinite" }}>
            {[0, -340].map((dy) => (
              <g key={dy} transform={`translate(0,${dy})`}>
                {rainNear.map((r, i) => (
                  <line key={i} x1={r.x} y1={r.y} x2={r.x2} y2={r.y + r.len} stroke="#f2f7f4" strokeWidth="2.4" strokeLinecap="round" opacity={0.62 * r.k} />
                ))}
              </g>
            ))}
          </g>

          {/* very occasional larger, softer close-pass droplets — each
              near-invisible for most of its own (long, randomized) cycle,
              then a single soft, blurred, bright streak drifts past
              before fading; never more than one or two visible at once */}
          {reduced ? null : rainClose.map((r, i) => (
            <line
              key={i} x1={r.x} y1={r.y} x2={r.x - 6} y2={r.y + 46}
              stroke="#f6faf8" strokeWidth="4.5" strokeLinecap="round"
              filter="url(#forestBlurSoft)"
              opacity={0}
              style={{ opacity: 0, animation: `forest-rain-close ${r.dur}ms ease-in ${r.delay}s infinite` }}
            />
          ))}
        </g>
      </svg>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// THE LIVING SHEET
// Treats the form panel itself as a premium physical sheet: a real
// spring simulation (not CSS easing pretending to be one) drives its
// tilt and lift in response to pointer proximity, field focus, typing
// impulses, and budget/timing "weight". Submission is a scripted
// physical release — the sheet lifts, bends, and slides through a
// thin exit slot rather than fading away like a generic form.
// ============================================================
function LivingSheet({
  ar, reduced, narrow, formRef, wrapRef: externalWrapRef,
  focusedField, typingPulse, svc, when, budgetIndex, sendPhase, children,
}) {
  const ownWrapRef = React.useRef(null);
  const wrapRef = externalWrapRef || ownWrapRef;
  const sheetRef = React.useRef(null);
  const shadowRef = React.useRef(null);
  const [rippleSeed, setRippleSeed] = React.useState(0);
  const [ripplePos, setRipplePos] = React.useState({ x: 50, y: 50 });

  // Physics state lives entirely in refs — the rAF loop writes directly
  // to the DOM every frame, never through setState, so 60fps motion
  // never fights React's render cycle.
  const phys = React.useRef({
    rx: { pos: 0, vel: 0 }, ry: { pos: 0, vel: 0 }, lift: { pos: 0, vel: 0 },
    targetRx: 0, targetRy: 0, originX: 50, originY: 50,
    pointerActive: false, t0: performance.now(),
  });

  // Pointer proximity → target tilt, with the transform-origin biased
  // toward the side away from the cursor, so that side stays anchored
  // while the near corner does the visible lifting.
  React.useEffect(() => {
    if (reduced || narrow) return;
    const el = wrapRef.current;
    if (!el) return;
    const MARGIN = 90; // "approaches an edge" reacts a little before the pointer is actually over it
    const onMove = (e) => {
      const r = el.getBoundingClientRect();
      const withinX = e.clientX > r.left - MARGIN && e.clientX < r.right + MARGIN;
      const withinY = e.clientY > r.top - MARGIN && e.clientY < r.bottom + MARGIN;
      if (!withinX || !withinY) { phys.current.pointerActive = false; phys.current.targetRx = 0; phys.current.targetRy = 0; return; }
      const fx = clamp((e.clientX - r.left) / r.width, -0.2, 1.2);
      const fy = clamp((e.clientY - r.top) / r.height, -0.2, 1.2);
      phys.current.pointerActive = true;
      phys.current.targetRy = (fx - 0.5) * 2 * MAX_TILT;
      phys.current.targetRx = (0.5 - fy) * 2 * MAX_TILT;
      phys.current.originX = (1 - fx) * 100;
      phys.current.originY = (1 - fy) * 100;
    };
    const onLeave = () => { phys.current.pointerActive = false; phys.current.targetRx = 0; phys.current.targetRy = 0; };
    window.addEventListener("mousemove", onMove);
    el.addEventListener("mouseleave", onLeave);
    return () => { window.removeEventListener("mousemove", onMove); el.removeEventListener("mouseleave", onLeave); };
  }, [reduced, narrow]);

  // Field focus → a persistent lean bias toward that field's position,
  // plus a one-shot ripple centred on it.
  React.useEffect(() => {
    if (!focusedField) { phys.current.focusBiasRx = 0; phys.current.focusBiasRy = 0; return; }
    const formEl = formRef.current;
    const sheetEl = sheetRef.current;
    if (!formEl || !sheetEl) return;
    const fieldEl = formEl.querySelector(`#${focusedField}`);
    if (!fieldEl) return;
    const fr = fieldEl.getBoundingClientRect();
    const sr = sheetEl.getBoundingClientRect();
    if (sr.width === 0 || sr.height === 0) return;
    const nx = clamp(((fr.left + fr.width / 2) - sr.left) / sr.width, 0, 1);
    const ny = clamp(((fr.top + fr.height / 2) - sr.top) / sr.height, 0, 1);
    phys.current.focusBiasRy = (nx - 0.5) * 2 * FOCUS_TILT;
    phys.current.focusBiasRx = (0.5 - ny) * 2 * FOCUS_TILT * (focusedField === "brief" ? 1.4 : 1);
    if (!reduced) {
      setRipplePos({ x: nx * 100, y: ny * 100 });
      setRippleSeed((s) => s + 1);
    }
  }, [focusedField, reduced, formRef]);

  // Typing → tiny cumulative impulses, as though the keystroke is
  // physically impressed into the material. Never a re-triggered
  // one-shot animation — a real velocity kick the spring absorbs.
  const lastPulse = React.useRef(typingPulse);
  React.useEffect(() => {
    if (reduced) { lastPulse.current = typingPulse; return; }
    if (typingPulse !== lastPulse.current) {
      lastPulse.current = typingPulse;
      const p = phys.current;
      p.rx.vel += (Math.random() - 0.5) * IMPULSE * (focusedField === "brief" ? 1.6 : 1);
      p.ry.vel += (Math.random() - 0.5) * IMPULSE * 0.7;
      p.lift.vel -= Math.random() * IMPULSE * 0.4;
      p.rx.vel = clamp(p.rx.vel, -9, 9);
      p.ry.vel = clamp(p.ry.vel, -9, 9);
    }
  }, [typingPulse, reduced, focusedField]);

  // The main spring loop. Paused entirely once the submit sequence
  // takes over (it drives its own scripted CSS transitions instead), and
  // — like reduced-motion — never started on mobile at all: there's no
  // mouse to tilt toward on a touchscreen, so this was purely the idle
  // "breathe" sway, running an UNTHROTTLED 60fps rAF that recomputed and
  // wrote a real `box-shadow` (a paint-triggering property, not just a
  // composite) every single frame for as long as the visitor stayed on
  // the form — a continuous, unconditional main-thread cost stacked on
  // top of every keystroke's own re-render, and a primary contributor to
  // the reported iPhone typing/selection stutter.
  React.useEffect(() => {
    if (reduced || narrow) {
      const sheet = sheetRef.current;
      if (sheet) sheet.style.transform = "none";
      return;
    }
    if (sendPhase !== "form") return;
    let raf;
    let last = performance.now();
    const timing = TIMING_SPRING[when] || TIMING_SPRING["Within a month"];
    const budgetMult = 0.85 + budgetIndex * 0.075;
    const step = (now) => {
      raf = requestAnimationFrame(step);
      const dt = Math.min(0.032, (now - last) / 1000);
      last = now;
      const p = phys.current;
      const k = BASE_K * budgetMult * timing.k;
      const c = BASE_C * budgetMult * timing.c;
      const breathe = Math.sin(((now - p.t0) / 4400) * Math.PI * 2) * 1.1;
      const targetRx = p.targetRx + (p.focusBiasRx || 0);
      const targetRy = p.targetRy + (p.focusBiasRy || 0);
      const targetLift = breathe - (focusedField === "brief" ? 3.4 : 0);
      springStep(p.rx, targetRx, k, c, dt);
      springStep(p.ry, targetRy, k, c, dt);
      springStep(p.lift, targetLift, k * 0.9, c, dt);
      const sheet = sheetRef.current;
      if (sheet) {
        const originX = p.pointerActive ? p.originX : 50;
        const originY = p.pointerActive ? p.originY : 50;
        sheet.style.transformOrigin = `${originX}% ${originY}%`;
        sheet.style.transform = `rotateX(${p.rx.pos.toFixed(3)}deg) rotateY(${p.ry.pos.toFixed(3)}deg) translateY(${p.lift.pos.toFixed(2)}px)`;
      }
      const shadow = shadowRef.current;
      if (shadow) {
        const mag = Math.abs(p.rx.pos) + Math.abs(p.ry.pos);
        const blur = 26 + mag * 3.2 + Math.abs(p.lift.pos) * 1.6;
        const spread = -6 - Math.abs(p.lift.pos) * 0.3;
        const alpha = 0.13 + Math.min(0.08, Math.abs(p.lift.pos) * 0.01);
        const offX = p.ry.pos * 1.6;
        const offY = 16 - p.lift.pos * 0.8;
        shadow.style.boxShadow = `${offX.toFixed(1)}px ${offY.toFixed(1)}px ${blur.toFixed(1)}px ${spread.toFixed(1)}px rgba(20,17,15,${alpha.toFixed(3)})`;
      }
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [reduced, sendPhase, when, budgetIndex, focusedField]);

  // ---- submit hand-off ---------------------------------------------
  // Once a real submission is in flight (or has succeeded), the sheet's
  // own idle physics have already stopped (the spring loop above bails
  // out the moment sendPhase leaves "form"). The sheet itself just
  // steps out of view — instantly, no transform choreography of its
  // own — because from that exact frame on, PaperPlane renders a
  // same-sized, same-position overlay that takes over the visual
  // entirely (see Contact()): first looking identical to this resting
  // card, then folding into the plane and launching. Two different
  // elements never need to be visually reconciled mid-transform this
  // way, and there's nothing here left to conflict with the watch-style
  // transform/CSS-animation bug this file has hit before.
  const phase = sendPhase;
  const sheetOpacity = phase === "success" ? 0 : 1;

  return (
    <div ref={wrapRef} style={{ position: "relative", perspective: 1400 }}>
      <style>{`
        @keyframes ink-sheet-ripple {
          0% { transform: translate(-50%,-50%) scale(0); opacity: 0.28; }
          100% { transform: translate(-50%,-50%) scale(1); opacity: 0; }
        }
      `}</style>

      {/* contact shadow — the sheet's only "depth" cue at idle */}
      <div
        ref={shadowRef}
        aria-hidden="true"
        style={{
          position: "absolute", inset: reduced ? 0 : "6px 4px -10px",
          borderRadius: "var(--radius-xl)",
          boxShadow: "0 16px 26px -6px rgba(20,17,15,0.13)",
          zIndex: 0, pointerEvents: "none",
          transition: reduced ? "none" : "box-shadow 60ms linear",
        }}
      />

      <div
        ref={sheetRef}
        style={{
          position: "relative", zIndex: 1, willChange: "transform",
          opacity: sheetOpacity,
        }}
      >
        <div style={{ position: "relative" }}>
          {children}

          {/* ripple — one-shot, centred on whichever field just gained focus */}
          {!reduced ? (
            <div
              key={rippleSeed}
              aria-hidden="true"
              style={{
                position: "absolute", left: `${ripplePos.x}%`, top: `${ripplePos.y}%`,
                width: 140, height: 140, borderRadius: "50%", pointerEvents: "none",
                background: "radial-gradient(circle, rgba(20,17,15,0.10) 0%, rgba(20,17,15,0) 70%)",
                animation: rippleSeed > 0 ? "ink-sheet-ripple 760ms var(--ease-standard) forwards" : "none",
              }}
            />
          ) : null}
        </div>
      </div>
    </div>
  );
}

// ============================================================
// THE PAPER PLANE
// The one-shot cinematic hand-off that plays once — and only once — a
// real submission has succeeded (see Contact(): mounted solely on
// sendPhase === "success"). It measures the now-hidden form sheet's own
// box on mount and opens looking identical to it (same size/position,
// same paper-white material), then folds itself into a paper-plane
// silhouette and launches. Living at the <section> level rather than
// inside LivingSheet/the two-column grid means it can fly anywhere in
// the section — including up past the beaver and off the top — without
// being clipped by either column's own box.
//
// clip-path is animated between two 8-point polygons (a chamfered
// rectangle and a dart/plane silhouette) so the browser interpolates
// every point's position continuously — that IS the fold, no manual
// in-between frames needed. transform (scale/translate/rotate) rides
// alongside on the same element for the fold, then a second animation
// (delayed to start exactly where the first leaves off) takes over
// `transform` alone for the launch — two animations sharing one
// element, never two competing transforms on one frame, so this never
// hits the SVG-transform-attribute-vs-CSS-animation bug this file has
// hit before with the watch (that bug was one element carrying BOTH an
// SVG transform ATTRIBUTE and a CSS transform animation; here both
// phases are plain CSS on a plain HTML element, so there's no
// attribute/animation split to fight).
// Fold destination points, named once and reused for both the clip-path
// keyframes below and the JS crease-line geometry, so the two can never
// drift out of sync with each other. Percentages of the origin card's own
// box. The point ORDER (nose -> wingtip -> notch pulled back toward
// center -> tail corner -> tail notch -> ...) is the same safe, tested
// topology the original symmetric dart used — only the asymmetric
// wingtip/notch/tail-corner VALUES are new. Reordering these (e.g.
// visiting the notch before its own wingtip on one side but after on
// the other) produced a self-intersecting polygon that rendered as two
// disconnected-looking triangles instead of one plane — worth noting so
// nobody "simplifies" this back into that shape.
const PLANE_NOSE = { x: 54, y: 0 };
const PLANE_RIGHT_NOTCH = { x: 60, y: 38 };
// A small, shallow indentation (not a deep V) — the rear edge stays a
// mostly-straight line between the two tail corners, with just this one
// point pulled in a little. Keep this shallow: a previous version had
// this at (49,82), a much deeper pull that read as a wide dip across
// almost the whole rear edge rather than one small folded section.
const PLANE_TAIL_NOTCH = { x: 52, y: 91 };
// The narrow internal fold-shadow strip — a thin parallelogram running
// alongside the spine, NOT a whole wing panel. Reads as the seam where
// the paper folds over itself, rather than "one half of the plane is
// dark" (which is what a full-wing shade actually looked like). Kept
// close to the nose's own x (54) the whole way down rather than
// swinging out toward the right wingtip, so the fold visually
// originates from the plane's true center rather than reading as
// biased to one side.
const PLANE_FOLD_A = { x: 54, y: 0 };
const PLANE_FOLD_B = { x: 58, y: 6 };
const PLANE_FOLD_C = { x: 52, y: 80 };
const PLANE_FOLD_D = { x: 48, y: 84 };

function PaperPlane({ ar, narrow, originRef }) {
  const [rect, setRect] = React.useState(null);

  React.useLayoutEffect(() => {
    const el = originRef && originRef.current;
    if (!el) return;
    const sectionEl = el.closest("section");
    const cardBox = el.getBoundingClientRect();
    const sectionBox = sectionEl ? sectionEl.getBoundingClientRect() : { left: 0, top: 0 };
    setRect({
      left: cardBox.left - sectionBox.left,
      top: cardBox.top - sectionBox.top,
      width: cardBox.width,
      height: cardBox.height,
    });
  }, [originRef]);

  if (!rect) return null;

  // Mirrored for Arabic so the plane launches toward the same side of
  // the scene the beaver actually favors in that layout (the whole
  // ForestScene mirrors too — see its own top-level flip) rather than
  // mechanically reusing the LTR-authored direction.
  const bank = ar ? 8 : -8;

  // Two permanent crease lines (spine + wing fold), computed once from
  // the plane's own final silhouette and the card's real pixel box, so
  // they sit exactly on the folded edges regardless of the card's actual
  // aspect ratio. left/top stay percentage-based (they track the box
  // correctly at any size); only the LENGTH of a diagonal line needs a
  // real pixel value, since a straight edge between two points at
  // different x/y percentages isn't itself expressible as a percentage.
  const lineGeo = (a, b) => {
    const dx = (b.x - a.x) / 100 * rect.width;
    const dy = (b.y - a.y) / 100 * rect.height;
    return { length: Math.hypot(dx, dy), angle: (Math.atan2(dy, dx) * 180) / Math.PI };
  };
  const spine = lineGeo(PLANE_NOSE, PLANE_TAIL_NOTCH);
  const wingFold = lineGeo(PLANE_NOSE, PLANE_RIGHT_NOTCH);

  // Desktop keeps its original, more storytelling-paced numbers untouched
  // (foldMs=800, no anticipation gap, a long 2800ms flight). Mobile gets
  // its own much faster clock: a compact ~550ms fold, a brief ~100ms
  // anticipation beat (small enough to never read as "sitting still"),
  // then a quick ~550ms flight — total ~1.2s from fold start to the plane
  // being gone, leaving the success sky/text to read within the
  // ~1.2-1.6s window the mobile pass asked for. Every dependent
  // sub-animation (shade/crease/trail) below is scaled off the SAME
  // foldMs/launchDelay so nothing drifts out of sync at the faster clock.
  const foldMs = narrow ? 550 : 800;
  const anticipationMs = narrow ? 100 : 0;
  const launchMs = narrow ? 550 : 2800;
  const launchDelay = foldMs + anticipationMs;
  const shadeDelay = Math.round(foldMs * 0.475);
  const shadeDur = foldMs - shadeDelay;
  const spineDelay = Math.round(foldMs * 0.775);
  const spineDur = narrow ? 150 : 220;
  const wingDelay = Math.round(foldMs * 0.7);
  const wingDur = narrow ? 140 : 200;
  const trailDelay1 = launchDelay + (narrow ? 20 : 50);
  const trailDur1 = narrow ? 130 : 620;
  const trailDelay2 = launchDelay + (narrow ? 40 : 100);
  const trailDur2 = narrow ? 115 : 560;
  // A stronger, thicker charcoal rim on mobile only — the faster flight
  // and smaller on-screen size otherwise let the white paper wash out
  // against the page background before it's identifiable as a plane.
  const outlineOpacity = narrow ? 0.78 : 0.55;
  const outlineInset = narrow ? "2.5px" : "1.5px";

  return (
    <div
      aria-hidden="true"
      style={{
        position: "absolute", zIndex: 2, pointerEvents: "none",
        left: rect.left, top: rect.top, width: rect.width, height: rect.height,
      }}
    >
      <style>{`
        /* Position only (scale/translate/rotate) — the clip-path morph
           lives on the two child layers below instead of here, so a
           single element never carries two independently-authored
           pieces of geometry at once. A single smooth pass rather than
           a "compress, then fold" two-step — the earlier compress stage
           read as a distracting pause where it just looked like a
           smaller rectangle, not paper folding. */
        @keyframes ink-plane-fold-transform {
          0%   { transform: translateY(0) scale(1) rotate(0deg); }
          50%  { transform: translateY(-1%) scale(0.42) rotate(calc(var(--ink-plane-bank) * 0.5)); }
          100% { transform: translateY(-4%) scale(0.22) rotate(var(--ink-plane-bank)); }
        }
        /* The paper body's own silhouette, morphing from the card's
           rounded-rect through ONE already-folding mid-state (already
           kite-like, never a plain smaller rectangle) into the final
           asymmetric plane shape. Every stage is an 8-point polygon so
           the browser interpolates each vertex individually rather than
           snapping. */
        @keyframes ink-plane-fold-shape {
          0%   { clip-path: polygon(50% 0%, 96% 4%, 100% 50%, 96% 96%, 55% 100%, 45% 100%, 0% 50%, 4% 4%); }
          50%  { clip-path: polygon(52% 2%, 90% 35%, 66% 42%, 70% 90%, 50% 80%, 36% 92%, 20% 48%, 30% 30%); }
          100% { clip-path: polygon(54% 0%, 100% 48%, 60% 38%, 62% 97%, 52% 91%, 33% 98%, 42% 44%, 4% 64%); }
        }
        /* The fold's inner shadow — a NARROW strip running alongside the
           spine (four points, a thin parallelogram), not a whole wing
           panel. This is the seam where the paper has folded over
           itself, not "one side of the plane is dark". Grows out from
           the nose in the last beat of the fold. */
        @keyframes ink-plane-fold-shade {
          0%   { clip-path: polygon(54% 0%, 54% 0%, 54% 0%, 54% 0%); opacity: 0; }
          100% { clip-path: polygon(54% 0%, 58% 6%, 52% 80%, 48% 84%); opacity: 0.75; }
        }
        @keyframes ink-plane-line-in { to { opacity: 1; } }
        /* One continuous flight, not several stitched stages. Two
           things used to make this read as stopping and re-starting:
           (1) the animation-timing-function applies PER SEGMENT between
           consecutive keyframes, so an ease-in-out curve repeated across
           five stops decelerated to near-zero and re-accelerated five
           separate times — every stop felt like a little hitch, even
           though nothing was technically frozen; (2) a deliberate
           "dip" reduced the climb rate partway through, which read as a
           pause once combined with that same per-segment easing. Fixed
           by switching to "linear" (below, on the animation shorthand)
           and doing ALL the "natural ease" work through keyframe VALUE
           spacing instead — each stop's x/y/scale/rotation is a fixed,
           always-increasing fraction of the same end target, so the
           whole flight has one smooth, continuously decelerating
           velocity curve with no re-acceleration anywhere. x and y use
           slightly different fraction curves (x advances a bit faster
           early) purely so the path arcs gently rather than running in
           a dead-straight line — that's the "very subtle curve" — but
           since both fractions only ever increase, momentum is never
           interrupted. */
        @keyframes ink-plane-launch {
          0%   { transform: translate(0,-4%) scale(0.22) rotate(var(--ink-plane-bank)); opacity: 1; }
          25%  { transform: translate(calc(var(--ink-plane-end-x) * 0.45), calc(var(--ink-plane-end-y) * 0.34)) scale(0.1547) rotate(calc(var(--ink-plane-bank) * 1.476)); opacity: 1; }
          50%  { transform: translate(calc(var(--ink-plane-end-x) * 0.72), calc(var(--ink-plane-end-y) * 0.6)) scale(0.1048) rotate(calc(var(--ink-plane-bank) * 1.84)); opacity: 0.95; }
          75%  { transform: translate(calc(var(--ink-plane-end-x) * 0.9), calc(var(--ink-plane-end-y) * 0.82)) scale(0.0626) rotate(calc(var(--ink-plane-bank) * 2.148)); opacity: 0.55; }
          100% { transform: translate(var(--ink-plane-end-x), var(--ink-plane-end-y)) scale(0.028) rotate(calc(var(--ink-plane-bank) * 2.4)); opacity: 0; }
        }
        @keyframes ink-plane-trail {
          0% { opacity: 0; transform: scaleY(0.3) translateY(0); }
          25% { opacity: 0.55; }
          100% { opacity: 0; transform: scaleY(1) translateY(30%); }
        }
      `}</style>
      <div
        style={{
          position: "absolute", inset: 0,
          "--ink-plane-bank": `${bank}deg`,
          /* Flight distance is deliberately in vh/vw (real screen real
             estate), not % (which would resolve against this box's OWN,
             already-shrinking size and produce wildly escalating pixel
             jumps — confirmed live: with %, the plane was rocketing
             thousands of px off-screen within the first few animation
             frames instead of gracefully ascending). Only one target
             (end-x/end-y) now — every keyframe stop above is a fixed
             fraction of this same target, which is what keeps the
             whole flight on one continuous curve. */
          "--ink-plane-end-x": narrow ? "-6vw" : "-11vw",
          "--ink-plane-end-y": narrow ? "-58vh" : "-86vh",
          transformOrigin: "50% 50%",
          /* linear on the launch animation is intentional — see the
             big comment on the ink-plane-launch keyframes above for
             why a repeated per-segment easing curve was the actual
             cause of the flight looking like it kept pausing. No delay
             on launch either: it starts the instant ink-plane-fold-
             transform's own 800ms ends (their values already match at
             that handoff), so there is no static hold between fold and
             flight — previously a 950ms delay left a 150ms gap where
             the plane just sat still right as it left the card. */
          animation: `ink-plane-fold-transform ${foldMs}ms cubic-bezier(0.32,1.1,0.4,1) forwards, ink-plane-launch ${launchMs}ms linear ${launchDelay}ms forwards`,
        }}
      >
        {/* A thin charcoal "stroke" layer, sitting directly behind the
            paper fill below and sharing the EXACT same clip-path keyframes
            — clip-path has no CSS stroke of its own, so the only way to
            keep an outline glued to a shape that's morphing through
            several fold stages is to clip a second, slightly larger copy
            of the same polygon animation and let it peek out as a rim.
            This is what makes the white paper legible against the page's
            own light background (the original complaint): without it the
            sheet had no edge definition at all while forming. Restrained
            on purpose — a thin line, not a cartoon outline. */}
        <div style={{ position: "absolute", inset: 0, background: `rgba(32,25,19,${outlineOpacity})`, animation: `ink-plane-fold-shape ${foldMs}ms cubic-bezier(0.32,1.1,0.4,1) forwards` }} />
        {/* the material itself — same paper-white as the card it came
            from, so the eye reads continuity rather than a swap. Its own
            clip-path animation (not the parent's) carries the fold. The
            minimal "brief" marks below are real CHILDREN of this div, not
            a separate layer — clip-path clips a box's whole rendered
            subtree, so the same animation that folds the paper folds
            these marks with it (they're partially, then fully, clipped
            away as the shape resolves into the plane) rather than
            needing any fade of their own. Inset 1.5px from the outline
            layer above so that layer's edge shows as a thin, consistent
            rim all the way around the current fold shape. */}
        <div style={{ position: "absolute", inset: outlineInset, background: "#f7f4ec", boxShadow: "0 18px 40px rgba(20,17,15,0.22)", animation: `ink-plane-fold-shape ${foldMs}ms cubic-bezier(0.32,1.1,0.4,1) forwards` }}>
          {/* extremely faint fiber hairlines — just enough that the sheet
              reads as paper rather than a flat CSS fill */}
          <div aria-hidden="true" style={{ position: "absolute", inset: 0, backgroundImage: "repeating-linear-gradient(114deg, rgba(20,17,15,0.025) 0px, rgba(20,17,15,0.025) 1px, transparent 1px, transparent 4px)" }} />
          {/* a tiny, editorial masthead — never a real logo lockup */}
          <div aria-hidden="true" style={{ position: "absolute", left: "12%", right: "12%", top: "6%" }}>
            <div style={{ fontSize: 11, fontWeight: 800, letterSpacing: "0.14em", color: "rgba(36,26,18,0.5)" }}>{ar ? "إنك · ملخص المشروع" : "INK · PROJECT BRIEF"}</div>
            <div style={{ marginTop: 7, height: 1.5, background: "rgba(36,26,18,0.16)" }} />
          </div>
          {/* short lines standing in for the submitted fields — never real copy */}
          {[0.6, 0.42, 0.7, 0.36].map((w, i) => (
            <div key={i} aria-hidden="true" style={{ position: "absolute", left: "12%", top: `${17 + i * 4.6}%`, width: `${w * 76}%`, height: 3, borderRadius: 2, background: "rgba(36,26,18,0.2)" }} />
          ))}
          {/* the idea field — a simple bordered area with two longer lines inside it */}
          <div aria-hidden="true" style={{ position: "absolute", left: "12%", right: "12%", top: "45%", height: "17%", border: "1.5px solid rgba(36,26,18,0.16)", borderRadius: 4 }}>
            <div style={{ position: "absolute", left: 8, right: 8, top: 9, height: 3, borderRadius: 2, background: "rgba(36,26,18,0.18)" }} />
            <div style={{ position: "absolute", left: 8, width: "62%", top: 20, height: 3, borderRadius: 2, background: "rgba(36,26,18,0.18)" }} />
          </div>
          {/* one small stamped checkmark — the only strong red mark on the
              sheet, slightly rotated for a hand-stamped feel */}
          <div aria-hidden="true" style={{ position: "absolute", left: "64%", top: "68%", width: 46, height: 46, borderRadius: "50%", border: "2px solid var(--vermilion-500)", opacity: 0.8, transform: "rotate(-8deg)", display: "flex", alignItems: "center", justifyContent: "center" }}>
            <svg width="18" height="14" viewBox="0 0 18 14"><path d="M1,7 L6.5,12.5 L17,1" fill="none" stroke="var(--vermilion-500)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" /></svg>
          </div>
        </div>
        {/* the fold's inner shadow — a narrow strip along the spine, not
            a whole panel — reads as the seam where the paper folds over
            itself rather than "one side of the plane is dark" */}
        {/* opacity: 0 here (not just in the 0% keyframe) matters: with
            an animation-delay, fill-mode "forwards" does NOT apply the
            0% state during the delay itself — only after the animation
            actually starts. Without this, the div spent its first
            380ms fully opaque and unclipped (its plain, un-animated
            state), covering the entire sheet — including the brief
            marks above — in a flat tan rectangle. That turned out to be
            exactly the "blank beige rectangle" this whole feature was
            meant to fix. */}
        <div style={{ position: "absolute", inset: 0, background: "#c9bfa4", opacity: 0, animation: `ink-plane-fold-shade ${shadeDur}ms ease-out ${shadeDelay}ms forwards` }} />
        {/* two permanent crease lines — the spine (nose to tail notch)
            and the wing fold (nose to the right panel's inner notch) —
            fading in right as the fold resolves and staying visible for
            the whole flight (they're children of the element the launch
            animation fades out, so they fade with it naturally rather
            than needing their own exit). */}
        <div style={{ position: "absolute", left: `${PLANE_NOSE.x}%`, top: `${PLANE_NOSE.y}%`, width: spine.length, height: 1.4, background: "rgba(20,17,15,0.34)", transformOrigin: "0 50%", transform: `rotate(${spine.angle}deg)`, opacity: 0, animation: `ink-plane-line-in ${spineDur}ms ease-out ${spineDelay}ms forwards` }} />
        <div style={{ position: "absolute", left: `${PLANE_NOSE.x}%`, top: `${PLANE_NOSE.y}%`, width: wingFold.length, height: 1.2, background: "rgba(20,17,15,0.24)", transformOrigin: "0 50%", transform: `rotate(${wingFold.angle}deg)`, opacity: 0, animation: `ink-plane-line-in ${wingDur}ms ease-out ${wingDelay}ms forwards` }} />
      </div>
      {/* a thin, hand-drawn-feeling motion trail — brief, restrained ink
          red + black, gone well before the plane reaches the treeline */}
      {/* opacity: 0 as a static base (matching each trail's own 0%
          keyframe) for the same reason as the fold-shade fix above:
          with an animation-delay and fill-mode "forwards", the pre-
          delay appearance falls back to whatever's set here, not to
          the animation's 0% state. Before this fix these two divs
          defaulted to opacity 1 and 0.6 respectively and were visible
          as thin lines the instant the plane mounted — part of why the
          sheet read as a stray-marked rectangle rather than a blank
          one waiting to fold. */}
      <div style={{ position: "absolute", left: "48%", top: "40%", width: "2px", height: "34%", background: "linear-gradient(to top, transparent, var(--vermilion-500) 55%, transparent)", opacity: 0, animation: `ink-plane-trail ${trailDur1}ms ease-out ${trailDelay1}ms forwards`, transformOrigin: "bottom" }} />
      <div style={{ position: "absolute", left: "54%", top: "44%", width: "1.4px", height: "24%", background: "linear-gradient(to top, transparent, var(--ink-900) 55%, transparent)", opacity: 0, animation: `ink-plane-trail ${trailDur2}ms ease-out ${trailDelay2}ms forwards`, transformOrigin: "bottom" }} />
    </div>
  );
}

// ============================================================
// SUCCESS TYPOGRAPHY
// The visual payoff, composed like a small motion-graphics end card —
// centered both ways in the sky, generous negative space, three
// deliberately mismatched typographic personalities (huge black hero,
// genuinely handwritten red accent line, small clean support line) —
// rather than a normal left-aligned block of website copy with
// decorative marks scattered around it.
//
// Two placements share this one component/copy so the two variants
// can't drift apart: "sky" appears once the camera ascent has revealed
// the sky (see its animation-delay), full-size, centered over that
// backdrop; "inline" appears quickly, in place, inside the same card
// slot the form used to occupy — the reduced-motion destination, and
// the only one ever mounted for reduced-motion visitors. It stays a
// single compact left/right-aligned line — it's the accessible
// fallback, not the moment to art-direct.
// ============================================================
function SuccessTypography({ ar, variant, reduced, waUrl, narrow }) {
  const sky = variant === "sky";
  // The "sky" delayBase used to be tuned to land after desktop's long
  // ~3600ms plane flight finishes. Mobile's flight is now ~1.2s (see
  // PaperPlane's narrow-only timing) and the ascend reveal is likewise
  // sped up (MobileAscentSky), so this needs its own much shorter delay
  // — the hero line should read as appearing right as the plane clears
  // the frame, not roughly two seconds after it's already gone.
  const delayBase = sky ? (narrow ? 900 : 3000) : (reduced ? 480 : 900);
  // Guards the WhatsApp CTA against a rapid double-click opening two tabs —
  // a real click a while later (the visitor came back and tries again) is
  // never blocked, only ones within the same accidental double-fire.
  const lastOpenRef = React.useRef(0);
  const openWhatsApp = () => {
    if (!waUrl) return;
    const now = Date.now();
    if (now - lastOpenRef.current < 1200) return;
    lastOpenRef.current = now;
    window.open(waUrl, "_blank", "noopener,noreferrer");
  };
  const ctaDelay = delayBase + 460 + 380;
  const heroLine = {
    fontFamily: ar ? "var(--font-arabic-display)" : "var(--font-display)",
    fontWeight: ar ? 800 : "var(--weight-black)",
    fontStretch: ar ? "normal" : "var(--stretch-display)",
    fontSize: sky ? "clamp(48px, 10.5vw, 132px)" : "clamp(24px, 6vw, 32px)",
    lineHeight: sky ? 0.92 : 1.05, color: "var(--ink-900)",
    textTransform: ar ? "none" : "uppercase",
    letterSpacing: ar ? 0 : "var(--tracking-display)",
    opacity: 0, transform: "translateY(14px)",
  };
  return (
    <div
      role="status"
      style={sky ? {
        position: "absolute", zIndex: 3, inset: 0,
        display: "flex", alignItems: "center", justifyContent: "center",
        padding: "0 var(--page-margin)",
        pointerEvents: "none",
      } : {
        display: "flex", flexDirection: "column", justifyContent: "center", minHeight: 360, position: "relative",
      }}
    >
      <style>{`
        @keyframes ink-hero-in { to { opacity: 1; transform: translateY(0); } }
        @keyframes ink-reveal-ltr { to { clip-path: inset(0 0% 0 0); } }
        @keyframes ink-reveal-rtl { to { clip-path: inset(0 0 0 0%); } }
        @keyframes ink-icon-flash { 0% { opacity: 0; transform: scale(0.7); } 45% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(0.88); } }
        /* Idle "alive" motion — only ever runs AFTER the entrance is
           fully settled (delay = delayBase + ~1.6s), extremely small
           amplitude, and lives on wrapper elements the entrance
           animations never touch, so the two never fight over the
           same transform. */
        @keyframes ink-compose-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.006); } }
        @keyframes ink-script-drift { 0%, 100% { transform: translateY(0) rotate(-1.2deg); } 50% { transform: translateY(-2px) rotate(-0.6deg); } }
      `}</style>

      {!sky ? (
        <div aria-hidden="true" style={{ position: "absolute", left: "50%", top: "18%", transform: "translate(-50%,-50%)", opacity: 0, animation: `ink-icon-flash 480ms ease-in-out forwards` }}>
          <svg width="34" height="34" viewBox="0 0 24 24"><path d="M2 12 L22 3 L14 21 L11 13 L2 12 Z" fill="none" stroke="var(--ink-900)" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" /></svg>
        </div>
      ) : null}

      <div
        style={sky ? {
          position: "relative", maxWidth: 760, width: "100%", textAlign: "center",
          animation: `ink-compose-breathe 8000ms ease-in-out ${delayBase + 1600}ms infinite`,
        } : { position: "relative", maxWidth: 420, width: "100%", textAlign: ar ? "right" : "left" }}
      >
        {sky ? (
          // aria-label on the wrapper (with the two visual lines hidden
          // from the accessibility tree) rather than two adjacent block
          // divs, which would otherwise concatenate as "IDEARECEIVED."
          // with no word boundary for assistive tech.
          <div aria-label={ar ? "فكرتك جاهزة." : "YOUR IDEA IS READY."}>
            <div aria-hidden="true" style={{ ...heroLine, animation: `ink-hero-in 560ms cubic-bezier(0.22,1,0.36,1) ${delayBase}ms forwards` }}>
              {ar ? "فكرتك" : "YOUR IDEA"}
            </div>
            <div aria-hidden="true" style={{ ...heroLine, animation: `ink-hero-in 560ms cubic-bezier(0.22,1,0.36,1) ${delayBase + 130}ms forwards` }}>
              {ar ? "جاهزة." : "IS READY."}
            </div>
          </div>
        ) : (
          <div style={{ ...heroLine, animation: `ink-hero-in 560ms cubic-bezier(0.22,1,0.36,1) ${delayBase}ms forwards` }}>
            {ar ? "فكرتك جاهزة." : "YOUR IDEA IS READY."}
          </div>
        )}

        {/* the red line: for "sky" this is a genuine handwritten/
            calligraphic display face (Caveat / Aref Ruqaa — see
            tokens/fonts.css), not the site sans-serif italicized, so it
            actually contrasts with the hero the way the reference's
            script line contrasts with its oversized wordmark. Nested
            one level deeper than the entrance-animated line itself so
            the later idle drift (a separate transform, on this wrapper)
            never fights the entrance's own translateY. */}
        <div style={{ marginTop: sky ? "-0.08em" : "var(--space-2)" }}>
          <div style={sky ? { animation: `ink-script-drift 6200ms ease-in-out ${delayBase + 1200}ms infinite` } : undefined}>
            <div style={{
              fontFamily: sky ? (ar ? "'Aref Ruqaa', serif" : "'Caveat', cursive") : (ar ? "var(--font-arabic-display)" : "var(--font-display)"),
              fontStyle: sky ? "normal" : (ar ? "normal" : "italic"),
              fontWeight: sky ? 700 : (ar ? 700 : "var(--weight-title)"),
              fontStretch: ar ? "normal" : "var(--stretch-title)",
              fontSize: sky ? "clamp(34px, 5.4vw, 58px)" : "clamp(16px, 4vw, 19px)",
              lineHeight: sky ? 1 : undefined,
              color: "var(--vermilion-500)",
              opacity: 0, transform: "translateY(10px)",
              animation: `ink-hero-in 460ms cubic-bezier(0.22,1,0.36,1) ${delayBase + 260}ms forwards`,
            }}>
              {ar ? "أرسلها لنا." : "Send it our way."}
            </div>
          </div>
        </div>

        <div style={{
          marginTop: sky ? "var(--space-5)" : "var(--space-4)",
          maxWidth: sky ? "38ch" : "34ch",
          fontSize: "var(--size-body)",
          color: "var(--text-muted)",
          fontFamily: ar ? "var(--font-arabic)" : "var(--font-body)",
          lineHeight: ar ? "var(--leading-arabic)" : "var(--leading-body)",
          clipPath: sky ? "inset(0 0 0 0)" : (ar ? "inset(0 0 0 100%)" : "inset(0 100% 0 0)"),
          animation: sky
            ? `ink-hero-in 400ms ease-out ${delayBase + 460}ms forwards`
            : `${ar ? "ink-reveal-rtl" : "ink-reveal-ltr"} 400ms steps(16,end) ${delayBase + 460}ms forwards`,
          /* Explicit longhands on both sides, never a shorthand mixed
             with a conditionally-undefined longhand of the same axis:
             React treats an `undefined` style value as "clear this
             property", which — when a shorthand had earlier expanded
             into two longhands — wiped out just this one side and left
             the block flush against the other edge instead of
             centered. Real values on both properties in every branch
             sidesteps that entirely. */
          marginInlineStart: sky ? "auto" : (ar ? "auto" : 0),
          marginInlineEnd: sky ? "auto" : 0,
          opacity: sky ? 0 : undefined,
          transform: sky ? "translateY(8px)" : undefined,
        }}>
          {ar ? "ملخص مشروعك جاهز على واتساب." : "Your project brief is ready on WhatsApp."}
        </div>

        {/* Single-shot entrance only (no infinite keyframe on this element
            itself) so the button settles and stays completely still —
            it just rides the same very subtle whole-composition
            "breathe" as everything else in the sky variant. */}
        <div style={{
          marginTop: "var(--space-6)",
          display: sky ? "flex" : "block",
          justifyContent: sky ? "center" : undefined,
          pointerEvents: "auto",
          opacity: 0, transform: "translateY(10px)",
          animation: `ink-hero-in 420ms cubic-bezier(0.22,1,0.36,1) ${ctaDelay}ms forwards`,
        }}>
          <Button variant="accent" size="lg" iconRight="arrow-up-right" onClick={openWhatsApp}>
            {ar ? "افتح واتساب" : "OPEN WHATSAPP"}
          </Button>
        </div>
      </div>
    </div>
  );
}

function Contact({ lang, target }) {
  const ar = lang === "ar";
  const narrow = useIsNarrow();
  const reduced = useReducedMotion();
  const [confirm, setConfirm] = React.useState(false);
  const [svc, setSvc] = React.useState({ media: false, motion: false, print: false, custom3d: false });
  const [when, setWhen] = React.useState("Within a month");
  const [budget, setBudget] = React.useState("Not sure yet");

  // Arriving here from a Home service card or a Services CTA: preselect
  // that service through the SAME setSvc setter a manual checkbox click
  // uses, so the beaver's hat reaction fires exactly as it would for a
  // manual selection — never a separate/fake selection path.
  React.useEffect(() => {
    if (target && Object.prototype.hasOwnProperty.call(svc, target)) {
      setSvc((prev) => ({ ...prev, [target]: true }));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [target]);

  const [focusedField, setFocusedField] = React.useState(null);
  const [typingPulse, setTypingPulse] = React.useState(0);
  // form: the normal interactive form.
  // submitting: confirmed, the brief is being folded into its send-off
  // form — the sheet's own idle motion stops here but the cinematic
  // sequence hasn't started yet.
  // success: the brief is folded, sent airborne and ready — this is what
  // starts the paper-plane send-off, the beaver's salute and the camera
  // ascent. It does NOT mean INK Studio has received anything yet: actual
  // delivery only happens once the visitor presses Send inside WhatsApp,
  // via the CTA the final screen hands them (see waUrl below).
  const [sendPhase, setSendPhase] = React.useState("form");
  const formRef = React.useRef(null);
  const sheetWrapRef = React.useRef(null);
  const sectionRef = React.useRef(null);
  const [snapshot, setSnapshot] = React.useState(null);

  // On narrow, the stacked single-column layout makes this section far
  // taller than one screen, so the sky/"YOUR IDEA IS READY." reveal — built
  // to sit centered in a single viewport-height stage (see
  // MobileAscentSky's ascent transform) — can land mostly above whatever
  // the visitor had scrolled to while filling the form. Starting the
  // scroll the instant "submitting" begins (rather than waiting for
  // "success") gives one continuous, unhurried glide the full ~2s+ of the
  // fold/launch/ascent sequence to run, so the page is already gliding
  // upward while the plane is still mid-flight rather than jump-cutting
  // once it's done. One continuous motion is the "camera transition" the
  // mobile pass calls for, not a jump-cut.
  React.useEffect(() => {
    if (!narrow || reduced || sendPhase === "form") return;
    const section = sectionRef.current;
    if (!section) return;
    const rect = section.getBoundingClientRect();
    const sectionTop = rect.top + window.scrollY;
    const target = Math.max(0, sectionTop + rect.height * 0.5 - window.innerHeight / 2);
    window.scrollTo({ top: target, behavior: "smooth" });
  }, [narrow, reduced, sendPhase]);

  const budgetIndex = Math.max(0, BUDGET_OPTIONS.indexOf(budget));

  const onFormFocusCapture = (e) => { if (e.target && e.target.id) setFocusedField(e.target.id); };
  const onFormBlurCapture = (e) => { setFocusedField((f) => (f === (e.target && e.target.id) ? null : f)); };
  const onFormKeyDownCapture = () => setTypingPulse((p) => p + 1);

  // This static prototype has no backend to actually deliver the enquiry
  // to, so instead of pretending a request was sent, the confirmed form
  // values are captured once here and handed to the visitor as a
  // pre-filled WhatsApp message on the final screen — they press Send
  // inside WhatsApp themselves. The short delay below is only the existing
  // fold-in beat before the cinematic sequence starts, not a network call.
  const captureSnapshot = () => {
    const el = formRef.current;
    const val = (id) => { const node = el && el.querySelector(`#${id}`); return node ? node.value.trim() : ""; };
    return {
      name: val("n"),
      organisation: val("o"),
      email: val("e"),
      whatsapp: val("p"),
      services: SERVICE_LIST.filter((s) => svc[s.key]).map((s) => s.key),
      budget,
      when,
      brief: val("brief"),
    };
  };

  const onFormSubmit = (e) => {
    e.preventDefault();
    const el = formRef.current;
    const name = el.querySelector("#n");
    const email = el.querySelector("#e");
    if (!name.value.trim()) { name.focus(); return; }
    // Email is optional, but if the visitor did type one, it should still
    // look like a real address rather than being sent along malformed.
    if (email.value.trim() && !isValidEmail(email.value.trim())) { email.focus(); return; }
    if (!Object.values(svc).some(Boolean)) { el.querySelector("#c1").focus(); return; }
    setConfirm(true);
  };

  const sendEnquiry = async () => {
    const snap = captureSnapshot();
    setConfirm(false);
    setFocusedField(null);
    setSendPhase("submitting");
    await new Promise((resolve) => setTimeout(resolve, reduced ? 120 : 420));
    setSnapshot(snap);
    setSendPhase("success");
  };

  const dir = ar ? "rtl" : "ltr";
  const success = sendPhase === "success";
  const waUrl = React.useMemo(() => (snapshot ? whatsappHref(buildWhatsAppMessage(ar, snapshot)) : null), [snapshot, ar]);
  return (
    <main dir={dir}>
      <section ref={sectionRef} style={{ position: "relative", padding: narrow ? "var(--space-7) var(--page-margin) var(--space-8)" : "var(--space-9) var(--page-margin) var(--space-10)", overflow: narrow ? "hidden" : undefined }}>
        {narrow ? (
          <>
            <ContactMobileDecor success={success} reduced={reduced} />
            <MobileAscentSky success={success} reduced={reduced} />
          </>
        ) : (
          <ForestScene ar={ar} reduced={reduced} narrow={narrow} sendPhase={sendPhase} success={success} svc={svc} when={when} focusedField={focusedField} typingPulse={typingPulse} budget={budget} />
        )}
        <div
          style={{
            position: "relative", zIndex: 1, maxWidth: "var(--container-max)", margin: "0 auto",
            display: "grid", gridTemplateColumns: narrow ? "1fr" : "1fr 1fr", gap: narrow ? "var(--space-8)" : "var(--space-9)",
            opacity: success && !reduced ? 0 : 1,
            transition: reduced ? "none" : "opacity 500ms ease 1400ms",
            pointerEvents: success ? "none" : "auto",
          }}
        >
          <div>
            <Reveal><Kicker>{ar ? "تواصل" : "Start a project"}</Kicker></Reveal>
            <Reveal delay={80} as="h1" style={{ fontFamily: ar ? "var(--font-arabic-display)" : "var(--font-display)", fontWeight: ar ? 800 : "var(--weight-display)", fontStretch: ar ? "normal" : "var(--stretch-display)", fontSize: ar ? 48 : "var(--size-display-md)", lineHeight: ar ? 1.2 : "var(--leading-display)", letterSpacing: ar ? 0 : "var(--tracking-display)", textTransform: ar ? "none" : "uppercase", marginTop: "var(--space-5)", maxWidth: "16ch" }}>
              {ar ? "أخبرنا بالفكرة فقط" : "Just tell us the idea"}
            </Reveal>
            <Reveal delay={150} as="p" style={{ marginTop: "var(--space-5)", fontSize: "var(--size-body-lg)", maxWidth: "44ch", fontFamily: ar ? "var(--font-arabic)" : "var(--font-body)" }}>
              {ar ? "لا حاجة لمعرفة ما تحتاجه بالتحديد. راسلنا وسنحدد الاتجاه معك." : "You don't need to know exactly what you need. Message us and we'll shape it with you."}
            </Reveal>
            <div style={{ margin: "var(--space-7) 0" }}><Rule weight="heavy" /></div>
            <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
              {[
                { icon: "message-circle", label: ar ? "واتساب الأعمال" : "WhatsApp Business", value: BUSINESS.whatsappDisplay, href: whatsappHref(), aria: ar ? "تواصل مع إنك ستوديو عبر واتساب" : "Contact INK Studio on WhatsApp" },
                { icon: "mail", label: ar ? "البريد الإلكتروني" : "Email", value: BUSINESS.email, href: emailHref(), aria: ar ? "راسل إنك ستوديو عبر البريد الإلكتروني" : "Email INK Studio" },
                { icon: "instagram", label: ar ? "إنستغرام" : "Instagram", value: `@${BUSINESS.instagramHandle}`, href: instagramHref(), aria: ar ? "إنك ستوديو على إنستغرام" : "INK Studio on Instagram" },
                { icon: "music-2", label: ar ? "تيك توك" : "TikTok", value: `@${BUSINESS.tiktokHandle}`, href: tiktokHref(), aria: ar ? "إنك ستوديو على تيك توك" : "INK Studio on TikTok" },
                { icon: "map-pin", label: ar ? "الاستوديو" : "Studio", value: ar ? BUSINESS.studio.ar : BUSINESS.studio.en },
              ].map((it, idx) => (
                <Reveal key={it.label} delay={idx * 70} style={{ display: "flex", gap: "var(--space-4)", alignItems: "center" }}>
                  <Icon name={it.icon} size={20} />
                  <div>
                    <div className="ink-label">{it.label}</div>
                    {it.href ? (
                      <a href={it.href} target="_blank" rel="noopener noreferrer" aria-label={it.aria} dir="ltr" style={{ fontSize: "var(--size-body-sm)", color: "var(--text-muted)", textDecoration: "none" }}>{it.value}</a>
                    ) : (
                      <div style={{ fontSize: "var(--size-body-sm)", color: "var(--text-muted)", fontFamily: ar ? "var(--font-arabic)" : "var(--font-body)" }}>{it.value}</div>
                    )}
                  </div>
                </Reveal>
              ))}
            </div>
            {/* --size-display-md is scoped to this row only (a local CSS
                custom-property override, not a global token edit) — on
                mobile the design system's default 56px display size made
                this compact stat pair read as oversized and cramped next
                to the page's own edge, worse than the intro heading right
                above it; shrinking it here only touches these two values. */}
            <div style={{ display: "flex", gap: narrow ? "var(--space-6)" : "var(--space-8)", marginTop: narrow ? "var(--space-6)" : "var(--space-8)", "--size-display-md": narrow ? "32px" : undefined }}>
              <StatBlock value={<CountUp to={24} suffix="h" />} label={ar ? "وقت الرد" : "Reply time"} />
              <StatBlock value="EN / ع" label={ar ? "نعمل باللغتين" : "We work in both"} />
            </div>
          </div>

          <Reveal delay={120}>
          {reduced && success ? (
            <Card tone="paper" stamped padding="var(--space-7)">
              <SuccessTypography ar={ar} variant="inline" reduced waUrl={waUrl} />
            </Card>
          ) : (
          <LivingSheet
            ar={ar} reduced={reduced} narrow={narrow} formRef={formRef} wrapRef={sheetWrapRef}
            focusedField={focusedField} typingPulse={typingPulse}
            svc={svc} when={when} budgetIndex={budgetIndex} sendPhase={sendPhase}
          >
            <Card tone="paper" stamped padding="var(--space-7)">
              <form
                ref={formRef}
                onSubmit={onFormSubmit}
                onFocusCapture={onFormFocusCapture}
                onBlurCapture={onFormBlurCapture}
                onKeyDownCapture={onFormKeyDownCapture}
                style={{ display: "flex", flexDirection: "column", gap: "var(--space-6)" }}
              >
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-5)" }}>
                  <FieldLift id="n" focusedField={focusedField} reduced={reduced}>
                    <Input id="n" dir={dir} label={ar ? "الاسم" : "Name"} placeholder={ar ? "سارة العتيبي" : "Sara Al-Otaibi"} required />
                  </FieldLift>
                  <FieldLift id="o" focusedField={focusedField} reduced={reduced}>
                    <Input id="o" dir={dir} label={ar ? "الجهة" : "Organisation"} placeholder={ar ? "نادي الجامعة" : "University club"} />
                  </FieldLift>
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-5)" }}>
                  <FieldLift id="e" focusedField={focusedField} reduced={reduced}>
                    <Input id="e" dir={dir} label={ar ? "البريد" : "Email"} type="email" placeholder="sara@club.sa" />
                  </FieldLift>
                  <FieldLift id="p" focusedField={focusedField} reduced={reduced}>
                    <Input id="p" dir={dir} label={ar ? "واتساب" : "WhatsApp"} placeholder="+966 5X XXX XXXX" />
                  </FieldLift>
                </div>
                <div>
                  <div className="ink-label" style={{ marginBottom: "var(--space-4)" }}>{ar ? "ما تحتاجه" : "What you need"}<span style={{ color: "var(--vermilion-500)" }}> *</span></div>
                  <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-4)" }}>
                    <Checkbox id="c1" label={ar ? "إنتاج إعلامي" : "Media Production"} checked={svc.media} onChange={() => setSvc({ ...svc, media: !svc.media })} />
                    <Checkbox id="c2" label={ar ? "موشن وتحريك" : "Motion & Animation"} checked={svc.motion} onChange={() => setSvc({ ...svc, motion: !svc.motion })} />
                    <Checkbox id="c3" label={ar ? "طباعة ومنتجات" : "Print & Merchandise"} checked={svc.print} onChange={() => setSvc({ ...svc, print: !svc.print })} />
                    <Checkbox id="c4" label={ar ? "ثلاثي الأبعاد ومخصص" : "3D & Custom Objects"} checked={svc.custom3d} onChange={() => setSvc({ ...svc, custom3d: !svc.custom3d })} />
                  </div>
                </div>
                <Select id="b" dir={dir} label={ar ? "الميزانية التقديرية" : "Budget band"} options={budgetSelectOptions(ar)} value={budget} onChange={(e) => setBudget(e.target.value)} required />
                <div>
                  <div className="ink-label" style={{ marginBottom: "var(--space-4)" }}>{ar ? "الموعد" : "Timing"}<span style={{ color: "var(--vermilion-500)" }}> *</span></div>
                  <RadioGroup name="when" direction={narrow ? "column" : "row"} options={timingOptions(ar)} value={when} onChange={setWhen} />
                </div>
                <FieldLift id="brief" focusedField={focusedField} reduced={reduced}>
                  <Textarea id="brief" dir={dir} rows={4} label={ar ? "الفكرة" : "The idea"} hint={ar ? "سطر واحد يكفي للبداية." : "One line is enough to start."} />
                </FieldLift>
                <Button variant="accent" size="lg" type="submit" fullWidth iconRight="arrow-up-right">{ar ? "إرسال" : "Send it over"}</Button>
              </form>
            </Card>
          </LivingSheet>
          )}
          </Reveal>
        </div>

        {/* The cinematic send-off. Deliberately outside the two-column
            grid (which only fades, never disappears from the layout) so
            these can paint above BOTH the grid and the ForestScene
            background regardless of that fade's own timing. Mounted only
            once the brief has been folded and is ready — the enquiry
            itself isn't delivered until the visitor presses the WhatsApp
            CTA this reveals, and then Send inside WhatsApp itself. */}
        {!reduced && success ? (
          <PaperPlane ar={ar} narrow={narrow} originRef={sheetWrapRef} />
        ) : null}
        {!reduced && success ? (
          <SuccessTypography ar={ar} variant="sky" waUrl={waUrl} narrow={narrow} />
        ) : null}
      </section>

      <Dialog open={confirm} title={ar ? "إرسال الطلب؟" : "Send this enquiry?"} onClose={() => setConfirm(false)}
        footer={<><Button variant="ghost" onClick={() => setConfirm(false)}>{ar ? "رجوع" : "Back"}</Button><Button variant="accent" onClick={sendEnquiry}>{ar ? "إرسال" : "Send"}</Button></>}>
        {ar ? "سنرد خلال ٢٤ ساعة على واتساب أو البريد." : "We reply within 24 hours on WhatsApp or by email."}
      </Dialog>
    </main>
  );
}

// Minimal editorial brand-statement page — no team/office/stat "About us"
// furniture. Reuses the exact approved watermark artwork from the Home hero
// (INK_MARK_PATH / ARABIC_MARK_PATH, both defined in Home.jsx and already
// global by script-load order) rather than redrawing or approximating it,
// just scaled up and re-centered as a huge, barely-there background mark.
const STUDIO_COPY = {
  en: {
    eyebrow: "The studio",
    heading: ["We turn ideas", "into reality."],
    blocks: [
      { n: "01", title: "Who we are", body: ["INK is a creative studio based in Saudi Arabia, working across design, media, print, and physical production.", "We bring the different parts of an idea together under one creative direction."] },
      { n: "02", title: "What we make", list: ["Identity.", "Campaigns.", "Printed matter.", "Media.", "Objects.", "Experiences."] },
      { n: "03", title: "What we believe", body: ["A good idea should not stop at looking good.", "It should feel intentional, work in the real world, and leave something behind."] },
    ],
    process: "Brief → Direction → Production → Delivery.",
  },
  ar: {
    eyebrow: "الاستوديو",
    heading: ["نحوّل الأفكار", "إلى واقع."],
    blocks: [
      { n: "01", title: "من نحن", body: ["حبر استوديو إبداعي في المملكة العربية السعودية، نعمل بين التصميم، والإنتاج الإعلامي، والطباعة، والتنفيذ المادي.", "نجمع مراحل الفكرة المختلفة تحت توجه إبداعي واحد."] },
      { n: "02", title: "ماذا نصنع", list: ["هويات.", "حملات.", "مطبوعات.", "محتوى.", "منتجات.", "تجارب."] },
      { n: "03", title: "ما نؤمن به", body: ["الفكرة الجيدة لا يكفي أن تبدو جميلة.", "يجب أن تكون مقصودة، تعمل في الواقع، وتترك أثرًا بعدها."] },
    ],
    process: "الفكرة ← التوجيه ← الإنتاج ← التسليم",
  },
};

function Studio({ lang }) {
  const ar = lang === "ar";
  const narrow = useIsNarrow();
  const reduced = useReducedMotion();
  const copy = ar ? STUDIO_COPY.ar : STUDIO_COPY.en;
  const sectionRef = React.useRef(null);
  const markRef = React.useRef(null);

  // Very subtle scroll-linked drift on the watermark only — a few px, never
  // enough to compete with reading. Imperative style write on a ref (no
  // setState) matching the perf-sensitive-animation pattern used on Home;
  // skipped entirely under prefers-reduced-motion.
  React.useEffect(() => {
    if (reduced) return;
    const mark = markRef.current;
    if (!mark) return;
    let raf = null;
    const update = () => {
      raf = null;
      const r = mark.getBoundingClientRect();
      const centerDelta = (r.top + r.height / 2) - window.innerHeight / 2;
      const shift = Math.max(-16, Math.min(16, centerDelta * -0.02));
      mark.style.transform = `translate(-50%, calc(-50% + ${shift}px))`;
    };
    const onScroll = () => { if (raf == null) raf = requestAnimationFrame(update); };
    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); if (raf != null) cancelAnimationFrame(raf); };
  }, [reduced, ar, narrow]);

  return (
    <main dir={ar ? "rtl" : "ltr"}>
      {/* overflow: hidden on BOTH axes, deliberately — setting only
          overflow-x makes the UA implicitly promote overflow-y to "auto"
          (a real CSS quirk: a non-visible x paired with a visible y is
          computed as auto), which turned this section into its own
          scroll container fighting the page's own scrollbar. Both axes
          hidden keeps this a plain block with zero scroll of its own; the
          watermark below is now sized well within the section's natural
          content height, so nothing needs to bleed past it. */}
      <section ref={sectionRef} style={{ position: "relative", overflow: "hidden", padding: narrow ? "var(--space-8) var(--page-margin) var(--space-8)" : "var(--space-9) var(--page-margin) var(--space-9)" }}>
        {/* Translucent watermark — the SAME approved silhouette used on the
            Home hero (never redrawn), centered behind the composition at
            roughly Home-hero scale. Background only: opacity stays subtle,
            it never intercepts pointer events, and it never sets the
            section's height. */}
        <div
          ref={markRef}
          aria-hidden="true"
          style={{
            position: "absolute",
            left: "50%",
            top: "50%",
            transform: "translate(-50%, -50%)",
            zIndex: 0,
            color: "var(--ink-900)",
            opacity: 0.06,
            pointerEvents: "none",
            userSelect: "none",
            willChange: "transform",
            width: !ar
              ? (narrow ? "clamp(200px, 56vw, 340px)" : "clamp(360px, 34vw, 620px)")
              : (narrow ? "clamp(200px, 60vw, 320px)" : "clamp(420px, 38vw, 700px)"),
          }}
        >
          {!ar ? (
            // The real approved logo asset (assets/ink-logo-black.png) —
            // full INK lettering, the "Studio" subtitle, and the ink
            // drop/nib detail all included — rather than INK_MARK_PATH, a
            // hand-traced Bézier simplification of just the bare "INK"
            // letters (built for the Home hero's much larger, closer-crop
            // watermark) that drops those smaller details entirely. Same
            // wrapper, same opacity/scroll-drift/positioning as before —
            // only the mark itself changes to the complete one.
            <img src="assets/ink-logo-black.png" alt="" aria-hidden="true" style={{ display: "block", width: "100%", height: "auto" }} />
          ) : (
            // Same specular-highlight treatment as the Home hero's حبر mark
            // (see Home.jsx's arGlow0-3 gradients) — reused verbatim, just
            // scaled down with the rest of this smaller watermark, so the
            // Studio wordmark reads as the same brand treatment, not a
            // flatter one-off.
            <svg viewBox="0 0 2346 1193" aria-hidden="true" style={{ display: "block", width: "100%", height: "auto" }}>
              <defs>
                <linearGradient id="arGlow0" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stopColor="#fff" stopOpacity="0" /><stop offset="14%" stopColor="#fff" stopOpacity="0.85" /><stop offset="45%" stopColor="#fff" stopOpacity="1" /><stop offset="80%" stopColor="#fff" stopOpacity="0.7" /><stop offset="100%" stopColor="#fff" stopOpacity="0" /></linearGradient>
                <linearGradient id="arGlow1" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stopColor="#fff" stopOpacity="0" /><stop offset="14%" stopColor="#fff" stopOpacity="0.85" /><stop offset="45%" stopColor="#fff" stopOpacity="1" /><stop offset="80%" stopColor="#fff" stopOpacity="0.7" /><stop offset="100%" stopColor="#fff" stopOpacity="0" /></linearGradient>
                <linearGradient id="arGlow2" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stopColor="#fff" stopOpacity="0" /><stop offset="14%" stopColor="#fff" stopOpacity="0.85" /><stop offset="45%" stopColor="#fff" stopOpacity="1" /><stop offset="80%" stopColor="#fff" stopOpacity="0.7" /><stop offset="100%" stopColor="#fff" stopOpacity="0" /></linearGradient>
                <linearGradient id="arGlow3" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stopColor="#fff" stopOpacity="0" /><stop offset="14%" stopColor="#fff" stopOpacity="0.85" /><stop offset="45%" stopColor="#fff" stopOpacity="1" /><stop offset="80%" stopColor="#fff" stopOpacity="0.7" /><stop offset="100%" stopColor="#fff" stopOpacity="0" /></linearGradient>
              </defs>
              <g transform="translate(0,1193) scale(0.1,-0.1)" fill="currentColor" stroke="none">
                <path d={ARABIC_MARK_PATH} />
              </g>
              <path d="M 1578.4,31.9 L 1576.4,32.1 L 1573.5,32.8 L 1569.6,34.0 L 1564.1,35.8 L 1558.2,37.8 L 1553.1,39.6 L 1548.4,41.4 L 1543.8,43.2 L 1539.5,45.0 L 1535.3,46.9 L 1531.2,48.7 L 1527.4,50.6 L 1523.6,52.5 L 1520.0,54.3 L 1516.4,56.2 L 1513.0,58.1 L 1509.6,60.0 L 1506.4,62.0 L 1503.2,63.9 L 1500.1,65.9 L 1497.1,67.8 L 1494.1,69.8 L 1491.1,71.7 L 1488.1,73.8 L 1485.1,75.8 L 1482.3,77.8 L 1479.5,79.8 L 1476.8,81.9 L 1474.0,83.9 L 1471.2,86.0 L 1468.5,88.1 L 1465.9,90.1 L 1463.4,92.1 L 1460.7,94.2 L 1457.9,96.5 L 1455.3,98.7 L 1453.1,100.7 L 1450.8,102.8 L 1448.4,104.9 L 1445.9,107.1 L 1443.6,109.3 L 1441.3,111.5 L 1439.0,113.7 L 1436.8,115.9 L 1434.6,118.1 L 1432.4,120.3 L 1430.3,122.6 L 1428.2,124.8 L 1426.2,127.0 L 1424.1,129.2 L 1422.0,131.5 L 1420.0,133.8 L 1418.0,136.1 L 1416.1,138.4 L 1414.2,140.7 L 1412.3,143.0 L 1410.5,145.3 L 1408.8,147.6 L 1407.0,150.0 L 1405.3,152.3 L 1403.6,154.6 L 1402.0,156.9 L 1400.4,159.2 L 1398.7,161.6 L 1397.1,164.0 L 1395.5,166.3 L 1394.0,168.7 L 1392.4,171.2 L 1391.0,173.5 L 1389.6,175.7 L 1388.1,178.1 L 1386.5,180.6 L 1385.1,182.9 L 1383.7,185.3 L 1382.2,187.7 L 1380.9,190.2 L 1379.8,192.4 L 1378.5,194.8 L 1377.3,197.2 L 1376.0,199.6 L 1374.8,201.9 L 1373.5,204.3 L 1372.3,206.8 L 1371.1,209.2 L 1370.0,211.6 L 1368.9,213.9 L 1367.7,216.4 L 1366.6,218.8 L 1365.4,221.2 L 1364.3,223.6 L 1363.1,226.1 L 1362.0,228.5 L 1360.9,230.9 L 1359.8,233.3 L 1358.7,235.7 L 1357.6,238.1 L 1356.5,240.6 L 1355.6,243.0 L 1354.6,245.4 L 1353.4,248.3 L 1352.4,250.7 L 1352.4,251.3 L 1355.8,253.4 L 1357.3,251.8 L 1359.3,249.4 L 1361.1,247.2 L 1362.7,245.2 L 1364.3,243.3 L 1366.0,241.4 L 1367.6,239.4 L 1369.4,237.4 L 1371.1,235.3 L 1372.8,233.2 L 1374.5,231.2 L 1376.2,229.1 L 1377.9,227.0 L 1379.6,224.9 L 1381.3,222.9 L 1383.0,220.8 L 1384.7,218.7 L 1386.3,216.7 L 1387.9,214.8 L 1389.5,212.9 L 1391.2,210.9 L 1393.0,208.8 L 1394.7,206.6 L 1396.5,204.6 L 1398.1,202.5 L 1399.7,200.6 L 1401.2,198.9 L 1402.8,197.1 L 1404.5,195.3 L 1406.3,193.3 L 1408.2,191.2 L 1410.2,189.0 L 1412.2,186.7 L 1414.0,184.6 L 1415.7,182.6 L 1417.4,180.7 L 1419.1,178.8 L 1420.9,176.8 L 1422.8,174.8 L 1424.7,172.6 L 1426.6,170.5 L 1428.4,168.4 L 1430.2,166.4 L 1432.0,164.4 L 1433.8,162.4 L 1435.7,160.4 L 1437.5,158.4 L 1439.4,156.3 L 1441.3,154.3 L 1443.2,152.2 L 1445.2,150.2 L 1447.2,148.0 L 1449.3,145.8 L 1451.4,143.6 L 1453.4,141.3 L 1455.3,139.2 L 1457.3,137.1 L 1459.3,134.9 L 1461.3,132.8 L 1463.4,130.6 L 1465.4,128.4 L 1467.5,126.2 L 1469.7,123.9 L 1471.9,121.6 L 1474.1,119.3 L 1476.1,117.1 L 1477.9,115.1 L 1480.1,113.0 L 1482.4,110.7 L 1484.9,108.2 L 1487.2,105.9 L 1489.5,103.5 L 1491.9,101.2 L 1494.2,98.8 L 1496.6,96.5 L 1499.0,94.1 L 1501.3,91.8 L 1503.7,89.5 L 1506.2,87.2 L 1508.8,84.8 L 1511.4,82.4 L 1513.9,80.0 L 1516.4,77.7 L 1519.1,75.4 L 1521.7,73.1 L 1524.5,70.8 L 1527.5,68.4 L 1530.5,66.1 L 1533.7,63.7 L 1536.9,61.3 L 1540.3,58.9 L 1543.9,56.6 L 1547.7,54.2 L 1551.6,51.7 L 1555.8,49.3 L 1560.2,46.8 L 1565.3,44.2 L 1570.8,41.5 L 1574.9,39.3 L 1577.8,37.6 L 1579.7,36.2 L 1580.1,35.8 Z" fill="url(#arGlow0)" />
              <path d="M 1348.6,259.6 L 1343.0,262.8 L 1337.4,269.0 L 1332.4,276.8 L 1328.9,284.9 L 1327.6,291.9 L 1328.1,293.9 L 1329.6,294.8 L 1335.2,291.7 L 1340.8,285.4 L 1345.8,277.6 L 1349.3,269.5 L 1350.6,262.5 L 1350.2,260.5 Z" fill="url(#arGlow0)" />
              <path d="M 938.0,201.7 L 937.7,202.3 L 937.4,203.1 L 937.2,204.4 L 937.1,206.3 L 937.0,208.4 L 936.8,210.4 L 936.7,212.2 L 936.6,214.1 L 936.4,216.0 L 936.3,217.9 L 936.2,219.8 L 936.1,221.7 L 936.0,223.6 L 935.9,225.5 L 935.8,227.4 L 935.7,229.3 L 935.6,231.3 L 935.5,233.3 L 935.4,235.1 L 935.2,236.9 L 935.1,238.8 L 935.0,240.9 L 934.9,242.7 L 934.8,244.5 L 934.8,246.5 L 934.7,248.4 L 934.6,250.2 L 934.5,252.1 L 934.4,254.1 L 934.3,255.9 L 934.2,257.7 L 934.1,259.7 L 934.1,261.8 L 934.1,263.6 L 934.0,265.3 L 933.9,267.1 L 933.9,269.1 L 933.8,271.1 L 933.7,273.0 L 933.7,274.8 L 933.6,276.5 L 933.6,278.4 L 933.6,280.5 L 933.7,282.6 L 933.8,284.3 L 933.8,285.9 L 933.8,287.8 L 933.8,289.9 L 933.8,291.8 L 933.9,293.6 L 933.9,295.5 L 933.9,297.4 L 934.0,299.2 L 934.1,301.1 L 934.1,303.0 L 934.2,304.9 L 934.3,306.7 L 934.4,308.6 L 934.5,310.5 L 934.6,312.4 L 934.8,314.2 L 934.9,316.1 L 935.0,318.0 L 935.2,319.9 L 935.3,321.7 L 935.5,323.6 L 935.6,325.5 L 935.8,327.5 L 936.0,329.4 L 936.1,331.1 L 936.3,333.0 L 936.4,335.0 L 936.5,336.9 L 936.6,338.8 L 936.8,340.6 L 936.9,342.5 L 937.1,344.4 L 937.2,346.2 L 937.4,348.1 L 937.5,350.0 L 937.7,351.9 L 937.9,353.8 L 938.0,355.6 L 938.2,357.3 L 938.4,359.2 L 938.6,361.3 L 938.9,363.2 L 939.2,364.9 L 939.4,366.7 L 939.6,368.6 L 939.8,370.6 L 940.0,372.5 L 940.2,374.4 L 940.4,376.2 L 940.6,378.1 L 940.8,380.0 L 941.1,382.0 L 941.3,383.9 L 941.5,385.8 L 941.6,387.7 L 941.8,389.7 L 941.9,391.7 L 942.1,393.5 L 942.2,395.4 L 942.4,397.4 L 942.6,399.4 L 942.7,401.3 L 942.8,403.4 L 942.9,405.5 L 943.0,407.6 L 943.0,409.7 L 943.0,411.7 L 943.0,413.9 L 943.0,416.0 L 943.1,416.9 L 946.2,417.4 L 946.8,416.0 L 947.5,413.9 L 948.1,411.7 L 948.6,409.7 L 949.2,407.5 L 949.6,405.4 L 950.1,403.2 L 950.5,401.1 L 950.8,399.0 L 951.1,397.0 L 951.4,395.1 L 951.7,393.3 L 952.1,391.4 L 952.4,389.5 L 952.7,387.5 L 953.0,385.4 L 953.2,383.3 L 953.4,381.3 L 953.6,379.4 L 953.9,377.5 L 954.1,375.6 L 954.3,373.8 L 954.5,371.9 L 954.7,370.0 L 954.9,368.1 L 955.1,366.0 L 955.3,363.8 L 955.4,361.8 L 955.5,360.0 L 955.6,358.5 L 955.8,356.8 L 956.0,355.0 L 956.2,353.1 L 956.4,351.3 L 956.5,349.4 L 956.7,347.5 L 956.8,345.6 L 957.0,343.8 L 957.1,341.9 L 957.3,340.0 L 957.4,338.1 L 957.6,336.3 L 957.7,334.4 L 957.8,332.4 L 957.9,330.2 L 958.0,328.0 L 958.0,325.9 L 958.0,323.9 L 958.0,322.1 L 958.0,320.2 L 958.0,318.3 L 958.0,316.4 L 958.0,314.6 L 958.0,312.7 L 957.9,310.8 L 957.9,308.9 L 957.8,307.1 L 957.8,305.2 L 957.7,303.3 L 957.7,301.4 L 957.6,299.6 L 957.5,297.7 L 957.4,295.8 L 957.3,293.9 L 957.2,292.1 L 957.1,290.2 L 956.9,288.3 L 956.8,286.3 L 956.6,284.2 L 956.4,281.9 L 956.2,279.9 L 955.9,278.4 L 955.8,276.9 L 955.6,275.2 L 955.4,273.4 L 955.2,271.5 L 955.0,269.6 L 954.8,267.7 L 954.6,265.7 L 954.3,263.5 L 954.0,261.3 L 953.7,259.5 L 953.4,258.0 L 953.1,256.4 L 952.9,254.7 L 952.6,252.8 L 952.4,250.8 L 952.1,248.6 L 951.7,246.4 L 951.3,244.4 L 951.0,242.5 L 950.6,240.8 L 950.2,239.2 L 949.9,237.6 L 949.6,235.8 L 949.3,233.7 L 948.9,231.6 L 948.5,229.6 L 948.1,227.7 L 947.7,225.8 L 947.3,224.0 L 946.9,222.1 L 946.5,220.3 L 946.1,218.4 L 945.6,216.6 L 945.2,214.7 L 944.8,212.9 L 944.4,211.0 L 943.9,209.2 L 943.5,207.2 L 943.0,205.2 L 942.6,203.5 L 942.2,202.3 L 941.7,201.6 L 941.4,201.3 Z" fill="url(#arGlow1)" />
              <path d="M 939.5,192.5 L 941.4,187.5 L 942.0,180.4 L 941.3,172.5 L 939.6,165.2 L 937.0,159.8 L 935.8,158.7 L 934.4,158.8 L 932.4,163.8 L 931.9,170.9 L 932.5,178.8 L 934.3,186.1 L 936.8,191.5 L 938.0,192.6 Z" fill="url(#arGlow1)" />
              <path d="M 487.1,230.2 L 485.0,230.4 L 481.9,231.0 L 477.6,232.0 L 471.8,233.5 L 465.5,235.2 L 459.6,236.8 L 453.7,238.4 L 447.8,239.9 L 442.0,241.5 L 436.1,243.1 L 430.2,244.7 L 424.3,246.3 L 418.5,247.9 L 412.6,249.5 L 406.8,251.1 L 400.9,252.7 L 395.1,254.3 L 389.3,255.9 L 383.6,257.5 L 377.7,259.2 L 371.8,260.8 L 366.0,262.4 L 360.2,264.1 L 354.5,265.7 L 348.7,267.4 L 342.9,269.0 L 337.5,270.7 L 332.5,272.3 L 327.9,273.9 L 323.7,275.6 L 320.2,277.2 L 317.4,278.6 L 314.2,280.8 L 313.0,281.7 L 329.4,284.3 L 330.2,294.0 L 330.8,293.0 L 332.0,289.4 L 332.0,289.6 L 313.1,294.7 L 310.9,291.6 L 311.2,292.7 L 311.7,294.5 L 312.2,296.2 L 312.9,298.3 L 313.5,300.3 L 314.1,302.2 L 314.7,304.0 L 315.3,305.9 L 315.9,307.7 L 316.6,309.7 L 317.3,311.6 L 317.9,313.5 L 318.4,315.0 L 319.0,316.8 L 319.7,318.9 L 320.4,320.9 L 321.1,322.7 L 321.8,324.4 L 322.5,326.2 L 323.2,328.1 L 324.0,329.9 L 324.7,331.7 L 325.5,333.6 L 326.2,335.4 L 327.0,337.2 L 327.7,339.0 L 328.5,340.8 L 329.3,342.7 L 330.1,344.6 L 330.9,346.5 L 331.7,348.2 L 332.4,350.0 L 333.2,351.9 L 333.9,353.8 L 334.7,355.6 L 335.4,357.4 L 336.2,359.2 L 336.9,361.0 L 337.7,362.8 L 338.4,364.7 L 339.2,366.5 L 340.0,368.3 L 340.8,370.1 L 341.5,371.9 L 342.3,373.6 L 343.0,375.3 L 343.8,377.1 L 344.7,379.0 L 345.6,380.8 L 346.4,382.5 L 347.2,384.3 L 348.1,386.3 L 348.9,388.1 L 349.7,389.9 L 350.5,391.7 L 351.4,393.6 L 352.4,395.5 L 352.9,396.3 L 355.9,395.4 L 355.9,393.8 L 355.7,391.7 L 355.5,389.6 L 355.3,387.7 L 355.0,385.7 L 354.8,383.7 L 354.5,381.6 L 354.2,379.5 L 353.8,377.4 L 353.5,375.4 L 353.2,373.7 L 353.0,371.9 L 352.8,370.1 L 352.5,368.1 L 352.3,366.2 L 352.0,364.2 L 351.7,362.3 L 351.4,360.4 L 351.1,358.4 L 350.8,356.5 L 350.5,354.5 L 350.2,352.6 L 349.9,350.7 L 349.6,348.7 L 349.3,346.8 L 348.9,344.6 L 348.5,342.4 L 348.0,340.2 L 347.6,338.2 L 347.1,336.3 L 346.7,334.3 L 346.3,332.4 L 345.9,330.5 L 345.4,328.6 L 345.0,326.6 L 344.5,324.7 L 344.1,322.8 L 343.6,320.9 L 343.1,318.9 L 342.6,317.0 L 342.1,315.2 L 341.7,313.5 L 341.3,311.9 L 340.9,310.2 L 340.4,308.1 L 339.8,305.9 L 339.2,303.8 L 338.6,301.9 L 338.1,300.0 L 337.5,298.1 L 336.9,296.2 L 336.3,294.4 L 335.7,292.5 L 335.1,290.6 L 334.4,288.5 L 333.6,286.4 L 333.0,284.7 L 332.0,282.3 L 317.7,275.5 L 309.7,285.1 L 309.7,285.7 L 310.4,283.9 L 309.8,285.5 L 322.8,300.5 L 328.2,297.1 L 328.4,296.7 L 328.9,296.3 L 330.9,294.9 L 333.5,293.5 L 336.8,291.8 L 340.9,290.0 L 345.6,288.1 L 350.7,286.2 L 356.3,284.2 L 361.9,282.1 L 367.5,280.0 L 373.2,277.9 L 378.8,275.8 L 384.5,273.7 L 390.2,271.5 L 395.9,269.4 L 401.5,267.3 L 407.1,265.1 L 412.8,263.0 L 418.4,260.8 L 424.1,258.7 L 429.8,256.5 L 435.5,254.4 L 441.2,252.2 L 446.9,250.1 L 452.5,247.9 L 458.2,245.7 L 463.9,243.5 L 469.7,241.3 L 475.8,238.9 L 480.9,236.9 L 484.6,235.3 L 487.1,234.1 L 488.2,233.3 Z" fill="url(#arGlow2)" />
              <path d="M 496.4,229.6 L 501.7,230.5 L 508.8,229.5 L 516.3,227.2 L 523.1,224.0 L 527.9,220.4 L 528.7,219.0 L 528.3,217.6 L 523.0,216.7 L 515.9,217.7 L 508.4,220.0 L 501.6,223.2 L 496.8,226.8 L 496.0,228.3 Z" fill="url(#arGlow2)" />
              <path d="M 951.1,994.6 L 952.4,992.6 L 953.7,989.7 L 955.0,987.0 L 956.2,984.4 L 957.6,981.8 L 958.9,979.1 L 960.3,976.5 L 961.6,973.8 L 963.1,971.1 L 964.5,968.3 L 965.9,965.5 L 967.3,962.6 L 968.7,959.7 L 970.1,956.7 L 971.5,953.6 L 972.9,950.5 L 974.2,947.3 L 975.5,944.1 L 976.9,940.7 L 978.2,937.3 L 979.4,933.9 L 980.7,930.3 L 982.0,926.7 L 983.3,923.1 L 984.6,919.3 L 985.9,915.5 L 987.2,911.7 L 988.6,907.8 L 989.9,903.8 L 991.4,899.6 L 993.0,895.0 L 993.9,891.5 L 991.9,890.5 L 989.6,893.2 L 986.8,897.2 L 984.1,900.9 L 981.7,904.3 L 979.3,907.7 L 977.0,911.1 L 974.7,914.4 L 972.5,917.7 L 970.4,921.1 L 968.3,924.4 L 966.3,927.7 L 964.5,931.0 L 962.7,934.2 L 961.0,937.5 L 959.4,940.8 L 957.9,944.1 L 956.6,947.4 L 955.3,950.6 L 954.2,953.9 L 953.2,957.2 L 952.3,960.4 L 951.5,963.6 L 950.8,966.8 L 950.2,970.0 L 949.7,973.2 L 949.3,976.3 L 949.0,979.4 L 948.8,982.4 L 948.7,985.4 L 948.6,988.6 L 948.6,991.8 L 948.9,994.2 Z" fill="url(#arGlow3)" />
              <path d="M 948.3,1000.2 L 946.0,1002.8 L 944.2,1007.1 L 943.1,1012.1 L 942.7,1016.9 L 943.3,1020.8 L 943.8,1021.7 L 944.8,1021.9 L 947.1,1019.2 L 948.9,1015.0 L 950.0,1010.0 L 950.4,1005.2 L 949.8,1001.3 L 949.3,1000.4 Z" fill="url(#arGlow3)" />
            </svg>
          )}
        </div>

        {/* Mobile-only: the centered INK watermark above was, on its own,
            reported as "a mark sitting alone in empty space" once the 3
            content blocks collapse into a single narrow stack with real
            vertical gaps between them (WHO WE ARE / WHAT WE MAKE / WHAT WE
            BELIEVE). Rather than enlarging the watermark itself (which the
            task explicitly rules out), a few small, edge-anchored pieces of
            the Work page's own decorative vocabulary (BG_SHAPES/BG_COLOR,
            the sitewide .ink-blob pastel-form technique) are distributed
            through those same gaps — restrained and lower in count/opacity
            than either Home's or Work's own decoration, since Studio is
            meant to read calmer than both. Same zIndex-0 layer as the
            watermark, so it never competes with the copy above it. */}
        {narrow ? (
          <div aria-hidden="true" style={{ position: "absolute", inset: 0, zIndex: 0, overflow: "hidden", pointerEvents: "none" }}>
            {[
              { size: 140, left: "-9%", top: "1%", color: "var(--cobalt-600)", op: 0.045 },
              { size: 110, right: "-8%", top: "20%", color: "var(--vermilion-500)", op: 0.04 },
              { size: 125, left: "-8%", top: "40%", color: "var(--saffron-500)", op: 0.04 },
              { size: 115, right: "-9%", top: "60%", color: "var(--pine-500)", op: 0.04 },
              { size: 130, left: "-9%", top: "80%", color: "var(--cobalt-600)", op: 0.04 },
              { size: 120, right: "-8%", bottom: "1%", color: "var(--saffron-500)", op: 0.04 },
            ].map((b, i) => (
              <div key={`b${i}`} className="ink-blob" style={{ position: "absolute", width: b.size, height: b.size, left: b.left, right: b.right, top: b.top, bottom: b.bottom, background: b.color, opacity: b.op, animation: reduced ? "none" : `ink-blob-morph 9s var(--ease-standard) infinite, ink-drift ${16 + i * 3}s var(--ease-standard) infinite ${-i * 4}s` }} />
            ))}
            {[
              { type: "cube", top: "6%", side: "right", dx: 6, size: 28 },
              { type: "tag", top: "16%", side: "left", dx: 4, size: 22 },
              { type: "camera", top: "26%", side: "right", dx: 6, size: 28 },
              { type: "notebook", top: "36%", side: "left", dx: 6, size: 26 },
              { type: "mCircle", top: "46%", side: "right", dx: 8, size: 22 },
              { type: "tote", top: "56%", side: "left", dx: 4, size: 28 },
              { type: "printer", top: "66%", side: "right", dx: 6, size: 26 },
              { type: "sticker", top: "76%", side: "left", dx: 8, size: 24 },
              { type: "geo", top: "86%", side: "right", dx: 6, size: 24 },
              { type: "mic", top: "95%", side: "left", dx: 6, size: 24 },
            ].map((m, i) => {
              const Shape = (window.BG_SHAPES || {})[m.type];
              if (!Shape) return null;
              return (
                <div key={`i${i}`} style={{ position: "absolute", width: m.size, height: m.size, top: m.top, [m.side]: m.dx, opacity: 0.14 }}>
                  <Shape color={(window.BG_COLOR || {})[m.type] || "var(--ink-400)"} />
                </div>
              );
            })}
          </div>
        ) : null}

        <div style={{ position: "relative", zIndex: 1, maxWidth: "var(--container-max)", margin: "0 auto" }}>
          <Reveal><Kicker>{copy.eyebrow}</Kicker></Reveal>
          <Reveal delay={90} as="h1" style={{
            fontFamily: ar ? "var(--font-arabic-display)" : "var(--font-display)",
            fontWeight: ar ? 800 : "var(--weight-display)",
            fontStretch: ar ? "normal" : "var(--stretch-display)",
            fontSize: ar ? (narrow ? 34 : 56) : "clamp(40px, 6.2vw, 84px)",
            lineHeight: ar ? 1.2 : "var(--leading-display)",
            letterSpacing: ar ? 0 : "var(--tracking-display)",
            textTransform: ar ? "none" : "uppercase",
            marginTop: "var(--space-6)",
          }}>
            {copy.heading.map((line, i) => (
              <React.Fragment key={i}>{line}{i < copy.heading.length - 1 ? <br /> : null}</React.Fragment>
            ))}
          </Reveal>

          {/* All three sections as one horizontal editorial row on desktop —
              a genuine equal three-column grid (minmax(0,1fr) rather than a
              bare 1fr, so a track can never be pushed wider than its true
              third by its own content's min-content size), never a card,
              just a hairline tick + numeral + copy. The middle column's
              list is much shorter than 01/03's paragraphs, so left-aligned
              it visually hugs column 01 while leaving a huge gap before 03
              even though the grid tracks themselves are exactly even —
              centering just that one grid item (justifySelf, not a text or
              font change) shrinks it to its own content width and centers
              THAT within its true-center track, closing the illusion.
              Collapses to a plain numeric-order stack on mobile/tablet,
              where centering a single column would look inconsistent next
              to the other two, so it's skipped there. */}
          <div style={{
            display: "grid",
            gridTemplateColumns: narrow ? "1fr" : "repeat(3, minmax(0, 1fr))",
            columnGap: "var(--space-8)",
            rowGap: narrow ? "var(--space-8)" : 0,
            marginTop: narrow ? "var(--space-8)" : "var(--space-9)",
            alignItems: "start",
          }}>
            {copy.blocks.map((b, i) => (
              <Reveal key={b.n} delay={160 + i * 90} style={!narrow && i === 1 ? { justifySelf: "center" } : undefined}>
                <Rule weight="thin" tone="hairline" style={{ width: 56 }} />
                <div style={{ display: "flex", alignItems: "baseline", gap: "var(--space-4)", marginTop: "var(--space-4)" }}>
                  <span className="ink-label" style={{ color: "var(--vermilion-500)" }}>{b.n}</span>
                  <span className="ink-label" style={{ color: "var(--text-body)" }}>{b.title}</span>
                </div>
                {b.list ? (
                  <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)", marginTop: "var(--space-5)" }}>
                    {b.list.map((w) => (
                      <span key={w} style={{ fontSize: "var(--size-body-lg)", lineHeight: ar ? "var(--leading-arabic)" : "var(--leading-body)", fontFamily: ar ? "var(--font-arabic)" : "var(--font-body)" }}>{w}</span>
                    ))}
                  </div>
                ) : (
                  b.body.map((p, pi) => (
                    <p key={pi} style={{ fontSize: "var(--size-body-lg)", lineHeight: ar ? "var(--leading-arabic)" : "var(--leading-body)", marginTop: pi === 0 ? "var(--space-5)" : "var(--space-4)", fontFamily: ar ? "var(--font-arabic)" : "var(--font-body)" }}>
                      {p}
                    </p>
                  ))
                )}
              </Reveal>
            ))}
          </div>

          <Reveal delay={460} style={{ marginTop: narrow ? "var(--space-7)" : "var(--space-7)", display: "flex", alignItems: "center", gap: "var(--space-3)" }}>
            <Rule weight="thin" tone="hairline" style={{ width: 32 }} />
            <span className="ink-label" style={{ color: "var(--text-muted)", letterSpacing: "var(--tracking-wide)" }}>{copy.process}</span>
          </Reveal>
        </div>
      </section>
    </main>
  );
}

Object.assign(window, { Contact, Studio });
