/* ============================================================================
   HUMANITY CONTAINED — the marketing site's new face (owner 2026-10-29).
   "Remodel the marketing site … speak on Outter Gates and how the game is a
   player driven virtual economy platform … in the SCP Universe after a flood
   from the goddess Abraxas … be heavily inspired by the Resident Evil Requiem
   site … remove all of the card game stuff … keep the shop, E broadcast,
   lore, news."

   This file is its own <script type="text/babel"> so the 1.1 MB inline app
   in index.html only has to swap a few lines. It hands everything over on
   window.HcSite; the inline App passes in what only it knows (news posts,
   page switches, the sign-in state). Styles: assets/hc/hc.css (all .hc-*).

   🖼 PICTURES: every slot reads IMG below. A slot left null draws a labelled
   "NO IMAGE ON FILE" placeholder, so new art is one line here, no layout work.
   ========================================================================== */
(function () {
  const { useState, useEffect, useRef, useMemo, useCallback } = React;

  const A = './assets/hc/';
  const IMG = {
    logo: A + 'logo.webp',
    heroPoster: A + 'hero-poster.jpg',
    aboutPoster: A + 'about-poster.jpg',
    story: [A + 'keyart-loading.webp', A + 'keyart-squad.webp', A + 'hero-poster.jpg'],
    join: A + 'keyart-squad.webp',
    friends: A + 'play-friends.webp?v=pf2',
    // One per THREATS row, by id. A missing id = the placeholder. (The Lunger's came later the
    // same day, pasted at 2000 px instead of 2752: same 16:9 sheet, so the same frame.)
    // These are the owner's character SHEETS (2026-10-29, 16:9 white paper: a big pose on the
    // left, turnarounds on the right), so they are drawn whole as a pinned sheet, never
    // cropped into a portrait frame. Thumbs (-sm) are cut from the left-hand pose.
    threats: Object.fromEntries(['bride', 'shy', '173', 'screamer', 'brute', 'lunger', 'infector', 'yaker', 'exe', 'snitch']
      .map((id) => [id, { full: A + 'threats/' + id + '.webp?v=t1', sm: A + 'threats/' + id + '-sm.webp?v=t1' }])),
    // One per LOCATIONS row, by id. null = placeholder.
    locations: {
      heights: A + 'keyart-squad.webp', gates: A + 'keyart-loading.webp',
      subway: A + 'loc-subway.webp?v=l1', haven: A + 'loc-haven.webp?v=l1',
      base: A + 'loc-base.webp?v=l1', sewers: A + 'loc-sewers.webp?v=l1',
    },
    // One per GAMEPLAY tab, by id.
    gameplay: { gates: A + 'play-gates.webp?v=p1', economy: A + 'play-economy.webp?v=p1', avatar: A + 'play-avatar.webp?v=p1', name: A + 'play-name.webp?v=p1' },
  };
  const VID = {
    hero: { hi: A + 'hero-1080.mp4', lo: A + 'hero-720.mp4' },
    about: A + 'about.mp4',
  };

  // ── content ───────────────────────────────────────────────────────────────
  const THREATS = [
    { id: 'bride', name: 'The Wicked Bride', cls: 'keter', file: 'OG-0006',
      text: 'She weeps in the dark rooms of Ethos Heights, alone, and she is harmless for exactly as long as you leave her that way. Walk too close, sprint past her, shine a light in her face or make a sound she can hear, and she will hunt the one who woke her until one of you is dead.',
      proc: 'Six instances recorded. Do not run near her. Do not use flashlights. If she has seen you, she has already chosen you.' },
    { id: 'shy', name: 'Shy Guy', cls: 'keter', file: 'SCP-096',
      text: 'It wanders the open streets with its face hidden in its hands. One look at that face, even a glance, even through a scope, and it screams, and it comes. Doors do not stop it. Walls do not stop it. It does not stop until the one who saw it is dead.',
      proc: 'Never look at the face. Never carry a mirror. A survivor who looked has, at most, a few seconds to make peace.' },
    { id: '173', name: 'The Sculpture', cls: 'euclid', file: 'SCP-173',
      text: 'Concrete and rebar, painted in something that is not paint. It cannot move while it is seen. The moment every survivor in the room blinks or looks away, it is already at your throat. Two walk the streets. One waits in the subway.',
      proc: 'Keep eye contact at all times. Travel in pairs and blink in turn. Listen for the scrape.' },
    { id: 'screamer', name: 'The Screamer', cls: 'euclid', file: 'OG-0021',
      text: 'She never fights fair. She runs from you, and when she has space, she screams. Glass bursts for a street around her. Your vision warps. Every dead thing in earshot turns toward the sound.',
      proc: 'Kill on sight from range. A Screamer left alive is a horde on the way.' },
    { id: 'brute', name: 'The Brute', cls: 'euclid', file: 'OG-0014',
      text: 'Three hundred pounds of grief and muscle. It roars when it sees you, and when its fists come down the street itself throws up dust. Walls between you are a suggestion.',
      proc: 'Do not trade blows. Keep moving. Keep obstacles between you.' },
    { id: 'lunger', name: 'The Lunger', cls: 'euclid', file: 'OG-0030',
      text: 'You will hear her before you see her, on the rooftops, in the dark. She picks the weakest survivor in the group, circles, waits, and leaps. Wound her and she vanishes into smoke to try again.',
      proc: 'Stay together. Light the roofs. Never be the one limping at the back.' },
    { id: 'infector', name: 'The Infector', cls: 'keter', file: 'OG-0042',
      text: 'She keeps her distance and throws glass vials of something brewed in a lab that no longer exists. Where they break, a coloured fog hangs. Breathe it, and you carry a sickness only a hospital can clear.',
      proc: 'Hold your breath. Leave the fog. Report to the nearest hospital within the cycle.' },
    { id: 'yaker', name: 'The Yaker', cls: 'euclid', file: 'OG-0047',
      text: 'He waits until you are busy: searching a cabinet, reloading, fighting something else. Then he comes in and vomits a slime that will not hurt you, but every runner in the district will smell it on you.',
      proc: 'Watch your back while looting. When he dies, he bursts. Do not stand close.' },
    { id: 'exe', name: 'The Corrupted .EXE', cls: 'keter', file: 'OG-0055',
      text: 'Some houses still have a computer in them. Some of those computers still turn on. If you hear laughing from the screen, leave the house. If the screen opens, it is already too late to leave.',
      proc: 'Weak to electricity and EMP. Its touch leaves you at one breath and carrying something corrupted.' },
    { id: 'snitch', name: 'Snitch Bots', cls: 'safe', file: 'OG-0060',
      text: 'Hovering machines down in the subway, scanning with a blue light that turns red when it finds you. They cannot hurt you. They do not have to. Their alarm brings everything else.',
      proc: 'Shoot them down quietly, or throw an EMP and walk past.' },
  ];

  const LOCATIONS = [
    { id: 'heights', name: 'Ethos Heights', tag: 'The last city',
      text: 'What is left of mankind lives behind its walls. Homes you can buy, rent and furnish. Businesses run by players. Twenty-five survivors to a server, each world owned and governed.' },
    { id: 'gates', name: 'The Outter Gates', tag: 'Extraction zone',
      text: 'The drowned city outside the wall. Loot it, survive it, get out alive. Every two hours it resets: the containers refill, the windows are glazed again, and the dead walk back in.' },
    { id: 'subway', name: 'The Subway', tag: 'Underground',
      text: 'A train still runs every thirty minutes. It will carry you, or it will kill you. Snitch Bots patrol the platforms, and a dealer in purple fire buys what no one else will.' },
    { id: 'haven', name: 'Haven', tag: 'Safe zone',
      text: 'Fenced, guarded, and the only place in the Gates where you can lower your weapon. Traders buy and sell. What players sell to Haven becomes stock for everyone.' },
    { id: 'base', name: 'The Military Base', tag: 'High risk',
      text: 'HESCO walls, dead soldiers, army trucks and a field tent still full of weapons, attachments, grenades and medicine. The dead gather at the gate.' },
    { id: 'sewers', name: 'Sewers & Basements', tag: 'Below the street',
      text: 'A second city under the first. Manholes, maintenance rooms, flooded tunnels, and basements under every house. Sound carries differently down here.' },
  ];

  const GAMEPLAY = [
    { id: 'gates', n: '01', tab: 'Survive the Gates', title: 'Go out. Get loot. Get back.',
      lead: 'Every run into the Outter Gates is a gamble with what you carry. Die, and your bag stays where you fell.',
      pts: [
        ['Search everything', 'houses, stores, police stations, cars, bodies: food, medicine, ammo, guns, parts, valuables.'],
        ['Fight or hide', 'melee, firearms with real magazines and ammo types, throwables, lockers, blinds and doors.'],
        ['Extract', 'reach a getaway truck and hold it while the dead close in. Only what you extract is yours.'],
        ['Day, night and weather', 'rain, storms, cold and heat change what you wear and how long you last.'],
      ] },
    { id: 'economy', n: '02', tab: 'Build a business', title: 'A city run by its survivors.',
      lead: 'Almost nothing in Ethos Heights is sold by the game. It is found, made, hauled and sold by players.',
      pts: [
        ['Own a business', 'gun stores, car customs, laundromats, furniture showrooms, garages, restaurants, farms, factories.'],
        ['Set your own prices', 'stock your floor, hire staff, and let other survivors walk in and buy.'],
        ['Trade on Luni', 'list anything you find or make on the player market. Prices move with supply and demand.'],
        ['Haul it', 'every shipment needs a driver. Truckers and freight captains get paid to move the city.'],
      ] },
    { id: 'avatar', n: '03', tab: 'Keep yourself alive', title: 'Your avatar is not a costume.',
      lead: 'Hunger, thirst, sleep, warmth, blood and sickness. Neglect them and you will feel it out in the Gates.',
      pts: [
        ['Eat, drink, rest', 'cook at home or buy a meal; a bed restores you; food spoils without cold storage.'],
        ['Wash up', 'blood left on your skin and clothes turns into infection. You will want more than one outfit.'],
        ['Learn by doing', 'forty-nine skills that grow as you use them, plus books that teach you to craft.'],
        ['Dress for it', 'clothing with real warmth and rain protection, armor that stops bullets, bags that decide what you carry.'],
      ] },
    { id: 'name', n: '04', tab: 'Become a name', title: 'Build a brand in a dead world.',
      lead: 'Your reputation travels further than you do. Make it worth knowing.',
      pts: [
        ['Hire out your gun', 'take mercenary contracts: gather for other players or guard them through a run.'],
        ['Own the land', 'buy homes, franchise locations and whole servers. Become the landlord of a district.'],
        ['Go live', 'post, clip and broadcast from inside the city on Emergency Broadcast.'],
        ['Play the long game', 'influence, raid bounties, survivors who join your camp: the city remembers.'],
      ] },
  ];

  const CHAIN = [
    ['Loot', 'Survivors pull raw goods out of the Gates: scrap, fuel, cloth, parts, food.'],
    ['Craft', 'Workbenches, kitchens and labs turn them into ammo, meals, medicine and gear.'],
    ['Trade', 'Everything lists on Luni. The price is whatever another survivor will pay.'],
    ['Run a business', 'Shops buy stock, set prices and sell to the players who walk in.'],
    ['Hire', 'Owners hire drivers, staff and mercenaries. The money stays with players.'],
  ];

  const BIZ = [
    ['🔫', 'Hidn Blicks', 'Gun store. Buy, sell and buy back weapons; craft elemental rounds.'],
    ['🎨', 'Hidn Customs', 'Car customs. Paint, neon, wheels and wings, made from the owner\'s own materials.'],
    ['🧺', 'The Laundromat', 'Washers and dryers priced by the owner. Clean clothes are survival.'],
    ['🛋', 'Furniture Store', 'Stock the showroom floor. Every home in the city is furnished from it.'],
    ['🔧', 'Mechanic Shop', 'Book cars in, work them on the lift, send them back road-ready.'],
    ['🍔', 'Restaurants', 'Cook a live shift with your crew. Hot food keeps survivors going.'],
    ['🚗', 'Car Factory', 'Build cars on the line and sell them to dealerships across the city.'],
    ['🌾', 'Farms & Harbor', 'Crops, livestock and fish, shipped to kitchens and freezers.'],
    ['⚒', 'Weapon Smith', 'Forge blades, assemble guns from blueprints and repair what the Gates broke.'],
    ['🩺', 'Medical Corporation', 'Run the hospital. Cure the infected and compound the medicine survivors carry out.'],
    ['🕳', 'Smuggling Network', 'Move what the law won\'t. Big money, and a cell in City Hall if you get caught.'],
  ];

  // ── helpers ───────────────────────────────────────────────────────────────
  // One IntersectionObserver for the whole page: anything with data-hc-rv gets
  // .hc-in the first time it is on screen (and keeps it).
  const useReveal = (deps) => {
    useEffect(() => {
      const els = Array.from(document.querySelectorAll('[data-hc-rv]:not(.hc-in)'));
      if (!('IntersectionObserver' in window)) { els.forEach((e) => e.classList.add('hc-in')); return; }
      const io = new IntersectionObserver((ents) => {
        ents.forEach((en) => { if (en.isIntersecting) { en.target.classList.add('hc-in'); io.unobserve(en.target); } });
      }, { threshold: 0.18, rootMargin: '0px 0px -8% 0px' });
      els.forEach((e) => io.observe(e));
      return () => io.disconnect();
    }, deps || []);
  };

  // A typewriter title: each letter is its own span with a staggered delay.
  const TypeTitle = ({ text, as }) => {
    const Tag = as || 'h2';
    return (
      <Tag className="hc-ttl" data-hc-rv aria-label={text}>
        {/* Letters are inline-blocks (each types in on its own), so every word sits in a
            no-wrap box, or a narrow phone breaks a word between ANY two letters
            ("Location | s" at 320 px; owner 10-29: "letters missing in the titles").
            The caret rides inside the last word so it never wraps onto a line alone. */}
        {(() => {
          let i = 0;
          const words = text.split(' ');
          return words.map((w, wi) => (
            <React.Fragment key={wi}>
              {wi > 0 ? ' ' : null}
              <span className="wd">
                {Array.from(w).map((c) => {
                  const d = (i++) * 0.09;
                  return <span key={i} className="ch" aria-hidden="true" style={{ animationDelay: d + 's' }}>{c}</span>;
                })}
                {wi === words.length - 1 ? <span className="caret" aria-hidden="true" /> : null}
              </span>
            </React.Fragment>
          ));
        })()}
      </Tag>
    );
  };

  const Ph = ({ label, sub }) => (
    <div className="hc-ph"><b>{label}</b><span>{sub || 'No image on file'}</span></div>
  );
  const Pic = ({ src, alt, label, sub }) => src
    ? <img src={src} alt={alt || ''} loading="lazy" onError={(e) => { e.currentTarget.style.display = 'none'; }} />
    : <Ph label={label || alt} sub={sub} />;

  const fmtNewsDate = (d) => {
    const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(d || ''));
    if (!m) return { md: '', y: '' };
    return { md: (+m[2]) + '/' + (+m[3]), y: m[1] };
  };
  const isFresh = (d) => { try { return (Date.now() - new Date(String(d).slice(0, 10) + 'T00:00:00').getTime()) < 14 * 864e5; } catch (e) { return false; } };

  // ── atmosphere + header + menu ────────────────────────────────────────────
  const Atmos = () => {
    useEffect(() => { document.body.classList.add('hc-on'); return () => document.body.classList.remove('hc-on'); }, []);
    return (
      <>
        <div className="hc-atmos" aria-hidden="true"><div className="crack" /><div className="vig" /></div>
        <div className="hc-grain" aria-hidden="true" />
        <div className="hc-scan" aria-hidden="true" />
      </>
    );
  };

  const Header = ({ go, playUrl, isSignedIn, displayName, page, socials }) => {
    const [solid, setSolid] = useState(false);
    const [open, setOpen] = useState(false);
    useEffect(() => {
      const on = () => setSolid(page !== 'home' || window.scrollY > window.innerHeight * 0.6);
      on(); window.addEventListener('scroll', on, { passive: true });
      return () => window.removeEventListener('scroll', on);
    }, [page]);
    useEffect(() => {
      if (!open) return;
      const k = (e) => { if (e.key === 'Escape') setOpen(false); };
      document.addEventListener('keydown', k);
      document.body.style.overflow = 'hidden';
      return () => { document.removeEventListener('keydown', k); document.body.style.overflow = ''; };
    }, [open]);
    const pick = (fn) => () => { setOpen(false); fn && fn(); };
    const items = [
      ['Home', go.home], ['About', () => go.section('about')], ['The Story', () => go.section('story')],
      ['Threats', () => go.section('threats')], ['Gameplay', () => go.section('gameplay')],
      ['Economy', () => go.section('economy')], ['Play with friends', () => go.section('friends')], ['News', go.news], ['Lore', go.lore],
      ['Emergency Broadcast', go.broadcast], ['Shop', go.shop],
    ];
    return (
      <div className="hc">
        <header className={'hc-head' + (solid ? ' solid' : '')}>
          <button className="hc-feed" onClick={pick(go.home)} title="Home">
            <i className="hc-dot" /><span className="t">SITE-ETHOS · FEED LIVE</span>
          </button>
          <button onClick={pick(go.home)} aria-label="Humanity Contained home"><img className="hc-head-logo" src={IMG.logo} alt="Humanity Contained" /></button>
          <nav className="hc-head-r">
            <button className="hc-head-link hc-head-hide-s" onClick={pick(go.news)}>News</button>
            <button className="hc-head-link hc-head-hide-s" onClick={pick(go.broadcast)}>Broadcast</button>
            <button className="hc-head-link hc-head-hide-s" onClick={pick(go.shop)}>Shop</button>
            <button className="hc-head-link" onClick={pick(go.hub)}>{isSignedIn ? (displayName || 'Profile') : 'Sign in'}</button>
            <button className="hc-menu-btn" onClick={() => setOpen(true)} aria-expanded={open}>Menu</button>
          </nav>
        </header>
        <div className={'hc-menu' + (open ? ' open' : '')} role="dialog" aria-modal="true" aria-label="Menu" aria-hidden={!open}>
          <button className="hc-menu-close hc-menu-btn" onClick={() => setOpen(false)}>Close</button>
          <div className="hc-menu-l">
            {items.map(([label, fn], i) => (
              <button key={label} className="hc-menu-item" style={{ transitionDelay: open ? (0.04 * i + 0.08) + 's' : '0s' }} onClick={pick(fn)}>
                <small>{String(i + 1).padStart(2, '0')}</small><span>{label}</span>
              </button>
            ))}
          </div>
          <div className="hc-menu-r">
            <img src={IMG.logo} alt="Humanity Contained" style={{ width: 'min(420px, 100%)' }} />
            <div className="hc-doc-meta" style={{ marginBottom: 0 }}>
              <dt>Site</dt><dd>Ethos Heights</dd>
              <dt>Status</dt><dd className="keter">Containment failed</dd>
              <dt>Clearance</dt><dd>{isSignedIn ? 'Survivor · ' + (displayName || 'on file') : 'Public'}</dd>
            </div>
            <a className="hc-paper-btn red" href={playUrl} onClick={() => setOpen(false)}>Enter the Gates <span className="arr">→</span></a>
            <button className="hc-ghost" style={{ alignSelf: 'flex-start' }} onClick={pick(go.hub)}>{isSignedIn ? 'Your profile' : 'Sign in'} →</button>
            <div className="hc-socials">
              {(socials || []).filter((s) => s.href).map((s) => <a key={s.id} href={s.href} target="_blank" rel="noopener noreferrer">{s.label}</a>)}
            </div>
          </div>
        </div>
      </div>
    );
  };

  // The torn-paper "Enter the Gates" that rides the bottom-right corner once
  // the hero's own button has scrolled away (the Requiem "Available now").
  const FloatCta = ({ playUrl }) => {
    const [show, setShow] = useState(false);
    useEffect(() => {
      const on = () => setShow(window.scrollY > window.innerHeight * 0.75);
      on(); window.addEventListener('scroll', on, { passive: true });
      return () => window.removeEventListener('scroll', on);
    }, []);
    return <a className={'hc-paper-btn red hc-float-cta' + (show ? ' show' : '')} href={playUrl}>Play free <span className="arr">→</span></a>;
  };

  // ── HERO ──────────────────────────────────────────────────────────────────
  const Hero = ({ posts, go, playUrl }) => {
    const vref = useRef(null);
    const [sound, setSound] = useState(false);
    const small = typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(max-width: 900px)').matches;
    // The 720p cut has no audio track, so sound always plays the 1080p file.
    const src = (small && !sound) ? VID.hero.lo : VID.hero.hi;
    useEffect(() => {
      const v = vref.current; if (!v) return;
      v.muted = !sound;
      const p = v.play(); if (p && p.catch) p.catch(() => {});
    }, [sound, src]);
    const latest = (posts || []).slice(0, 6);
    return (
      <section className="hc-hero" id="top">
        <video ref={vref} key={src} autoPlay muted={!sound} loop playsInline preload="auto" poster={IMG.heroPoster} aria-hidden="true">
          <source src={src} type="video/mp4" />
        </video>
        <div className="hc-hero-mid">
          <div className="hc-hero-banner">The flood <b>woke</b> what we contained</div>
          <h1 className="hc-sr">Humanity Contained</h1>
          <img className="hc-hero-logo" src={IMG.logo} alt="Humanity Contained" />
          <p className="hc-hero-sub">The world drowned. The nightmares stayed.</p>
          <div className="hc-hero-tag">Free to play<i>/</i>Play in your browser<i>/</i>A player-driven world</div>
          <div className="hc-hero-ctas">
            <a className="hc-paper-btn" href={playUrl}>Enter the Gates <span className="arr">→</span></a>
            <button className="hc-ghost" onClick={() => go.section('about')}>What is Humanity Contained ↓</button>
          </div>
        </div>
        <button className={'hc-sound' + (sound ? ' on' : '')} onClick={() => setSound((s) => !s)} aria-pressed={sound}>
          <span className="bars"><i /><i /><i /></span>{sound ? 'Sound on' : 'Sound off'}
        </button>
        {latest.length > 0 && (
          <div className="hc-whatsnew">
            <h2>What's New</h2>
            <div className="hc-wn-row">
              {latest.map((p, i) => (
                <button key={p.id || p.title || i} className="hc-wn-item" onClick={go.news}>
                  <time>{String(p.date || '').slice(0, 10).replace(/-/g, '/')}</time>
                  <p>{p.title}</p>
                </button>
              ))}
            </div>
          </div>
        )}
      </section>
    );
  };

  // ── NEWS ──────────────────────────────────────────────────────────────────
  const News = ({ posts, go }) => {
    const track = useRef(null);
    const list = (posts || []).slice(0, 10);
    if (!list.length) return null;
    const step = (d) => { const t = track.current; if (t) t.scrollBy({ left: d * (t.clientWidth * 0.8), behavior: 'smooth' }); };
    return (
      <section className="hc-sec" id="news">
        <div className="hc-wrap">
          <div className="hc-kicker">Transmissions</div>
          <TypeTitle text="News" />
          <div className="hc-news-track" ref={track} data-hc-rv>
            {list.map((p, i) => {
              const d = fmtNewsDate(p.date);
              return (
                <button key={p.id || p.title || i} className={'hc-news-card' + (isFresh(p.date) ? ' new' : '')} onClick={go.news}>
                  <time>{d.md} <small>{d.y}</small></time>
                  <div className="hc-news-img"><Pic src={p.heroImage} alt={p.title} label={p.category || 'Bulletin'} sub="Transmission" /></div>
                  <h3>{p.title}</h3>
                </button>
              );
            })}
          </div>
          <div className="hc-nav-arrows">
            <button className="hc-arrow prev" aria-label="Previous" onClick={() => step(-1)} />
            <button className="hc-ghost" onClick={go.news}>All transmissions →</button>
            <button className="hc-arrow next" aria-label="Next" onClick={() => step(1)} />
          </div>
        </div>
      </section>
    );
  };

  // ── FOOTAGE (the Requiem "Trailers") ──────────────────────────────────────
  const Footage = () => {
    const [open, setOpen] = useState(null);
    // The owner's Emergency Broadcast News clips (2026-10-30), re-encoded by the
    // scratchpad footenc.ps1: 1080p H.264, ≤ 3 Mbps, faststart (masters were 18–50 MB,
    // over the 25 MiB asset limit).
    const F = A + 'footage/';
    const clips = [
      { src: F + 'gameplay.mp4?v=f1', poster: F + 'gameplay.jpg?v=f1', cap: 'Crazy Man Leaves Hell\'s Kitchen Gate' },
      { src: F + 'dumps.mp4?v=f1', poster: F + 'dumps.jpg?v=f1', cap: 'Governor Lets the City Go to Waste' },
      { src: F + 'airstrike.mp4?v=f1', poster: F + 'airstrike.jpg?v=f1', cap: 'Man Unalives Himself With an Air Strike' },
      { src: F + 'governor.mp4?v=f1', poster: F + 'governor.jpg?v=f1', cap: 'Governor Found' },
      { src: F + 'sparta.mp4?v=f1', poster: F + 'sparta.jpg?v=f1', cap: 'Man Calls Down a Mech From the Sky' },
    ];
    useEffect(() => {
      if (!open) return;
      const k = (e) => { if (e.key === 'Escape') setOpen(null); };
      document.addEventListener('keydown', k); return () => document.removeEventListener('keydown', k);
    }, [open]);
    return (
      <section className="hc-sec" id="footage" style={{ paddingTop: 0 }}>
        <div className="hc-wrap">
          <div className="hc-kicker">Recovered footage</div>
          <TypeTitle text="Footage" />
          <div className="hc-footage" data-hc-rv>
            {clips.map((c, i) => (
              <button key={i} className="hc-clip hc-rv" style={{ transitionDelay: (i * 0.12) + 's' }} onClick={() => setOpen(c)} aria-label={'Play ' + c.cap}>
                <img src={c.poster} alt="" loading="lazy" />
                <span className="play" />
                <span className="cap">{c.cap}</span>
              </button>
            ))}
          </div>
          <div className="hc-nav-arrows" style={{ justifyContent: 'flex-start' }}>
            <span className="hc-count">{String(clips.length).padStart(2, '0')} files</span>
          </div>
        </div>
        {open && (
          <div className="hc-modal" role="dialog" aria-modal="true" aria-label={open.cap} onClick={() => setOpen(null)}>
            <button className="hc-modal-x hc-menu-btn" onClick={() => setOpen(null)}>Close</button>
            <video src={open.src} poster={open.poster} controls autoPlay playsInline onClick={(e) => e.stopPropagation()} />
          </div>
        )}
      </section>
    );
  };

  // ── ABOUT: title, paper band, pinned video, word-by-word reveal ──────────
  const ABOUT_TEXT = 'Humanity Contained is a survival-horror world where the players are the economy. When the goddess Abraxas woke and drowned the earth, the flood carried every horror the Foundation had ever locked away back into the light. You are one of the last survivors behind the walls of Ethos Heights. Loot the drowned city beyond the Outter Gates, build a business, keep your body alive, and make a name that the whole city knows.';
  const ABOUT_RED = new Set(['Abraxas', 'Outter', 'Gates,', 'economy.', 'Ethos', 'Heights.']);
  const About = () => {
    const stage = useRef(null);
    const words = useMemo(() => ABOUT_TEXT.split(' '), []);
    const [lit, setLit] = useState(0);
    useEffect(() => {
      const on = () => {
        const el = stage.current; if (!el) return;
        const r = el.getBoundingClientRect();
        const span = r.height - window.innerHeight;
        const k = Math.max(0, Math.min(1, (-r.top + window.innerHeight * 0.35) / Math.max(1, span)));
        setLit(Math.round(k * words.length));
      };
      on(); window.addEventListener('scroll', on, { passive: true }); window.addEventListener('resize', on);
      return () => { window.removeEventListener('scroll', on); window.removeEventListener('resize', on); };
    }, [words.length]);
    return (
      <section className="hc-sec" id="about" style={{ paddingBottom: 0 }}>
        <div className="hc-wrap hc-about-head">
          <div className="hc-kicker">Briefing</div>
          <TypeTitle text="About" />
          <div data-hc-rv><span className="hc-band">The gods drowned the world. The nightmares stayed.</span></div>
        </div>
        <div className="hc-about-stage" ref={stage}>
          <div className="hc-about-pin">
            <video autoPlay muted loop playsInline preload="metadata" poster={IMG.aboutPoster} aria-hidden="true"><source src={VID.about} type="video/mp4" /></video>
            <div className="hc-about-lead">
              <div className="hc-wrap">
                <p>{words.map((w, i) => (
                  <span key={i} className={'hc-word' + (i < lit ? ' on' : '') + (ABOUT_RED.has(w) ? ' red' : '')}>{w + (i < words.length - 1 ? ' ' : '')}</span>
                ))}</p>
              </div>
            </div>
          </div>
        </div>
      </section>
    );
  };

  // ── STORY: an SCP incident file with an evidence photo ───────────────────
  const R = ({ children }) => <span className="hc-redact" tabIndex={0} title="Clearance required">{children}</span>;
  const Story = () => {
    const [img, setImg] = useState(0);
    const caps = ['Exhibit A — The walls of Ethos Heights, day 1', 'Exhibit B — Survivor squad, Outter Gates', 'Exhibit C — Recovered from a field camera'];
    useEffect(() => {
      const t = setInterval(() => setImg((i) => (i + 1) % IMG.story.length), 6000);
      return () => clearInterval(t);
    }, []);
    return (
      <section className="hc-sec" id="story">
        <div className="hc-wrap">
          <div className="hc-kicker">Incident file</div>
          <TypeTitle text="The Story" />
          <div className="hc-file">
            <div className="hc-evid hc-rv" data-hc-rv>
              <div className="hc-evid-pager">IMAGES
                {IMG.story.map((_, i) => <button key={i} className={i === img ? 'on' : ''} onClick={() => setImg(i)}>{i + 1}</button>)}
              </div>
              <div className="hc-evid-frame">
                <div className="hc-stamp">Classified</div>
                <div className="hc-evid-img">
                  {IMG.story.map((s, i) => s
                    ? <img key={i} src={s} alt={caps[i] || ''} className={i === img ? 'on' : ''} loading="lazy" />
                    : (i === img ? <Ph key={i} label={'Exhibit ' + (i + 1)} /> : null))}
                </div>
                <div className="hc-evid-cap">{caps[img] || ''}</div>
              </div>
            </div>
            <div className="hc-doc hc-rv" data-hc-rv style={{ transitionDelay: '.15s' }}>
              <dl className="hc-doc-meta">
                <dt>Item #</dt><dd>ABX-Ø "The Deluge"</dd>
                <dt>Object class</dt><dd className="keter">Keter — containment failed</dd>
                <dt>Location</dt><dd>Worldwide · last stand: Ethos Heights</dd>
              </dl>
              <p>For decades the Foundation kept the world's worst secrets behind steel doors and paperwork. Monsters, plagues, statues that move when you blink. Deep beneath <R>Site-██</R>, it kept one more: a sleeping goddess the old files call <b>Abraxas</b>.</p>
              <p>She woke. The water came with her. It rose over every continent in <R>forty days</R>, and when it pulled back, it did not leave the world the way it found it. The flood had reached into every containment cell, every sealed file, every nightmare mankind had ever told itself in the dark, and it carried them out into the streets.</p>
              <h3>What is left</h3>
              <p>The survivors gathered behind the walls of <b>Ethos Heights</b>. There is no government left to run it, so the survivors do. They trade, build, hire, and argue over what a loaf of bread is worth. Outside the walls are the <b>Outter Gates</b>, the drowned city where everything worth having is still lying in the ruins, and everything that wants you dead is waiting next to it.</p>
              <p>Nobody is coming to save Ethos Heights. <R>Nobody ever was.</R></p>
            </div>
          </div>
        </div>
      </section>
    );
  };

  // ── THREATS (the Characters section) ──────────────────────────────────────
  const Threats = () => {
    const [i, setI] = useState(0);
    const [zoom, setZoom] = useState(null);
    const t = THREATS[i];
    const art = IMG.threats[t.id] || null;
    useEffect(() => {
      if (!zoom) return;
      const k = (e) => { if (e.key === 'Escape') setZoom(null); };
      document.addEventListener('keydown', k); return () => document.removeEventListener('keydown', k);
    }, [zoom]);
    return (
      <section className="hc-sec" id="threats">
        <div className="hc-wrap">
          <div className="hc-kicker">Anomaly index</div>
          <TypeTitle text="Threats" />
          <div className="hc-thumbs" data-hc-rv role="tablist" aria-label="Threats">
            {THREATS.map((x, k) => (
              <button key={x.id} role="tab" aria-selected={k === i} className={'hc-thumb' + (k === i ? ' on' : '')} onClick={() => setI(k)}>
                {IMG.threats[x.id] ? <img src={IMG.threats[x.id].sm} alt="" loading="lazy" /> : <Ph label={x.file} sub="" />}
                <span className="lbl">{x.name}</span>
              </button>
            ))}
          </div>
          <div className={'hc-threat hc-fade' + (art ? ' sheet' : '')} key={t.id}>
            {art ? (
              <button className="hc-threat-sheet" onClick={() => setZoom(art.full)} aria-label={'Open the ' + t.name + ' file full size'}>
                <span className="num">FILE {t.file} · {String(i + 1).padStart(2, '0')}/{String(THREATS.length).padStart(2, '0')}</span>
                <span className="hc-stamp">{t.cls}</span>
                <img src={art.full} alt={t.name + ' character sheet'} loading="lazy" />
                <span className="zoom">Click to enlarge</span>
              </button>
            ) : (
              <div className="hc-threat-art">
                <span className="num">FILE {t.file} · {String(i + 1).padStart(2, '0')}/{String(THREATS.length).padStart(2, '0')}</span>
                <Pic src={null} alt={t.name} label={t.name} sub="Image redacted · pending" />
              </div>
            )}
            <div>
              <h3>{t.name}</h3>
              <div className={'hc-class ' + t.cls}>Object class · {t.cls}</div>
              <p>{t.text}</p>
              <div className="proc"><b>SPECIAL CONTAINMENT PROCEDURES:</b> {t.proc}</div>
              <div className="hc-nav-arrows" style={{ maxWidth: 420, marginTop: 36 }}>
                <button className="hc-arrow prev" aria-label="Previous threat" onClick={() => setI((i - 1 + THREATS.length) % THREATS.length)} />
                <span className="hc-count">{String(i + 1).padStart(2, '0')}/{String(THREATS.length).padStart(2, '0')}</span>
                <button className="hc-arrow next" aria-label="Next threat" onClick={() => setI((i + 1) % THREATS.length)} />
              </div>
            </div>
          </div>
        </div>
        {zoom && (
          <div className="hc-modal" role="dialog" aria-modal="true" aria-label={t.name + ' file'} onClick={() => setZoom(null)}>
            <button className="hc-modal-x hc-menu-btn" onClick={() => setZoom(null)}>Close</button>
            <img className="hc-zoom-img" src={zoom} alt={t.name + ' character sheet'} onClick={(e) => e.stopPropagation()} />
          </div>
        )}
      </section>
    );
  };

  // ── LOCATIONS ─────────────────────────────────────────────────────────────
  const Locations = () => (
    <section className="hc-sec" id="locations" style={{ paddingBottom: 60 }}>
      <div className="hc-wrap">
        <div className="hc-kicker">Survey photographs</div>
        <TypeTitle text="Locations" />
        <p className="hc-rv" data-hc-rv style={{ maxWidth: 820, color: '#d8d8cf' }}>From the last lit windows of Ethos Heights to the flooded tunnels under the Outter Gates, every place has something worth taking, and something that does not want you to take it.</p>
      </div>
      <div className="hc-locs">
        {LOCATIONS.map((l) => (
          <article key={l.id} className="hc-loc">
            <div className="hc-loc-img"><Pic src={IMG.locations[l.id]} alt={l.name} label={l.name} sub="Photograph missing" /></div>
            <h3>{l.name}</h3>
            <p>{l.text}</p>
            <span className="tag">{l.tag}</span>
          </article>
        ))}
      </div>
    </section>
  );

  // ── GAMEPLAY ──────────────────────────────────────────────────────────────
  const Gameplay = () => {
    const [i, setI] = useState(0);
    const g = GAMEPLAY[i];
    return (
      <section className="hc-sec" id="gameplay">
        <div className="hc-wrap">
          <div className="hc-kicker">Field manual</div>
          <TypeTitle text="Gameplay" />
          <div data-hc-rv><span className="hc-band red">Loot it. Build it. Live it. Own it.</span></div>
          <div className="hc-tabs" role="tablist">
            {GAMEPLAY.map((x, k) => (
              <button key={x.id} role="tab" aria-selected={k === i} className={'hc-tab' + (k === i ? ' on' : '')} onClick={() => setI(k)}>
                <small>{x.n}</small><span>{x.tab}</span>
              </button>
            ))}
          </div>
          <div className="hc-play hc-fade" key={g.id}>
            <div className="hc-play-art"><Pic src={IMG.gameplay[g.id]} alt={g.tab} label={g.tab} sub="Screenshot pending" /></div>
            <div>
              <h3>{g.title}</h3>
              <p style={{ color: '#d8d8cf', margin: 0 }}>{g.lead}</p>
              <ul>{g.pts.map(([b, t]) => <li key={b}><span><b>{b}:</b> {t}</span></li>)}</ul>
            </div>
          </div>
        </div>
      </section>
    );
  };

  // ── ECONOMY ───────────────────────────────────────────────────────────────
  const Economy = () => (
    <section className="hc-sec" id="economy">
      <div className="hc-wrap">
        <div className="hc-kicker">A player-driven economy</div>
        <TypeTitle text="Economy" />
        <p className="hc-econ-quote hc-rv" data-hc-rv>Every bullet, every meal and every car on the road was <em>found, made, hauled and sold</em> by a survivor.</p>
        <div className="hc-chain hc-rv" data-hc-rv>
          {CHAIN.map(([h, p]) => <div key={h}><h4>{h}</h4><p>{p}</p></div>)}
        </div>
        <div className="hc-biz hc-rv" data-hc-rv>
          {BIZ.map(([ic, h, p]) => <div key={h}><div className="ic" aria-hidden="true">{ic}</div><h4>{h}</h4><p>{p}</p></div>)}
        </div>
      </div>
    </section>
  );

  // ── PLAY WITH FRIENDS (owner 2026-10-31: the referral pitch; the 3 % fee is
  // the game's own referral rule — change both together) ─────────────────────
  const Friends = ({ playUrl }) => (
    <section className="hc-sec" id="friends">
      <div className="hc-wrap">
        <div className="hc-kicker">Survive together</div>
        <TypeTitle text="Play with friends" />
        <div className="hc-friends">
          <figure className="hc-friends-pic hc-rv" data-hc-rv>
            <img src={IMG.friends} alt="Three survivors fighting through a horde, with the resources you earn by bringing friends" loading="lazy" />
          </figure>
          <div className="hc-friends-copy hc-rv" data-hc-rv>
            <p>Play with your friends, refer new survivors, and earn resources and items to help you stay alive!</p>
            <div className="hc-friends-fee">
              <b>3%</b>
              <span>Whenever a player you refer spends in-game currency, you earn a 3% referral fee in that currency.</span>
            </div>
            <p className="hc-friends-tag">Friends who survive and grind together <em>shine together!</em></p>
            <a className="hc-paper-btn red" href={playUrl}>Bring your squad <span className="arr">→</span></a>
          </div>
        </div>
      </div>
    </section>
  );

  // ── JOIN ──────────────────────────────────────────────────────────────────
  const Join = ({ go, playUrl }) => (
    <section className="hc-join" id="join">
      <img className="bg" src={IMG.join} alt="" loading="lazy" />
      <div className="hc-kicker" style={{ justifyContent: 'center' }}>Clearance granted</div>
      <h2 className="hc-rv" data-hc-rv>The gates are open.</h2>
      <p className="hc-rv" data-hc-rv>Free to play, right in your browser. Make an account, pick your survivor, and walk out into Ethos Heights.</p>
      <a className="hc-paper-btn red" href={playUrl}>Enter the Gates <span className="arr">→</span></a>
      <div className="hc-join-links">
        <button className="hc-ghost" onClick={go.broadcast}>Emergency Broadcast</button>
        <button className="hc-ghost" onClick={go.lore}>Read the lore</button>
        <button className="hc-ghost" onClick={go.shop}>Visit the shop</button>
      </div>
    </section>
  );

  // ── the whole home page ───────────────────────────────────────────────────
  const Home = ({ posts: rawPosts, go, playUrl }) => {
    // Newest first here (the News page keeps pinned posts on top).
    const posts = useMemo(() => (rawPosts || []).slice().sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))), [rawPosts]);
    useReveal([posts.length]);
    return (
      <main className="hc hc-main">
        <Hero posts={posts} go={go} playUrl={playUrl} />
        <News posts={posts} go={go} />
        <Footage />
        <About />
        <Story />
        <Threats />
        <Locations />
        <Gameplay />
        <Economy />
        <Friends playUrl={playUrl} />
        <Join go={go} playUrl={playUrl} />
        <FloatCta playUrl={playUrl} />
      </main>
    );
  };

  const Footer = ({ go, playUrl, socials }) => (
    <footer className="hc hc-foot">
      <div className="hc-foot-grid">
        <div>
          <img className="logo" src={IMG.logo} alt="Humanity Contained" loading="lazy" />
          <p style={{ color: 'var(--hc-paper-dim)', margin: 0, maxWidth: 380 }}>A survival-horror world where the players are the economy.</p>
          <div className="hc-socials">
            {(socials || []).filter((s) => s.href).map((s) => <a key={s.id} href={s.href} target="_blank" rel="noopener noreferrer">{s.label}</a>)}
          </div>
        </div>
        <div>
          <h5>The game</h5>
          <a href={playUrl}>Play free</a>
          <button onClick={() => go.section('about')}>About</button>
          <button onClick={() => go.section('gameplay')}>Gameplay</button>
          <button onClick={() => go.section('economy')}>Economy</button>
          <button onClick={() => go.section('friends')}>Play with friends</button>
        </div>
        <div>
          <h5>The world</h5>
          <button onClick={go.lore}>Lore</button>
          <button onClick={() => go.section('story')}>The Story</button>
          <button onClick={() => go.section('threats')}>Threats</button>
          <button onClick={go.news}>News</button>
        </div>
        <div>
          <h5>Survivors</h5>
          <button onClick={go.broadcast}>Emergency Broadcast</button>
          <button onClick={go.shop}>Shop</button>
          <button onClick={go.hub}>Sign in / Profile</button>
        </div>
      </div>
      <div className="hc-foot-legal">
        <span>© 2026 Hidn Studios · Humanity Contained</span>
        <span>SCP-096 and SCP-173 are based on works from the <a href="https://scp-wiki.wikidot.com/" target="_blank" rel="noopener noreferrer">SCP Wiki</a>, licensed under <a href="https://creativecommons.org/licenses/by-sa/3.0/" target="_blank" rel="noopener noreferrer">CC BY-SA 3.0</a>.</span>
      </div>
    </footer>
  );

  window.HcSite = { Atmos, Header, Home, Footer, useReveal, IMG };
})();
