nextui.dev
Home
Docs
Saved
Pricing
Components37
  • Forms & Inputs7
  • Cards13
  • Buttons, Badges & Avatars8
  • Feedback & Display6
  • Effects3
Agents46
  • Input & Context9
  • Thinking & Status6
  • Tools & Execution7
  • Human-in-the-Loop & Safety7
  • Multi-Agent & Workflows4
  • Output & Artifacts8
  • Voice & Memory5
Blocks20
  • Agent chatNew
  • Pill heroNew
  • Grid hero
  • Moodboard generatorNew
  • Waitlist heroNew
  • Intro bannerNew
  • Creative home heroNew
  • Chat heroNew
  • App shelfNew
  • Usage card
  • Design resultsNew
  • Pricing tiers
  • Dashboard home
  • The shiftNew
  • Launch perksNew
  • Testimonials
  • Quote
  • Team
  • Leads tableNew
  • Sign in
Pages16
  • ASCII workspace$49
  • Research agent$49
  • Support agent$49
  • Coding agent$49
  • CRM overviewNew
  • Marketing dashboardNew
  • Developer portfolio$49
  • Particle landing$49
  • Particle story$49
  • Statement converter$49
  • AI photo studio$49
  • Group card launch$49
  • Month-end storyNew
  • Task HubNew
  • Global MediaNew
  • Hexora ecosystem$49
Themes9
  • Default
  • Ember
  • Ocean
  • Forest
  • Grape
  • Rose
  • Amber
  • Clay
  • Brutal
UpgradeDark modeLight modeFeedbackGitHub
nextui.dev

nextui components, blocks and themes you install with one command.

Product

  • Components
  • Blocks
  • Themes
  • Pricing

Resources

  • Docs
  • Roadmap
  • GitHub

Legal

  • License
  • Privacy
  • Terms
  • Refunds

nextUI

[ 200 OK ] · nextui.dev

© 2026 nextui.dev • All rights reserved.

UpgradeDark modeLight modeFeedbackGitHub
PagesGlobal Media
Copy promptCopy prompt

Global MediaNew

Dark editorial landing page for a global media group: a draggable 3D dotted globe that turns to each bureau, kinetic condensed headlines, a divisions index, a live bureau map with local clocks, a campaign reach planner and a newsroom.

Installation

Set your access key from API keys as NEXTUI_TOKEN in .env.local and add the @nextui namespace below to components.json once. Then every item installs by name. Each install counts as a copy on a free account.

$pnpm dlx shadcn@latest add @nextui/global-media
1{
2 "registries": {
3 "@nextui": {
4 "url": "https://nextui.dev/r/{name}.json",
5 "headers": { "Authorization": "Bearer ${NEXTUI_TOKEN}" }
6 }
7 }
8}

Related

All pages
$49

Particle landingNew

$49

Particle storyNew

Free template
components/pages/global-media.tsx
"use client";

import {
  useEffect,
  useMemo,
  useRef,
  useState,
  useSyncExternalStore,
  type FormEvent,
  type KeyboardEvent as ReactKeyboardEvent,
  type ReactNode,
} from "react";
import { AnimatePresence, MotionConfig, motion, useInView } from "motion/react";
import {
  ArrowLeft,
  ArrowRight,
  ArrowUpRight,
  Check,
  Command,
  CornerDownLeft,
  Download,
  Mail,
  Play,
  Radio,
  Search,
  X,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { LAND_H, LAND_MASK, LAND_W } from "./world-land";

/* ─── Fonts & scoped styles ─────────────────────────────────────────────────── */

const EASE = [0.22, 1, 0.36, 1] as const;

function PageStyles() {
  return (
    <>
      {/* The page is a self-contained registry item, so it brings its own fonts. */}
      {/* eslint-disable-next-line @next/next/no-page-custom-font */}
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Antonio:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Playfair+Display:ital,wght@0,600;0,700;1,600&display=swap"
        precedence="default"
      />
      <style
        dangerouslySetInnerHTML={{
          __html: `
.gm-root{--gm-bg:#0E0E12;--gm-ink:#F4F5FA;--gm-blue:#2F54FF;--gm-blue-soft:#8EA2FF;font-family:'Inter',ui-sans-serif,system-ui,sans-serif;background:var(--gm-bg);color:var(--gm-ink);-webkit-font-smoothing:antialiased}
.gm-display{font-family:'Antonio','Oswald','Bebas Neue',Impact,sans-serif;letter-spacing:-0.01em}
.gm-serif{font-family:'Playfair Display',Georgia,serif}
.gm-mono{font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,monospace}
.gm-grain{background-image:url("data:image/svg+xml;utf8,<svg xmlns=%27http://www.w3.org/2000/svg%27 width=%27160%27 height=%27160%27><filter id=%27n%27><feTurbulence type=%27fractalNoise%27 baseFrequency=%270.9%27 numOctaves=%272%27 stitchTiles=%27stitch%27/></filter><rect width=%27100%25%27 height=%27100%25%27 filter=%27url(%23n)%27/></svg>")}
.gm-link{position:relative}
.gm-link::after{content:"";position:absolute;left:0;bottom:-3px;height:1px;width:100%;background:currentColor;transform:scaleX(0);transform-origin:right;transition:transform .45s cubic-bezier(.22,1,.36,1)}
.gm-link:hover::after,.gm-link:focus-visible::after{transform:scaleX(1);transform-origin:left}
@keyframes gm-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.gm-marquee{animation:gm-marquee 48s linear infinite}
.gm-marquee-fast{animation:gm-marquee 70s linear infinite}
.gm-marquee-host:hover .gm-marquee,.gm-marquee-host:hover .gm-marquee-fast{animation-play-state:paused}
@keyframes gm-dash{to{stroke-dashoffset:-24}}
.gm-arc{stroke-dasharray:3 3;animation:gm-dash 1.6s linear infinite}
@keyframes gm-ping{0%{transform:scale(1);opacity:.7}100%{transform:scale(3.2);opacity:0}}
.gm-ping{transform-box:fill-box;transform-origin:center;animation:gm-ping 2.2s cubic-bezier(0,0,.2,1) infinite}
@media (prefers-reduced-motion:reduce){.gm-marquee,.gm-marquee-fast,.gm-arc,.gm-ping{animation:none}}
`,
        }}
      />
    </>
  );
}

/* ─── Data ──────────────────────────────────────────────────────────────────── */

const deg = (d: number) => (d * Math.PI) / 180;

interface Bureau {
  id: string;
  city: string;
  country: string;
  lat: number;
  lon: number;
  utc: number;
  staff: number;
  role: string;
  langs: string;
  beats: string[];
}

// 12 bureaus, 200 journalists in total: the numbers on the page all add up.
const BUREAUS: Bureau[] = [
  { id: "dxb", city: "Dubai", country: "UAE", lat: 25.2, lon: 55.27, utc: 4, staff: 64, role: "Headquarters & flagship studio", langs: "EN · AR", beats: ["Business", "Luxury", "Live events"] },
  { id: "ruh", city: "Riyadh", country: "Saudi Arabia", lat: 24.71, lon: 46.68, utc: 3, staff: 22, role: "Gulf policy & sport desk", langs: "EN · AR", beats: ["Policy", "Sport", "Giga-projects"] },
  { id: "cai", city: "Cairo", country: "Egypt", lat: 30.04, lon: 31.24, utc: 2, staff: 18, role: "Culture & Arabic video hub", langs: "AR · EN", beats: ["Film", "Music", "Culture"] },
  { id: "lon", city: "London", country: "United Kingdom", lat: 51.51, lon: -0.13, utc: 0, staff: 20, role: "European markets desk", langs: "EN", beats: ["Markets", "Fashion", "Tech"] },
  { id: "par", city: "Paris", country: "France", lat: 48.86, lon: 2.35, utc: 1, staff: 9, role: "Fashion & luxury houses", langs: "FR · EN", beats: ["Couture", "Beauty", "Design"] },
  { id: "bom", city: "Mumbai", country: "India", lat: 19.08, lon: 72.88, utc: 5.5, staff: 16, role: "South Asia startup desk", langs: "EN · HI", beats: ["Startups", "Cinema", "Retail"] },
  { id: "nbo", city: "Nairobi", country: "Kenya", lat: -1.29, lon: 36.82, utc: 3, staff: 8, role: "East Africa correspondents", langs: "EN · SW", beats: ["Fintech", "Climate"] },
  { id: "los", city: "Lagos", country: "Nigeria", lat: 6.52, lon: 3.38, utc: 1, staff: 9, role: "West Africa culture desk", langs: "EN", beats: ["Music", "Fintech"] },
  { id: "sin", city: "Singapore", country: "Singapore", lat: 1.35, lon: 103.82, utc: 8, staff: 11, role: "Asia-Pacific markets", langs: "EN · ZH", beats: ["Markets", "Travel"] },
  { id: "hkg", city: "Hong Kong", country: "China", lat: 22.32, lon: 114.17, utc: 8, staff: 7, role: "Luxury retail & finance", langs: "EN · ZH", beats: ["Retail", "Finance"] },
  { id: "nyc", city: "New York", country: "United States", lat: 40.71, lon: -74.0, utc: -5, staff: 12, role: "Americas markets & media", langs: "EN", beats: ["Media", "Markets"] },
  { id: "gru", city: "São Paulo", country: "Brazil", lat: -23.55, lon: -46.63, utc: -3, staff: 4, role: "LatAm sport & design", langs: "PT · EN", beats: ["Football", "Design"] },
];

const TOTAL_STAFF = BUREAUS.reduce((s, b) => s + b.staff, 0);

interface HeroSlide {
  id: string;
  eyebrow: string;
  lines: [string, string, string, string];
  side: [string, string];
  stats: { value: number; decimals: number; suffix?: string; label: string }[];
  focus: string; // bureau id the globe turns to
}

const HERO_SLIDES: HeroSlide[] = [
  {
    id: "01",
    eyebrow: "Audience",
    lines: ["LARGEST", "AUDIENCE.", "DEEPEST", "ENGAGEMENT."],
    side: ["The region's largest independent media group", "Headquartered in Dubai since 1994"],
    stats: [
      { value: 104.6, decimals: 1, suffix: "M", label: "monthly audience" },
      { value: 84, decimals: 0, label: "titles and channels" },
    ],
    focus: "dxb",
  },
  {
    id: "02",
    eyebrow: "Newsroom",
    lines: ["TWELVE", "BUREAUS.", "ONE", "NEWSROOM."],
    side: ["Reporting from four continents", "around the clock, in five languages"],
    stats: [
      { value: TOTAL_STAFF, decimals: 0, label: "journalists" },
      { value: BUREAUS.length, decimals: 0, label: "bureaus worldwide" },
    ],
    focus: "lon",
  },
  {
    id: "03",
    eyebrow: "Advertising",
    lines: ["STORIES", "THAT MOVE", "MARKETS", "& MINDS."],
    side: ["Print, digital, video and live events", "sold as one audience"],
    stats: [
      { value: 12.4, decimals: 1, suffix: "B", label: "annual impressions" },
      { value: 4.8, decimals: 1, suffix: "x", label: "dwell time vs. benchmark" },
    ],
    focus: "bom",
  },
];

interface Division {
  code: string;
  name: string;
  short: string;
  blurb: string;
  titles: string[];
  reach: number; // millions, monthly
  growth: number; // % YoY
  cpm: number; // USD
  engagement: number; // % of impressions
  mix: { print: number; web: number; social: number; video: number; events: number };
  audience: { core: string; corePct: number; affluent: number };
}

const DIVISIONS: Division[] = [
  {
    code: "01",
    name: "Luxury & Lifestyle",
    short: "Luxury",
    blurb: "Glossy monthlies and video franchises read by the region's highest-spending households.",
    titles: ["Gilded", "Maison Arabia", "The Collector"],
    reach: 38.4,
    growth: 24.8,
    cpm: 24,
    engagement: 2.4,
    mix: { print: 18, web: 28, social: 34, video: 14, events: 6 },
    audience: { core: "25–44", corePct: 61, affluent: 48 },
  },
  {
    code: "02",
    name: "Culture & Entertainment",
    short: "Culture",
    blurb: "City guides, film and music coverage, and the weekend lists everyone forwards.",
    titles: ["Nocturne", "Weekend City", "Marquee"],
    reach: 44.2,
    growth: 31.2,
    cpm: 14,
    engagement: 3.1,
    mix: { print: 6, web: 34, social: 38, video: 18, events: 4 },
    audience: { core: "18–34", corePct: 66, affluent: 29 },
  },
  {
    code: "03",
    name: "Business & Technology",
    short: "Business",
    blurb: "Market-moving reporting, founder interviews and the daily briefing for decision makers.",
    titles: ["Signal Weekly", "Byte Desk", "Founders Gulf"],
    reach: 22.0,
    growth: 19.5,
    cpm: 21,
    engagement: 1.9,
    mix: { print: 12, web: 46, social: 20, video: 14, events: 8 },
    audience: { core: "30–54", corePct: 58, affluent: 54 },
  },
  {
    code: "04",
    name: "Travel & Hospitality",
    short: "Travel",
    blurb: "Destination journalism and trade titles for the people who book, build and run hotels.",
    titles: ["Arcadia Travel", "Field & Frame", "Concierge"],
    reach: 12.6,
    growth: 14.1,
    cpm: 17,
    engagement: 2.2,
    mix: { print: 16, web: 36, social: 30, video: 12, events: 6 },
    audience: { core: "28–49", corePct: 57, affluent: 41 },
  },
  {
    code: "05",
    name: "Automotive & Sport",
    short: "Sport",
    blurb: "Road tests, race weekends and matchday coverage with a fiercely loyal audience.",
    titles: ["Torque Arabia", "Matchday", "Paddock"],
    reach: 9.8,
    growth: 22.3,
    cpm: 16,
    engagement: 2.6,
    mix: { print: 8, web: 30, social: 36, video: 22, events: 4 },
    audience: { core: "21–44", corePct: 63, affluent: 33 },
  },
  {
    code: "06",
    name: "Events & Live",
    short: "Events",
    blurb: "Summits, award nights and streamed conversations that bring each title to life.",
    titles: ["Meridian Summit", "The Gilded Awards", "Founders Night"],
    reach: 3.4,
    growth: 38.6,
    cpm: 38,
    engagement: 6.5,
    mix: { print: 0, web: 18, social: 30, video: 22, events: 30 },
    audience: { core: "30–54", corePct: 52, affluent: 62 },
  },
];

const MIX_KEYS = ["print", "web", "social", "video", "events"] as const;
const MIX_LABEL: Record<(typeof MIX_KEYS)[number], string> = {
  print: "Print",
  web: "Web",
  social: "Social",
  video: "Video",
  events: "Events",
};
const MIX_COLOR: Record<(typeof MIX_KEYS)[number], string> = {
  print: "#E8EBF7",
  web: "#2F54FF",
  social: "#8EA2FF",
  video: "#5B3FD9",
  events: "#F2B544",
};

// Each title gets its own wordmark style so the strip reads like a newsstand.
const MASTHEADS: { name: string; style: string }[] = [
  { name: "Gilded", style: "gm-serif italic text-[30px] tracking-tight" },
  { name: "NOCTURNE", style: "gm-display text-[30px] tracking-[0.12em]" },
  { name: "Signal Weekly", style: "font-semibold text-[22px] tracking-[-0.04em]" },
  { name: "Maison Arabia", style: "gm-serif text-[24px] uppercase tracking-[0.18em]" },
  { name: "byte/desk", style: "gm-mono text-[21px] font-medium" },
  { name: "ARCADIA", style: "gm-serif text-[26px] font-bold tracking-[0.3em]" },
  { name: "Marquee", style: "gm-display text-[32px] font-bold uppercase" },
  { name: "Torque", style: "font-bold italic text-[26px] uppercase tracking-[-0.03em]" },
  { name: "The Collector", style: "gm-serif italic text-[24px]" },
  { name: "Weekend City", style: "font-medium text-[22px] uppercase tracking-[0.2em]" },
  { name: "MATCHDAY", style: "gm-display text-[30px] tracking-[0.04em]" },
  { name: "Founders Gulf", style: "gm-serif text-[24px] font-bold" },
];

const LIVE_DESK = [
  { time: "09:42", bureau: "Dubai", text: "Flagship studio goes live with a 4K morning markets show" },
  { time: "09:18", bureau: "London", text: "European tech funding rebounds for a third straight quarter" },
  { time: "08:55", bureau: "Paris", text: "Couture week opens with three regional designers on the schedule" },
  { time: "08:31", bureau: "Riyadh", text: "Season tickets sell out in under an hour for the new stadium" },
  { time: "08:07", bureau: "Mumbai", text: "Streaming deal brings two regional films to 190 countries" },
  { time: "07:46", bureau: "Lagos", text: "Afrobeats festival announces its first Gulf edition" },
];

type DeskId = "business" | "luxury" | "culture" | "travel" | "sport";

const DESKS: { id: DeskId | "all"; label: string }[] = [
  { id: "all", label: "All desks" },
  { id: "business", label: "Business" },
  { id: "luxury", label: "Luxury" },
  { id: "culture", label: "Culture" },
  { id: "travel", label: "Travel" },
  { id: "sport", label: "Sport" },
];

const DESK_LABEL: Record<DeskId, string> = {
  business: "Business & Technology",
  luxury: "Luxury & Lifestyle",
  culture: "Culture & Entertainment",
  travel: "Travel & Hospitality",
  sport: "Automotive & Sport",
};

type ArtPattern = "dots" | "rings" | "lines" | "grid";

interface StoryArtSpec {
  from: string;
  to: string;
  glyph: string;
  accent: string;
  pattern: ArtPattern;
}

interface Story {
  id: string;
  desk: DeskId;
  bureau: string;
  title: string;
  dek: string;
  author: string;
  when: string;
  read: number; // minutes
  art: StoryArtSpec;
}

const STORIES: Story[] = [
  {
    id: "s1",
    desk: "business",
    bureau: "Dubai",
    title: "Inside the desert studio rewriting how the region watches the news",
    dek: "Twelve cameras, one control room and a newsroom that never closes. A week with the team behind the morning markets show.",
    author: "Layla Haddad",
    when: "2h ago",
    read: 8,
    art: { from: "#1B2BB8", to: "#0B0E2E", glyph: "DXB", accent: "#8EA2FF", pattern: "rings" },
  },
  {
    id: "s2",
    desk: "luxury",
    bureau: "Paris",
    title: "After hours in the ateliers dressing the next season",
    dek: "Three regional designers, one borrowed workroom and eleven days to the runway.",
    author: "Camille Roux",
    when: "4h ago",
    read: 6,
    art: { from: "#3A1F8F", to: "#120A2E", glyph: "PAR", accent: "#F2B544", pattern: "lines" },
  },
  {
    id: "s3",
    desk: "culture",
    bureau: "Lagos",
    title: "The sound of Lagos is everywhere. Meet the people building its pipes",
    dek: "Studios, distributors and the playlist editors who turned a city scene into a global export.",
    author: "Tobi Adeyemi",
    when: "5h ago",
    read: 5,
    art: { from: "#0F4C81", to: "#08162B", glyph: "LOS", accent: "#7FD3FF", pattern: "dots" },
  },
  {
    id: "s4",
    desk: "travel",
    bureau: "Riyadh",
    title: "The 72-hour route: how Riyadh became a weekend city",
    dek: "Desert dinners, new museums and a late-night food scene. A long weekend, hour by hour.",
    author: "Omar Al-Rashid",
    when: "Yesterday",
    read: 4,
    art: { from: "#5B3FD9", to: "#16113A", glyph: "RUH", accent: "#E8EBF7", pattern: "grid" },
  },
  {
    id: "s5",
    desk: "business",
    bureau: "Mumbai",
    title: "The founders betting that the next big consumer app is built in Pune",
    dek: "Cheaper talent, faster payments and a generation of operators who came home.",
    author: "Ananya Iyer",
    when: "Yesterday",
    read: 7,
    art: { from: "#0E5A6B", to: "#061A20", glyph: "BOM", accent: "#7FE0D0", pattern: "lines" },
  },
  {
    id: "s6",
    desk: "sport",
    bureau: "São Paulo",
    title: "Why Gulf clubs are scouting Brazilian teenagers before they turn pro",
    dek: "Academies, agents and the data models deciding who gets a ticket out.",
    author: "Rafael Costa",
    when: "Yesterday",
    read: 6,
    art: { from: "#1F6F3F", to: "#08190F", glyph: "GRU", accent: "#B7F27A", pattern: "grid" },
  },
  {
    id: "s7",
    desk: "luxury",
    bureau: "Hong Kong",
    title: "Quiet money: the collectors buying watches they will never wear",
    dek: "Inside the auction rooms where a steel sports watch now outsells a Monet sketch.",
    author: "Grace Lam",
    when: "2 days ago",
    read: 9,
    art: { from: "#6B2A1F", to: "#1E0B08", glyph: "HKG", accent: "#F2B544", pattern: "rings" },
  },
  {
    id: "s8",
    desk: "culture",
    bureau: "Cairo",
    title: "A century-old cinema reopens, and the queue goes round the block",
    dek: "Restored seats, a new projector and a programme built from the city's own film archive.",
    author: "Nour El-Sayed",
    when: "2 days ago",
    read: 5,
    art: { from: "#7A3C8F", to: "#1C0E24", glyph: "CAI", accent: "#FFB4E1", pattern: "dots" },
  },
  {
    id: "s9",
    desk: "travel",
    bureau: "Singapore",
    title: "The airport that wants you to miss your connection",
    dek: "Gardens, cinemas and a waterfall: how a layover became a destination.",
    author: "Wei Ling Tan",
    when: "3 days ago",
    read: 4,
    art: { from: "#1B5E8C", to: "#081A2A", glyph: "SIN", accent: "#9FE8FF", pattern: "rings" },
  },
  {
    id: "s10",
    desk: "sport",
    bureau: "Riyadh",
    title: "Sold out in 52 minutes: the season ticket rush, explained",
    dek: "A new stadium, a new league format and fans who no longer fly abroad for big games.",
    author: "Faisal Al-Otaibi",
    when: "3 days ago",
    read: 3,
    art: { from: "#2F54FF", to: "#0B1240", glyph: "RUH", accent: "#E8EBF7", pattern: "lines" },
  },
];

const MOST_READ = ["s7", "s1", "s5", "s10", "s3"];

const VIDEOS: { title: string; series: string; duration: string; art: StoryArtSpec }[] = [
  {
    title: "Twelve cameras, one control room: the 6am markets show",
    series: "Behind the desk",
    duration: "12:48",
    art: { from: "#1B2BB8", to: "#070A22", glyph: "LIVE", accent: "#8EA2FF", pattern: "grid" },
  },
  {
    title: "Couture week in four looks",
    series: "Gilded in Paris",
    duration: "06:15",
    art: { from: "#3A1F8F", to: "#0E0822", glyph: "LOOK", accent: "#F2B544", pattern: "lines" },
  },
  {
    title: "Matchday: 24 hours inside the new stadium",
    series: "Matchday Films",
    duration: "18:02",
    art: { from: "#1F6F3F", to: "#06140B", glyph: "90'", accent: "#B7F27A", pattern: "rings" },
  },
];

/* ─── Small helpers ─────────────────────────────────────────────────────────── */

function formatCompact(n: number) {
  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
  if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
  return n.toFixed(0);
}

/** Tweens from the last shown value to the new one, so live numbers roll instead of jumping. */
function RollingNumber({
  value,
  format,
  start = true,
  duration = 900,
}: {
  value: number;
  format: (n: number) => string;
  start?: boolean;
  duration?: number;
}) {
  const [display, setDisplay] = useState(0);
  const fromRef = useRef(0);

  useEffect(() => {
    if (!start) return;
    const from = fromRef.current;
    const t0 = performance.now();
    let raf = 0;
    const tick = (now: number) => {
      const p = Math.min((now - t0) / duration, 1);
      const v = from + (value - from) * (1 - Math.pow(1 - p, 4));
      fromRef.current = v;
      setDisplay(v);
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [value, start, duration]);

  return <>{format(display)}</>;
}

/** Minute-resolution clock that renders nothing on the server, so local times never mismatch on hydrate. */
const clockStore = {
  subscribe(cb: () => void) {
    const id = window.setInterval(cb, 15_000);
    return () => window.clearInterval(id);
  },
  snapshot() {
    return Math.floor(Date.now() / 60_000);
  },
  server() {
    return null;
  },
};

function useMinute() {
  return useSyncExternalStore(clockStore.subscribe, clockStore.snapshot, clockStore.server);
}

function localTime(minute: number | null, utc: number) {
  if (minute === null) return { label: "--:--", hour: 12 };
  const d = new Date(minute * 60_000 + utc * 3_600_000);
  const h = d.getUTCHours();
  const m = d.getUTCMinutes();
  return { label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, hour: h };
}

function SectionLabel({ index, children }: { index: string; children: string }) {
  return (
    <div className="gm-mono flex items-center gap-3 text-[11px] uppercase tracking-[0.22em] text-[var(--gm-blue-soft)]">
      <span className="text-white/40">{index}</span>
      <span className="h-px w-8 bg-white/20" />
      {children}
    </div>
  );
}

function Reveal({ children, className, delay = 0 }: { children: ReactNode; className?: string; delay?: number }) {
  return (
    <motion.div
      className={className}
      initial={{ opacity: 0, y: 28 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-10% 0px" }}
      transition={{ duration: 0.8, delay, ease: EASE }}
    >
      {children}
    </motion.div>
  );
}

/* ─── Hero globe ────────────────────────────────────────────────────────────── */

interface GlobeDot {
  wx: number;
  wy: number;
  wz: number;
  land: boolean;
}

/** Unit vector for a lat/lon, in the globe's own (unrotated) frame. */
function toWorld(lat: number, lon: number) {
  const c = Math.cos(lat);
  return { x: c * Math.sin(lon), y: Math.sin(lat), z: c * Math.cos(lon) };
}

/** Deterministic star positions, so the background never shuffles between renders. */
function buildStars(count: number) {
  let seed = 7;
  const rand = () => {
    seed = (seed * 16807) % 2147483647;
    return seed / 2147483647;
  };
  return Array.from({ length: count }, () => ({
    x: rand(),
    y: rand(),
    size: rand() > 0.85 ? 1.6 : 1,
    a: 0.25 + rand() * 0.55,
    speed: 0.6 + rand() * 1.6,
    phase: rand() * Math.PI * 2,
  }));
}

const GLOBE_SAMPLES = 32000;
const LIGHT = (() => {
  const v = [0.78, 0.42, 0.28];
  const len = Math.hypot(v[0], v[1], v[2]);
  return [v[0] / len, v[1] / len, v[2] / len];
})();

function decodeLandMask() {
  return Uint8Array.from(atob(LAND_MASK), (ch) => ch.charCodeAt(0));
}

function isLand(mask: Uint8Array, row: number, col: number) {
  const idx = row * LAND_W + col;
  return (mask[idx >> 3] & (1 << (idx & 7))) !== 0;
}

function buildGlobeDots(): GlobeDot[] {
  const mask = decodeLandMask();
  const golden = Math.PI * (3 - Math.sqrt(5));
  const dots: GlobeDot[] = [];
  for (let i = 0; i < GLOBE_SAMPLES; i++) {
    const lat = Math.asin(1 - (2 * (i + 0.5)) / GLOBE_SAMPLES);
    const lon = ((i * golden) % (Math.PI * 2)) - Math.PI;
    const row = Math.min(LAND_H - 1, Math.max(0, Math.floor(((Math.PI / 2 - lat) / Math.PI) * LAND_H)));
    const col = Math.min(LAND_W - 1, Math.floor(((((lon + Math.PI) / (Math.PI * 2)) % 1) + 1) % 1 * LAND_W));
    const land = isLand(mask, row, col);
    if (land || i % 9 === 0) {
      const w = toWorld(lat, lon);
      dots.push({ wx: w.x, wy: w.y, wz: w.z, land });
    }
  }
  return dots;
}

function MediaGlobe({ focus }: { focus: Bureau }) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const focusRef = useRef(focus);
  const rotRef = useRef({ lon: -deg(focus.lon) + 0.3, lat: deg(focus.lat) - 0.12, spin: 0 });
  const dragRef = useRef({ active: false, lastX: 0, lastY: 0, offLon: 0, offLat: 0 });

  useEffect(() => {
    focusRef.current = focus;
    rotRef.current.spin = 0;
    // Let the slide take the globe back from a drag.
    dragRef.current.offLon *= 0.2;
    dragRef.current.offLat = 0;
  }, [focus]);

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas?.getContext("2d");
    if (!canvas || !ctx) return;

    const dots = buildGlobeDots();
    const stars = buildStars(160);
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let raf = 0;
    let time = 0;
    let labelAlpha = 0;
    let lastFocus = focusRef.current.id;
    // Size is read by a ResizeObserver, not getBoundingClientRect() every frame (which forced a layout).
    let W = 0;
    let H = 0;
    // The loop only runs while the canvas is on screen and the tab is visible.
    let onScreen = false;

    // Dots are drawn in batches that share a colour: one fill per bucket instead of a
    // fillStyle string per dot (about 12k a frame), which kept the CPU pinned and the GC busy.
    const LIGHT_STEPS = 12;
    const TINT_STEPS = 4;
    const landFill: string[] = [];
    for (let l = 0; l < LIGHT_STEPS; l++) {
      for (let t = 0; t < TINT_STEPS; t++) {
        const light = (l + 0.5) / LIGHT_STEPS;
        const tint = (t + 0.5) / TINT_STEPS;
        const r = Math.min(255, Math.round(110 + light * 120 - tint * 40));
        const g = Math.min(255, Math.round(104 + light * 130 + tint * 40));
        landFill.push(`rgba(${r}, ${g}, 255, ${Math.min(1, 0.5 + light * 0.5).toFixed(3)})`);
      }
    }
    const oceanFill = Array.from({ length: LIGHT_STEPS }, (_, l) => `rgba(120, 112, 255, ${(0.05 + ((l + 0.5) / LIGHT_STEPS) * 0.14).toFixed(3)})`);

    const project = (lat: number, lon: number, lon0: number, cosLat0: number, sinLat0: number) => {
      const cosPhi = Math.cos(lat);
      const sinPhi = Math.sin(lat);
      const theta = lon + lon0;
      const x = cosPhi * Math.sin(theta);
      const ct = cosPhi * Math.cos(theta);
      return { x, y: sinPhi * cosLat0 - ct * sinLat0, z: sinPhi * sinLat0 + ct * cosLat0 };
    };

    const render = () => {
      raf = 0;
      if (!onScreen || document.hidden) return;
      time += 0.016;
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      if (W === 0 || H === 0) {
        raf = requestAnimationFrame(render);
        return;
      }
      const bw = Math.round(W * dpr);
      const bh = Math.round(H * dpr);
      if (canvas.width !== bw || canvas.height !== bh) {
        canvas.width = bw;
        canvas.height = bh;
      }
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.clearRect(0, 0, W, H);
      const cx = W / 2;
      const cy = H / 2;
      const R = Math.min(W, H) * 0.34; // leaves room for the orbit rings

      const f = focusRef.current;
      if (f.id !== lastFocus) {
        lastFocus = f.id;
        labelAlpha = 0;
      }
      const target = { lon: -deg(f.lon) + 0.3, lat: deg(f.lat) - 0.12 };
      const rot = rotRef.current;
      if (!dragRef.current.active) {
        if (!reduceMotion) rot.spin += 0.00025;
        rot.lon += (target.lon + rot.spin - rot.lon) * 0.035;
        rot.lat += (target.lat - rot.lat) * 0.035;
      }
      const lon0 = rot.lon + dragRef.current.offLon;
      const lat0 = Math.max(-0.6, Math.min(0.7, rot.lat + dragRef.current.offLat));
      const cosLat0 = Math.cos(lat0);
      const sinLat0 = Math.sin(lat0);

      // Rotate a world-space vector into view space (same transform as `project`).
      const cosL = Math.cos(lon0);
      const sinL = Math.sin(lon0);
      const view = (x: number, y: number, z: number) => {
        const x1 = x * cosL + z * sinL;
        const z1 = -x * sinL + z * cosL;
        return { x: x1, y: y * cosLat0 - z1 * sinLat0, z: y * sinLat0 + z1 * cosLat0 };
      };
      const fv = toWorld(deg(f.lat), deg(f.lon));

      // Starfield: faint, twinkling, and only outside the planet.
      for (let i = 0; i < stars.length; i++) {
        const s = stars[i];
        const sx = s.x * W;
        const sy = s.y * H;
        if (Math.hypot(sx - cx, sy - cy) < R * 1.05) continue;
        const tw = reduceMotion ? 0.6 : 0.45 + 0.55 * Math.abs(Math.sin(time * s.speed + s.phase));
        ctx.fillStyle = `rgba(200, 210, 255, ${s.a * tw})`;
        ctx.fillRect(sx, sy, s.size, s.size);
      }

      // Orbit rings: the far half is drawn behind the planet, the near half in front.
      const orbits = [
        { rx: R * 1.36, ry: R * 0.3, rot: -0.32, speed: 0.22, color: "142, 162, 255" },
        { rx: R * 1.2, ry: R * 0.5, rot: 0.48, speed: -0.15, color: "242, 181, 68" },
      ];
      const drawOrbit = (o: (typeof orbits)[number], front: boolean) => {
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate(o.rot);
        const grad = ctx.createLinearGradient(-o.rx, 0, o.rx, 0);
        grad.addColorStop(0, `rgba(${o.color}, 0)`);
        grad.addColorStop(0.5, `rgba(${o.color}, ${front ? 0.55 : 0.18})`);
        grad.addColorStop(1, `rgba(${o.color}, 0)`);
        ctx.strokeStyle = grad;
        ctx.lineWidth = front ? 1.1 : 0.8;
        ctx.beginPath();
        ctx.ellipse(0, 0, o.rx, o.ry, 0, front ? 0 : Math.PI, front ? Math.PI : Math.PI * 2);
        ctx.stroke();
        // A satellite riding the ring.
        const t = (reduceMotion ? 1 : time * o.speed) % (Math.PI * 2);
        const inFront = Math.sin(t) > 0;
        if (inFront === front) {
          const sx = Math.cos(t) * o.rx;
          const sy = Math.sin(t) * o.ry;
          const glow = ctx.createRadialGradient(sx, sy, 0, sx, sy, 10);
          glow.addColorStop(0, `rgba(${o.color}, ${front ? 0.9 : 0.35})`);
          glow.addColorStop(1, `rgba(${o.color}, 0)`);
          ctx.fillStyle = glow;
          ctx.beginPath();
          ctx.arc(sx, sy, 10, 0, Math.PI * 2);
          ctx.fill();
          ctx.fillStyle = front ? "#FFFFFF" : `rgba(${o.color}, 0.6)`;
          ctx.beginPath();
          ctx.arc(sx, sy, front ? 2 : 1.4, 0, Math.PI * 2);
          ctx.fill();
        }
        ctx.restore();
      };
      for (const o of orbits) drawOrbit(o, false);

      // Atmosphere: a wide soft halo plus a tight rim glow.
      const atmo = ctx.createRadialGradient(cx + R * 0.25, cy - R * 0.2, R * 0.7, cx, cy, R * 1.45);
      atmo.addColorStop(0, "rgba(96, 84, 255, 0.30)");
      atmo.addColorStop(0.55, "rgba(70, 60, 220, 0.12)");
      atmo.addColorStop(1, "rgba(14, 14, 18, 0)");
      ctx.fillStyle = atmo;
      ctx.beginPath();
      ctx.arc(cx, cy, R * 1.45, 0, Math.PI * 2);
      ctx.fill();
      const rimGlow = ctx.createRadialGradient(cx, cy, R * 0.94, cx, cy, R * 1.08);
      rimGlow.addColorStop(0, "rgba(120, 140, 255, 0)");
      rimGlow.addColorStop(0.5, "rgba(120, 140, 255, 0.28)");
      rimGlow.addColorStop(1, "rgba(120, 140, 255, 0)");
      ctx.fillStyle = rimGlow;
      ctx.beginPath();
      ctx.arc(cx, cy, R * 1.08, 0, Math.PI * 2);
      ctx.fill();

      // Sphere body.
      const body = ctx.createRadialGradient(cx + R * 0.45, cy - R * 0.35, R * 0.05, cx, cy, R);
      body.addColorStop(0, "#2E26A0");
      body.addColorStop(0.4, "#1C1666");
      body.addColorStop(0.8, "#110D3F");
      body.addColorStop(1, "#0A0828");
      ctx.fillStyle = body;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.fill();

      // Land and ocean dots: Lambert shading, a cyan-to-violet tint across the disc,
      // and a soft spotlight around the bureau the slide is about. Dots outside the
      // spotlight go into colour buckets (one path each); spotlit ones are drawn exactly.
      const landPaths: (Path2D | undefined)[] = [];
      const oceanPaths: (Path2D | undefined)[] = [];
      const spotlit: { x: number; y: number; s: number; fill: string }[] = [];
      for (let i = 0; i < dots.length; i++) {
        const d = dots[i];
        const p = view(d.wx, d.wy, d.wz);
        if (p.z < 0.02) continue;
        const lambert = Math.max(0, p.x * LIGHT[0] + p.y * LIGHT[1] + p.z * LIGHT[2]);
        const rim = Math.pow(1 - p.z, 2.2);
        const light = Math.min(1, lambert * lambert * 1.15 + rim * lambert * 1.6);
        const near = d.wx * fv.x + d.wy * fv.y + d.wz * fv.z; // cos of angular distance to focus
        const spot = near > 0.9 ? Math.pow((near - 0.9) / 0.1, 1.6) : 0;
        if (spot === 0) {
          const l = Math.min(LIGHT_STEPS - 1, Math.floor(light * LIGHT_STEPS));
          if (d.land) {
            const t = Math.min(TINT_STEPS - 1, Math.floor(((p.x + 1) / 2) * TINT_STEPS));
            const k = l * TINT_STEPS + t;
            const s = 1.6 * (0.5 + p.z * 0.5);
            (landPaths[k] ??= new Path2D()).rect(cx + p.x * R - s / 2, cy - p.y * R - s / 2, s, s);
          } else {
            const s = 0.5 + p.z * 0.5;
            (oceanPaths[l] ??= new Path2D()).rect(cx + p.x * R - s / 2, cy - p.y * R - s / 2, s, s);
          }
          continue;
        }
        if (d.land) {
          const tint = (p.x + 1) / 2; // 0 left (violet) .. 1 right (cyan)
          const r = 110 + light * 120 - tint * 40 + spot * 90;
          const g = 104 + light * 130 + tint * 40 + spot * 70;
          const b = 255;
          const s = (1.6 + spot * 0.9) * (0.5 + p.z * 0.5);
          const fill = `rgba(${Math.min(255, Math.round(r))}, ${Math.min(255, Math.round(g))}, ${b}, ${Math.min(1, 0.5 + light * 0.5 + spot * 0.4)})`;
          spotlit.push({ x: cx + p.x * R - s / 2, y: cy - p.y * R - s / 2, s, fill });
        } else {
          const s = 0.5 + p.z * 0.5;
          const fill = `rgba(120, 112, 255, ${0.05 + light * 0.14 + spot * 0.2})`;
          spotlit.push({ x: cx + p.x * R - s / 2, y: cy - p.y * R - s / 2, s, fill });
        }
      }

      for (let k = 0; k < oceanPaths.length; k++) {
        const path = oceanPaths[k];
        if (!path) continue;
        ctx.fillStyle = oceanFill[k];
        ctx.fill(path);
      }
      for (let k = 0; k < landPaths.length; k++) {
        const path = landPaths[k];
        if (!path) continue;
        ctx.fillStyle = landFill[k];
        ctx.fill(path);
      }
      // The spotlight around the focused bureau goes on top, with exact colours.
      for (const d of spotlit) {
        ctx.fillStyle = d.fill;
        ctx.fillRect(d.x, d.y, d.s, d.s);
      }

      // Story routes: arcs from HQ out to every bureau, lifted off the surface,
      // with a bright packet travelling along each one.
      const hq = toWorld(deg(BUREAUS[0].lat), deg(BUREAUS[0].lon));
      ctx.save();
      ctx.globalCompositeOperation = "lighter";
      ctx.lineCap = "round";
      for (let k = 1; k < BUREAUS.length; k++) {
        const b = BUREAUS[k];
        const tv = toWorld(deg(b.lat), deg(b.lon));
        const omega = Math.acos(Math.max(-1, Math.min(1, hq.x * tv.x + hq.y * tv.y + hq.z * tv.z)));
        if (omega < 0.01) continue;
        const sinO = Math.sin(omega);
        const lift = 0.06 + (omega / Math.PI) * 0.32;
        const steps = 48;
        const pts: { x: number; y: number; vis: boolean }[] = [];
        for (let s = 0; s <= steps; s++) {
          const t = s / steps;
          const a = Math.sin((1 - t) * omega) / sinO;
          const c = Math.sin(t * omega) / sinO;
          const h = 1 + lift * Math.sin(Math.PI * t);
          const p = view((hq.x * a + tv.x * c) * h, (hq.y * a + tv.y * c) * h, (hq.z * a + tv.z * c) * h);
          const vis = p.z > 0 || p.x * p.x + p.y * p.y > 1;
          pts.push({ x: cx + p.x * R, y: cy - p.y * R, vis });
        }
        const active = b.id === f.id;
        ctx.strokeStyle = active ? "rgba(255, 255, 255, 0.75)" : "rgba(110, 140, 255, 0.32)";
        ctx.lineWidth = active ? 1.4 : 0.9;
        ctx.beginPath();
        let pen = false;
        for (const pt of pts) {
          if (!pt.vis) {
            pen = false;
            continue;
          }
          if (pen) ctx.lineTo(pt.x, pt.y);
          else ctx.moveTo(pt.x, pt.y);
          pen = true;
        }
        ctx.stroke();
        // Travelling packet with a short tail.
        const head = reduceMotion ? 0.6 : (time * 0.28 + k * 0.137) % 1;
        const hi = Math.round(head * steps);
        for (let tail = 0; tail < 8; tail++) {
          const pt = pts[hi - tail];
          if (!pt || !pt.vis) break;
          const alpha = (1 - tail / 8) * (active ? 1 : 0.8);
          ctx.fillStyle = active ? `rgba(255, 255, 255, ${alpha})` : `rgba(160, 190, 255, ${alpha})`;
          ctx.beginPath();
          ctx.arc(pt.x, pt.y, (active ? 2.2 : 1.6) * (1 - tail / 10), 0, Math.PI * 2);
          ctx.fill();
        }
      }
      ctx.restore();

      // Bureau lights.
      let focusPoint: { x: number; y: number } | null = null;
      ctx.save();
      ctx.globalCompositeOperation = "lighter";
      for (let i = 0; i < BUREAUS.length; i++) {
        const b = BUREAUS[i];
        const p = project(deg(b.lat), deg(b.lon), lon0, cosLat0, sinLat0);
        if (p.z < 0.12) continue;
        const px = cx + p.x * R;
        const py = cy - p.y * R;
        const pulse = reduceMotion ? 0.5 : (Math.sin(time * 2.4 + i * 1.7) + 1) / 2;
        const glow = ctx.createRadialGradient(px, py, 0, px, py, 12 + pulse * 6);
        glow.addColorStop(0, `rgba(170, 190, 255, ${0.35 + pulse * 0.2})`);
        glow.addColorStop(1, "rgba(170, 190, 255, 0)");
        ctx.fillStyle = glow;
        ctx.beginPath();
        ctx.arc(px, py, 12 + pulse * 6, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = "#FFFFFF";
        ctx.beginPath();
        ctx.arc(px, py, i === 0 ? 2.4 : 1.8, 0, Math.PI * 2);
        ctx.fill();
        if (b.id === f.id && p.z > 0.3) focusPoint = { x: px, y: py };
      }
      ctx.restore();

      // Specular crescent and night side.
      ctx.save();
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.clip();
      const crescent = ctx.createRadialGradient(cx - R * 0.18, cy + R * 0.12, R * 0.9, cx - R * 0.18, cy + R * 0.12, R * 1.18);
      crescent.addColorStop(0, "rgba(255, 255, 255, 0)");
      crescent.addColorStop(0.7, "rgba(214, 220, 255, 0.28)");
      crescent.addColorStop(1, "rgba(255, 255, 255, 0.75)");
      ctx.fillStyle = crescent;
      ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
      const night = ctx.createLinearGradient(cx + R * 0.6, cy - R * 0.6, cx - R, cy + R);
      night.addColorStop(0, "rgba(8, 6, 30, 0)");
      night.addColorStop(0.55, "rgba(8, 6, 30, 0.05)");
      night.addColorStop(1, "rgba(8, 6, 30, 0.6)");
      ctx.fillStyle = night;
      ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
      ctx.restore();

      ctx.save();
      ctx.shadowColor = "rgba(190, 200, 255, 0.9)";
      ctx.shadowBlur = 18;
      ctx.strokeStyle = "rgba(255, 255, 255, 0.85)";
      ctx.lineWidth = 1.6;
      ctx.lineCap = "round";
      ctx.beginPath();
      ctx.arc(cx, cy, R - 0.5, -Math.PI * 0.46, Math.PI * 0.2);
      ctx.stroke();
      ctx.restore();

      for (const o of orbits) drawOrbit(o, true);

      // Focused bureau: ring, leader line and a mono label that fades in once the globe settles.
      // Small screens put the headline over the globe, so the label only shows on wide canvases.
      if (focusPoint && W >= 640) {
        labelAlpha = Math.min(1, labelAlpha + 0.03);
        const { x, y } = focusPoint;
        const a = labelAlpha;
        const ring = reduceMotion ? 0.5 : (time * 0.7) % 1;
        ctx.strokeStyle = `rgba(255, 255, 255, ${a * (1 - ring) * 0.8})`;
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(x, y, 6 + ring * 18, 0, Math.PI * 2);
        ctx.stroke();
        ctx.strokeStyle = `rgba(255, 255, 255, ${a * 0.9})`;
        ctx.beginPath();
        ctx.arc(x, y, 6, 0, Math.PI * 2);
        ctx.stroke();
        const lx = x + 34;
        const ly = y - 34;
        ctx.beginPath();
        ctx.moveTo(x + 4.5, y - 4.5);
        ctx.lineTo(lx, ly);
        ctx.lineTo(lx + 14, ly);
        ctx.stroke();
        ctx.font = "500 11px 'JetBrains Mono', ui-monospace, monospace";
        ctx.fillStyle = `rgba(255, 255, 255, ${a})`;
        ctx.fillText(f.city.toUpperCase(), lx + 20, ly + 4);
        ctx.fillStyle = `rgba(174, 186, 255, ${a * 0.85})`;
        ctx.fillText(`${f.staff} journalists`, lx + 20, ly + 19);
      }

      raf = requestAnimationFrame(render);
    };

    const start = () => {
      if (onScreen && !document.hidden && !raf) raf = requestAnimationFrame(render);
    };
    const ro = new ResizeObserver(([entry]) => {
      W = entry.contentRect.width;
      H = entry.contentRect.height;
    });
    ro.observe(canvas);
    const io = new IntersectionObserver(([entry]) => {
      onScreen = entry.isIntersecting;
      start();
    });
    io.observe(canvas);
    document.addEventListener("visibilitychange", start);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      io.disconnect();
      document.removeEventListener("visibilitychange", start);
    };
  }, []);

  return (
    <canvas
      ref={canvasRef}
      aria-label={`Globe turned to the ${focus.city} bureau. Drag to rotate.`}
      role="img"
      onPointerDown={(e) => {
        dragRef.current.active = true;
        dragRef.current.lastX = e.clientX;
        dragRef.current.lastY = e.clientY;
        e.currentTarget.setPointerCapture(e.pointerId);
      }}
      onPointerMove={(e) => {
        if (!dragRef.current.active) return;
        const dx = e.clientX - dragRef.current.lastX;
        const dy = e.clientY - dragRef.current.lastY;
        dragRef.current.lastX = e.clientX;
        dragRef.current.lastY = e.clientY;
        dragRef.current.offLon += dx * 0.005;
        dragRef.current.offLat = Math.max(-0.45, Math.min(0.45, dragRef.current.offLat + dy * 0.004));
      }}
      onPointerUp={() => {
        dragRef.current.active = false;
      }}
      onPointerCancel={() => {
        dragRef.current.active = false;
      }}
      className="size-full cursor-grab touch-none select-none active:cursor-grabbing"
    />
  );
}

/* ─── Header, menu and hero ─────────────────────────────────────────────────── */

const NAV = [
  { label: "Divisions", href: "#divisions" },
  { label: "Bureaus", href: "#bureaus" },
  { label: "Advertise", href: "#advertise" },
  { label: "Newsroom", href: "#newsroom" },
];

function Wordmark({ small = false }: { small?: boolean }) {
  return (
    <span className="flex select-none items-center gap-3">
      <span className={cn("gm-serif font-bold leading-none tracking-tight text-white", small ? "text-[22px]" : "text-[28px]")}>
        Meridian
      </span>
      <span className={cn("w-px bg-white/30", small ? "h-5" : "h-7")} />
      <span className="flex flex-col text-[10.5px] font-medium uppercase leading-[1.25] tracking-[0.16em] text-white/70">
        <span>Media</span>
        <span>Group</span>
      </span>
    </span>
  );
}

const SLIDE_MS = 9000;

function Hero({ onOpenSearch, menuOpen, setMenuOpen }: { onOpenSearch: () => void; menuOpen: boolean; setMenuOpen: (v: boolean) => void }) {
  const [slideIdx, setSlideIdx] = useState(0);
  const [paused, setPaused] = useState(false);
  const slide = HERO_SLIDES[slideIdx];
  const focus = BUREAUS.find((b) => b.id === slide.focus) ?? BUREAUS[0];

  useEffect(() => {
    if (paused) return;
    const id = window.setTimeout(() => setSlideIdx((i) => (i + 1) % HERO_SLIDES.length), SLIDE_MS);
    return () => window.clearTimeout(id);
  }, [slideIdx, paused]);

  const go = (dir: 1 | -1) => setSlideIdx((i) => (i + dir + HERO_SLIDES.length) % HERO_SLIDES.length);

  return (
    <section
      id="top"
      aria-roledescription="carousel"
      aria-label="Meridian Media Group highlights"
      className="relative isolate flex min-h-[max(100svh,720px)] w-full flex-col overflow-hidden bg-[var(--gm-bg)]"
    >
      <div className="absolute left-1/2 top-[46%] z-0 aspect-square w-[158vw] -translate-x-1/2 -translate-y-1/2 sm:w-[112vw] lg:left-[60%] lg:top-[50%] lg:w-[min(76vw,132vh)] lg:max-w-[1260px]">
        <MediaGlobe focus={focus} />
      </div>

      <div aria-hidden className="pointer-events-none absolute inset-0 z-[1] bg-[linear-gradient(90deg,var(--gm-bg)_0%,rgba(14,14,18,0.78)_28%,rgba(14,14,18,0)_55%)]" />
      <div aria-hidden className="pointer-events-none absolute inset-0 z-[1] bg-[radial-gradient(120%_90%_at_60%_45%,transparent_55%,rgba(8,8,11,0.9)_100%)]" />
      <div aria-hidden className="pointer-events-none absolute inset-x-0 bottom-0 z-[1] h-48 bg-gradient-to-t from-[var(--gm-bg)] to-transparent" />
      <div aria-hidden className="gm-grain pointer-events-none absolute inset-0 z-[2] opacity-[0.07] mix-blend-overlay" />

      {/* Header */}
      <header className="relative z-40 flex w-full items-center justify-between gap-6 px-6 pt-7 sm:px-12 sm:pt-9 lg:px-16">
        <a href="#top" aria-label="Meridian Media Group home">
          <Wordmark />
        </a>

        <nav aria-label="Primary" className="hidden items-center gap-9 text-[13px] text-white/75 lg:flex">
          {NAV.map((item) => (
            <a key={item.href} href={item.href} className="gm-link transition-colors hover:text-white">
              {item.label}
            </a>
          ))}
        </nav>

        <div className="flex items-center gap-2 sm:gap-3">
          <button
            type="button"
            onClick={onOpenSearch}
            className="group inline-flex h-9 items-center gap-2.5 rounded-full border border-white/15 bg-white/[0.04] px-3 text-white/70 backdrop-blur-md transition-colors hover:border-white/30 hover:text-white"
          >
            <Search className="size-4" />
            <span className="sr-only sm:not-sr-only sm:text-[12px]">Search</span>
            <kbd className="gm-mono hidden rounded border border-white/15 px-1.5 py-px text-[10px] text-white/50 sm:inline">⌘K</kbd>
          </button>
          <a
            href="#advertise"
            className="hidden h-9 items-center gap-2 rounded-full bg-white px-4 text-[12px] font-semibold text-[#0E0E12] transition-colors hover:bg-[#DCE2FF] md:inline-flex"
          >
            Advertise with us
            <ArrowUpRight className="size-3.5" />
          </a>
          <button
            type="button"
            onClick={() => setMenuOpen(!menuOpen)}
            aria-label={menuOpen ? "Close menu" : "Open menu"}
            aria-expanded={menuOpen}
            className="group relative z-50 flex h-9 w-10 flex-col items-end justify-center gap-[7px] text-white lg:hidden"
          >
            <span className={cn("h-[1.5px] w-7 bg-white transition-all duration-300", menuOpen && "translate-y-[4.25px] rotate-45")} />
            <span className={cn("h-[1.5px] bg-white transition-all duration-300", menuOpen ? "w-7 -translate-y-[4.25px] -rotate-45" : "w-4 group-hover:w-7")} />
          </button>
        </div>
      </header>

      {/* Stage */}
      <div
        className="relative z-20 flex flex-1 items-center px-6 sm:px-12 lg:px-16"
        onPointerEnter={() => setPaused(true)}
        onPointerLeave={() => setPaused(false)}
      >
        <div className="w-full py-10 lg:w-[48%]">
          <div key={`eyebrow-${slide.id}`} className="gm-mono mb-6 flex items-center gap-3 text-[11px] uppercase tracking-[0.24em] text-[var(--gm-blue-soft)]">
            <motion.span initial={{ width: 0 }} animate={{ width: 32 }} transition={{ duration: 0.8, ease: EASE }} className="h-px bg-[var(--gm-blue-soft)]" />
            <span>
              {slide.id} — {slide.eyebrow}
            </span>
          </div>
          <h1
            key={slide.id}
            aria-live="polite"
            className="gm-display select-none text-[16.5vw] font-bold uppercase leading-[0.88] text-white sm:text-[96px] lg:text-[clamp(78px,7.4vw,124px)]"
          >
            {slide.lines.map((line, i) => (
              <span key={line} className="block overflow-hidden pb-[0.04em]">
                <motion.span
                  className={cn("block", i === 1 && "text-white/90", i === 3 && "text-[#A9B8FF]")}
                  initial={{ y: "105%" }}
                  animate={{ y: 0 }}
                  transition={{ duration: 0.9, delay: 0.08 * i, ease: EASE }}
                >
                  {line}
                </motion.span>
              </span>
            ))}
          </h1>

        </div>

        <div
          className="absolute right-6 top-[4%] hidden select-none flex-col gap-1 text-[12px] leading-relaxed tracking-wide text-white/70 sm:right-12 sm:flex lg:right-16"
          style={{ writingMode: "vertical-rl" }}
        >
          <AnimatePresence mode="wait">
            <motion.span key={slide.id} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.4 }} className="flex flex-col gap-1">
              <span>{slide.side[0]}</span>
              <span className="text-white/45">{slide.side[1]}</span>
            </motion.span>
          </AnimatePresence>
        </div>
      </div>

      {/* Controls and stats */}
      <div className="relative z-20 grid w-full grid-cols-1 items-end gap-8 px-6 pb-8 sm:px-12 sm:pb-10 lg:grid-cols-12 lg:px-16">
        <div className="space-y-5 lg:col-span-6">
          <div className="flex items-center gap-4">
            <button
              type="button"
              onClick={() => go(-1)}
              aria-label="Previous slide"
              className="grid size-10 place-items-center rounded-full border border-white/15 text-white/70 transition-colors hover:border-white/40 hover:text-white"
            >
              <ArrowLeft className="size-[18px] stroke-[1.5]" />
            </button>
            <button
              type="button"
              onClick={() => go(1)}
              aria-label="Next slide"
              className="grid size-10 place-items-center rounded-full border border-white/15 text-white/70 transition-colors hover:border-white/40 hover:text-white"
            >
              <ArrowRight className="size-[18px] stroke-[1.5]" />
            </button>
            <span className="gm-mono ml-2 text-[12px] tabular-nums text-white/50">
              <span className="text-white">{slide.id}</span> / {String(HERO_SLIDES.length).padStart(2, "0")}
            </span>
          </div>

          <div className="flex max-w-[620px] gap-2">
            {HERO_SLIDES.map((s, i) => (
              <button
                key={s.id}
                type="button"
                onClick={() => setSlideIdx(i)}
                aria-label={`Go to slide ${i + 1}: ${s.eyebrow}`}
                aria-current={i === slideIdx}
                className="group flex-1 py-2"
              >
                <span className="relative block h-[2px] w-full overflow-hidden bg-white/15">
                  {i < slideIdx && <span className="absolute inset-0 bg-white/60" />}
                  {i === slideIdx && (
                    <motion.span
                      key={`${s.id}-${paused}`}
                      className="absolute inset-y-0 left-0 bg-white"
                      initial={{ width: paused ? "100%" : "0%" }}
                      animate={{ width: "100%" }}
                      transition={{ duration: paused ? 0 : SLIDE_MS / 1000, ease: "linear" }}
                    />
                  )}
                </span>
                <span className="gm-mono mt-2 block text-left text-[10px] uppercase tracking-[0.2em] text-white/40 transition-colors group-hover:text-white/80">
                  {s.eyebrow}
                </span>
              </button>
            ))}
          </div>
        </div>

        <div className="flex items-end justify-start gap-10 lg:col-span-6 lg:justify-end">
          {slide.stats.map((stat) => (
            <div key={`${slide.id}-${stat.label}`} className="lg:text-right">
              <div className="gm-display text-[44px] font-bold leading-none tabular-nums text-white sm:text-[56px]">
                <RollingNumber value={stat.value} format={(n) => n.toFixed(stat.decimals) + (stat.suffix ?? "")} />
              </div>
              <div className="mt-1.5 text-[12px] text-white/60">{stat.label}</div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

function MobileMenu({ open, onClose }: { open: boolean; onClose: () => void }) {
  return (
    <AnimatePresence>
      {open && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.3 }}
          className="fixed inset-0 z-30 flex flex-col justify-center bg-[#0B0B0F]/96 px-8 backdrop-blur-xl sm:px-16"
        >
          <nav aria-label="Menu" className="mx-auto w-full max-w-4xl">
            {[{ label: "Home", href: "#top" }, ...NAV].map((item, i) => (
              <motion.a
                key={item.href}
                href={item.href}
                onClick={onClose}
                initial={{ opacity: 0, y: 24 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.06 * i + 0.1, duration: 0.5, ease: EASE }}
                className="group flex items-baseline gap-6 border-b border-white/10 py-3"
              >
                <span className="gm-mono text-xs text-white/40">0{i + 1}</span>
                <span className="gm-display text-5xl uppercase text-white transition-colors group-hover:text-[#8EA2FF] sm:text-7xl">{item.label}</span>
              </motion.a>
            ))}
          </nav>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

/* ─── Titles marquee ───────────────────────────────────────────────────────── */

function MastheadStrip() {
  const row = [...MASTHEADS, ...MASTHEADS];
  return (
    <section aria-label="Our titles" className="gm-marquee-host relative border-y border-white/10 bg-[#0B0B0F] py-8">
      <div className="mx-auto mb-6 flex max-w-[1520px] items-center justify-between gap-4 px-6 sm:px-12 lg:px-16">
        <p className="gm-mono text-[11px] uppercase tracking-[0.22em] text-white/45">Home to 84 titles across six divisions</p>
        <a href="#divisions" className="gm-link hidden text-[12px] text-white/60 hover:text-white sm:inline">
          Browse divisions
        </a>
      </div>
      <div className="relative overflow-hidden [mask-image:linear-gradient(90deg,transparent,black_8%,black_92%,transparent)]">
        <div className="gm-marquee flex w-max items-center">
          {row.map((m, i) => (
            <span key={i} aria-hidden={i >= MASTHEADS.length} className={cn("shrink-0 px-10 text-white/55 transition-colors hover:text-white", m.style)}>
              {m.name}
            </span>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ─── Reach in numbers ─────────────────────────────────────────────────────── */

const REACH_STATS = [
  { value: 104.6, decimals: 1, suffix: "M", label: "Monthly unique audience", note: "Deduplicated across print, web, social and video", spark: [32, 38, 41, 47, 52, 58, 63, 71, 76, 84, 95, 104] },
  { value: 12.4, decimals: 1, suffix: "B", label: "Annual impressions", note: "Up 28.4% year on year", spark: [6, 6.4, 7.1, 7.6, 8.2, 8.9, 9.4, 10.1, 10.8, 11.3, 11.9, 12.4] },
  { value: 4.5, decimals: 1, suffix: "M", label: "Newsletter & social followers", note: "Across 31 owned channels", spark: [2.1, 2.3, 2.6, 2.8, 3.0, 3.2, 3.5, 3.7, 3.9, 4.1, 4.3, 4.5] },
  { value: 4.8, decimals: 1, suffix: "x", label: "Dwell time vs. benchmark", note: "Average 4m 18s per visit", spark: [2.9, 3.1, 3.3, 3.2, 3.6, 3.8, 4.0, 4.1, 4.3, 4.5, 4.6, 4.8] },
];

function Sparkline({ data }: { data: number[] }) {
  const min = Math.min(...data);
  const max = Math.max(...data);
  const pts = data.map((v, i) => [(i / (data.length - 1)) * 100, 28 - ((v - min) / (max - min || 1)) * 24 - 2]);
  const line = pts.map(([x, y], i) => `${i ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`).join(" ");
  return (
    <svg viewBox="0 0 100 30" preserveAspectRatio="none" className="h-8 w-full" aria-hidden>
      <path d={`${line} L100 30 L0 30 Z`} fill="url(#gm-spark)" opacity={0.35} />
      <path d={line} fill="none" stroke="#8EA2FF" strokeWidth={1.4} vectorEffect="non-scaling-stroke" />
      <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r={1.8} fill="#fff" />
    </svg>
  );
}

function StatCell({ stat, index }: { stat: (typeof REACH_STATS)[number]; index: number }) {
  const ref = useRef<HTMLDivElement | null>(null);
  const inView = useInView(ref, { once: true, margin: "-15% 0px" });
  return (
    <div ref={ref} className="flex flex-col justify-between gap-8 border-white/10 p-6 sm:p-8 [&:not(:first-child)]:border-t sm:[&:nth-child(2)]:border-t-0 sm:[&:nth-child(even)]:border-l lg:[&:not(:first-child)]:border-l lg:[&:not(:first-child)]:border-t-0">
      <div className="gm-mono flex items-center justify-between text-[11px] text-white/40">
        <span>0{index + 1}</span>
        <span>12 mo</span>
      </div>
      <div>
        <div className="gm-display text-[64px] font-bold leading-none tabular-nums text-white sm:text-[76px]">
          <RollingNumber value={stat.value} start={inView} duration={1400} format={(n) => n.toFixed(stat.decimals)} />
          <span className="text-[var(--gm-blue-soft)]">{stat.suffix}</span>
        </div>
        <div className="mt-3 text-[15px] font-medium text-white">{stat.label}</div>
        <div className="mt-1 text-[13px] text-white/50">{stat.note}</div>
      </div>
      <Sparkline data={stat.spark} />
    </div>
  );
}

function ReachSection() {
  return (
    <section id="about" className="relative scroll-mt-6 bg-[var(--gm-bg)] py-24 lg:py-32">
      <svg width="0" height="0" className="absolute" aria-hidden>
        <defs>
          <linearGradient id="gm-spark" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stopColor="#2F54FF" />
            <stop offset="1" stopColor="#2F54FF" stopOpacity="0" />
          </linearGradient>
        </defs>
      </svg>
      <div className="mx-auto max-w-[1520px] px-6 sm:px-12 lg:px-16">
        <div className="grid gap-10 lg:grid-cols-12 lg:items-end">
          <Reveal className="lg:col-span-7">
            <SectionLabel index="01">Reach</SectionLabel>
            <h2 className="gm-display mt-6 text-[52px] font-bold uppercase leading-[0.92] text-white sm:text-[80px]">
              One audience.
              <br />
              <span className="text-white/35">Every format.</span>
            </h2>
          </Reveal>
          <Reveal className="lg:col-span-5" delay={0.1}>
            <p className="max-w-md text-[16px] leading-relaxed text-white/65">
              Thirty years of trusted journalism, now read, watched and attended by one in three adults across the Gulf. Advertisers buy it as a single, measurable audience.
            </p>
          </Reveal>
        </div>

        <Reveal delay={0.15} className="mt-14 grid grid-cols-1 overflow-hidden rounded-2xl border border-white/10 bg-white/[0.02] sm:grid-cols-2 lg:grid-cols-4">
          {REACH_STATS.map((stat, i) => (
            <StatCell key={stat.label} stat={stat} index={i} />
          ))}
        </Reveal>
      </div>
    </section>
  );
}

/* ─── Divisions index ──────────────────────────────────────────────────────── */

function MixBar({ mix }: { mix: Division["mix"] }) {
  return (
    <div>
      <div className="flex h-2.5 w-full overflow-hidden rounded-full bg-white/10">
        {MIX_KEYS.map((k) =>
          mix[k] > 0 ? (
            <motion.span
              key={k}
              layout
              className="h-full"
              style={{ background: MIX_COLOR[k] }}
              initial={false}
              animate={{ width: `${mix[k]}%` }}
              transition={{ duration: 0.6, ease: EASE }}
            />
          ) : null,
        )}
      </div>
      <div className="mt-3 flex flex-wrap gap-x-4 gap-y-1.5">
        {MIX_KEYS.map((k) => (
          <span key={k} className="inline-flex items-center gap-1.5 text-[11.5px] text-white/55">
            <span className="size-2 rounded-full" style={{ background: MIX_COLOR[k] }} />
            {MIX_LABEL[k]} <span className="gm-mono tabular-nums text-white/80">{mix[k]}%</span>
          </span>
        ))}
      </div>
    </div>
  );
}

function DivisionsSection({ active, setActive, onPlan }: { active: number; setActive: (i: number) => void; onPlan: (i: number) => void }) {
  const d = DIVISIONS[active];
  const maxReach = Math.max(...DIVISIONS.map((x) => x.reach));

  return (
    <section id="divisions" className="relative scroll-mt-6 border-t border-white/10 bg-[#0B0B0F] py-24 lg:py-32">
      <div className="mx-auto max-w-[1520px] px-6 sm:px-12 lg:px-16">
        <div className="grid gap-10 lg:grid-cols-12 lg:items-end">
          <Reveal className="lg:col-span-7">
            <SectionLabel index="02">Divisions</SectionLabel>
            <h2 className="gm-display mt-6 text-[52px] font-bold uppercase leading-[0.92] text-white sm:text-[80px]">
              Six divisions.
              <br />
              <span className="text-white/35">Eighty-four titles.</span>
            </h2>
          </Reveal>
          <Reveal className="lg:col-span-5" delay={0.1}>
            <p className="max-w-md text-[16px] leading-relaxed text-white/65">
              Each division runs its own editorial team and sells its own audience. Pick one to see who it reaches and where.
            </p>
          </Reveal>
        </div>

        <div className="mt-14 grid gap-8 lg:grid-cols-12">
          <div role="tablist" aria-label="Divisions" aria-orientation="vertical" className="border-t border-white/10 lg:col-span-7">
            {DIVISIONS.map((div, i) => {
              const on = i === active;
              return (
                <button
                  key={div.code}
                  type="button"
                  role="tab"
                  id={`gm-div-tab-${i}`}
                  aria-selected={on}
                  aria-controls="gm-div-panel"
                  onClick={() => setActive(i)}
                  onKeyDown={(e) => {
                    if (e.key === "ArrowDown" || e.key === "ArrowUp") {
                      e.preventDefault();
                      const next = (i + (e.key === "ArrowDown" ? 1 : -1) + DIVISIONS.length) % DIVISIONS.length;
                      setActive(next);
                      document.getElementById(`gm-div-tab-${next}`)?.focus();
                    }
                  }}
                  className="group relative grid w-full grid-cols-[2.5rem_1fr_auto] items-center gap-4 border-b border-white/10 py-5 text-left outline-none sm:grid-cols-[3rem_1fr_9rem_auto] sm:py-6"
                >
                  <span className={cn("absolute inset-y-0 -left-4 w-[3px] origin-top bg-[var(--gm-blue)] transition-transform duration-500", on ? "scale-y-100" : "scale-y-0")} />
                  <span className={cn("gm-mono text-[12px] transition-colors", on ? "text-[var(--gm-blue-soft)]" : "text-white/35")}>{div.code}</span>
                  <span
                    className={cn(
                      "gm-display text-[30px] font-semibold uppercase leading-none transition-all duration-500 sm:text-[44px] group-focus-visible:underline",
                      on ? "translate-x-1 text-white" : "text-white/40 group-hover:text-white/70",
                    )}
                  >
                    {div.name}
                  </span>
                  <span className="hidden h-1 overflow-hidden rounded-full bg-white/10 sm:block">
                    <span
                      className={cn("block h-full rounded-full transition-colors", on ? "bg-[var(--gm-blue)]" : "bg-white/30")}
                      style={{ width: `${(div.reach / maxReach) * 100}%` }}
                    />
                  </span>
                  <span className={cn("gm-display text-right text-[24px] tabular-nums transition-colors sm:text-[28px]", on ? "text-white" : "text-white/40")}>
                    {div.reach.toFixed(1)}M
                  </span>
                </button>
              );
            })}
          </div>

          <div className="lg:col-span-5">
            <div
              id="gm-div-panel"
              role="tabpanel"
              aria-labelledby={`gm-div-tab-${active}`}
              className="relative overflow-hidden rounded-2xl border border-white/10 bg-[#12131A] lg:sticky lg:top-8"
            >
              <div aria-hidden className="pointer-events-none absolute -right-24 -top-24 size-72 rounded-full bg-[var(--gm-blue)] opacity-20 blur-[90px]" />
              <AnimatePresence mode="wait">
                <motion.div
                  key={d.code}
                  initial={{ opacity: 0, y: 12 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, y: -8 }}
                  transition={{ duration: 0.35, ease: EASE }}
                  className="relative p-6 sm:p-8"
                >
                  <div className="flex items-start justify-between gap-4">
                    <div>
                      <div className="gm-mono text-[11px] uppercase tracking-[0.2em] text-white/40">Division {d.code}</div>
                      <h3 className="mt-2 text-[22px] font-semibold tracking-tight text-white">{d.name}</h3>
                    </div>
                    <span className="gm-mono shrink-0 rounded-full border border-emerald-400/25 bg-emerald-400/10 px-2.5 py-1 text-[11px] text-emerald-300">
                      +{d.growth.toFixed(1)}% YoY
                    </span>
                  </div>
                  <p className="mt-3 text-[14px] leading-relaxed text-white/60">{d.blurb}</p>

                  <div className="mt-6 flex flex-wrap gap-2">
                    {d.titles.map((t) => (
                      <span key={t} className="gm-serif rounded-full border border-white/12 bg-white/[0.04] px-3 py-1 text-[13px] italic text-white/85">
                        {t}
                      </span>
                    ))}
                  </div>

                  <div className="mt-8 grid grid-cols-3 gap-px overflow-hidden rounded-xl border border-white/10 bg-white/10">
                    {[
                      { k: "Monthly reach", v: `${d.reach.toFixed(1)}M` },
                      { k: `Aged ${d.audience.core}`, v: `${d.audience.corePct}%` },
                      { k: "Top-decile income", v: `${d.audience.affluent}%` },
                    ].map((m) => (
                      <div key={m.k} className="bg-[#12131A] p-3.5">
                        <div className="gm-display text-[28px] font-semibold leading-none text-white">{m.v}</div>
                        <div className="mt-1.5 text-[11px] leading-snug text-white/45">{m.k}</div>
                      </div>
                    ))}
                  </div>

                  <div className="mt-8">
                    <div className="gm-mono mb-3 text-[11px] uppercase tracking-[0.2em] text-white/40">Where the audience reads</div>
                    <MixBar mix={d.mix} />
                  </div>

                  <a
                    href="#advertise"
                    onClick={() => onPlan(active)}
                    className="group mt-8 inline-flex items-center gap-2 text-[13px] font-medium text-white"
                  >
                    <span className="gm-link">Plan a campaign in {d.short}</span>
                    <ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
                  </a>
                </motion.div>
              </AnimatePresence>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

/* ─── Bureau network map ───────────────────────────────────────────────────── */

const MAP_TOP = 12; // 78°N
const MAP_HEIGHT = 136; // down to 58°S
const HQ = BUREAUS[0];

function buildLandPath() {
  const mask = decodeLandMask();
  let d = "";
  for (let row = MAP_TOP; row < MAP_TOP + MAP_HEIGHT; row += 2) {
    for (let col = 0; col < LAND_W; col += 2) {
      if (isLand(mask, row, col) || isLand(mask, row + 1, col + 1)) d += `M${col + 1} ${row + 1}h0`;
    }
  }
  return d;
}

const toMap = (b: Bureau) => ({ x: b.lon + 180, y: 90 - b.lat });

function arcPath(a: Bureau, b: Bureau) {
  const p = toMap(a);
  const q = toMap(b);
  const mx = (p.x + q.x) / 2;
  const my = (p.y + q.y) / 2;
  const dist = Math.hypot(q.x - p.x, q.y - p.y);
  return `M${p.x} ${p.y} Q${mx} ${my - dist * 0.32} ${q.x} ${q.y}`;
}

function BureausSection({ active, setActive }: { active: string; setActive: (id: string) => void }) {
  const landPath = useMemo(() => buildLandPath(), []);
  const minute = useMinute();
  const bureau = BUREAUS.find((b) => b.id === active) ?? HQ;
  const t = localTime(minute, bureau.utc);
  const onAir = t.hour >= 6 && t.hour < 23;

  const onPinKey = (e: ReactKeyboardEvent, id: string) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      setActive(id);
    }
  };

  return (
    <section id="bureaus" className="relative scroll-mt-6 overflow-hidden border-t border-white/10 bg-[var(--gm-bg)] py-24 lg:py-32">
      <div className="mx-auto max-w-[1520px] px-6 sm:px-12 lg:px-16">
        <div className="grid gap-10 lg:grid-cols-12 lg:items-end">
          <Reveal className="lg:col-span-7">
            <SectionLabel index="03">Bureaus</SectionLabel>
            <h2 className="gm-display mt-6 text-[52px] font-bold uppercase leading-[0.92] text-white sm:text-[80px]">
              {BUREAUS.length} bureaus.
              <br />
              <span className="text-white/35">{TOTAL_STAFF} journalists.</span>
            </h2>
          </Reveal>
          <Reveal className="lg:col-span-5" delay={0.1}>
            <p className="max-w-md text-[16px] leading-relaxed text-white/65">
              Stories are reported where they happen and edited in Dubai. When one desk signs off for the night, another is already on air.
            </p>
          </Reveal>
        </div>

        <Reveal delay={0.1} className="mt-14 grid gap-6 lg:grid-cols-12">
          <div className="relative flex flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#0B0B0F] lg:col-span-8">
            <div className="gm-mono flex items-center justify-between border-b border-white/10 px-5 py-3 text-[11px] text-white/45">
              <span className="inline-flex items-center gap-2">
                <span className="relative flex size-2">
                  <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-60" />
                  <span className="relative inline-flex size-2 rounded-full bg-emerald-400" />
                </span>
                Live network
              </span>
              <span className="hidden sm:inline">Select a bureau</span>
            </div>
            <div className="flex flex-1 items-center px-2 py-4 sm:px-4">
            <svg viewBox={`0 ${MAP_TOP} 360 ${MAP_HEIGHT}`} className="block h-auto w-full" role="group" aria-label="Map of Meridian bureaus">
              <path d={landPath} stroke="rgba(142,162,255,0.32)" strokeWidth={1.05} strokeLinecap="round" fill="none" />
              {BUREAUS.slice(1).map((b) => (
                <path
                  key={b.id}
                  d={arcPath(HQ, b)}
                  fill="none"
                  stroke={b.id === active ? "#FFFFFF" : "rgba(47,84,255,0.75)"}
                  strokeWidth={b.id === active ? 0.6 : 0.4}
                  className="gm-arc transition-[stroke]"
                />
              ))}
              {BUREAUS.map((b) => {
                const p = toMap(b);
                const on = b.id === active;
                const hq = b.id === HQ.id;
                return (
                  <g
                    key={b.id}
                    role="button"
                    tabIndex={0}
                    aria-label={`${b.city} bureau`}
                    aria-pressed={on}
                    onClick={() => setActive(b.id)}
                    onKeyDown={(e) => onPinKey(e, b.id)}
                    className="group cursor-pointer outline-none"
                  >
                    <circle cx={p.x} cy={p.y} r={7} fill="transparent" />
                    {on && <circle cx={p.x} cy={p.y} r={2.2} fill="none" stroke="#fff" strokeWidth={0.4} className="gm-ping" />}
                    <circle
                      cx={p.x}
                      cy={p.y}
                      r={on ? 2.4 : hq ? 2.1 : 1.6}
                      fill={on ? "#FFFFFF" : hq ? "#8EA2FF" : "#2F54FF"}
                      stroke="#0B0B0F"
                      strokeWidth={0.6}
                      className="transition-all group-hover:fill-white group-focus-visible:fill-white"
                    />
                    <text
                      x={p.x + 3.6}
                      y={p.y + 1.2}
                      className={cn(
                        "gm-mono pointer-events-none fill-white text-[3.4px] transition-opacity",
                        on || hq ? "opacity-100" : "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100",
                      )}
                    >
                      {b.city.toUpperCase()}
                    </text>
                  </g>
                );
              })}
            </svg>
            </div>
            <div className="flex flex-wrap gap-1.5 border-t border-white/10 p-4">
              {BUREAUS.map((b) => (
                <button
                  key={b.id}
                  type="button"
                  onClick={() => setActive(b.id)}
                  aria-pressed={b.id === active}
                  className={cn(
                    "rounded-full border px-3 py-1 text-[12px] transition-colors",
                    b.id === active ? "border-white bg-white text-[#0E0E12]" : "border-white/15 text-white/65 hover:text-white",
                  )}
                >
                  {b.city}
                </button>
              ))}
            </div>
          </div>

          <div className="flex flex-col gap-6 lg:col-span-4">
            <div className="relative flex-1 overflow-hidden rounded-2xl border border-white/10 bg-[#12131A] p-6 sm:p-7">
              <AnimatePresence mode="wait">
                <motion.div key={bureau.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.3, ease: EASE }}>
                  <div className="flex items-center justify-between gap-3">
                    <span className="gm-mono text-[11px] uppercase tracking-[0.2em] text-white/40">{bureau.id === HQ.id ? "Headquarters" : "Bureau"}</span>
                    <span
                      className={cn(
                        "gm-mono inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10.5px]",
                        onAir ? "bg-emerald-400/10 text-emerald-300" : "bg-amber-400/10 text-amber-300",
                      )}
                    >
                      <Radio className="size-3" />
                      {minute === null ? "Connecting" : onAir ? "On air" : "Overnight desk"}
                    </span>
                  </div>
                  <div className="mt-4 flex items-end justify-between gap-4">
                    <div>
                      <h3 className="gm-display text-[48px] font-bold uppercase leading-none text-white">{bureau.city}</h3>
                      <div className="mt-1 text-[13px] text-white/55">{bureau.country}</div>
                    </div>
                    <div className="text-right">
                      <div className="gm-mono text-[26px] font-medium tabular-nums text-white">{t.label}</div>
                      <div className="gm-mono text-[10.5px] text-white/40">UTC{bureau.utc >= 0 ? "+" : ""}{bureau.utc}</div>
                    </div>
                  </div>
                  <p className="mt-5 text-[14px] text-white/70">{bureau.role}</p>
                  <dl className="mt-6 grid grid-cols-2 gap-4 border-t border-white/10 pt-5 text-[13px]">
                    <div>
                      <dt className="text-white/40">Journalists</dt>
                      <dd className="gm-display mt-1 text-[28px] font-semibold leading-none text-white">{bureau.staff}</dd>
                    </div>
                    <div>
                      <dt className="text-white/40">Languages</dt>
                      <dd className="gm-mono mt-2 text-white/85">{bureau.langs}</dd>
                    </div>
                  </dl>
                  <div className="mt-5 flex flex-wrap gap-1.5">
                    {bureau.beats.map((beat) => (
                      <span key={beat} className="rounded-md border border-white/10 bg-white/[0.04] px-2 py-1 text-[12px] text-white/70">
                        {beat}
                      </span>
                    ))}
                  </div>
                </motion.div>
              </AnimatePresence>
            </div>
            <div className="grid grid-cols-3 gap-px overflow-hidden rounded-2xl border border-white/10 bg-white/10 text-center">
              {[
                { v: "4", k: "Continents" },
                { v: "5", k: "Languages" },
                { v: "24/7", k: "Coverage" },
              ].map((m) => (
                <div key={m.k} className="bg-[#0B0B0F] px-3 py-4">
                  <div className="gm-display text-[26px] font-semibold text-white">{m.v}</div>
                  <div className="text-[11px] text-white/45">{m.k}</div>
                </div>
              ))}
            </div>
          </div>
        </Reveal>
      </div>
    </section>
  );
}

/* ─── Campaign planner ─────────────────────────────────────────────────────── */

const OBJECTIVES = [
  { id: "awareness", label: "Awareness", cpm: 1, eng: 0.8, hint: "Maximise unique reach" },
  { id: "consideration", label: "Consideration", cpm: 1.2, eng: 1.15, hint: "Video and long reads" },
  { id: "conversion", label: "Conversion", cpm: 1.5, eng: 1.5, hint: "Sponsored content and lead forms" },
] as const;

const WEEKS = [2, 4, 8, 12] as const;

function planCampaign(budget: number, weeks: number, objective: (typeof OBJECTIVES)[number], picked: number[]) {
  const divs = picked.map((i) => DIVISIONS[i]);
  const totalWeight = divs.reduce((s, d) => s + d.reach, 0);
  const frequency = 1.4 + weeks * 0.22;
  const rows = divs.map((d) => {
    const spend = (budget * d.reach) / totalWeight;
    const impressions = (spend / (d.cpm * objective.cpm)) * 1000;
    const reach = Math.min(impressions / frequency, d.reach * 1e6 * 0.85);
    return { d, spend, impressions, reach, engagements: impressions * (d.engagement / 100) * objective.eng };
  });
  const impressions = rows.reduce((s, r) => s + r.impressions, 0);
  const reach = rows.reduce((s, r) => s + r.reach, 0);
  const engagements = rows.reduce((s, r) => s + r.engagements, 0);
  const mix = MIX_KEYS.map((k) => ({ k, v: rows.reduce((s, r) => s + r.impressions * r.d.mix[k], 0) / (impressions || 1) }));
  return { rows, impressions, reach, engagements, cpm: (budget / impressions) * 1000, frequency: impressions / (reach || 1), mix };
}

function CampaignPlanner({ request }: { request: { division: number; n: number } }) {
  const [budget, setBudget] = useState(150_000);
  const [weeks, setWeeks] = useState<(typeof WEEKS)[number]>(4);
  const [objectiveId, setObjectiveId] = useState<(typeof OBJECTIVES)[number]["id"]>("awareness");
  const [picked, setPicked] = useState<number[]>([0, 1, 2]);
  const [lastRequest, setLastRequest] = useState(request.n);
  const [email, setEmail] = useState("");
  const [sent, setSent] = useState(false);

  // "Plan a campaign in …" from the divisions index adds that division here.
  if (request.n !== lastRequest) {
    setLastRequest(request.n);
    if (!picked.includes(request.division)) setPicked([...picked, request.division].sort((a, b) => a - b));
  }

  const objective = OBJECTIVES.find((o) => o.id === objectiveId) ?? OBJECTIVES[0];
  const plan = useMemo(() => planCampaign(budget, weeks, objective, picked), [budget, weeks, objective, picked]);
  const maxRowReach = Math.max(...plan.rows.map((r) => r.reach));

  const toggle = (i: number) => {
    setPicked((cur) => (cur.includes(i) ? (cur.length > 1 ? cur.filter((x) => x !== i) : cur) : [...cur, i].sort((a, b) => a - b)));
  };

  const submit = (e: FormEvent) => {
    e.preventDefault();
    if (!/^\S+@\S+\.\S+$/.test(email)) return;
    setSent(true);
  };

  const budgetPct = ((budget - 25_000) / (500_000 - 25_000)) * 100;

  return (
    <section id="advertise" className="relative scroll-mt-6 overflow-hidden border-t border-white/10 bg-[#0B0B0F] py-24 lg:py-32">
      <div aria-hidden className="pointer-events-none absolute left-1/2 top-24 h-[420px] w-[900px] -translate-x-1/2 rounded-full bg-[radial-gradient(circle,#2F54FF_0%,#5B3FD9_45%,transparent_72%)] opacity-[0.16] blur-[120px]" />
      <div className="relative mx-auto max-w-[1520px] px-6 sm:px-12 lg:px-16">
        <div className="grid gap-10 lg:grid-cols-12 lg:items-end">
          <Reveal className="lg:col-span-7">
            <SectionLabel index="04">Advertise</SectionLabel>
            <h2 className="gm-display mt-6 text-[52px] font-bold uppercase leading-[0.92] text-white sm:text-[80px]">
              Plan a campaign
              <br />
              <span className="text-white/35">in thirty seconds.</span>
            </h2>
          </Reveal>
          <Reveal className="lg:col-span-5" delay={0.1}>
            <p className="max-w-md text-[16px] leading-relaxed text-white/65">
              Set a budget and pick your divisions. The estimate uses this year&apos;s rate card and audience data, and our team will turn it into a full media plan.
            </p>
          </Reveal>
        </div>

        <Reveal delay={0.1} className="mt-14 grid overflow-hidden rounded-2xl border border-white/10 bg-[#12131A] lg:grid-cols-12">
          {/* Controls */}
          <div className="space-y-9 border-b border-white/10 p-6 sm:p-8 lg:col-span-5 lg:border-b-0 lg:border-r">
            <div>
              <div className="flex items-baseline justify-between">
                <label htmlFor="gm-budget" className="text-[13px] font-medium text-white/80">
                  Budget
                </label>
                <span className="gm-display text-[34px] font-semibold tabular-nums leading-none text-white">${formatCompact(budget)}</span>
              </div>
              <input
                id="gm-budget"
                type="range"
                min={25_000}
                max={500_000}
                step={5_000}
                value={budget}
                onChange={(e) => setBudget(Number(e.target.value))}
                className="mt-4 h-1.5 w-full cursor-pointer appearance-none rounded-full outline-none [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-white [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-[0_0_0_6px_rgba(47,84,255,0.35)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_8px_rgba(47,84,255,0.6)]"
                style={{ background: `linear-gradient(90deg,#2F54FF ${budgetPct}%,rgba(255,255,255,0.12) ${budgetPct}%)` }}
              />
              <div className="gm-mono mt-2 flex justify-between text-[10.5px] text-white/35">
                <span>$25K</span>
                <span>$500K</span>
              </div>
            </div>

            <fieldset>
              <legend className="mb-3 text-[13px] font-medium text-white/80">Flight</legend>
              <div className="grid grid-cols-4 gap-1 rounded-xl border border-white/10 bg-black/20 p-1">
                {WEEKS.map((w) => (
                  <button
                    key={w}
                    type="button"
                    aria-pressed={weeks === w}
                    onClick={() => setWeeks(w)}
                    className={cn(
                      "relative rounded-lg py-2 text-[13px] transition-colors",
                      weeks === w ? "text-[#0E0E12]" : "text-white/60 hover:text-white",
                    )}
                  >
                    {weeks === w && <motion.span layoutId="gm-weeks" className="absolute inset-0 rounded-lg bg-white" transition={{ type: "spring", stiffness: 500, damping: 40 }} />}
                    <span className="relative">{w} wks</span>
                  </button>
                ))}
              </div>
            </fieldset>

            <fieldset>
              <legend className="mb-3 text-[13px] font-medium text-white/80">Objective</legend>
              <div className="space-y-2">
                {OBJECTIVES.map((o) => {
                  const on = o.id === objectiveId;
                  return (
                    <button
                      key={o.id}
                      type="button"
                      aria-pressed={on}
                      onClick={() => setObjectiveId(o.id)}
                      className={cn(
                        "flex w-full items-center justify-between rounded-xl border px-4 py-3 text-left transition-colors",
                        on ? "border-[var(--gm-blue)] bg-[var(--gm-blue)]/12" : "border-white/10 hover:border-white/25",
                      )}
                    >
                      <span>
                        <span className="block text-[14px] font-medium text-white">{o.label}</span>
                        <span className="block text-[12px] text-white/45">{o.hint}</span>
                      </span>
                      <span className={cn("grid size-5 place-items-center rounded-full border", on ? "border-[var(--gm-blue)] bg-[var(--gm-blue)]" : "border-white/25")}>
                        {on && <Check className="size-3 text-white" />}
                      </span>
                    </button>
                  );
                })}
              </div>
            </fieldset>

            <fieldset>
              <legend className="mb-3 text-[13px] font-medium text-white/80">Divisions</legend>
              <div className="flex flex-wrap gap-2">
                {DIVISIONS.map((d, i) => {
                  const on = picked.includes(i);
                  return (
                    <button
                      key={d.code}
                      type="button"
                      aria-pressed={on}
                      onClick={() => toggle(i)}
                      className={cn(
                        "inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-[12.5px] transition-colors",
                        on ? "border-white bg-white text-[#0E0E12]" : "border-white/15 text-white/60 hover:border-white/35 hover:text-white",
                      )}
                    >
                      {on && <Check className="size-3" />}
                      {d.short}
                    </button>
                  );
                })}
              </div>
            </fieldset>
          </div>

          {/* Estimate */}
          <div className="flex flex-col p-6 sm:p-8 lg:col-span-7">
            <div className="flex items-center justify-between">
              <span className="gm-mono text-[11px] uppercase tracking-[0.2em] text-white/40">Estimated delivery</span>
              <span className="gm-mono text-[11px] text-white/40">
                {weeks} weeks · {objective.label}
              </span>
            </div>

            <div className="mt-6 grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-white/10 bg-white/10 sm:grid-cols-4">
              {[
                { k: "Unique reach", v: plan.reach, f: formatCompact, big: true },
                { k: "Impressions", v: plan.impressions, f: formatCompact },
                { k: "Effective CPM", v: plan.cpm, f: (n: number) => `$${n.toFixed(2)}` },
                { k: "Engagements", v: plan.engagements, f: formatCompact },
              ].map((m) => (
                <div key={m.k} className={cn("bg-[#12131A] p-4", m.big && "bg-[var(--gm-blue)]")}>
                  <div className="gm-display text-[34px] font-semibold leading-none tabular-nums text-white sm:text-[38px]">
                    <RollingNumber value={m.v} format={m.f} duration={600} />
                  </div>
                  <div className={cn("mt-2 text-[11.5px]", m.big ? "text-white/80" : "text-white/45")}>{m.k}</div>
                </div>
              ))}
            </div>

            <div className="mt-8">
              <div className="gm-mono mb-3 flex justify-between text-[11px] uppercase tracking-[0.2em] text-white/40">
                <span>Reach by division</span>
                <span>Spend</span>
              </div>
              <ul className="space-y-3">
                {plan.rows.map((r) => (
                  <li key={r.d.code} className="grid grid-cols-[7.5rem_1fr_4.5rem] items-center gap-3 text-[13px]">
                    <span className="truncate text-white/75">{r.d.short}</span>
                    <span className="h-2 overflow-hidden rounded-full bg-white/[0.07]">
                      <motion.span
                        className="block h-full rounded-full bg-gradient-to-r from-[#2F54FF] to-[#8EA2FF]"
                        initial={false}
                        animate={{ width: `${(r.reach / maxRowReach) * 100}%` }}
                        transition={{ duration: 0.5, ease: EASE }}
                      />
                    </span>
                    <span className="gm-mono text-right tabular-nums text-white/60">${formatCompact(r.spend)}</span>
                  </li>
                ))}
              </ul>
            </div>

            <dl className="mt-8 grid grid-cols-3 gap-4 border-y border-white/10 py-4 text-[12px]">
              {[
                { k: "Avg. frequency", v: `${plan.frequency.toFixed(1)}x` },
                { k: "Per week", v: `${formatCompact(plan.impressions / weeks)} imp.` },
                { k: "Cost per person", v: `$${(budget / (plan.reach || 1)).toFixed(3)}` },
              ].map((m) => (
                <div key={m.k}>
                  <dt className="text-white/40">{m.k}</dt>
                  <dd className="gm-mono mt-1 text-[14px] tabular-nums text-white">{m.v}</dd>
                </div>
              ))}
            </dl>

            <div className="mt-8">
              <div className="gm-mono mb-3 text-[11px] uppercase tracking-[0.2em] text-white/40">Format mix</div>
              <MixBar mix={Object.fromEntries(plan.mix.map(({ k, v }) => [k, Math.round(v)])) as Division["mix"]} />
            </div>

            <div className="mt-auto pt-8">
              <div className="rounded-xl border border-white/10 bg-black/20 p-4">
                <AnimatePresence mode="wait" initial={false}>
                  {sent ? (
                    <motion.div key="sent" initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} className="flex items-center justify-between gap-3">
                      <span className="flex items-center gap-3 text-[13px] text-white/80">
                        <span className="grid size-8 place-items-center rounded-full bg-emerald-400/15 text-emerald-300">
                          <Check className="size-4" />
                        </span>
                        Plan and media kit sent to <span className="font-medium text-white">{email}</span>
                      </span>
                      <button type="button" onClick={() => setSent(false)} className="gm-link shrink-0 text-[12px] text-white/50 hover:text-white">
                        Change
                      </button>
                    </motion.div>
                  ) : (
                    <motion.form key="form" onSubmit={submit} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="flex flex-col gap-2 sm:flex-row">
                      <label htmlFor="gm-email" className="sr-only">
                        Work email
                      </label>
                      <div className="flex flex-1 items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 focus-within:border-[var(--gm-blue)]">
                        <Mail className="size-4 text-white/40" />
                        <input
                          id="gm-email"
                          type="email"
                          required
                          value={email}
                          onChange={(e) => setEmail(e.target.value)}
                          placeholder="you@brand.com"
                          className="h-11 w-full bg-transparent text-[14px] text-white placeholder:text-white/30 focus:outline-none"
                        />
                      </div>
                      <button type="submit" className="inline-flex h-11 items-center justify-center gap-2 rounded-lg bg-white px-5 text-[13px] font-semibold text-[#0E0E12] transition-colors hover:bg-[#DCE2FF]">
                        <Download className="size-4" />
                        Send me this plan
                      </button>
                    </motion.form>
                  )}
                </AnimatePresence>
              </div>
              <p className="mt-3 text-[11.5px] text-white/35">Estimates only. Final delivery depends on inventory and creative.</p>
            </div>
          </div>
        </Reveal>
      </div>
    </section>
  );
}

/* ─── Newsroom ─────────────────────────────────────────────────────────────── */

function StoryArt({ art, large = false }: { art: StoryArtSpec; large?: boolean }) {
  const pattern: Record<ArtPattern, { backgroundImage: string; backgroundSize: string }> = {
    dots: { backgroundImage: `radial-gradient(${art.accent}66 1px, transparent 1.5px)`, backgroundSize: large ? "14px 14px" : "11px 11px" },
    grid: {
      backgroundImage: `linear-gradient(${art.accent}26 1px, transparent 1px), linear-gradient(90deg, ${art.accent}26 1px, transparent 1px)`,
      backgroundSize: large ? "36px 36px" : "22px 22px",
    },
    lines: { backgroundImage: `repeating-linear-gradient(115deg, ${art.accent}24 0 1px, transparent 1px 9px)`, backgroundSize: "auto" },
    rings: { backgroundImage: `repeating-radial-gradient(circle at 78% 82%, ${art.accent}30 0 1px, transparent 1px 16px)`, backgroundSize: "auto" },
  };
  return (
    <div className="relative size-full overflow-hidden" style={{ background: `linear-gradient(140deg, ${art.from}, ${art.to})` }}>
      <div aria-hidden className="absolute inset-0 transition-transform duration-[1.2s] ease-out group-hover:scale-110" style={pattern[art.pattern]} />
      <div
        aria-hidden
        className="absolute -bottom-[30%] -right-[18%] aspect-square w-[70%] rounded-full transition-transform duration-[1.2s] ease-out group-hover:-translate-x-[4%] group-hover:-translate-y-[4%]"
        style={{ background: `radial-gradient(circle at 32% 30%, ${art.accent}CC, ${art.accent}33 45%, transparent 70%)` }}
      />
      <div aria-hidden className="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-black/45 to-transparent" />
      <div aria-hidden className="gm-grain absolute inset-0 opacity-[0.12] mix-blend-overlay" />
      <span className={cn("gm-display absolute bottom-3 left-4 font-bold leading-none text-white/90", large ? "text-[96px] sm:text-[140px]" : "text-[56px]")}>{art.glyph}</span>
    </div>
  );
}

function Byline({ story }: { story: Story }) {
  const initials = story.author
    .split(" ")
    .map((w) => w[0])
    .join("")
    .slice(0, 2);
  return (
    <div className="flex items-center gap-2.5 text-[12px] text-white/45">
      <span className="grid size-6 place-items-center rounded-full bg-white/10 text-[10px] font-semibold text-white/80">{initials}</span>
      <span className="text-white/70">{story.author}</span>
      <span>·</span>
      <span>{story.when}</span>
      <span>·</span>
      <span>{story.read} min</span>
    </div>
  );
}

function Kicker({ story }: { story: Story }) {
  return (
    <div className="gm-mono flex flex-wrap items-center gap-x-2.5 gap-y-1 text-[10.5px] uppercase tracking-[0.16em] text-white/45">
      <span className="text-[var(--gm-blue-soft)]">{DESK_LABEL[story.desk]}</span>
      <span>·</span>
      <span>{story.bureau}</span>
    </div>
  );
}

function NewsletterSignup() {
  const [email, setEmail] = useState("");
  const [done, setDone] = useState(false);
  return (
    <div className="relative mt-6 overflow-hidden rounded-2xl border border-white/10 bg-[#12131A] p-6 sm:p-8">
      <div aria-hidden className="pointer-events-none absolute -left-20 -top-24 size-72 rounded-full bg-[var(--gm-blue)] opacity-25 blur-[90px]" />
      <div className="relative grid gap-6 lg:grid-cols-12 lg:items-center">
        <div className="lg:col-span-6">
          <div className="gm-mono text-[11px] uppercase tracking-[0.2em] text-[var(--gm-blue-soft)]">The Morning Brief</div>
          <h3 className="gm-serif mt-3 text-[26px] font-semibold leading-tight text-white sm:text-[30px]">Five stories from twelve bureaus, before your first coffee.</h3>
          <p className="mt-2 text-[14px] text-white/55">Every weekday at 6am Gulf time. 410,000 readers. One click to leave.</p>
        </div>
        <div className="lg:col-span-6">
          <AnimatePresence mode="wait" initial={false}>
            {done ? (
              <motion.div key="ok" initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-3 rounded-xl border border-emerald-400/25 bg-emerald-400/10 px-4 py-4 text-[14px] text-emerald-200">
                <Check className="size-4" />
                You&apos;re in. The first brief lands tomorrow at 6am.
              </motion.div>
            ) : (
              <motion.form
                key="form"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                onSubmit={(e) => {
                  e.preventDefault();
                  if (/^\S+@\S+\.\S+$/.test(email)) setDone(true);
                }}
                className="flex flex-col gap-2 sm:flex-row"
              >
                <label htmlFor="gm-brief" className="sr-only">
                  Email
                </label>
                <input
                  id="gm-brief"
                  type="email"
                  required
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  placeholder="you@email.com"
                  className="h-12 flex-1 rounded-xl border border-white/10 bg-white/[0.04] px-4 text-[14px] text-white placeholder:text-white/30 focus:border-[var(--gm-blue)] focus:outline-none"
                />
                <button type="submit" className="inline-flex h-12 items-center justify-center gap-2 rounded-xl bg-white px-5 text-[13px] font-semibold text-[#0E0E12] transition-colors hover:bg-[#DCE2FF]">
                  Subscribe
                  <ArrowRight className="size-4" />
                </button>
              </motion.form>
            )}
          </AnimatePresence>
        </div>
      </div>
    </div>
  );
}

function NewsroomSection() {
  const [desk, setDesk] = useState<DeskId | "all">("all");
  const ticker = [...LIVE_DESK, ...LIVE_DESK];
  const filtered = desk === "all" ? STORIES : STORIES.filter((s) => s.desk === desk);
  const [lead, ...rest] = filtered;
  // A single desk only has a couple of stories, so top the row up from the other desks.
  const grid = [...rest, ...STORIES.filter((s) => !filtered.includes(s))].slice(0, 4);
  const mostRead = MOST_READ.map((id) => STORIES.find((s) => s.id === id)).filter((s): s is Story => Boolean(s));

  return (
    <section id="newsroom" className="relative scroll-mt-6 border-t border-white/10 bg-[var(--gm-bg)] pb-24 lg:pb-32">
      {/* Live desk ticker */}
      <div className="gm-marquee-host flex items-stretch border-b border-white/10 bg-[#0B0B0F]">
        <div className="gm-mono z-10 flex shrink-0 items-center gap-2 bg-[var(--gm-blue)] px-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-white sm:px-6">
          <Radio className="size-3.5" />
          Live desk
        </div>
        <div className="relative flex-1 overflow-hidden py-3.5 [mask-image:linear-gradient(90deg,transparent,black_4%,black_96%,transparent)]">
          <div className="gm-marquee-fast flex w-max items-center">
            {ticker.map((item, i) => (
              <span key={i} aria-hidden={i >= LIVE_DESK.length} className="flex shrink-0 items-center gap-3 px-6 text-[13px]">
                <span className="gm-mono text-white/40">{item.time}</span>
                <span className="gm-mono text-[11px] uppercase tracking-[0.16em] text-[var(--gm-blue-soft)]">{item.bureau}</span>
                <span className="text-white/80">{item.text}</span>
                <span className="ml-3 size-1 rounded-full bg-white/25" />
              </span>
            ))}
          </div>
        </div>
      </div>

      <div className="mx-auto max-w-[1520px] px-6 pt-24 sm:px-12 lg:px-16 lg:pt-32">
        <div className="grid gap-10 lg:grid-cols-12 lg:items-end">
          <Reveal className="lg:col-span-7">
            <SectionLabel index="05">Newsroom</SectionLabel>
            <h2 className="gm-display mt-6 text-[52px] font-bold uppercase leading-[0.92] text-white sm:text-[80px]">
              From the desks
              <br />
              <span className="text-white/35">this week.</span>
            </h2>
          </Reveal>
          <Reveal className="lg:col-span-5" delay={0.1}>
            <p className="max-w-md text-[16px] leading-relaxed text-white/65">
              {STORIES.length * 31} stories filed this week from {BUREAUS.length} bureaus. Here is what our editors are reading, and what you are.
            </p>
          </Reveal>
        </div>

        {/* Desk filter */}
        <div role="tablist" aria-label="Filter by desk" className="mt-12 flex gap-2 overflow-x-auto border-b border-white/10 pb-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
          {DESKS.map((d) => {
            const on = d.id === desk;
            const count = d.id === "all" ? STORIES.length : STORIES.filter((s) => s.desk === d.id).length;
            return (
              <button
                key={d.id}
                type="button"
                role="tab"
                aria-selected={on}
                onClick={() => setDesk(d.id)}
                className={cn(
                  "relative inline-flex shrink-0 items-center gap-2 rounded-full px-4 py-2 text-[13px] transition-colors",
                  on ? "text-[#0E0E12]" : "text-white/60 hover:text-white",
                )}
              >
                {on && <motion.span layoutId="gm-desk" className="absolute inset-0 rounded-full bg-white" transition={{ type: "spring", stiffness: 500, damping: 40 }} />}
                <span className="relative">{d.label}</span>
                <span className={cn("gm-mono relative text-[10.5px]", on ? "text-[#0E0E12]/50" : "text-white/30")}>{count}</span>
              </button>
            );
          })}
        </div>

        <div className="mt-8 grid gap-6 lg:grid-cols-12">
          {/* Lead story */}
          <div className="lg:col-span-8">
            <AnimatePresence mode="wait">
              <motion.a
                key={lead.id}
                href="#newsroom"
                initial={{ opacity: 0, y: 12 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0, y: -8 }}
                transition={{ duration: 0.35, ease: EASE }}
                className="group grid h-full overflow-hidden rounded-2xl border border-white/10 bg-[#12131A] transition-colors hover:border-white/25 md:grid-cols-5"
              >
                <div className="relative aspect-[16/10] md:col-span-3 md:aspect-auto md:min-h-[440px]">
                  <StoryArt art={lead.art} large />
                  <span className="gm-mono absolute left-4 top-4 rounded-full bg-black/40 px-2.5 py-1 text-[10.5px] uppercase tracking-[0.16em] text-white backdrop-blur-md">Lead story</span>
                </div>
                <div className="flex flex-col justify-between gap-8 p-6 sm:p-8 md:col-span-2">
                  <div>
                    <Kicker story={lead} />
                    <h3 className="gm-serif mt-4 text-[28px] font-semibold leading-[1.12] text-white transition-colors group-hover:text-[#C9D2FF] sm:text-[32px]">{lead.title}</h3>
                    <p className="mt-4 text-[15px] leading-relaxed text-white/60">{lead.dek}</p>
                  </div>
                  <div className="flex flex-col gap-5">
                    <Byline story={lead} />
                    <span className="inline-flex items-center gap-2 text-[13px] font-medium text-white">
                      <span className="gm-link">Read the story</span>
                      <ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
                    </span>
                  </div>
                </div>
              </motion.a>
            </AnimatePresence>
          </div>

          {/* Most read */}
          <aside className="rounded-2xl border border-white/10 bg-[#0B0B0F] p-6 lg:col-span-4">
            <div className="flex items-center justify-between">
              <div className="gm-mono text-[11px] uppercase tracking-[0.2em] text-white/45">Most read</div>
              <div className="gm-mono text-[11px] text-white/30">24h</div>
            </div>
            <ol className="mt-4 divide-y divide-white/10">
              {mostRead.map((s, i) => (
                <li key={s.id}>
                  <a href="#newsroom" className="group grid grid-cols-[2.5rem_1fr] gap-3 py-4">
                    <span className="gm-display text-[34px] font-bold leading-none text-white/20 transition-colors group-hover:text-[var(--gm-blue-soft)]">{i + 1}</span>
                    <span>
                      <span className="gm-serif block text-[16px] font-semibold leading-snug text-white/90 transition-colors group-hover:text-white">{s.title}</span>
                      <span className="mt-1.5 block text-[12px] text-white/40">
                        {s.bureau} · {s.read} min
                      </span>
                    </span>
                  </a>
                </li>
              ))}
            </ol>
          </aside>
        </div>

        {/* Story grid */}
        {grid.length > 0 && (
          <div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
            <AnimatePresence mode="popLayout">
              {grid.map((s, i) => (
                <motion.a
                  key={s.id}
                  layout
                  href="#newsroom"
                  initial={{ opacity: 0, y: 16 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, scale: 0.97 }}
                  transition={{ duration: 0.4, delay: 0.05 * i, ease: EASE }}
                  className="group flex flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#12131A] transition-colors hover:border-white/25"
                >
                  <div className="aspect-[4/3]">
                    <StoryArt art={s.art} />
                  </div>
                  <div className="flex flex-1 flex-col justify-between gap-5 p-5">
                    <div>
                      <Kicker story={s} />
                      <h3 className="gm-serif mt-3 text-[19px] font-semibold leading-snug text-white transition-colors group-hover:text-[#C9D2FF]">{s.title}</h3>
                      <p className="mt-2 line-clamp-2 text-[13px] leading-relaxed text-white/50">{s.dek}</p>
                    </div>
                    <Byline story={s} />
                  </div>
                </motion.a>
              ))}
            </AnimatePresence>
          </div>
        )}

        {/* Watch */}
        <div className="mt-16">
          <div className="flex items-end justify-between gap-4">
            <div>
              <div className="gm-mono text-[11px] uppercase tracking-[0.2em] text-white/45">Watch</div>
              <h3 className="gm-display mt-2 text-[34px] font-semibold uppercase leading-none text-white">From the studio</h3>
            </div>
            <a href="#newsroom" className="group hidden items-center gap-2 text-[13px] font-medium text-white sm:inline-flex">
              <span className="gm-link">All videos</span>
              <ArrowUpRight className="size-4" />
            </a>
          </div>
          <div className="mt-6 grid gap-6 md:grid-cols-3">
            {VIDEOS.map((v, i) => (
              <Reveal key={v.title} delay={0.06 * i}>
                <a href="#newsroom" className="group block">
                  <div className="relative aspect-video overflow-hidden rounded-2xl border border-white/10">
                    <StoryArt art={v.art} />
                    <span className="absolute inset-0 grid place-items-center">
                      <span className="grid size-14 place-items-center rounded-full bg-white/90 text-[#0E0E12] shadow-[0_10px_40px_rgba(0,0,0,0.5)] transition-transform duration-300 group-hover:scale-110">
                        <Play className="ml-0.5 size-5 fill-current" />
                      </span>
                    </span>
                    <span className="gm-mono absolute bottom-3 right-3 rounded-md bg-black/60 px-2 py-0.5 text-[11px] text-white backdrop-blur-md">{v.duration}</span>
                  </div>
                  <div className="mt-4">
                    <div className="gm-mono text-[10.5px] uppercase tracking-[0.16em] text-[var(--gm-blue-soft)]">{v.series}</div>
                    <div className="mt-1.5 text-[16px] font-medium leading-snug text-white transition-colors group-hover:text-[#C9D2FF]">{v.title}</div>
                  </div>
                </a>
              </Reveal>
            ))}
          </div>
        </div>

        <NewsletterSignup />
      </div>
    </section>
  );
}

/* ─── Closing CTA & footer ─────────────────────────────────────────────────── */

function ClosingCta() {
  return (
    <section className="relative overflow-hidden border-t border-white/10 bg-[var(--gm-blue)] py-24 lg:py-32">
      <div aria-hidden className="absolute inset-0 opacity-30" style={{ backgroundImage: "radial-gradient(rgba(255,255,255,0.35) 1px, transparent 1.4px)", backgroundSize: "18px 18px" }} />
      <div aria-hidden className="absolute -right-40 -top-40 size-[640px] rounded-full bg-[radial-gradient(circle_at_35%_35%,#8EA2FF,transparent_60%)] opacity-60" />
      <div className="relative mx-auto max-w-[1520px] px-6 sm:px-12 lg:px-16">
        <Reveal>
          <h2 className="gm-display max-w-5xl text-[56px] font-bold uppercase leading-[0.9] text-white sm:text-[88px] lg:text-[108px]">
            Put your brand in front of 104.6M people.
          </h2>
        </Reveal>
        <Reveal delay={0.1} className="mt-10 flex flex-col gap-3 sm:flex-row">
          <a href="#advertise" className="inline-flex h-12 items-center justify-center gap-2 rounded-full bg-white px-6 text-[14px] font-semibold text-[#0E0E12] transition-transform hover:-translate-y-0.5">
            Plan a campaign
            <ArrowRight className="size-4" />
          </a>
          <a href="#advertise" className="inline-flex h-12 items-center justify-center gap-2 rounded-full border border-white/40 px-6 text-[14px] font-medium text-white transition-colors hover:bg-white/10">
            <Download className="size-4" />
            2026 media kit
          </a>
        </Reveal>
      </div>
    </section>
  );
}

const FOOTER_COLS = [
  { title: "Company", links: ["About", "Leadership", "Careers", "Press"] },
  { title: "Divisions", links: DIVISIONS.slice(0, 4).map((d) => d.short) },
  { title: "Advertise", links: ["Media kit", "Rate card", "Branded content", "Events"] },
  { title: "Follow", links: ["Instagram", "LinkedIn", "YouTube", "Newsletter"] },
];

function Footer() {
  return (
    <footer className="bg-[#08080B] px-6 pb-10 pt-16 sm:px-12 lg:px-16">
      <div className="mx-auto max-w-[1392px]">
        <div className="grid gap-12 lg:grid-cols-12">
          <div className="lg:col-span-4">
            <Wordmark small />
            <p className="mt-5 max-w-xs text-[13px] leading-relaxed text-white/50">
              Independent journalism and the audiences it builds, from Dubai to the world since 1994.
            </p>
          </div>
          <div className="grid grid-cols-2 gap-8 sm:grid-cols-4 lg:col-span-8">
            {FOOTER_COLS.map((col) => (
              <div key={col.title}>
                <div className="gm-mono text-[11px] uppercase tracking-[0.2em] text-white/35">{col.title}</div>
                <ul className="mt-4 space-y-2.5">
                  {col.links.map((l) => (
                    <li key={l}>
                      <a href="#top" className="gm-link text-[13px] text-white/65 transition-colors hover:text-white">
                        {l}
                      </a>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        </div>
        <div className="mt-16 flex flex-col justify-between gap-4 border-t border-white/10 pt-6 text-[12px] text-white/40 sm:flex-row sm:items-center">
          <span>© 2026 Meridian Media Group. All rights reserved.</span>
          <span className="gm-mono inline-flex items-center gap-2 text-emerald-400/80">
            <span className="size-1.5 rounded-full bg-emerald-400" />
            All {BUREAUS.length} bureaus online
          </span>
        </div>
      </div>
    </footer>
  );
}

/* ─── ⌘K search ────────────────────────────────────────────────────────────── */

interface SearchHit {
  id: string;
  group: "Divisions" | "Titles" | "Bureaus" | "Sections";
  label: string;
  hint: string;
  run: () => void;
}

function SearchDialog({
  open,
  onClose,
  onDivision,
  onBureau,
}: {
  open: boolean;
  onClose: () => void;
  onDivision: (i: number) => void;
  onBureau: (id: string) => void;
}) {
  const [query, setQuery] = useState("");
  const [cursor, setCursor] = useState(0);
  const inputRef = useRef<HTMLInputElement | null>(null);

  const all = useMemo<SearchHit[]>(() => {
    const jump = (id: string) => document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
    return [
      ...DIVISIONS.map((d, i) => ({ id: `d${i}`, group: "Divisions" as const, label: d.name, hint: `${d.reach.toFixed(1)}M monthly`, run: () => { onDivision(i); jump("divisions"); } })),
      ...DIVISIONS.flatMap((d, i) => d.titles.map((t) => ({ id: `t${t}`, group: "Titles" as const, label: t, hint: d.short, run: () => { onDivision(i); jump("divisions"); } }))),
      ...BUREAUS.map((b) => ({ id: `b${b.id}`, group: "Bureaus" as const, label: b.city, hint: `${b.staff} journalists`, run: () => { onBureau(b.id); jump("bureaus"); } })),
      { id: "s-adv", group: "Sections" as const, label: "Plan a campaign", hint: "Advertise", run: () => jump("advertise") },
      { id: "s-news", group: "Sections" as const, label: "Latest stories", hint: "Newsroom", run: () => jump("newsroom") },
    ];
  }, [onDivision, onBureau]);

  const q = query.trim().toLowerCase();
  const hits = q ? all.filter((h) => `${h.label} ${h.hint} ${h.group}`.toLowerCase().includes(q)) : all.filter((h) => h.group !== "Titles");
  const safeCursor = Math.min(cursor, Math.max(0, hits.length - 1));

  useEffect(() => {
    if (open) inputRef.current?.focus();
  }, [open]);

  const choose = (hit: SearchHit | undefined) => {
    if (!hit) return;
    onClose();
    setQuery("");
    setCursor(0);
    window.setTimeout(hit.run, 60);
  };

  const onKey = (e: ReactKeyboardEvent) => {
    if (e.key === "ArrowDown") {
      e.preventDefault();
      setCursor((c) => Math.min(c + 1, hits.length - 1));
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setCursor((c) => Math.max(c - 1, 0));
    } else if (e.key === "Enter") {
      e.preventDefault();
      choose(hits[safeCursor]);
    }
  };

  let lastGroup = "";

  return (
    <AnimatePresence>
      {open && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          onClick={onClose}
          className="fixed inset-0 z-50 flex items-start justify-center bg-black/70 px-4 pt-[12vh] backdrop-blur-md"
        >
          <motion.div
            role="dialog"
            aria-modal="true"
            aria-label="Search Meridian"
            initial={{ scale: 0.97, y: -10 }}
            animate={{ scale: 1, y: 0 }}
            exit={{ scale: 0.97, y: -10 }}
            transition={{ duration: 0.2, ease: EASE }}
            onClick={(e) => e.stopPropagation()}
            className="w-full max-w-xl overflow-hidden rounded-2xl border border-white/15 bg-[#12131A] shadow-[0_30px_90px_rgba(0,0,0,0.8)]"
          >
            <div className="flex items-center gap-3 border-b border-white/10 px-4">
              <Search className="size-4 text-white/40" />
              <input
                ref={inputRef}
                value={query}
                onChange={(e) => {
                  setQuery(e.target.value);
                  setCursor(0);
                }}
                onKeyDown={onKey}
                placeholder="Search divisions, titles, bureaus…"
                aria-label="Search"
                className="h-14 flex-1 bg-transparent text-[15px] text-white placeholder:text-white/35 focus:outline-none"
              />
              <button type="button" onClick={onClose} aria-label="Close search" className="rounded-md p-1 text-white/40 hover:text-white">
                <X className="size-4" />
              </button>
            </div>
            <div className="max-h-[50vh] overflow-y-auto p-2" role="listbox" aria-label="Results">
              {hits.length === 0 && <div className="px-3 py-10 text-center text-[13px] text-white/45">No results for “{query}”</div>}
              {hits.map((h, i) => {
                const header = h.group !== lastGroup ? h.group : null;
                lastGroup = h.group;
                return (
                  <div key={h.id}>
                    {header && <div className="gm-mono px-3 pb-1.5 pt-3 text-[10.5px] uppercase tracking-[0.18em] text-white/35">{header}</div>}
                    <button
                      type="button"
                      role="option"
                      aria-selected={i === safeCursor}
                      onMouseMove={() => setCursor(i)}
                      onClick={() => choose(h)}
                      className={cn(
                        "flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-left transition-colors",
                        i === safeCursor ? "bg-white/[0.07]" : "",
                      )}
                    >
                      <span className={cn("text-[14px]", h.group === "Titles" ? "gm-serif italic text-white" : "text-white")}>{h.label}</span>
                      <span className="flex items-center gap-2 text-[12px] text-white/40">
                        {h.hint}
                        {i === safeCursor && <CornerDownLeft className="size-3.5 text-white/60" />}
                      </span>
                    </button>
                  </div>
                );
              })}
            </div>
            <div className="gm-mono flex items-center justify-between border-t border-white/10 px-4 py-2.5 text-[10.5px] text-white/35">
              <span className="inline-flex items-center gap-1.5">
                <Command className="size-3" />K to toggle
              </span>
              <span>↑ ↓ to move · ↵ to open · esc to close</span>
            </div>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

/* ─── Page ─────────────────────────────────────────────────────────────────── */

export function GlobalMediaPage() {
  const [menuOpen, setMenuOpen] = useState(false);
  const [searchOpen, setSearchOpen] = useState(false);
  const [division, setDivision] = useState(0);
  const [bureau, setBureau] = useState(HQ.id);
  const [planRequest, setPlanRequest] = useState({ division: 0, n: 0 });

  useEffect(() => {
    const onKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        setSearchOpen((v) => !v);
      } else if (e.key === "Escape") {
        setSearchOpen(false);
        setMenuOpen(false);
      }
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, []);

  return (
    <MotionConfig reducedMotion="user">
      <div className="gm-root relative h-svh overflow-x-hidden overflow-y-auto [scrollbar-width:thin] selection:bg-[#2F54FF] selection:text-white">
        <PageStyles />
        <Hero onOpenSearch={() => setSearchOpen(true)} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
        <MobileMenu open={menuOpen} onClose={() => setMenuOpen(false)} />
        <main>
          <MastheadStrip />
          <ReachSection />
          <DivisionsSection active={division} setActive={setDivision} onPlan={(i) => setPlanRequest((r) => ({ division: i, n: r.n + 1 }))} />
          <BureausSection active={bureau} setActive={setBureau} />
          <CampaignPlanner request={planRequest} />
          <NewsroomSection />
          <ClosingCta />
        </main>
        <Footer />
        <SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} onDivision={setDivision} onBureau={setBureau} />
      </div>
    </MotionConfig>
  );
}

export default GlobalMediaPage;
components/pages/world-land.ts
// Land mask for the particle globe: 360x180 cells of 1 degree, row 0 at 90°N, column 0 at 180°W.
// One bit per cell (LSB first), base64. Generated from Natural Earth land-50m (public domain) via world-atlas.
export const LAND_W = 360;
export const LAND_H = 180;
export const LAND_MASK =
  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/P8/AGD//z8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8//+F/////3/5AAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAH8H/+//////8HAAAAAAAAADSAAQAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAPD//wf4//////8BAACAdT8AAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAB4+Jzn//D//////38AAACAewMAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAIA4BHAyO0D8/////38AAAAAPAoAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAFhAmPPVH4D//////z8AAAAAAAAAAAAA4AEAAAD8HwAAAAAAAAAAAAAAAAAAAAD/Az/fDwAA/P////8BAAAAAAAAAADABwAAwP//PwAA4FsAAAAAAAAAAAAAAAQMAAQQAAAA8P////8BAAAAAAAAAABwAAAA+P//FwAAAAAAAAAAAAAAAAAAAP8BxvMxfwAA8P///3cAAAAAAAAAAAAcAIDh//9/3IcLAAcAAAAAAAAAAAAAgN9/xy+8xwEA4P///zcAAAAAAAAAAAAGAJ7i//////8fAD4AAAAA/P//////////////////////////////////////////////////////////AADwTwAAAADwH/D4//8AwP7/fxEAAAAAANgHAAAAgN9/////////////DwAAAAD+//+Bvob/H9SBof8AwP7//w8AAAAAoP8PAAAAD9///////////////48//P//////////////////////////////HwAA/PyFAAECAAAAAAAAAAAAAAAA4P//////////////////////////////DwAA4A4AADAAAAAAAAAAAAAAAAAAAP7/////////////////////////////AQBg4AAAAAwAAAAAAAAAAAAAAAAADv//////////////////////////////AByADwAAAAAAAAAAAAAAAAAAAAAAwAD4///////////Pw/gHAP4PAMA/AACA//F//v////////////////////8fAAP4///////////BAGAHAP4HAAADAACw//z///////////////////////9/AID//////////38AQ4AJAPwDAAAAAAD8P/7/////////////////////f/5fAMD//////////z8AwA8AAPgDAAAAAAD+H/z/////////////////////Mf8BAID/7/z//////x8AwD8AAMAAAAAAAAT+P7z+//////////////////9/wG8AAAD8AwD//////x8AwD8IAAAAAAAAAAC+P2D///////////////////91cAAAAACACwD4/////38AwP8eAAAAAAAAgAGcH/T//////////////////wEAOAAAAACADgDw/////38AgP8/AAAAAAAA4AMAX/T//////////////////wAAfgAAAAAwAABA//////8PgP8/AAAAAAAAgAFwHv7/////////////////PwAAfwAAAAAAAABA//////8/gP//AAAAAAAAgAPwAv7/////////////////DwAAPwAAAIAAAAAA/P//////8///AwAAAAAAcAcgcv//////////////////HwAAHwAAAAAAAACA/P//////4///DwAAAAAAPA7o/////////////////////wEADwAAAAAAAAAA8f//////4///DwAAAAAAPD/+/////////////////////wUAAwAAAAAAAAAA8P//////////FwAAAAAAAB///////////////////////wUAAwAAAAAAAAAAkP//////////CAAAAAAAgMD//////////////////////wwAAAAAAAAAAAAAYP////////8xLAAAAAAAAPT//////////////////////wwAAAAAAAAAAAAAAP///////38PfAAAAAAAgP///////////////////////wQAAAAAAAAAAAAAAP3///////8HUAAAAAAAAPz/////////////////////fwQAAAAAAAAAAAAAAP////////+XAAAAAAAAAPz////3/B/+////////////PwgAAAAAAAAAAAAAAP////////9/AAAAAAAAAPj//f9j/A/+////////////HwAAAAAAAAAAAAAAAP////////8MAAAAAAAAAPj/+P8B/Mf/////////////DwQAAAAAAAAAAAAAAP///////z8AAAAAAAAAcPzH4/8B8I//////////////Bx4AAAAAAAAAAAAAAP///////x8AAAAAAAAA+H+gw/8AwA/+//////////9/AAQAAAAAAAAAAAAAAP///////x8AAAAAAAAA+D8Ajv/w4B/8//////////8/AAAAAAAAAAAAAAAAAP///////wMAAAAAAAAA+B8wuA/+/z/+/////////98fAAMAAAAAAAAAAAAAAP///////wMAAAAAAAAA+E8wEIf//x/+/////////2cOAAMAAAAAAAAAAAAAAP7//////wEAAAAAgAAA+A8AEM7//x/8/////////wMOAAMAAAAAAAAAAAAAAPz//////wAAAAAAAAAA+AcABoT//x/8/////////zcYgAEAAAAAAAAAAAAAAPz//////wAAAAAAAAAAQOB/AAA2/////////////x8Y8AEAAAAAAAAAAAAAAPj//////wAAAAAAAAAAQPh/AAAA/////////////w8c/AAAAAAAAAAAAAAAAPj/////fwAAAAAAAAAA4P8/AAAA/////////////w+EGwAAAAAAAAAAAAAAAMD/////HwAAAAAAAAAA8P9/AACA/////////////x/EAwAAAAAAAAAAAAAAAID/////BwAAAAAAAAAA+P//BweA/////////////x+AAAAAAAAAAAAAAAAAAID9////BwAAAAAAAAAA/P//D3/A/////////////x9AAAAAAAAAAAAAAAAAAAD5////BwAAAAAAAAAA/P//f////////////////x8AAAAAAAAAAAAAAAAAAAD2/x8CBgAAAAAAAAAA/P/////v/4///////////z8AAAAAAAAAAAAAAAAAAAD0/wcABgAAAAAAAAAA/v///////4///////////z8AAAAAAAAAAAAAAAAAAADu/wcADgAAAAAAAACA//////8//x/+/////////x8AAAAAAAAAAAAAAAAAAACY/wcADAAAAAAAAADA//////8//z/g/////////w8AAAAAAAAAAAAAAAAAAACQ/wcACAAAAAAAAADg//////9//j+Av////////w8AAAAAAAAAAAAAAAAAAAAQ/wMAAAAAAAAAAADg//////9//n8YgP///////yMAAAAAAAAAAAAAAAAAAABA/gMAAAAAAAAAAADw////////+P9/AP///////xEAAAAAAAAAAAAAAAAAAAAA/AMAPQAAAAAAAADw////////+P//APz/////PxAAAAAAAAAAAAAAAAAAAAAA+AMIYAAAAAAAAAD4////////+f9/AOz/B///BAAAAAAAAAAAAAAAAAAAAAAA/AccgAEAAAAAAADw////////8f9/AOD/B/5/BAAAAAAAAAAAAAAAAQAAAAAA+A8eADgAAAAAAADw////////4f8/AOD/Afw/BgAAAAAAAAAAAAAAAAAAAAAA8J8OAPwAAAAAAADw////////4/8fAOD/APw/AgAAAAAAAAAAAAAAAAAAAAAAgP8PAAAAAAAAAADw////////x/8HAOB/APh/ADAAAAAAAAAAAAAAAAAAAAAAAP4PAAAAAAAAAADw////////h/8BAOA/AMz/ADAAAAAAAAAAAAAAAAAAAAAAAID/AAAAAAAAAAD4////////j/8AAMAPAMD/ARAAAAAAAAAAAAAAAAAAAAAAAAD/AQAAAAAAAAD4////////nx8AAMAPAMD/ATAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAD4////////vwcAAIAPAMD+AdAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAD4////////fwACAIAPAID8AQABAAAAAAAAAAAAAAAAAAAAAADAAEgAAAAAAADg////////f0AAAAAPAID4AUADAAAAAAAAAAAAAAAAAAAAAADAAe8TAAAAAADg/////////34AAAAPAEBwAAgAAAAAAAAAAAAAAAAAAAAAAAAAE+9/AAAAAADA/////////38AAAAXAEAgAAQCAAAAAAAAAAAAAAAAAAAAAAAAz///AAAAAACA/////////z8AAAASAMAAAIADAAAAAAAAAAAAAAAAAAAAAAAAyP//AQAAAAAA/////////z8AAAAwAIAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAgP//AwAAAAAA/n///////x8AAAAwAAADAAMDAAAAAAAAAAAAAAAAAAAAAAAAgP//fwAAAAAA/A/+/////x8AAAAAAAgHgAcAAAAAAAAAAAAAAAAAAAAAAAAAgP///wAAAAAAEADk/////w8AAAAAADAGwAcAAAAAAAAAAAAAAAAAAAAAAAAAgP///wEAAAAAAADA/////wcAAAAAAGAG4AEAAAAAAAAAAAAAAAAAAAAAAAAAwP///wEAAAAAAADA/////wMAAAAAAMAM+AMQAAAAAAAAAAAAAAAAAAAAAAAA4P///wMAAAAAAADA/////wAAAAAAAIAL/gMQAAAAAAAAAAAAAAAAAAAAAAAA8P///wEAAAAAAADg////fwAAAAAAAIAH/uMQAAAAAAAAAAAAAAAAAAAAAAAA8P///w8AAAAAAADg////PwAAAAAAAAAX/gNIAQAAAAAAAAAAAAAAAAAAAAAA+P//f3cAAAAAAADg////PwAAAAAAAAAO/DkIAwAAAAAAAAAAAAAAAAAAAAAA+P///3sAAAAAAADA////HwAAAAAAAAB+/DkA8AEAAAAAAAAAAAAAAAAAAAAA+P////8fAAAAAACA////DwAAAAAAAAA8gChE/g8AAAAAAAAAAAAAAAAAAAAA+P////8/AAAAAAAA////BwAAAAAAAAA4AAAA8B8AAAAAAAAAAAAAAAAAAAAA+P//////AQAAAAAA////BwAAAAAAAAAwAAgAxD8NAAAAAAAAAAAAAAAAAAAA8P//////AQAAAAAA////BwAAAAAAAADAAQAAhP+AAAAAAAAAAAAAAAAAAAAA8P//////AQAAAAAA/v//BwAAAAAAAACAHwCAwH8AAAAAAAAAAAAAAAAAAAAA4P//////AQAAAAAA/v//BwAAAAAAAAAAgN8EAMcAAAAAAAAAAAAAAAAAAAAAwP//////AAAAAAAA/v//DwAAAAAAAAAAAAgBAIAFAAAAAAAAAAAAAAAAAAAAwP//////AAAAAAAA/P//DwAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAgP////9/AAAAAAAA/P//DwAAAAAAAAAAAABAAQQgAAAAAAAAAAAAAAAAAAAAgP////8/AAAAAAAA/v//HyAAAAAAAAAAAACAHwQAAAAAgAAAAAAAAAAAAAAAAP////8fAAAAAAAA/v//HzAAAAAAAAAAAADADwwAAAAAAAAAAAAAAAAAAAAAAP////8fAAAAAAAA////HzAAAAAAAAAAAADMDxwAAAAAAAAAAAAAAAAAAAAAAP7///8fAAAAAAAA////HzwAAAAAAAAAAAD/Dx4AAAAAAAAAAAAAAAAA/P////////8fAAAAAAAA////Dz8AAAAAAAAAAID/vx4AAAiAAAAAQAAAAAAAAAAAAOD///8fAAAAAAAA////Ax8AAAAAAAAAAMD//z4AAAAAAAAAAAAAAAAAAAAAAMD///8PAAAAAAAA////AR8AAAAAAAAAAMD//z8AAAAAAAAAAAAAAAAAAAAAAMD///8PAAAAAAAA/v9/AB8AAAAAAAAAAOD///8AAAAAAAAAAAAAAAAAAAAAAMD///8PAAAAAAAA/v9/AB8gAAAAAAAAAPz///8BAAEAAAAAAAAAAAAAAAAAAMD///8HAAAAAAAA/P9/gA8AAAAAAAAAgP////8BAAIAAAAAAAAAAAAAAAAAAMD///8DAAAAAAAA/P//gA8AAAAAAAAAwP////8DAAAAAAAAAAAAAAAAAAAAAMD//38AAAAAAAAA/P9/AA8AAAAAAAAAwP////8HAAAAAAAAAAAAAAAAAAAAAOD//x8AAAAAAAAA+P9/AAcAAAAAAAAA4P////8PAAAAAAAAAAAAAAAAAAAAAOD//wcAAAAAAAAA+P8fAAIAAAAAAAAAwP////8fAAAAAAAAAAAAAAAAAAAAAOD//wcAAAAAAAAA+P8fAAAAAAAAAAAA4P////8fAAAAAAAAAAAAAAAAAAAAAOD//w8AAAAAAAAA+P8fAAAAAAAAAAAAwP////8/AAAAAAAAAAAAAAAAAAAAAOD//wcAAAAAAAAA8P8PAAAAAAAAAAAAgP////8/AAAAAAAAAAAAAAAAAAAAAOD//wMAAAAAAAAA4P8HAAAAAAAAAAAAgP////8fAAAAAAAAAAAAAAAAAAAAAPD//wMAAAAAAAAA4P8HAAAAAAAAAAAAgP////8fAAAAAAAAAAAAAAAAAAAAAPD//wAAAAAAAAAAwP8DAAAAAAAAAAAAAP////8fAAAAAAAAAAAAAAAAAAAAAOD//wAAAAAAAAAAwP8BAAAAAAAAAAAAAP8D/P8PAAAAAAAAAAAAAAAAAAAAAPD/fQAAAAAAAAAAwH8AAAAAAAAAAAAAAP8A2P8HAAAAAAAAAAAAAAAAAAAAAPD/OwAAAAAAAAAAgAAAAAAAAAAAAAAAAAcA6P8HAAAAAAAAAAAAAAAAAAAAAPj/BwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwP8DAAACAAAAAAAAAAAAAAAAAPj/BwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8DAAAEAAAAAAAAAAAAAAAAAPz/BwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8DAAAIAAAAAAAAAAAAAAAAAPz/AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGgAAAA4AAAAAAAAAAAAAAAAAPg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAPw/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAPwHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAALAAAAAAAAAAAAAAAAAPgPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAIADAAAAAAAAAAAAAAAAAPgHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAMABAAAAAAAAAAAAAAAAAPgHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAP4BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAP4BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAPwDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANADAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAMAfAAAAAAB/eADGPwAAQAAAAAAAAAAAAAAAAAAAAIADAAAAAAAAAAAAAAAAAOT/AQCA/////////w8AAAAAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAAAAAAA4P///wP4//////////8HAAAAAAAAAAAAAAAAAAAAALgfAAAAAAAAAAAAAABg8P///wH/////////////DwAAAAAAAAAAAAAAAAAAAGA/AAAAAAAAAAA4fwL+/////+H/////////////XwAAAAAAAAAAAAAAAAAAAPg+AAAAAAAA9/f///////////j//////////////38AAAAAAAAAAAAAAA4AAHh+AAAAAAAA////////////f/7//////////////x8AAAAAAAAAIAAAAP9/WPp/AAAAAADw/////////////////////////////wcAAAAAAAAAEeA3AP////9/AAAAAAD8////////////////////////////fwEAAAAAAID///////////8PAAAAAID+////////////////////////////PwAAAAAAAP7//////////z8AAAAAgP//////////////////////////////fwAAAADAH////////////wMAAAAA/P///////////////////////////////xwAAAAA////////////fAABAPwA/////////////////////////////////wcAAAAe4P///////////zAAAPwBwP//////////////////////////////DwAAAAAAwP///////////wD8wB8A+P//////////////////////////////HwAAAAAG/v////////////8HAAD8////////////////////////////////HwAAAAAA+P////////////8f8P///////////////////////////////////wEAAPjr/////////////////////////////////////////////////////38AgP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
Sign in