/* ============================================================================
   app.jsx  —  THE MACHINERY THAT DRAWS THE SITE
   ----------------------------------------------------------------------------
   You normally DON'T need to edit this file. All words, sections, layout and
   per-section content live in data.js. This file reads those and renders.

   THE MODEL
     • One section is "active": it fills the big panel on the LEFT as white type
       laid over a full-frame video (no box of its own).
     • The other six sections sit as a stacked column of thumbnail boxes down
       the RIGHT side.
     • Click a box → it glides over to fill the left panel; the previously-active
       section drops into the right column as the first box.
     • Click the logo (top-left) → return to the Intro.
   ============================================================================ */

const { useState, useEffect, useRef, useLayoutEffect } = React;


/* ============================================================================
   STYLES
   ============================================================================ */
const CSS = `
/* The full-bleed background layer (photo / video / labeled placeholder) */
.bg-layer{ position:fixed; inset:0; z-index:0; overflow:hidden; background:var(--bgph); }
.bg-layer video,.bg-layer img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.bg-ph{ position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:10px; text-align:center; color:rgba(255,255,255,.34);
  background:repeating-linear-gradient(135deg, rgba(255,255,255,.03) 0 12px, transparent 12px 24px); }
.bg-ph .bg-ph-k{ font-family:ui-monospace,Menlo,monospace; font-size:12px; letter-spacing:.22em; text-transform:uppercase; }
.bg-ph .bg-ph-d{ font-weight:300; font-size:14px; opacity:.8; max-width:30ch; line-height:1.45; }

/* ---- Header (logo only — nav removed) ---- */
.bm-header{ position:relative; z-index:5; display:flex; align-items:center;
  padding:clamp(16px,2.2vw,28px) 6%; flex:0 0 auto; }
.bm-logo{ font-family:"Poppins",system-ui,sans-serif; font-size:24px; font-weight:600; letter-spacing:-.01em;
  line-height:1; color:var(--paper); background:none; border:none; padding:0; white-space:nowrap;
  cursor:pointer; opacity:.96; transition:opacity .25s, transform .25s; }
.bm-logo:hover{ opacity:1; transform:translateY(-1px); }

/* The content area below the header where the cells live */
.stage{ position:relative; flex:1 1 auto; min-height:0; z-index:1; }
.group{ position:absolute; top:3%; left:6%; right:6%; bottom:5%; }
/* On mobile the stage grows with the (scrolling) accordion instead of filling
   one locked viewport, and the footer gets clear separation below section 07. */
@media (max-width:720px){
  .stage{ flex:0 0 auto; }
  .bm-footer{ padding-top:clamp(16px,5vw,24px); }
}

/* Each "cell" is the invisible slot a section sits in. Positions are PIXELS
   (computed from the % layout × the measured group size) so the glide animates
   reliably. Enabled only after mount (.group.anim) so nothing flies in from 0. */
.cell{ position:absolute; }
.group.anim .cell{ transition:
  top .7s var(--ease), left .7s var(--ease),
  width .7s var(--ease), height .7s var(--ease); }

/* The visible surface. inset:3px makes the 6px gaps between neighbours.
   Grey when a box; fades fully transparent when active (white type on photo). */
.surface{ position:absolute; inset:3px; border-radius:var(--radius); overflow:hidden;
  background:rgba(10,18,14,.42); background-size:cover; background-position:center; color:var(--paper); cursor:pointer; will-change:transform;
  transition:background-color .55s var(--ease), transform .25s ease, box-shadow .25s ease; }
.surface:not(.active):hover{ transform:translateY(-2px); box-shadow:0 16px 40px rgba(10,9,7,.28); }
.surface:not(.active):active{ transform:translateY(0); }
/* Active = a rounded translucent panel sitting in the main area (keeps the
   3px inset gap + corner radius of the small boxes, just bigger). */
.surface.active{ background:rgba(10,18,14,.42); -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
  cursor:default; }

/* ---- THE BOX FACE (small box: index + headline, white over photo/black) ---- */
.box-face{ position:absolute; inset:0; z-index:2; transition:opacity .32s ease; }
.box-face::before{ content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  background:linear-gradient(180deg, rgba(8,14,11,.6) 0%, rgba(8,14,11,.12) 52%, transparent 100%); }
.surface.active .box-face{ opacity:0; pointer-events:none; }
.b-index{ position:absolute; z-index:3; pointer-events:none; color:var(--paper);
  top:clamp(13px,1.5vw,24px); right:clamp(13px,1.5vw,22px);
  font-family:ui-monospace,Menlo,monospace; font-size:12px; letter-spacing:.16em; opacity:.7; }
.b-head{ position:absolute; top:0; left:0; right:0; z-index:3; pointer-events:none;
  padding:clamp(14px,1.6vw,26px); padding-right:clamp(40px,3.8vw,64px); }
/* Desktop right-column boxes: fill the whole cell and center the title
   vertically; the font size is set per-cell in JS from the cell's pixel height
   (see Cell), so long titles shrink to fit short cells instead of overflowing. */
.box-face .b-head{ inset:0; display:flex; align-items:center;
  padding:clamp(10px,1.3vw,20px) clamp(36px,3.4vw,56px) clamp(10px,1.3vw,20px) clamp(14px,1.6vw,26px); }
.box-face .b-label{ display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:4; overflow:hidden; }
.b-label{ display:block; text-align:left; color:var(--paper); font-family:var(--display); text-transform:uppercase; font-weight:400; letter-spacing:.01em; line-height:1.1; text-wrap:balance;
  font-size:13px; }

/* ---- THE EXPANDED FACE (active section: white type on the photo) ---- */
.exp-face{ position:absolute; inset:0; z-index:1; opacity:0; pointer-events:none;
  transition:opacity .5s ease .12s; }
.surface.active .exp-face{ opacity:1; pointer-events:auto; }
/* Legibility now comes from a translucent black box behind the text (.exp-col),
   not a full-face gradient. */
.exp-face::before{ content:""; position:absolute; inset:0; z-index:1; pointer-events:none; background:transparent; }
/* Full-frame looping video behind a content section's expanded text */
.exp-bg-video{ position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit:cover; }
/* Pause/play + rewind controls, pinned lower-right of the video frame */
.vid-ctrls{ position:absolute; z-index:6; bottom:clamp(14px,1.6vw,24px); right:clamp(14px,1.6vw,24px);
  display:flex; gap:8px; }
.vid-btn{ width:34px; height:34px; display:flex; align-items:center; justify-content:center;
  border:1px solid rgba(255,255,255,.32); border-radius:999px; background:rgba(10,18,14,.42);
  -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px); color:var(--paper); cursor:pointer;
  transition:background-color .2s, border-color .2s, transform .15s; padding:0; }
.vid-btn:hover{ background:rgba(10,18,14,.7); border-color:rgba(255,255,255,.6); }
.vid-btn:active{ transform:scale(.92); }
.e-index{ position:absolute; z-index:5; pointer-events:none;
  top:clamp(14px,1.7vw,26px); right:clamp(18px,2vw,30px);
  font-family:ui-monospace,Menlo,monospace; font-size:clamp(12px,1vw,14px); letter-spacing:.16em;
  color:var(--paper); opacity:.7; }

/* Persistent contact footer — right-aligned to the content boxes (right:6%) */
.bm-footer{ position:relative; z-index:5; flex:0 0 auto; display:flex; justify-content:flex-end;
  padding:0 6% clamp(14px,2vw,26px); }
.bm-contact{ margin:0; display:flex; flex-direction:column; gap:2px; text-align:right;
  font-family:"Work Sans",system-ui,sans-serif; font-size:12px; font-weight:400; line-height:1.55;
  color:var(--paper); opacity:.85; }
.bm-contact a{ color:var(--paper); text-decoration:none; transition:opacity .2s; }
.bm-contact a:hover{ opacity:.6; }

.exp-scroll{ position:absolute; inset:0; z-index:2; overflow-y:auto;
  padding:clamp(20px,3vw,56px) clamp(22px,6vw,120px) clamp(28px,5vw,70px) clamp(20px,3vw,56px); }
.exp-scroll::-webkit-scrollbar{ width:9px; }
.exp-scroll::-webkit-scrollbar-thumb{ background:rgba(255,255,255,.26); border-radius:9px; }

/* Content sections: heading + body + closing line + list, white on a translucent black box */
.exp-col{ position:relative; z-index:1; max-width:50%; color:var(--paper);
  background:rgba(0,0,0,.66); border-radius:var(--radius);
  padding:clamp(20px,2.4vw,38px) clamp(22px,2.6vw,42px);
  /* Fluid body copy: grows with viewport up to a cap. --e-head is locked to
     1.2x --e-para in px, and the cap keeps --e-head at most 20px. */
  --e-para:clamp(14px, 1.2vw, 16.667px);
  --e-head:calc(var(--e-para) * 1.2); }
/* Section heading: top of the text box, white, Oswald uppercase. */
.e-head{ font-family:var(--display); text-transform:uppercase; font-weight:500; letter-spacing:.02em; line-height:1.2; text-wrap:balance; color:var(--paper);
  font-size:var(--e-head, 16px); margin:0 0 2em; }
.e-para{ font-weight:400; font-size:var(--e-para, 14px); line-height:1.55; margin:0 0 1em; opacity:.94; }
.e-close{ font-weight:700; font-size:var(--e-para, 14px); line-height:1.55; margin:1em 0 0; opacity:1; }

/* Special-sauce list — head matches .e-head, items match .e-para */
.e-list-head{ font-family:var(--display); text-transform:uppercase; font-weight:500; font-size:var(--e-head, 16px);
  letter-spacing:.02em; line-height:1.2; opacity:1; margin:2em 0 1em; }
.e-list{ list-style:none; margin:0; padding:0; display:grid; gap:.5em;
  grid-template-columns:repeat(2, minmax(0,1fr)); grid-template-rows:repeat(5, auto);
  grid-auto-flow:column; column-gap:clamp(20px,3vw,52px); max-width:46em; }
.e-list li{ display:flex; gap:.6em; align-items:baseline; font-weight:400;
  font-size:var(--e-para, 14px); line-height:1.55; opacity:.94; }
.e-list li .n{ font-family:ui-monospace,Menlo,monospace; font-size:.8em; opacity:.5; min-width:1.8em; }

/* Section 07: text + client-logo wall side by side */
.exp-cols.has-logos{ display:flex; gap:clamp(16px,2vw,30px); align-items:stretch; }
.exp-cols.has-logos .exp-col{ flex:1 1 0; max-width:none; }
.logo-box{ flex:0 0 40%; align-self:stretch; background:rgba(0,0,0,.66); border-radius:var(--radius);
  padding:5% clamp(14px,1.6vw,24px); display:flex; align-items:stretch; }
.logo-grid{ width:100%; height:100%; display:grid; grid-template-columns:repeat(3,1fr);
  grid-auto-rows:1fr; gap:0; align-content:stretch; }
.logo-tile{ position:relative; display:flex; align-items:center; justify-content:center;
  padding:clamp(10px,1.5vw,22px); }
.logo-tile img{ max-width:100%; max-height:100%; object-fit:contain; opacity:0; }
.logo-tile.has-img img{ opacity:1; }
.logo-fallback{ font-family:"Work Sans",system-ui,sans-serif; font-size:clamp(10px,.85vw,12px);
  font-weight:400; letter-spacing:.01em; line-height:1.15; text-align:center; text-wrap:balance;
  color:var(--paper); opacity:.62; }

/* Our Work: a full-frame highlight reel with the headline + index floating on top */
.reel-fill{ position:absolute; inset:0; z-index:1; overflow:hidden; border-radius:inherit;
  display:flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,.05);
  background-image:repeating-linear-gradient(135deg, rgba(255,255,255,.04) 0 16px, transparent 16px 32px); }
.reel-fill video,.reel-fill img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.reel-head{ position:absolute; z-index:4; margin:0;
  top:clamp(14px,1.7vw,26px); left:clamp(20px,3vw,56px);
  text-shadow:0 2px 22px rgba(0,0,0,.55); }
.reel-label{ position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; gap:8px; text-align:center; }
.reel-k{ font-weight:500; font-size:clamp(20px,2.4vw,34px); letter-spacing:-.01em; color:var(--paper); }
.reel-d{ font-family:ui-monospace,Menlo,monospace; font-size:12px; letter-spacing:.2em; text-transform:uppercase;
  color:var(--paper); opacity:.55; }

/* ---- MOBILE: accordion (panels stay stacked in numerical order) ---- */
.m-accordion{ position:relative; z-index:1; display:flex; flex-direction:column; gap:10px; padding:0 4% 7%; }

/* One section = a rounded card. overflow:hidden clips the header + video + text
   panel into a single continuous card when the section is open. */
.m-acc-item{ border-radius:var(--radius); overflow:hidden; background:rgba(10,18,14,.5); }

/* Closed section = a thumbnail tile (click target) with title/index overlaid. */
.m-acc-head{ position:relative; display:block; width:100%; min-height:96px; border:none; cursor:pointer;
  background:rgba(10,18,14,.6); background-size:cover; background-position:center; color:var(--paper);
  padding:0; text-align:left; }
.m-acc-head .b-index{ opacity:.65; }

/* Open content: a clean 16:9 video banner with the title/index OVERLAID on it
   (no black bar), then white copy on a 50%-black panel below the video. */
.m-active{ display:flex; flex-direction:column; }
.m-video{ position:relative; width:100%; aspect-ratio:16/9; overflow:hidden; background:#000; }
.m-video-el{ position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit:cover; }
/* legibility scrim for the overlaid title/index */
.m-video::before{ content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(180deg, rgba(8,14,11,.6) 0%, rgba(8,14,11,.12) 34%, transparent 58%); }
/* section 07 has no video — a compact title row instead of an empty banner */
.m-titlebar{ position:relative; min-height:64px; }
.m-text{ display:flex; flex-direction:column; background:rgba(0,0,0,.5); color:var(--paper);
  padding:clamp(20px,5.5vw,30px) 6% clamp(24px,6vw,34px); }
.m-text .e-para{ font-size:15px; color:var(--paper); opacity:.96; }
.m-text .e-close{ font-size:15px; color:var(--paper); opacity:1; }
.m-text .e-list-head{ margin-top:1.6em; }
/* On a narrow screen the "what we believe" list and logo wall go full width */
.m-list{ grid-template-columns:1fr; grid-template-rows:none; grid-auto-flow:row; max-width:none; }
.m-text .logo-box{ flex:none; width:100%; align-self:stretch; margin-top:1.6em;
  background:transparent; padding:0; }
`;


/* ============================================================================
   SMALL HELPERS
   ============================================================================ */
function useIsMobile() {
  const [m, setM] = useState(() => window.matchMedia("(max-width:720px)").matches);
  useEffect(() => {
    const mq = window.matchMedia("(max-width:720px)");
    const on = () => setM(mq.matches);
    mq.addEventListener("change", on);
    return () => mq.removeEventListener("change", on);
  }, []);
  return m;
}

/* Measures an element's pixel size and keeps it current as the window resizes. */
function useSize() {
  const ref = useRef(null);
  const [size, setSize] = useState({ w: 0, h: 0 });
  useLayoutEffect(() => {
    const el = ref.current;if (!el) return;
    const measure = () => setSize({ w: el.clientWidth, h: el.clientHeight });
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return [ref, size];
}


/* ============================================================================
   THE FULL-BLEED BACKGROUND
   ============================================================================ */
function Background() {
  if (BG_VIDEO) return <div className="bg-layer"><video src={BG_VIDEO} autoPlay muted loop playsInline></video></div>;
  if (BG_IMAGE) return <div className="bg-layer"><img src={BG_IMAGE} alt="" /></div>;
  return (
    <div className="bg-layer">
      <div className="bg-ph">
        <div className="bg-ph-k">Full-screen background</div>
        <div className="bg-ph-d">Supply a photo (2560 × 1440) or a video (1920 × 1080, MP4) — see data.js</div>
      </div>
    </div>);

}


/* ============================================================================
   EXPANDED CONTENT (the white-on-video body of the active section)
   ============================================================================ */

/* A full-frame video that only plays while its section is active. It plays
   through MAX_PLAYS times, then rests on the final frame. Small pause/play +
   rewind controls (and a mute toggle when `sound`) are pinned lower-right. */
function ExpVideo({ src, active, className, sound }) {
  const ref = useRef(null);
  const playsRef = useRef(0); /* how many times it's played so far */
  const MAX_PLAYS = 1;
  const [paused, setPaused] = useState(false);
  const [ended, setEnded] = useState(false); /* finished MAX_PLAYS, parked on last frame */
  const [muted, setMuted] = useState(!sound); /* sound videos start UNmuted */
  /* React doesn't reliably reflect the `muted` attribute, so set it imperatively. */
  useEffect(() => {
    const v = ref.current;if (v) v.muted = muted;
  }, [muted]);
  useEffect(() => {
    const v = ref.current;if (!v) return;
    v.muted = muted;
    if (active) {playsRef.current = 0;setEnded(false);setPaused(false);
      v.currentTime = 0;const p = v.play();if (p && p.catch) p.catch(() => {});} else
    {v.pause();}
  }, [active]);
  /* Loop manually so we can stop after MAX_PLAYS and rest on the final frame. */
  const onEnded = () => {
    const v = ref.current;if (!v) return;
    playsRef.current += 1;
    if (playsRef.current < MAX_PLAYS) {v.currentTime = 0;const p = v.play();if (p && p.catch) p.catch(() => {});} else
    {setEnded(true);} /* leave it parked on the last frame */
  };
  const toggle = (e) => {e.stopPropagation();const v = ref.current;if (!v) return;
    if (v.paused) {
      if (ended) {playsRef.current = 0;setEnded(false);v.currentTime = 0;} /* replay from start */
      const p = v.play();if (p && p.catch) p.catch(() => {});setPaused(false);
    } else {v.pause();setPaused(true);}};
  const rewind = (e) => {e.stopPropagation();const v = ref.current;if (!v) return;
    playsRef.current = 0;setEnded(false);setPaused(false);
    v.currentTime = 0;const p = v.play();if (p && p.catch) p.catch(() => {});};
  const showPlay = paused || ended;
  return (
    <React.Fragment>
      <video ref={ref} className={className} src={src} playsInline onEnded={onEnded}></video>
      <div className="vid-ctrls">
        {sound &&
        <button className="vid-btn" onClick={(e) => {e.stopPropagation();setMuted((m) => !m);}} aria-label={muted ? "Unmute" : "Mute"}>
          {muted ?
          <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M11 5L6 9H3v6h3l5 4zM16 9l5 6M21 9l-5 6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg> :
          <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M11 5L6 9H3v6h3l5 4zM16 8.5a4 4 0 0 1 0 7M18.5 6a7.5 7.5 0 0 1 0 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>}
        </button>}
        <button className="vid-btn" onClick={toggle} aria-label={showPlay ? "Play" : "Pause"}>
          {showPlay ?
          <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M7 5l13 7-13 7z" fill="currentColor" /></svg> :
          <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor" /></svg>}
        </button>
        <button className="vid-btn" onClick={rewind} aria-label="Rewind">
          <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M12 5V2L7 6l5 4V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z" fill="currentColor" /></svg>
        </button>
      </div>
    </React.Fragment>);

}

function ExpandedContent({ id, active }) {
  /* Intro: just the full-frame video — its statement is baked into the footage. */
  if (id === "intro")
    return CONTENT.intro && CONTENT.intro.expVideo ?
      <ExpVideo src={CONTENT.intro.expVideo} active={active} className="exp-bg-video" /> : null;

  const s = SECTIONS[id];
  const c = CONTENT[id] || {};

  if (c.reel) return (
    <div className="reel-fill">
      {c.reelVideo ?
      <ExpVideo src={c.reelVideo} active={active} className="reel-video" sound={c.reelSound} /> :
      <div className="reel-label">
            <span className="reel-k">Highlight Reel</span>
            <span className="reel-d">Full-frame video — placeholder</span>
          </div>}
      <h2 className="e-head reel-head">{s.label}</h2>
    </div>);


  return (
    <React.Fragment>
      {c.expVideo && <ExpVideo src={c.expVideo} active={active} className="exp-bg-video" />}
      <div className="exp-scroll">
      <div className={"exp-cols" + (c.logos ? " has-logos" : "")}>
      <div className="exp-col">
        <h2 className="e-head">{s.label}</h2>
        {(c.paras || []).map((p, i) => <p className="e-para" key={i}>{p}</p>)}
        {c.close && <p className="e-close">{c.close}</p>}
        {c.list &&
            <React.Fragment>
            <div className="e-list-head">{c.list.head}</div>
            <ul className="e-list">
              {c.list.items.map((it, i) =>
                <li key={i}><span className="n">{String(i + 1).padStart(2, "0")}</span><span>{it}</span></li>
                )}
            </ul>
          </React.Fragment>
            }
      </div>
      {c.logos &&
          <div className="logo-box">
          <div className="logo-grid">
            {c.logos.map((lg, i) => <LogoTile key={i} logo={lg} />)}
          </div>
        </div>
          }
      </div>
      </div>
    </React.Fragment>);

}

/* A single client logo: shows the transparent PNG once it loads, and falls back
   to the company name (so the wall reads even before art is supplied). */
function LogoTile({ logo }) {
  const [ok, setOk] = useState(false);
  const imgRef = useRef(null);
  /* onLoad doesn't fire for images already cached/complete before React attaches
     the listener — so also check completeness on mount. */
  useEffect(() => {
    const i = imgRef.current;
    if (i && i.complete && i.naturalWidth > 0) setOk(true);
  }, []);
  return (
    <div className={"logo-tile" + (ok ? " has-img" : "")}>
      <img ref={imgRef} src={logo.img} alt={logo.name}
      onLoad={() => setOk(true)} onError={() => setOk(false)} />
      {!ok && <span className="logo-fallback">{logo.name}</span>}
    </div>);

}


/* ============================================================================
   MOBILE ACTIVE SECTION
   On phones we stop overlaying copy on the video. Instead: a clean 16:9 video
   banner (footage already carries its own text), then the headline + body copy
   stacked below on the solid background where it's fully legible.
   ============================================================================ */
function MobileActive({ id }) {
  const s = SECTIONS[id];
  const c = CONTENT[id] || {};
  const video = c.expVideo || c.reelVideo;
  const hasText = (c.paras && c.paras.length) || c.close || c.list || c.logos;
  return (
    <div className="m-active">
      {video ?
      <div className="m-video">
          <ExpVideo src={video} active={true} className="m-video-el" sound={c.reel && c.reelSound} />
          <span className="b-index">{s.index}</span>
          <div className="b-head"><span className="b-label">{s.label}</span></div>
        </div> :
      <div className="m-titlebar">
          <span className="b-index">{s.index}</span>
          <div className="b-head"><span className="b-label">{s.label}</span></div>
        </div>
      }
      {hasText &&
      <div className="m-text">
          {(c.paras || []).map((p, i) => <p className="e-para" key={i}>{p}</p>)}
          {c.close && <p className="e-close">{c.close}</p>}
          {c.list &&
        <React.Fragment>
              <div className="e-list-head">{c.list.head}</div>
              <ul className="e-list m-list">
                {c.list.items.map((it, i) =>
            <li key={i}><span className="n">{String(i + 1).padStart(2, "0")}</span><span>{it}</span></li>
            )}
              </ul>
            </React.Fragment>
        }
          {c.logos &&
        <div className="logo-box m-logo-box">
              <div className="logo-grid">
                {c.logos.map((lg, i) => <LogoTile key={i} logo={lg} />)}
              </div>
            </div>
        }
        </div>
      }
    </div>);

}


/* ============================================================================
   ONE CELL (renders both faces; crossfades by `active`)
   ============================================================================ */
function Cell({ id, px, tier, active, onClick }) {
  const s = SECTIONS[id];
  const thumb = s.thumb;
  /* Scale the box title to the cell's height so long titles fit short cells.
     Floor 11px keeps it legible; cap 15px stops it ballooning on tall cells. */
  const labelSize = Math.max(11, Math.min(15, px.height * 0.16));
  return (
    <div className="cell" style={{
      top: `${px.top}px`, left: `${px.left}px`, width: `${px.width}px`, height: `${px.height}px`
    }}>
      <div className={"surface t-" + tier + (active ? " active" : "")} onClick={active ? undefined : onClick}
      style={!active && thumb ? { backgroundImage: `linear-gradient(rgba(0,0,0,.3), rgba(0,0,0,.3)), url(${thumb})` } : undefined}>
        <div className="box-face">
          <span className="b-index">{s.index}</span>
          <div className="b-head"><span className="b-label" style={{ fontSize: `${labelSize}px` }}>{s.label}</span></div>
        </div>
        <div className="exp-face">
          <span className="e-index">{s.index}</span>
          <ExpandedContent id={id} active={active} />
        </div>
      </div>
    </div>);

}


/* ============================================================================
   LAYOUTS  — given the layout mode + which section is active + the measured
   group size (px), return a pixel rect { top,left,width,height,tier } for every
   section. Surfaces carry a 3px inset, so adjacent cells get a 6px visual gap;
   we therefore lay cells edge-to-edge here.
   ============================================================================ */
function getLayout(active, size) {
  const W = size.w,H = size.h;
  const others = ORDER.filter((id) => id !== active);
  const rects = {};

  /* The active panel is a 16:9 box. Size it to the available area while leaving
     at least MINCOL of width for the right-hand box column, clamp so it never
     exceeds the panel height, then center the whole cluster vertically. The box
     column matches the panel's height so the two stay aligned. */
  const MINCOL = Math.max(200, W * 0.18);
  let mainW = Math.min(W - MINCOL, H * 16 / 9);
  let mainH = mainW * 9 / 16;
  if (mainH > H) {mainH = H;mainW = mainH * 16 / 9;}
  const top = (H - mainH) / 2;
  const colLeft = mainW;
  const colW = W - mainW;
  rects[active] = { top, left: 0, width: mainW, height: mainH, tier: "lg" };
  const n = others.length;
  const boxH = mainH / n;
  others.forEach((id, i) => {
    rects[id] = { top: top + i * boxH, left: colLeft, width: colW, height: boxH, tier: "md" };
  });
  return rects;
}


/* ============================================================================
   THE WHOLE APP
   ============================================================================ */
function App() {
  const [active, setActive] = useState("intro");
  const isMobile = useIsMobile();
  const [groupRef, groupSize] = useSize();

  /* Enable the glide only after mount so cells don't slide in from zero size. */
  const [anim, setAnim] = useState(false);
  useEffect(() => {
    const r = requestAnimationFrame(() => requestAnimationFrame(() => setAnim(true)));
    return () => cancelAnimationFrame(r);
  }, []);

  /* Pixel rect (+ tier) for every section (left panel + right box column). */
  const rects = getLayout(active, groupSize);

  const goHome = () => setActive("intro");

  return (
    <React.Fragment>
      <style>{CSS}</style>
      <Background />

      <header className="bm-header">
        <button className="bm-logo" onClick={goHome} aria-label="Back to intro">BROTHER MATTHEW</button>
      </header>

      <div className="stage">
        {isMobile ?
        <div className="m-accordion">
            {ORDER.map((id) => {
            const s = SECTIONS[id];
            const open = id === active;
            if (open) return (
              <div className="m-acc-item open" key={id}>
                <MobileActive id={id} />
              </div>);
            return (
              <button className="m-acc-item m-acc-head" key={id} onClick={() => setActive(id)}
                style={s.thumb ? { backgroundImage: `linear-gradient(rgba(0,0,0,.42), rgba(0,0,0,.42)), url(${s.thumb})` } : undefined}>
                <span className="b-index">{s.index}</span>
                <div className="b-head"><span className="b-label">{s.label}</span></div>
              </button>);
          })}
          </div> :

        <div className={"group group-stack" + (anim ? " anim" : "")} ref={groupRef}>
            {groupSize.w > 0 && ORDER.map((id) =>
          <Cell key={id} id={id} px={rects[id]} tier={rects[id].tier}
          active={id === active} onClick={() => setActive(id)} />
          )}
          </div>
        }
      </div>

      <footer className="bm-footer">
        {isMobile ?
        <p className="bm-contact">
          <span>For more information, or to arrange a discussion,</span>
          <span>contact John Thompson at {CONTACT.phone}</span>
          <span>email: <a href={"mailto:" + CONTACT.email}>{CONTACT.email}</a></span>
          <span>portfolio: <a href={"https://" + CONTACT.web} target="_blank" rel="noopener noreferrer">{CONTACT.web}</a></span>
        </p> :
        <p className="bm-contact">
          <span>{CONTACT.lead} {CONTACT.phone}</span>
          <span>email: <a href={"mailto:" + CONTACT.email}>{CONTACT.email}</a>{"  |  "}portfolio: <a href={"https://" + CONTACT.web} target="_blank" rel="noopener noreferrer">{CONTACT.web}</a></span>
        </p>
        }
      </footer>
    </React.Fragment>);

}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);