/* ============================================================================
   HOME — single-column layout after liam.cv (structure originally from
   danielwhite.uk). Sections: Bio → Work → Favorite things → Kind words →
   Connect → footer clock.
   Single mode (the old "human mode" won — Lucas, 2026-09-09).
   The AI chat interface is intentionally absent — see TASKLIST.md.

   All page copy lives in CONTENT below. Facts trace to basic-info.js /
   resume.js — keep them in sync if the story changes.
   ========================================================================= */

(() => {
  const { useState, useEffect, useRef } = React;
  const { SITE } = window;
  const HOME_DESIGN = window.PortraitMotion.resolveHomeDesign();
  const PORTRAIT_EXPERIENCE = window.PortraitMotion.resolvePortraitExperience(
    window.PORTRAIT_LAB_CONFIG,
  );
  const LAB_THEME_OPTIONS = PORTRAIT_EXPERIENCE.isLab
    && Array.isArray(window.PORTRAIT_LAB_CONFIG?.themes)
    ? window.PORTRAIT_LAB_CONFIG.themes.filter(
      (option) => option?.value === "light" || option?.value === "dark",
    )
    : [];
  const LAB_DEFAULT_THEME = LAB_THEME_OPTIONS.some(
    (option) => option.value === window.PORTRAIT_LAB_CONFIG?.defaultTheme,
  )
    ? window.PORTRAIT_LAB_CONFIG.defaultTheme
    : "light";
  const LAB_ENTRANCE_OPTIONS = [
    { value: "cascade", label: "Cascade" },
    { value: "quick", label: "Quick" },
    { value: "editorial", label: "Editorial" },
  ];
  const LAB_NAME_OPTIONS = [
    { value: "classic", label: "Classic" },
    { value: "magazine", label: "Magazine" },
  ];

  // Deliberately uneven print scraps, drawn with text so the name stays crisp.
  // Widths are px at the 43px design size; tiles scale with the letter size.
  const MAGAZINE_LETTERS = [
    { letter: "L", paper: "#e6e5df", ink: "#394145", font: "Georgia, serif", tilt: -3, lift: 3, width: 42 },
    { letter: "U", paper: "#52383d", ink: "#ffd0bc", font: "Georgia, serif", tilt: 3, lift: -2, width: 46 },
    { letter: "C", paper: "#f5da56", ink: "#325a9c", font: "Arial Black, sans-serif", tilt: -2, lift: 2, width: 44 },
    { letter: "A", paper: "#66aa9f", ink: "#e4eee7", font: "Georgia, serif", tilt: 3, lift: 0, width: 44 },
    { letter: "S", paper: "#65bd87", ink: "#142d23", font: "Times New Roman, serif", tilt: 7, lift: 3, width: 42 },
    { letter: "R", paper: "#e7a786", ink: "#372e55", font: "Georgia, serif", tilt: -4, lift: -1, width: 44 },
    { letter: "O", paper: "#d6c7ec", ink: "#3c365f", font: "Arial Black, sans-serif", tilt: 4, lift: 2, width: 44 },
    { letter: "L", paper: "#eadfbb", ink: "#b14938", font: "Times New Roman, serif", tilt: -5, lift: 1, width: 38 },
    { letter: "L", paper: "#f4a6a2", ink: "#4b3542", font: "Georgia, serif", tilt: 3, lift: -2, width: 38 },
    { letter: "O", paper: "#b8d2e3", ink: "#263c52", font: "Arial Black, sans-serif", tilt: -3, lift: 2, width: 44 },
  ];
  const MAGAZINE_DESIGN_SIZE = 43;

  const FONT = '"IBM Plex Mono", "SFMono-Regular", Consolas, monospace';
  const DISPLAY = '"Young Serif", Georgia, "Times New Roman", serif';
  const MONO = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
  const SECTION_GAP = "1.25rem";

  /* Type & spacing tokens. Inline styles read custom properties; both the
     public page and the lab set them on <html> from TUNE_LIVE / the lab bench
     (see tuneTokens). The fallbacks are the pre-2026-09-23 design. */
  const v = (name, fallback) => `var(--${name}, ${fallback})`;

  /* Temporary launch switches — keep deferred sections ready to restore. */
  const SHOW_KIND_WORDS = false;
  const SHOW_FAVORITE_THINGS = false;

  /* Brand tokens render as highlighter marks (chosen 2026-09-15). */
  const HIGHLIGHT_COLORS = {
    moveworks: "var(--ui-mark-moveworks)",
    integrative: "var(--ui-mark-integrative)",
    qualcomm: "var(--ui-mark-qualcomm)",
    contact: "var(--ui-mark-contact)",
  };

  const WINK_DURATION_MS = 900;
  const WAVE_DURATION_MS = 1100;
  const DIRECTION_DELAY_MS = 32;
  const PORTRAIT_LAYOUT = {
    key: "right",
    headingFlex: "1 1 180px",
    headingTextAlign: "left",
    columnGap: 16,
    rowGap: 8,
    portraitMargin: `var(--portrait-lift, ${HOME_DESIGN.portraitTopLift}px) 0 ${HOME_DESIGN.portraitBottomMargin}px`,
  };

  /* Palette after liam.cv: forest-green ink on green-tinted near-white. */
  const T = {
    paper: "var(--ui-bg)",
    ink: "var(--ui-fg)",
    muted: "var(--ui-muted)",
    hairline: "var(--ui-line)",
    accent: "var(--ui-accent)",
    bubble: "var(--ui-bubble)",
    bubbleInk: "var(--ui-bubble-ink)",
    tilt: 1,
  };

  /* ── copy ────────────────────────────────────────────────────────────── */

  const BIO = {
    lead: "I build consumer software that brings people health, fun, and community.",
    looking: "",
  };

  const CONTENT = {
    /* One row per thing, styled after liam.cv's Work list.
       `details` (optional) reveal on click. */
    work: [
      {
        key: "reprabbit",
        title: "RepRabbit",
        href: "https://reprabbit.app",
        logo: "/reprabbit-icon.svg",
        role: "Founder",
      },
      {
        key: "moveworks",
        title: "Moveworks",
        href: "https://www.moveworks.com/us/en/platform/ai-agent-builder",
        logo: "https://www.google.com/s2/favicons?domain=moveworks.com&sz=64",
        role: "Senior Product Manager",
      },
    ],

    favorites: [
      { label: "Video game", value: "Counter-Strike" },
      { label: "Sports teams", value: "Rockets, Texans, Cougars" },
      { label: "Movie", value: "Frankenstein (2025)" },
      { label: "Non-fiction", value: "Man's Search for Meaning" },
      { label: "Fiction", value: "The Alchemist" },
      { label: "Sushi spot", value: "Yama Izakaya — or Uchi in Houston" },
    ],

    connectLead: "Building something people will actually love? I want in.",

    /* The email "link" copies the address instead of opening a mail app */
    email: { label: "email", address: "charleslucasrollo@gmail.com" },
    connectLinks: [
      { label: "LinkedIn", href: "https://www.linkedin.com/in/lucasrollo/" },
      { label: "X", href: "https://x.com/RolloLucas" },
    ],

    footerCity: "Portland, Oregon",
    footerTimeZone: "America/Los_Angeles",
  };

  /* Companies named in the lab's new bio. */
  const COMPANIES = {
    moveworks: { label: "Moveworks", href: "https://www.moveworks.com" },
    servicenow: { label: "ServiceNow", href: "https://www.servicenow.com" },
  };

  /* ── styles ──────────────────────────────────────────────────────────── */
  const S = {
    page: {
      maxWidth: v("column", `${HOME_DESIGN.contentMaxWidth}px`),
      margin: "0 auto",
      padding: `clamp(1.5rem, 3vw, 2rem) ${v("gutter", "1rem")} 2rem`,
      position: "relative",
      fontFamily: v("font-body", FONT),
      fontSize: v("fs-body", "1rem"),
      fontWeight: 400,
      lineHeight: v("lh-body", "1.5rem"),
      letterSpacing: v("ls-body", "0.14px"),
      color: T.ink,
    },
    portraitStyleRow: {
      display: "flex",
      alignItems: "center",
      justifyContent: "flex-end",
      gap: 8,
    },
    portraitStyleLabel: {
      color: T.muted,
      fontFamily: FONT,
      fontSize: 11,
      lineHeight: "20px",
    },
    portraitStyleControl: {
      display: "inline-flex",
      flexWrap: "wrap",
      justifyContent: "flex-end",
      alignItems: "center",
      padding: 2,
      border: `1px solid ${T.hairline}`,
      borderRadius: 14,
      background: T.bubble,
    },
    portraitStyleOption: (active) => ({
      appearance: "none",
      border: 0,
      borderRadius: 999,
      background: active ? T.ink : "transparent",
      color: active ? T.paper : T.muted,
      cursor: "pointer",
      fontFamily: FONT,
      fontSize: 11,
      fontWeight: 500,
      lineHeight: "20px",
      padding: "1px 8px",
      whiteSpace: "nowrap",
    }),

    /* Bio header after liam.cv: serif name, hairline below, then body */
    headerRow: {
      display: "flex",
      alignItems: HOME_DESIGN.headerAlignment,
      flexDirection: "row",
      flexWrap: "wrap",
      justifyContent: "space-between",
      columnGap: PORTRAIT_LAYOUT.columnGap,
      rowGap: PORTRAIT_LAYOUT.rowGap,
      paddingBottom: v("header-pad", "0.9rem"),
      marginBottom: v("header-gap", "1.1rem"),
      transform: `translateY(${v("header-lift", `${HOME_DESIGN.headerLift}px`)})`,
    },
    h1: {
      fontFamily: v("font-display", DISPLAY),
      fontSize: v("fs-h1", "29px"),
      lineHeight: v("lh-h1", "36px"),
      fontWeight: v("w-display", 400),
      letterSpacing: v("ls-display", "0.005em"),
      margin: 0,
      color: T.ink,
      flex: PORTRAIT_LAYOUT.headingFlex,
      textAlign: PORTRAIT_LAYOUT.headingTextAlign,
      transform: `translateY(${v("h1-drop", `${HOME_DESIGN.headingOpticalDrop}px`)})`,
    },
    /* Keeps "Product Manager" together so the title breaks after the comma. */
    h1Role: { color: T.muted, whiteSpace: v("role-wrap", "normal") },
    magazineWords: {
      display: "flex",
      flexWrap: "wrap",
      alignItems: "center",
      gap: "4px 13px",
      padding: "3px 0 6px",
      /* 43px on desktop; shrinks on phones so "LUCAS" still fits beside
         the portrait instead of pushing it onto its own row. */
      fontSize: `clamp(32px, 9vw, ${MAGAZINE_DESIGN_SIZE}px)`,
    },
    magazineWord: {
      display: "inline-flex",
      alignItems: "center",
      gap: 2,
      whiteSpace: "nowrap",
    },
    magazineLetter: (tile) => ({
      display: "inline-flex",
      flex: "0 0 auto",
      alignItems: "center",
      justifyContent: "center",
      width: `${tile.width / MAGAZINE_DESIGN_SIZE}em`,
      height: `${52 / MAGAZINE_DESIGN_SIZE}em`,
      paddingBottom: 2,
      backgroundColor: tile.paper,
      backgroundImage: "radial-gradient(rgba(0,0,0,.12) .45px, transparent .6px)",
      backgroundSize: "3px 3px",
      color: tile.ink,
      clipPath: "polygon(3% 2%, 31% 0, 56% 2%, 96% 0, 100% 19%, 97% 53%, 100% 97%, 71% 100%, 38% 98%, 0 100%, 3% 72%, 0 32%)",
      fontFamily: tile.font,
      fontSize: "1em",
      fontStyle: tile.letter === "U" || tile.letter === "S" ? "italic" : "normal",
      fontWeight: tile.font.includes("Arial") ? 900 : 700,
      letterSpacing: 0,
      lineHeight: 1,
      textShadow: "1px 1px 0 rgba(0,0,0,.12)",
      transform: `translateY(${tile.lift}px) rotate(${tile.tilt}deg)`,
    }),
    magazineRole: {
      display: "block",
      marginTop: 4,
      color: T.muted,
      fontFamily: FONT,
      fontSize: 12,
      lineHeight: "18px",
      letterSpacing: "0.02em",
    },
    portraitButton: {
      appearance: "none",
      width: "clamp(112px, 18vw, 152px)",
      aspectRatio: "1",
      flex: "0 0 auto",
      display: "block",
      margin: PORTRAIT_LAYOUT.portraitMargin,
      padding: 0,
      border: 0,
      background: "transparent",
      color: "inherit",
      cursor: "pointer",
      lineHeight: 0,
      touchAction: "manipulation",
    },
    portraitImage: {
      display: "block",
      width: "100%",
      height: "100%",
      objectFit: "contain",
      userSelect: "none",
      WebkitUserDrag: "none",
    },
    portraitWaveStage: {
      position: "relative",
      display: "block",
      width: "100%",
      height: "100%",
      overflow: "visible",
      /* Paper backdrop inside the header's stacking context, so the wave
         GIF's multiply blend has something to blend with (its #fefefe
         background otherwise shows as a faint box). */
      background: T.paper,
    },
    portraitWaveLayer: {
      position: "absolute",
      inset: 0,
      display: "block",
      width: "100%",
      height: "100%",
      objectFit: "contain",
      pointerEvents: "none",
      userSelect: "none",
      WebkitUserDrag: "none",
    },
    /* Bio body: muted, roomy leading. */
    paragraph: {
      margin: `0 0 ${v("para-gap", "0.6rem")}`,
      color: T.ink,
      fontSize: v("fs-body", `${HOME_DESIGN.bodyFontSize}px`),
      fontWeight: v("w-bio", HOME_DESIGN.bioFontWeight),
      lineHeight: v("lh-body", `${HOME_DESIGN.bodyLineHeight}px`),
      textWrap: "pretty",
    },
    brandToken: (interactive, brand, kind) => {
      const mark = kind === "company" ? HIGHLIGHT_COLORS[brand] : HIGHLIGHT_COLORS.contact;
      return {
        alignItems: "center",
        appearance: "none",
        backgroundColor: "transparent",
        backgroundImage: `linear-gradient(to bottom, transparent 48%, ${mark} 48%, ${mark} 91%, transparent 91%)`,
        backgroundRepeat: "no-repeat",
        border: "1px solid transparent",
        color: T.ink,
        cursor: interactive ? "pointer" : "default",
        display: "inline-flex",
        fontFamily: v("font-body", FONT),
        fontSize: v("fs-body", "16px"),
        fontWeight: v("w-brand", 600),
        lineHeight: v("lh-brand", "21px"),
        margin: v("brand-margin", "0 1px"),
        padding: "0 1px",
        whiteSpace: "nowrap",
      };
    },

    /* Lab bio: company names are plain underlined links at body weight
       (no highlight, no bold — Lucas, 2026-09-23). */
    companyLink: { fontWeight: 400, whiteSpace: "nowrap" },
    ctaRow: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, marginTop: 4 },
    ctaButton: (primary) => ({
      appearance: "none",
      display: "inline-flex",
      alignItems: "center",
      height: 36,
      padding: "0 14px",
      border: `1px solid ${primary ? T.ink : "var(--ui-link-underline)"}`,
      borderRadius: 0,
      background: primary ? T.ink : "transparent",
      color: primary ? T.paper : T.ink,
      cursor: "pointer",
      fontFamily: v("font-body", FONT),
      fontSize: 15,
      fontWeight: 500,
      lineHeight: "20px",
      textDecoration: "none",
      whiteSpace: "nowrap",
    }),

    section: { marginTop: v("section-gap", SECTION_GAP) },
    workSection: { marginTop: v("intro-gap", `${HOME_DESIGN.workGap}px`) },
    h2: {
      fontFamily: v("font-display", DISPLAY),
      fontSize: v("fs-h2", "21px"),
      lineHeight: v("lh-h2", "27px"),
      fontWeight: v("w-display", 400),
      letterSpacing: v("ls-display", "0.005em"),
      margin: `0 0 ${v("h2-gap", "0.8rem")}`,
      color: T.ink,
    },

    /* Work rows after liam.cv: full-bleed app icon · linked name · role right */
    workHead: {
      display: "grid",
      gridTemplateColumns: "48px minmax(0,1fr) auto",
      alignItems: "center",
      gap: 16,
      padding: `${v("row-pad", "0.6rem")} 0`,
    },
    workList: { display: "grid", rowGap: v("row-gap", "0px") },
    workLogo: {
      width: 48,
      height: 48,
      borderRadius: 12,
      display: "block",
      objectFit: "cover",
    },
    workLogoBlank: {
      width: 48,
      height: 48,
      borderRadius: 12,
      background: T.bubble,
      border: `1px solid ${T.hairline}`,
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      fontFamily: v("font-display", DISPLAY),
      fontSize: 19,
      color: T.ink,
    },
    workTitle: { fontWeight: 500, color: T.ink, fontSize: v("fs-work", "17px") },
    workMeta: { fontSize: v("fs-meta", "15px"), color: T.muted, whiteSpace: "nowrap" },

    /* Favorite things: spec-sheet grid — mono label over ink value */
    favGrid: {
      display: "grid",
      gridTemplateColumns: "repeat(auto-fit, minmax(110px, 1fr))",
      gap: "20px 16px",
    },
    favLabel: {
      fontFamily: MONO,
      fontSize: 10.5,
      letterSpacing: "0.06em",
      textTransform: "uppercase",
      color: T.muted,
      marginBottom: 6,
    },
    favValue: { fontSize: 14, lineHeight: "19px", fontWeight: 500, color: T.ink },

    /* iMessage-style "received" bubbles (after vilinskyy.com's Reviews).
       Two columns via the .quote-grid class in index.html. */
    /* Equal-height bubbles: the figure fills its grid cell, the bubble
       flexes, so all three attributions share one baseline. */
    quote: (i) => ({
      margin: 0,
      display: "flex",
      flexDirection: "column",
      height: "100%",
      transform: `rotate(${i % 2 ? T.tilt : -T.tilt}deg)`,
    }),
    quoteBlock: { margin: 0, flex: 1, display: "flex" },
    quoteBubble: {
      display: "block",
      width: "100%",
      padding: "7px 12px",
      borderRadius: 16,
      borderBottomLeftRadius: 4,
      background: T.bubble,
      color: T.bubbleInk,
      fontSize: 13.5,
      lineHeight: "19px",
      textWrap: "pretty",
    },
    /* Evidence-style attribution — mono caps, so quotes read as verification */
    quoteName: {
      fontFamily: MONO,
      fontSize: 11.5,
      letterSpacing: "0.06em",
      textTransform: "uppercase",
      color: T.muted,
      marginTop: 6,
    },
    quoteCol: { display: "flex", flexDirection: "column", gap: 12 },

    connectBody: {
      margin: 0,
      color: T.ink,
      fontSize: v("fs-body", `${HOME_DESIGN.bodyFontSize}px`),
      lineHeight: v("lh-body", `${HOME_DESIGN.bodyLineHeight}px`),
      textWrap: "pretty",
    },

    /* Copy-email button + its "Copied to clipboard" pop-up */
    copyWrap: { position: "relative", display: "inline-block" },
    copyTip: (visible) => ({
      position: "absolute",
      bottom: "calc(100% + 7px)",
      left: "50%",
      whiteSpace: "nowrap",
      background: T.ink,
      color: T.paper,
      fontSize: 12,
      lineHeight: "16px",
      padding: "5px 9px",
      borderRadius: 7,
      pointerEvents: "none",
      opacity: visible ? 1 : 0,
      transform: visible ? "translate(-50%, 0)" : "translate(-50%, 4px)",
      transition: "opacity 0.18s ease, transform 0.18s ease",
      zIndex: 10,
    }),

    /* Projects (lab) */
    projectFrame: {
      display: "block",
      aspectRatio: "16 / 10",
      overflow: "hidden",
      border: `1px solid ${T.hairline}`,
      background: T.bubble,
    },
    projectMedia: { display: "block", width: "100%", height: "100%", objectFit: "cover" },
    projectFrameLink: { display: "block", color: "inherit" },
    projectTrack: {
      display: "grid",
      gridAutoFlow: "column",
      gridAutoColumns: "100%",
      overflowX: "auto",
      scrollSnapType: "x mandatory",
      overscrollBehaviorX: "contain",
      scrollbarWidth: "none",
    },
    projectSlide: { scrollSnapAlign: "start", minWidth: 0 },
    projectGroupTag: {
      flex: "0 0 auto",
      fontFamily: MONO,
      fontSize: 11,
      letterSpacing: "0.06em",
      textTransform: "uppercase",
      color: T.muted,
      whiteSpace: "nowrap",
    },
    projectControls: {
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      gap: 12,
      marginTop: 12,
    },
    projectArrow: {
      appearance: "none",
      width: 28,
      height: 28,
      border: 0,
      borderRadius: 0,
      padding: 0,
      background: "transparent",
      color: T.ink,
      fontSize: 20,
      lineHeight: "28px",
      cursor: "pointer",
    },
    projectDots: { display: "inline-flex", alignItems: "center", gap: 6 },
    /* Square bars (DESIGN.md: nothing rounded); 18px tall hit area. */
    projectDot: (active) => ({
      appearance: "none",
      width: active ? 22 : 14,
      height: 18,
      padding: 0,
      border: 0,
      borderRadius: 0,
      background: `linear-gradient(${active ? T.ink : T.muted}, ${active ? T.ink : T.muted}) center / 100% 2px no-repeat`,
      opacity: active ? 1 : 0.4,
      cursor: "pointer",
      transition: "width 0.2s ease",
    }),
    projectPlaceholder: {
      display: "flex", alignItems: "center", justifyContent: "center",
      width: "100%", height: "100%", padding: 24, boxSizing: "border-box",
      fontFamily: MONO, fontSize: 12, lineHeight: "18px", color: T.muted, textAlign: "center",
    },
    flowFrame: {
      display: "grid", gridTemplateColumns: "auto auto auto", alignContent: "center",
      justifyContent: "center", justifyItems: "center", alignItems: "center",
      gap: "6px 10px", width: "100%", height: "100%", padding: 16, boxSizing: "border-box",
    },
    flowStep: (key) => ({
      padding: "6px 10px",
      border: `1px solid ${key ? T.ink : "var(--ui-link-underline)"}`,
      background: key ? T.ink : T.paper,
      color: key ? T.paper : T.ink,
      fontSize: 12, lineHeight: "16px", fontWeight: 500, whiteSpace: "nowrap", justifySelf: "stretch", textAlign: "center",
    }),
    flowArrow: { color: T.muted, fontSize: 14 },
    storyDetails: { marginTop: 4 },
    storySummary: {
      cursor: "pointer", fontFamily: MONO, fontSize: 11, letterSpacing: "0.06em",
      textTransform: "uppercase", color: T.muted, width: "fit-content",
    },
    storyBody: { display: "grid", rowGap: 8, marginTop: 8, fontSize: 14, lineHeight: "20px", color: T.ink },
    storyRow: { display: "grid", gridTemplateColumns: "88px minmax(0, 1fr)", columnGap: 12 },
    storyLabel: {
      fontFamily: MONO, fontSize: 11, lineHeight: "20px", letterSpacing: "0.06em",
      textTransform: "uppercase", color: T.muted,
    },
    projectTabs: {
      display: "flex",
      gap: 20,
      margin: `0 0 ${v("h2-gap", "0.8rem")}`,
    },
    projectTab: (active) => ({
      appearance: "none",
      background: "none",
      border: 0,
      padding: 0,
      cursor: "pointer",
      fontFamily: v("font-display", DISPLAY),
      fontSize: v("fs-h2", "21px"),
      lineHeight: v("lh-h2", "27px"),
      fontWeight: v("w-display", 400),
      letterSpacing: v("ls-display", "0.005em"),
      color: active ? T.ink : T.muted,
      opacity: active ? 1 : 0.7,
      transition: "color 0.2s ease, opacity 0.2s ease",
    }),
    projectStack: { display: "grid", rowGap: 28 },
    projectFigure: { margin: 0, minWidth: 0 },
    projectTiles: { display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 16 },
    projectTileCaption: { display: "grid", rowGap: 2, marginTop: 8 },
    projectTileTitleRow: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12 },
    projectTileLine: {
      color: T.muted,
      fontSize: 14,
      lineHeight: "20px",
      display: "-webkit-box",
      WebkitLineClamp: 2,
      WebkitBoxOrient: "vertical",
      overflow: "hidden",
    },

    tunePanel: {
      position: "fixed",
      right: 12,
      bottom: 12,
      zIndex: 50,
      display: "flex",
      flexDirection: "column",
      alignItems: "flex-end",
      gap: 6,
      padding: 8,
      borderRadius: 14,
      background: "color-mix(in srgb, var(--ui-bg) 92%, transparent)",
      border: `1px solid ${T.hairline}`,
      boxShadow: "0 4px 20px rgba(0, 0, 0, 0.06)",
      backdropFilter: "blur(6px)",
      maxWidth: "min(360px, calc(100vw - 24px))",
      fontFamily: FONT,
      letterSpacing: 0,
      lineHeight: "20px",
    },

    footer: {
      marginTop: v("footer-gap", "1rem"),
      fontFamily: v("font-footer", "inherit"),
      fontSize: v("fs-footer", "12.5px"),
      color: T.muted,
    },
  };

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

  function useLocalTime(timeZone) {
    const format = () =>
      new Intl.DateTimeFormat("en-US", {
        hour: "numeric",
        minute: "2-digit",
        hour12: true,
        timeZone,
      })
        .format(new Date())
        .replace(" ", "")
        .toLowerCase();
    const [time, setTime] = useState(format);
    useEffect(() => {
      const id = setInterval(() => setTime(format()), 30 * 1000);
      return () => clearInterval(id);
    }, []);
    return time;
  }

  /* Key terms read at the same level as the body (chosen 2026-09-23: ink
     body, even emphasis); the component only keeps metrics unbroken. */
  const Ink = ({ children, nowrap = false }) => (
    <span style={{ whiteSpace: nowrap ? "nowrap" : undefined }}>{children}</span>
  );

  // Funnel instrumentation. window.track comes from analytics.js and is a no-op when
  // no analytics sink is configured, so this never needs a guard beyond the ?. .
  const track = (name, props) => window.track?.(name, props);

  function BrandToken({ brand, label, kind = "company", href, onClick, placement = "connect", style }) {
    const interactive = Boolean(href || onClick);
    const props = {
      className: `brand-token${interactive ? " brand-token--contact" : ""}`,
      "data-brand": brand,
      "data-brand-kind": kind,
      style: style ? { ...S.brandToken(interactive, brand, kind), ...style } : S.brandToken(interactive, brand, kind),
    };
    const contents = label;
    // The last funnel step on this site: a visitor reaching for a way to contact me.
    const report = () => { if (kind === "contact") track("contact_click", { channel: brand, placement }); };

    if (href) {
      return <a {...props} href={href} target="_blank" rel="noopener noreferrer" onClick={report}>{contents}</a>;
    }
    if (onClick) {
      return <button {...props} type="button" onClick={(e) => { report(); onClick(e); }}>{contents}</button>;
    }
    return <span {...props}>{contents}</span>;
  }

  /* A company named in the new bio: a plain underlined link. */
  function CompanyName({ id }) {
    const company = COMPANIES[id];
    return (
      <a
        className="basic-link"
        href={company.href}
        target="_blank"
        rel="noopener noreferrer"
        style={S.companyLink}
        onClick={() => track("company_link_click", { company: id })}
      >
        {company.label}
      </a>
    );
  }

  /* Square outreach buttons: copy email (filled), then LinkedIn and
     optionally X (outlined). */
  function CtaButtons({ placement, onEmailActivate, withX = false }) {
    const links = withX ? CONTENT.connectLinks : CONTENT.connectLinks.slice(0, 1);
    return (
      <div style={S.ctaRow} data-cta={placement}>
        <CopyEmail button label="Email" placement={placement} onActivate={onEmailActivate} />
        {links.map((link) => (
          <a
            key={link.label}
            className="cta-button"
            href={link.href}
            target="_blank"
            rel="noopener noreferrer"
            style={S.ctaButton(false)}
            onClick={() => track("contact_click", { channel: link.label.toLowerCase(), placement })}
          >
            {link.label} ↗
          </a>
        ))}
      </div>
    );
  }

  /* ── sections ────────────────────────────────────────────────────────── */

  function InteractivePortrait({ waveRequest, portraitStyle }) {
    const {
      directionFromVector,
      createDirectionStabilizer,
      isPortraitActivationKey,
      canPlayEmailWave,
      canTrackPortraitPointer,
      createPointerIdleReset,
      resolvePortraitRestingFrame,
      resolvePortraitAssets,
      portraitSourcesForPreload,
    } = window.PortraitMotion;
    const portraitAssets = resolvePortraitAssets(portraitStyle, PORTRAIT_EXPERIENCE);
    const { frames: portraitFrames, wave: portraitWave } = portraitAssets;
    const waveHandScale = window.PortraitMotion.resolveWaveHandScale(portraitStyle);
    const [frame, setFrame] = useState("thinking");
    const [waveActive, setWaveActive] = useState(false);
    const buttonRef = useRef(null);
    const directionTimerRef = useRef(null);
    const winkTimerRef = useRef(null);
    const waveTimerRef = useRef(null);
    const winkActiveRef = useRef(false);
    const pointerActiveRef = useRef(false);
    const visibleRef = useRef(true);
    const reducedMotionRef = useRef(false);
    const lastDirectionRef = useRef("front");
    const stabilizerRef = useRef(createDirectionStabilizer("front", DIRECTION_DELAY_MS));
    const idleResetRef = useRef(null);

    const clearDirectionTimer = () => {
      clearTimeout(directionTimerRef.current);
      directionTimerRef.current = null;
    };

    const resetToThinking = () => {
      idleResetRef.current?.cancel();
      clearDirectionTimer();
      clearTimeout(winkTimerRef.current);
      winkTimerRef.current = null;
      clearTimeout(waveTimerRef.current);
      waveTimerRef.current = null;
      winkActiveRef.current = false;
      setWaveActive(false);
      pointerActiveRef.current = false;
      lastDirectionRef.current = "front";
      stabilizerRef.current.reset("front");
      setFrame("thinking");
    };

    if (!idleResetRef.current) {
      idleResetRef.current = createPointerIdleReset(resetToThinking);
    }

    const preloadSources = (sources) => {
      sources.forEach((src) => {
        const image = new Image();
        image.src = src;
      });
    };

    const useFallback = (event, fallbackSrc) => {
      const image = event.currentTarget;
      if (!fallbackSrc || image.dataset.fallbackApplied === "true") return;
      image.dataset.fallbackApplied = "true";
      image.src = fallbackSrc;
    };

    useEffect(() => {
      preloadSources(portraitSourcesForPreload(PORTRAIT_EXPERIENCE, portraitStyle));
    }, [portraitStyle]);

    useEffect(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return undefined;
      const preloadRemainingStyles = () => {
        PORTRAIT_EXPERIENCE.styles
          .filter((style) => style.value !== portraitStyle)
          .forEach((style) => {
            preloadSources(portraitSourcesForPreload(PORTRAIT_EXPERIENCE, style.value));
          });
      };
      if (window.requestIdleCallback) {
        const idleId = window.requestIdleCallback(preloadRemainingStyles);
        return () => window.cancelIdleCallback?.(idleId);
      }
      const timeoutId = setTimeout(preloadRemainingStyles, 250);
      return () => clearTimeout(timeoutId);
    }, []);

    useEffect(() => {
      const media = window.matchMedia("(prefers-reduced-motion: reduce)");
      const applyPreference = () => {
        reducedMotionRef.current = media.matches;
        if (media.matches) resetToThinking();
      };
      applyPreference();
      if (media.addEventListener) media.addEventListener("change", applyPreference);
      else media.addListener(applyPreference);
      return () => {
        if (media.removeEventListener) media.removeEventListener("change", applyPreference);
        else media.removeListener(applyPreference);
      };
    }, []);

    useEffect(() => {
      const commitCandidate = (candidate, now) => {
        const nextDirection = stabilizerRef.current.update(candidate, now);
        lastDirectionRef.current = nextDirection;
        if (!winkActiveRef.current) setFrame(nextDirection);
      };

      const onPointerMove = (event) => {
        if (!canTrackPortraitPointer({
          pointerType: event.pointerType,
          reducedMotion: reducedMotionRef.current,
          visible: visibleRef.current,
          hasTarget: Boolean(buttonRef.current),
        })) return;

        pointerActiveRef.current = true;
        idleResetRef.current.bump();
        const rect = buttonRef.current.getBoundingClientRect();
        const x = event.clientX - (rect.left + rect.width / 2);
        const y = event.clientY - (rect.top + rect.height / 2);
        const candidate = directionFromVector(x, y, {
          neutralRadius: Math.max(42, rect.width * 0.42),
          previousDirection: lastDirectionRef.current,
        });

        commitCandidate(candidate, performance.now());
        clearDirectionTimer();
        directionTimerRef.current = setTimeout(
          () => commitCandidate(candidate, performance.now()),
          DIRECTION_DELAY_MS,
        );
      };

      const onVisibilityChange = () => {
        if (document.hidden) resetToThinking();
      };

      window.addEventListener("pointermove", onPointerMove, { passive: true });
      document.addEventListener("visibilitychange", onVisibilityChange);
      return () => {
        window.removeEventListener("pointermove", onPointerMove);
        document.removeEventListener("visibilitychange", onVisibilityChange);
        idleResetRef.current.cancel();
        clearDirectionTimer();
      };
    }, []);

    useEffect(() => {
      if (!buttonRef.current || !window.IntersectionObserver) return undefined;
      const observer = new IntersectionObserver(([entry]) => {
        visibleRef.current = entry.isIntersecting;
        if (!entry.isIntersecting) resetToThinking();
      }, { threshold: 0.1 });
      observer.observe(buttonRef.current);
      return () => observer.disconnect();
    }, []);

    useEffect(() => () => {
      clearDirectionTimer();
      idleResetRef.current.cancel();
      clearTimeout(winkTimerRef.current);
      clearTimeout(waveTimerRef.current);
    }, []);

    useEffect(() => {
      if (!waveRequest || !canPlayEmailWave({
        visible: visibleRef.current,
        documentHidden: document.hidden,
      })) return;

      clearTimeout(winkTimerRef.current);
      winkTimerRef.current = null;
      winkActiveRef.current = false;
      clearTimeout(waveTimerRef.current);
      setWaveActive(true);

      waveTimerRef.current = setTimeout(() => {
        setWaveActive(false);
        setFrame(resolvePortraitRestingFrame({
          pointerActive: pointerActiveRef.current,
          reducedMotion: reducedMotionRef.current,
          visible: visibleRef.current,
          lastDirection: lastDirectionRef.current,
        }));
      }, WAVE_DURATION_MS);
    }, [waveRequest]);

    const wink = () => {
      clearTimeout(waveTimerRef.current);
      waveTimerRef.current = null;
      setWaveActive(false);
      clearTimeout(winkTimerRef.current);
      winkActiveRef.current = true;
      setFrame("wink");
      winkTimerRef.current = setTimeout(() => {
        winkActiveRef.current = false;
        setFrame(resolvePortraitRestingFrame({
          pointerActive: pointerActiveRef.current,
          reducedMotion: reducedMotionRef.current,
          visible: visibleRef.current,
          lastDirection: lastDirectionRef.current,
        }));
      }, WINK_DURATION_MS);
    };

    return (
      <button
        ref={buttonRef}
        className="portrait-button"
        type="button"
        style={S.portraitButton}
        onClick={wink}
        onKeyDown={(event) => {
          if (!isPortraitActivationKey(event.key)) return;
          event.preventDefault();
          wink();
        }}
        aria-label="Interactive portrait of Lucas. Move the pointer to change his gaze; activate to make him wink."
        data-portrait-frame={waveActive ? "wave" : frame}
      >
        {waveActive ? (
          <span style={S.portraitWaveStage} aria-hidden="true">
            {portraitWave.animation ? (
              <>
                <img
                  key={`experiment-${portraitStyle}-${waveRequest}`}
                  className="portrait-wave-experiment"
                  src={portraitWave.animation}
                  alt=""
                  draggable="false"
                  style={S.portraitWaveLayer}
                />
                <img
                  className="portrait-wave-experiment-static"
                  src={portraitWave.still}
                  alt=""
                  draggable="false"
                  style={S.portraitWaveLayer}
                />
              </>
            ) : (
              <>
                <img
                  key={`body-${portraitStyle}-${waveRequest}`}
                  src={portraitWave.body}
                  alt=""
                  draggable="false"
                  style={S.portraitWaveLayer}
                  onError={(event) => useFallback(event, portraitAssets.fallback?.wave.body)}
                />
                <img
                  key={`hand-${portraitStyle}-${waveRequest}`}
                  className="portrait-wave-hand"
                  src={portraitWave.hand}
                  alt=""
                  draggable="false"
                  style={{
                    ...S.portraitWaveLayer,
                    "--portrait-wave-scale": waveHandScale,
                  }}
                  onError={(event) => useFallback(event, portraitAssets.fallback?.wave.hand)}
                />
              </>
            )}
          </span>
        ) : (
          <img
            key={`${portraitStyle}-${frame}`}
            src={portraitFrames[frame]}
            alt=""
            draggable="false"
            style={S.portraitImage}
            onError={(event) => useFallback(event, portraitAssets.fallback?.frames[frame])}
          />
        )}
      </button>
    );
  }

  function MagazineName() {
    return (
      <span style={S.magazineWords} aria-hidden="true">
        {[MAGAZINE_LETTERS.slice(0, 5), MAGAZINE_LETTERS.slice(5)].map((word, wordIndex) => (
          <span key={wordIndex} style={S.magazineWord}>
            {word.map((tile, letterIndex) => (
              <span key={letterIndex} style={S.magazineLetter(tile)}>{tile.letter}</span>
            ))}
          </span>
        ))}
      </span>
    );
  }

  /* Bio copy (lab: the "Bio" row). "current" is the shipped text. The
     new copy is Lucas's own three lines (2026-09-23): who, proof, ask.
     "lines" sets each on its own line; "paragraph" runs them together.
     0 → $29M ARR and 5+ years trace to resume.js. */
  function BioCopy({ copy }) {
    const lines = [
      <>PM with 5+ years in <Ink nowrap>0→1</Ink> product development, AI agents, and growth.</>,
      <>
        Most recently, I worked at <CompanyName id="moveworks" /> (Acq. by{" "}
        <CompanyName id="servicenow" />) where I led the launch and growth of the AI automation
        platform from <Ink nowrap>0→$29M ARR</Ink>.
      </>,
      <>Currently looking for my next role on an ambitious, consumer-facing product.</>,
    ];
    if (copy === "lines") {
      return lines.map((line, index) => <p key={index} style={S.paragraph}>{line}</p>);
    }
    if (copy === "paragraph") {
      return (
        <p style={S.paragraph}>
          {lines.map((line, index) => <React.Fragment key={index}>{index > 0 && " "}{line}</React.Fragment>)}
        </p>
      );
    }
    return (
      <>
        <p style={S.paragraph}>{BIO.lead}</p>
        <p style={S.paragraph}>
          Most recently I took <Ink>Agent Studio</Ink> at{" "}
          <BrandToken brand="moveworks" label="Moveworks" />{" "}
          from zero to <Ink nowrap>$29M in ARR</Ink>. Before that I was an Analyst at{" "}
          <BrandToken brand="integrative" label="Integrative Ventures" />{" "}
          &amp; Software Engineer at{" "}
          <BrandToken brand="qualcomm" label="Qualcomm" />.
        </p>
      </>
    );
  }

  function Bio({
    waveRequest, portraitStyle, entranceStyle, magazineName,
    copy = "current", cta = "off", onEmailActivate,
  }) {
    return (
      <header className="entrance-item" style={entranceStyle}>
        <div style={S.headerRow} data-portrait-layout={PORTRAIT_LAYOUT.key}>
          {magazineName ? (
            <h1 style={S.h1} aria-label={`${SITE.name}, Product Manager`} data-name-treatment="magazine">
              <MagazineName />
              <span style={S.magazineRole} aria-hidden="true">Product Manager</span>
            </h1>
          ) : (
            <h1 style={S.h1}>
              <span style={{ whiteSpace: "nowrap" }}>
                {SITE.name}<span style={{ color: T.muted }}>,</span>
              </span>{" "}
              <span style={S.h1Role}>Product Manager</span>
            </h1>
          )}
          <InteractivePortrait waveRequest={waveRequest} portraitStyle={portraitStyle} />
        </div>
        <BioCopy copy={copy} />
        {BIO.looking && <p style={S.paragraph}>{BIO.looking}</p>}
        {cta === "buttons" && <CtaButtons placement="bio" onEmailActivate={onEmailActivate} />}
      </header>
    );
  }

  function WorkRow({ item }) {
    return (
      <div className="work-head" style={S.workHead}>
        {item.logo
          ? <img src={item.logo} alt="" style={S.workLogo} />
          : <span style={S.workLogoBlank} aria-hidden="true">{item.title[0]}</span>}
        <span style={S.workTitle}>
          <a
            className="basic-link"
            href={item.href}
            target="_blank"
            rel="noopener noreferrer"
            onClick={() => track("work_link_click", { company: item.key })}
          >
            {item.title}
          </a>
        </span>
        <span style={S.workMeta}>{item.role}</span>
      </div>
    );
  }

  /* ── projects (lab experiment) ─────────────────────────────────────── */
  /* Three candidate layouts for a media-led Projects section, compared in
     the unlisted lab: carousel (after liam.cv), framed stack (after
     danielwhite.uk + anishfn), and 2-up tiles that replace the Work list
     (after anishfn + brianlovin). One 16:10 media ratio for every project.
     Numbers trace to resume.js (3,000 AI agents is Lucas's figure, added
     there 2026-09-24). The Agent Studio still is Lucas's own
     screenshot (2026-09-24). */
  /* The media path comes from the lab page's config, so this public file
     never names the unlisted route. */
  const PROJECT_MEDIA_BASE = window.PORTRAIT_LAB_CONFIG?.projectMediaBase || "";
  /* Two tabs head the section (Lucas, 2026-09-24): Work leads. */
  const PROJECT_TABS = [
    { key: "work", label: "Work" },
    { key: "projects", label: "Projects" },
  ];
  const PROJECTS = [
    {
      key: "agent-studio",
      group: "work",
      title: "Agent Studio",
      meta: "Moveworks · Senior PM",
      line: "Moveworks AI automation platform ($29M ARR, 3,000 AI agents).",
      href: "https://www.moveworks.com/us/en/platform/ai-agent-builder",
      media: { kind: "image", src: `${PROJECT_MEDIA_BASE}/agent-studio.webp`, alt: "Agent Studio's conversational process editor: an AI agent's steps as a flow, with the action's settings beside it" },
    },
    /* Moveworks work (draft, 2026-09-24, from Lucas's handoff). Scope is his
       own words: Message API = adoption + developer relations (he did not
       build it); Events API = defined, launched, grew; hackathons = NYSE
       (Oct 2024) and San Jose at Moveworks.global (Apr 2025) only, nothing
       from fall 2025. Marketing wrote the linked posts, so they are linked
       as sources, not as his writing. [BRACKETS] are gaps Lucas must fill;
       no figure here is invented. */
    {
      key: "message-api",
      group: "work",
      title: "Message API",
      meta: "Moveworks",
      line: "Proactive messages to employees in chat. I led adoption and developer relations.",
      href: null,
      media: { kind: "placeholder", label: "[IMAGE: Message API docs or Message Tester screenshot]" },
      story: {
        problem: "[CONFIRM: what customers couldn't do before, e.g. warn employees in chat before an IT issue hit them]",
        did: "Led adoption and developer relations: [DETAIL: docs, sample code, customer onboarding?]",
        result: "[METRIC: customers using it / messages sent per month]",
      },
      links: [
        { label: "Launch post", href: "https://www.moveworks.com/us/en/resources/blog/prevent-issues-with-moveworks-api" },
        { label: "Press release", href: "https://www.moveworks.com/us/en/company/news/press-releases/moveworks-announces-api-proactively-prevent-issues" },
      ],
    },
    {
      key: "events-api",
      group: "work",
      title: "Events API",
      meta: "Moveworks",
      line: "Lets any system message employees in chat. I defined, launched, and grew it.",
      href: null,
      media: { kind: "diagram", diagram: "events", alt: "Your system sends an event to the Events API, the Moveworks bot turns it into a message, and it arrives in Slack or Teams" },
      story: {
        problem: "[CONFIRM: the problem Events API solved]",
        did: "Defined the API, launched it, and grew adoption: [DETAIL: what launch and growth took]",
        result: "[METRIC: customers / events per month]",
      },
      links: [
        { label: "Docs: message delivery", href: "https://help.moveworks.com/api-reference/events-api/message-delivery" },
        { label: "Docs: chat markup", href: "https://help.moveworks.com/api-reference/events-api/chat-markup" },
      ],
    },
    {
      key: "hackathons",
      group: "work",
      title: "Hackathons",
      meta: "Moveworks",
      line: "Ran AI agent hackathons at the NYSE (Oct 2024) and Moveworks.global (Apr 2025).",
      href: null,
      media: { kind: "placeholder", label: "[IMAGE: photo from the NYSE or San Jose hackathon]" },
      story: {
        problem: "[CONFIRM: the goal, e.g. get developers building agents on Agent Studio]",
        did: "Ran both events: [DETAIL: what running them covered]",
        result: "[METRIC: teams / participants / agents built]",
      },
      links: [
        { label: "NYSE recap", href: "https://www.moveworks.com/us/en/resources/blog/agentic-automation-nyse-hackathon-recap" },
        { label: "San Jose recap", href: "https://www.moveworks.com/us/en/resources/blog/moveworks-global-agent-studio-hackathon-recap" },
      ],
    },
    {
      key: "reprabbit",
      group: "projects",
      title: "RepRabbit",
      meta: null,
      line: "A workout tracker that adapts to your routine.",
      href: "https://reprabbit.app",
      media: { kind: "image", src: `${PROJECT_MEDIA_BASE}/reprabbit.webp`, alt: "RepRabbit: \"overhead press 95 for 8, 8, then drop to 65 for 12\" becomes a logged Overhead Press row" },
    },
    {
      key: "matchping",
      group: "projects",
      title: "MatchPing",
      meta: null,
      line: "Pro Counter-Strike matches in your Discord, edited live as the score changes.",
      href: null,
      /* Lucas's MatchPing logo beside a product screenshot (2026-09-24). */
      media: {
        kind: "image",
        src: `${PROJECT_MEDIA_BASE}/matchping.webp`,
        alt: "The MatchPing logo beside a Discord channel where MatchPing posts a live match card, MOUZ 1–0 FaZe, with the Twitch stream open",
      },
    },
  ];

  function usePrefersReducedMotion() {
    const [reduced, setReduced] = useState(
      () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false,
    );
    useEffect(() => {
      const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
      if (!media) return undefined;
      const onChange = () => setReduced(media.matches);
      media.addEventListener?.("change", onChange);
      return () => media.removeEventListener?.("change", onChange);
    }, []);
    return reduced;
  }

  /* Media frame: 16:10, square corners, hairline (DESIGN.md: nothing else is
     rounded). Video is muted and inline, plays only while mostly in view
     (and, in the carousel, only on the active slide); reduced motion keeps
     the poster. */
  function ProjectMedia({ project, active = true }) {
    const ref = useRef(null);
    const reduced = usePrefersReducedMotion();
    const { media } = project;
    useEffect(() => {
      const video = ref.current;
      if (media.kind !== "video" || !video) return undefined;
      if (reduced || !active) { video.pause(); return undefined; }
      if (!window.IntersectionObserver) return undefined;
      const observer = new IntersectionObserver(([entry]) => {
        if (entry.intersectionRatio >= 0.6) video.play().catch(() => {});
        else video.pause();
      }, { threshold: [0, 0.6, 1] });
      observer.observe(video);
      return () => observer.disconnect();
    }, [media.kind, reduced, active]);
    const frame = (
      <span className="project-frame" style={S.projectFrame}>
        {media.kind === "video" ? (
          <video
            ref={ref}
            style={S.projectMedia}
            muted
            loop
            playsInline
            preload="none"
            poster={media.poster}
            aria-label={media.alt}
          >
            <source src={media.src} type="video/mp4" />
          </video>
        ) : media.kind === "diagram" ? (
          <EventsFlow alt={media.alt} />
        ) : media.kind === "placeholder" ? (
          <span style={S.projectPlaceholder}>{media.label}</span>
        ) : (
          <img src={media.src} alt={media.alt} loading="lazy" style={S.projectMedia} />
        )}
      </span>
    );
    if (!project.href) return frame;
    /* The title is the keyboard/screen-reader link; the frame is the same
       target for the mouse, so it stays out of the tab order. */
    return (
      <a
        href={project.href}
        target="_blank"
        rel="noopener noreferrer"
        tabIndex={-1}
        aria-hidden="true"
        style={S.projectFrameLink}
        onClick={() => track("project_link_click", { project: project.key, via: "media" })}
      >
        {frame}
      </a>
    );
  }

  /* Events API in four steps, drawn in the page's own type and hairlines
     so it needs no image. A U-shape so it fits a half-width tile:
     your system → Events API ↓ Moveworks bot → Slack or Teams (right to left). */
  function EventsFlow({ alt }) {
    const step = (label, key) => <span style={S.flowStep(key)}>{label}</span>;
    const arrow = (glyph) => <span style={S.flowArrow} aria-hidden="true">{glyph}</span>;
    return (
      <span style={S.flowFrame} role="img" aria-label={alt}>
        {step("Your system")}{arrow("→")}{step("Events API", true)}
        <span />{<span />}{arrow("↓")}
        {step("Slack or Teams")}{arrow("←")}{step("Moveworks bot")}
      </span>
    );
  }

  /* Optional case study: problem, what Lucas did, result, then sources.
     Closed by default so the list stays short. */
  function ProjectStory({ project }) {
    const { story, links } = project;
    if (!story) return null;
    const rows = [["Problem", story.problem], ["What I did", story.did], ["Result", story.result]];
    return (
      <details style={S.storyDetails} onToggle={(e) => e.currentTarget.open && track("project_story_open", { project: project.key })}>
        <summary className="project-story" style={S.storySummary}>Case study</summary>
        <span style={S.storyBody}>
          {rows.map(([label, text]) => (
            <span key={label} style={S.storyRow}>
              <span style={S.storyLabel}>{label}</span>
              <span>{text}</span>
            </span>
          ))}
          {links?.length > 0 && (
            <span style={S.storyRow}>
              <span style={S.storyLabel}>Sources</span>
              <span>
                {links.map((link, index) => (
                  <React.Fragment key={link.href}>
                    {index > 0 && " · "}
                    <a className="basic-link" href={link.href} target="_blank" rel="noopener noreferrer" style={{ fontWeight: 400 }}>
                      {link.label}
                    </a>
                  </React.Fragment>
                ))}
              </span>
            </span>
          )}
        </span>
      </details>
    );
  }

  /* Title + role on one row, one muted line under it, then the optional
     case study — the same caption in every layout. */
  function ProjectCaption({ project, tag = project.meta }) {
    return (
      <>
        <span style={S.projectTileTitleRow}>
          <ProjectTitle project={project} />
          {tag && <span style={S.projectGroupTag}>{tag}</span>}
        </span>
        <span style={S.projectTileLine}>{project.line}</span>
        <ProjectStory project={project} />
      </>
    );
  }

  function ProjectTitle({ project }) {
    return project.href ? (
      <a
        className="basic-link"
        href={project.href}
        target="_blank"
        rel="noopener noreferrer"
        onClick={() => track("project_link_click", { project: project.key })}
      >
        {project.title}
      </a>
    ) : (
      <span style={{ color: T.ink, fontWeight: 500 }}>{project.title}</span>
    );
  }

  /* Option 1 — carousel. Native scroll-snap track (swipe/trackpad work
     without JS), arrows + named dots, arrow keys on the focused track.
     No auto-advance. Arrows and dots only show with more than one item. */
  function ProjectsCarousel({ items }) {
    const trackRef = useRef(null);
    const [index, setIndex] = useState(0);
    const reduced = usePrefersReducedMotion();
    const go = (next) => {
      const trackEl = trackRef.current;
      if (!trackEl) return;
      const clamped = Math.max(0, Math.min(items.length - 1, next));
      trackEl.scrollTo({ left: clamped * trackEl.clientWidth, behavior: reduced ? "auto" : "smooth" });
    };
    const onScroll = () => {
      const trackEl = trackRef.current;
      if (trackEl) setIndex(Math.round(trackEl.scrollLeft / Math.max(1, trackEl.clientWidth)));
    };
    const current = items[index] || items[0];
    return (
      <div aria-roledescription="carousel" aria-label="Projects">
        <div
          ref={trackRef}
          className="project-track"
          style={S.projectTrack}
          tabIndex={0}
          onScroll={onScroll}
          onKeyDown={(event) => {
            if (event.key === "ArrowRight") { event.preventDefault(); go(index + 1); }
            if (event.key === "ArrowLeft") { event.preventDefault(); go(index - 1); }
          }}
        >
          {items.map((project, i) => (
            <div
              key={project.key}
              style={S.projectSlide}
              role="group"
              aria-roledescription="slide"
              aria-label={`${i + 1} of ${items.length}: ${project.title}`}
            >
              <ProjectMedia project={project} active={i === index} />
            </div>
          ))}
        </div>
        <div style={S.projectTileCaption} aria-live="polite" data-caption="">
          <ProjectCaption project={current} />
        </div>
        {items.length > 1 && (
          <div style={S.projectControls}>
            <button type="button" className="project-arrow" style={S.projectArrow} aria-label="Previous project" disabled={index === 0} onClick={() => go(index - 1)}>‹</button>
            <span style={S.projectDots}>
              {items.map((project, i) => (
                <button
                  key={project.key}
                  type="button"
                  className="project-dot"
                  aria-label={project.title}
                  aria-current={i === index ? "true" : undefined}
                  style={S.projectDot(i === index)}
                  onClick={() => go(i)}
                />
              ))}
            </span>
            <button type="button" className="project-arrow" style={S.projectArrow} aria-label="Next project" disabled={index === items.length - 1} onClick={() => go(index + 1)}>›</button>
          </div>
        )}
      </div>
    );
  }

  /* Option 2 — framed stack, full column width. */
  function ProjectsStack({ items }) {
    return (
      <div style={S.projectStack}>
        {items.map((project) => (
          <figure key={project.key} style={S.projectFigure}>
            <ProjectMedia project={project} />
            <figcaption style={S.projectTileCaption}>
              <ProjectCaption project={project} />
            </figcaption>
          </figure>
        ))}
      </div>
    );
  }

  /* Option 3 — 2-up tiles. A lone item (Work today) spans both columns. */
  function ProjectsTiles({ items }) {
    return (
      <div className="project-tiles" style={S.projectTiles}>
        {items.map((project) => (
          <figure
            key={project.key}
            style={items.length === 1 ? { ...S.projectFigure, gridColumn: "1 / -1" } : S.projectFigure}
          >
            <ProjectMedia project={project} />
            <figcaption style={S.projectTileCaption}>
              <ProjectCaption project={project} />
            </figcaption>
          </figure>
        ))}
      </div>
    );
  }

  /* Tabs styled as the section heading: the active tab in ink, the other
     muted. Arrow keys move between them (WAI-ARIA tabs pattern). */
  function ProjectTabs({ tab, onChange }) {
    const refs = useRef({});
    const move = (step) => {
      const at = PROJECT_TABS.findIndex((option) => option.key === tab);
      const next = PROJECT_TABS[(at + step + PROJECT_TABS.length) % PROJECT_TABS.length];
      onChange(next.key);
      refs.current[next.key]?.focus();
    };
    return (
      <div role="tablist" aria-label="Work and projects" style={S.projectTabs}>
        {PROJECT_TABS.map((option) => (
          <button
            key={option.key}
            ref={(el) => { refs.current[option.key] = el; }}
            type="button"
            role="tab"
            id={`tab-${option.key}`}
            aria-selected={tab === option.key}
            aria-controls="project-panel"
            tabIndex={tab === option.key ? 0 : -1}
            className="project-tab"
            style={S.projectTab(tab === option.key)}
            onClick={() => { onChange(option.key); track("projects_tab", { tab: option.key }); }}
            onKeyDown={(event) => {
              if (event.key === "ArrowRight") { event.preventDefault(); move(1); }
              if (event.key === "ArrowLeft") { event.preventDefault(); move(-1); }
            }}
          >
            {option.label}
          </button>
        ))}
      </div>
    );
  }

  function WorkSection({ entranceStyle, projectsLayout }) {
    const [tab, setTab] = useState("work");
    /* Each Projects layout replaces the Work rows, so nothing is listed twice. */
    const layouts = { carousel: ProjectsCarousel, stack: ProjectsStack, tiles: ProjectsTiles };
    const Layout = layouts[projectsLayout];
    if (Layout) {
      const items = PROJECTS.filter((project) => project.group === tab);
      return (
        <section className="entrance-item" style={{ ...S.workSection, ...entranceStyle }} data-projects={projectsLayout}>
          <ProjectTabs tab={tab} onChange={setTab} />
          <div role="tabpanel" id="project-panel" aria-labelledby={`tab-${tab}`}>
            <Layout key={tab} items={items} />
          </div>
        </section>
      );
    }
    return (
      <section className="entrance-item" style={{ ...S.workSection, ...entranceStyle }}>
        <h2 style={S.h2}>Work</h2>
        <div style={S.workList}>
          {CONTENT.work.map((item) => (
            <WorkRow key={item.key} item={item} />
          ))}
        </div>
      </section>
    );
  }

  function FavoritesSection({ entranceStyle }) {
    return (
      <section className="entrance-item" style={{ ...S.section, ...entranceStyle }}>
        <h2 style={S.h2}>Favorite things</h2>
        <div style={S.favGrid}>
          {CONTENT.favorites.map((f) => (
            <div key={f.label}>
              <div style={S.favLabel}>{f.label}</div>
              <div style={S.favValue}>{f.value}</div>
            </div>
          ))}
        </div>
      </section>
    );
  }

  function Quote({ q, i }) {
    return (
      <figure style={S.quote(i)}>
        <blockquote style={S.quoteBlock}>
          <span style={S.quoteBubble}>{q.quote}</span>
        </blockquote>
        <figcaption style={S.quoteName}>
          {q.name}{q.relation ? `, ${q.relation}` : ""}
        </figcaption>
      </figure>
    );
  }

  function KindWordsSection({ entranceStyle }) {
    // Bracketed entries in SITE.recommendations are placeholders — they render
    // so the section is designable, and must be replaced with real quotes.
    const quotes = SITE.recommendations || [];
    if (quotes.length === 0) return null;
    return (
      <section className="entrance-item" style={{ ...S.section, ...entranceStyle }}>
        <h2 style={S.h2}>Kind words</h2>
        <div className="quote-grid">
          {quotes.map((q, i) => (
            <Quote key={i} q={q} i={i} />
          ))}
        </div>
      </section>
    );
  }

  function CopyEmail({ onActivate, label = CONTENT.email.label, placement = "connect", button = false }) {
    const [copied, setCopied] = useState(false);
    const timer = useRef(null);
    useEffect(() => () => clearTimeout(timer.current), []);
    const copy = async () => {
      onActivate?.();
      const address = CONTENT.email.address;
      try {
        await navigator.clipboard.writeText(address);
      } catch {
        // Older browsers / non-secure contexts
        const ta = document.createElement("textarea");
        ta.value = address;
        document.body.appendChild(ta);
        ta.select();
        document.execCommand("copy");
        ta.remove();
      }
      setCopied(true);
      clearTimeout(timer.current);
      timer.current = setTimeout(() => setCopied(false), 1600);
    };
    return (
      <span style={S.copyWrap}>
        {button ? (
          <button
            className="cta-button cta-button--primary"
            type="button"
            style={S.ctaButton(true)}
            onClick={() => { track("contact_click", { channel: "email", placement }); copy(); }}
          >
            {label}
          </button>
        ) : (
          <BrandToken brand="email" label={label} kind="contact" onClick={copy} placement={placement} />
        )}
        <span style={S.copyTip(copied)} role="status" aria-live="polite">
          {copied ? "Copied to clipboard" : ""}
        </span>
      </span>
    );
  }

  function ConnectSection({ onEmailActivate, entranceStyle, variant = "current" }) {
    const [linkedin, x] = CONTENT.connectLinks;
    const links = (
      <>
        Get in touch by <CopyEmail onActivate={onEmailActivate} /> or find me on{" "}
        <BrandToken brand="linkedin" label={linkedin.label} kind="contact" href={linkedin.href} />{" "}
        or{" "}
        <BrandToken brand="x" label={x.label} kind="contact" href={x.href} />.
      </>
    );
    /* Lab: the lead line, then buttons instead of a sentence of links. */
    if (variant === "buttons") {
      return (
        <section className="entrance-item" style={{ ...S.section, ...entranceStyle }}>
          <h2 style={S.h2}>Connect</h2>
          <p style={{ ...S.connectBody, marginBottom: 12 }}>{CONTENT.connectLead}</p>
          <CtaButtons placement="connect" onEmailActivate={onEmailActivate} withX />
        </section>
      );
    }
    return (
      <section className="entrance-item" style={{ ...S.section, ...entranceStyle }}>
        <h2 style={S.h2}>Connect</h2>
        <p style={S.connectBody}>
          {CONTENT.connectLead}{" "}
          {links}
        </p>
      </section>
    );
  }

  function Footer({ entranceStyle }) {
    const time = useLocalTime(CONTENT.footerTimeZone);
    return (
      <footer className="entrance-item" style={{ ...S.footer, ...entranceStyle }}>
        <span style={{ color: T.ink }}>{time}</span> in {CONTENT.footerCity}
      </footer>
    );
  }

  /* ── type & spacing experiments (lab route only) ──────────────────────── */
  /* A temporary A/B bench for fonts and spacing that sits beside the other
     lab experiments. The public page never renders the bench; it applies
     TUNE_LIVE's tokens and loads its own fonts statically in index.html.
     State lives in the URL: ?display=young|bricolage
     &body=plex|bricolage|inter|public|geist&space=current|snug|rhythm
     &projects=off|carousel|stack|tiles &bio=current|lines|paragraph
     &cta=off|buttons &connect=current|buttons */

  const TUNE_ENABLED = PORTRAIT_EXPERIENCE.isLab;
  const BRICOLAGE = '"Bricolage Grotesque", system-ui, sans-serif';
  const SANS_FALLBACK = 'system-ui, -apple-system, "Segoe UI", sans-serif';
  /* Keeps "Product Manager" on one line on desktop. At <= 480px the lab
     stylesheet sets --role-wrap-narrow: normal, because the role can't fit
     beside the portrait and would push it onto its own row. */
  const ROLE_NOWRAP = "var(--role-wrap-narrow, nowrap)";
  const TUNE_OPTIONS = {
    display: [
      { value: "young", label: "Young Serif" },
      { value: "bricolage", label: "Bricolage" },
    ],
    body: [
      { value: "plex", label: "Plex Mono" },
      { value: "bricolage", label: "Bricolage" },
      { value: "inter", label: "Inter" },
      { value: "public", label: "Public Sans" },
      { value: "geist", label: "Geist" },
    ],
    space: [
      { value: "current", label: "Current" },
      { value: "snug", label: "Snug" },
      { value: "rhythm", label: "Rhythm" },
    ],
    projects: [
      { value: "off", label: "Off" },
      { value: "carousel", label: "Carousel" },
      { value: "stack", label: "Stack" },
      { value: "tiles", label: "Tiles" },
    ],
    bio: [
      { value: "current", label: "Current" },
      { value: "lines", label: "Lines" },
      { value: "paragraph", label: "Paragraph" },
    ],
    cta: [
      { value: "off", label: "Off" },
      { value: "buttons", label: "Buttons" },
    ],
    connect: [
      { value: "current", label: "Current" },
      { value: "buttons", label: "Buttons" },
    ],
  };
  /* What the public page renders (shipped 2026-09-23; bio lines, bio and
     connect buttons shipped 2026-09-24). The public page applies
     these same tokens, so the lab's "Reset to live" can never drift from it. */
  const TUNE_LIVE = {
    display: "young", body: "inter", space: "snug", projects: "off",
    bio: "lines", cta: "buttons", connect: "buttons",
  };
  const TUNE_DEFAULTS = { ...TUNE_LIVE };
  const TUNE_FIELDS = Object.keys(TUNE_LIVE);
  /* Spacing presets on a 4px grid, tiered by the law of proximity:
     row < paragraph = heading→content < section <= intro break.
     The space above a heading is >= 3x the space below it, so each heading
     visibly belongs to its own content. Header lifts are zeroed so the
     margins are the gaps you actually see. */
  const SPACE_PRESETS = {
    current: {},
    snug: {
      "row-gap": "8px", "para-gap": "12px", "h2-gap": "12px",
      "header-gap": "24px", "section-gap": "48px", "intro-gap": "56px", "footer-gap": "36px",
    },
    rhythm: {
      "row-gap": "12px", "para-gap": "16px", "h2-gap": "16px",
      "header-gap": "32px", "section-gap": "56px", "intro-gap": "72px", "footer-gap": "44px",
    },
  };
  const SPACE_BASE = {
    "role-wrap": ROLE_NOWRAP, "font-footer": FONT, "row-pad": "0px", "header-pad": "0px", "header-lift": "0px",
    "portrait-lift": "0px", "h1-drop": "0px",
  };

  /* Columns calibrated in the browser so the bio sets as two balanced lines
     (~79 characters, inside Butterick's 45–90) with no one-word last line,
     the title keeps >= 40px from the portrait, and the widest body line
     ends within ~10px of the column edge — so the portrait's right edge
     lines up with the text instead of floating past a ragged margin. (A
     few px of slack keeps a 1px rendering difference from rewrapping.) */
  const BODY_FACES = {
    bricolage: { family: BRICOLAGE, size: "17px", lineHeight: "26px", column: "696px" },
    inter: { family: `"Inter", ${SANS_FALLBACK}`, size: "17px", lineHeight: "26px", column: "704px" },
    public: { family: `"Public Sans", ${SANS_FALLBACK}`, size: "17px", lineHeight: "26px", column: "720px" },
    geist: { family: `"Geist", ${SANS_FALLBACK}`, size: "17px", lineHeight: "26px", column: "684px" },
  };

  function tuneTokens({ display, body, space }) {
    const tokens = {};
    if (display === "bricolage") {
      Object.assign(tokens, {
        /* A grotesque reads smaller than Young Serif at the same size, so it
           steps up while keeping the ~1.36 ratio between levels. */
        "font-display": BRICOLAGE, "w-display": "600", "ls-display": "-0.02em",
        "fs-h1": "30px", "lh-h1": "36px", "fs-h2": "22px", "lh-h2": "28px",
      });
    }
    const proportional = BODY_FACES[body];
    if (proportional) {
      /* Each proportional face gets its calibrated size and column (see
         BODY_FACES). The footer clock stays mono: proportional digits
         collapse at 12.5px. */
      Object.assign(tokens, {
        "font-body": proportional.family, "fs-body": proportional.size,
        "lh-body": proportional.lineHeight, "ls-body": "0px", "column": proportional.column,
        /* The mark's 3px side bleed reads as a double space next to a
           proportional face's narrow word space, so it tucks back in. */
        "w-bio": "400", "w-brand": "600", "lh-brand": "22px", "brand-margin": "0 -2px",
        "font-footer": FONT, "role-wrap": ROLE_NOWRAP,
      });
    }
    if (space !== "current") {
      Object.assign(tokens, SPACE_BASE, SPACE_PRESETS[space]);
    }
    if (display === "bricolage") tokens["role-wrap"] = ROLE_NOWRAP;
    return tokens;
  }

  function readTune() {
    const pick = (key, raw) => (
      TUNE_OPTIONS[key].some((option) => option.value === raw) ? raw : TUNE_DEFAULTS[key]
    );
    const params = new URLSearchParams(window.location.search);
    let stored = {};
    try {
      stored = JSON.parse(window.sessionStorage.getItem("type-tune") || "{}");
    } catch {
      // Storage may be blocked; the URL and defaults still work.
    }
    const source = TUNE_FIELDS.some((key) => params.has(key)) ? {} : stored;
    const choice = Object.fromEntries(
      TUNE_FIELDS.map((key) => [key, pick(key, params.get(key) || source[key])]),
    );
    return {
      ...choice,
      open: params.has("panel") ? params.get("panel") !== "closed" : stored.open !== false,
    };
  }

  let appliedTuneKeys = [];
  function applyTune(tune) {
    const root = document.documentElement;
    appliedTuneKeys.forEach((key) => root.style.removeProperty(`--${key}`));
    const tokens = tuneTokens(tune);
    appliedTuneKeys = Object.keys(tokens);
    appliedTuneKeys.forEach((key) => root.style.setProperty(`--${key}`, tokens[key]));
  }

  if (TUNE_ENABLED && !document.getElementById("tune-fonts")) {
    const link = document.createElement("link");
    link.id = "tune-fonts";
    link.rel = "stylesheet";
    link.href = "https://fonts.googleapis.com/css2"
      + "?family=Bricolage+Grotesque:opsz,wdth,wght@12..96,75..100,200..800"
      + "&family=Inter:wght@400;500;600"
      + "&family=Public+Sans:wght@400;500;600"
      + "&family=Geist:wght@400;500;600"
      + "&display=swap";
    document.head.appendChild(link);
  }

  function TuneRow({ tune, onChange, field, label }) {
    return (
      <div style={S.portraitStyleRow}>
        <span style={S.portraitStyleLabel}>{label}</span>
        <div role="group" aria-label={label} style={S.portraitStyleControl}>
          {TUNE_OPTIONS[field].map((option) => (
            <button
              key={option.value}
              className="portrait-style-option"
              type="button"
              aria-pressed={tune[field] === option.value}
              style={S.portraitStyleOption(tune[field] === option.value)}
              onClick={() => onChange({ [field]: option.value })}
            >
              {option.label}
            </button>
          ))}
        </div>
      </div>
    );
  }

  /* ── page ────────────────────────────────────────────────────────────── */

  function HomeVariant() {
    const [waveRequest, setWaveRequest] = useState(0);
    const [themeMode, setThemeMode] = useState(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return "light";
      try {
        const storedTheme = window.sessionStorage.getItem("portrait-lab-theme");
        return LAB_THEME_OPTIONS.some((option) => option.value === storedTheme)
          ? storedTheme
          : LAB_DEFAULT_THEME;
      } catch {
        return LAB_DEFAULT_THEME;
      }
    });
    const [portraitStyle, setPortraitStyle] = useState(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return "color";
      try {
        return window.PortraitMotion.resolvePortraitStylePreference(
          window.sessionStorage.getItem("portrait-lab-style"),
          PORTRAIT_EXPERIENCE,
        );
      } catch {
        return PORTRAIT_EXPERIENCE.defaultStyle;
      }
    });
    const [entrancePreference, setEntrancePreference] = useState(() => {
      /* Editorial entrance shipped 2026-09-23; the lab starts there too. */
      if (!PORTRAIT_EXPERIENCE.isLab) return "editorial";
      try {
        const stored = window.sessionStorage.getItem("portrait-lab-entrance");
        return stored ? window.PortraitMotion.resolveEntrancePreference(stored) : "editorial";
      } catch {
        return "editorial";
      }
    });
    const [namePreference, setNamePreference] = useState(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return "classic";
      try {
        return window.sessionStorage.getItem("portrait-lab-name") === "magazine"
          ? "magazine"
          : "classic";
      } catch {
        return "classic";
      }
    });
    const [entranceReplay, setEntranceReplay] = useState(0);
    const [tune, setTune] = useState(() => (TUNE_ENABLED ? readTune() : null));
    applyTune(tune || TUNE_LIVE);
    const updateTune = (patch) => setTune((prev) => ({ ...prev, ...patch }));
    useEffect(() => {
      if (!tune) return;
      const { open, ...choice } = tune;
      try {
        window.sessionStorage.setItem("type-tune", JSON.stringify({ ...choice, open }));
      } catch {
        // The tuner still works when browser storage is unavailable.
      }
      const params = new URLSearchParams(window.location.search);
      Object.entries(choice).forEach(([key, value]) => params.set(key, value));
      if (open) params.delete("panel");
      else params.set("panel", "closed");
      window.history.replaceState(null, "", `${window.location.pathname}?${params}`);
    }, [tune]);
    useEffect(() => {
      document.documentElement.dataset.theme = PORTRAIT_EXPERIENCE.isLab
        ? themeMode
        : "light";
      document.body.style.background = T.paper;
    }, [themeMode]);
    useEffect(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return;
      try {
        window.sessionStorage.setItem("portrait-lab-theme", themeMode);
      } catch {
        // The preview still works when browser storage is unavailable.
      }
    }, [themeMode]);
    useEffect(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return;
      try {
        window.sessionStorage.setItem("portrait-lab-style", portraitStyle);
      } catch {
        // The preview still works when browser storage is unavailable.
      }
    }, [portraitStyle]);
    useEffect(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return;
      try {
        window.sessionStorage.setItem("portrait-lab-entrance", entrancePreference);
      } catch {
        // The preview still works when browser storage is unavailable.
      }
    }, [entrancePreference]);
    useEffect(() => {
      if (!PORTRAIT_EXPERIENCE.isLab) return;
      try {
        window.sessionStorage.setItem("portrait-lab-name", namePreference);
      } catch {
        // The comparison still works when browser storage is unavailable.
      }
    }, [namePreference]);
    const entranceStyle = (index) => ({ "--entrance-index": index });
    /* Copy and markup switches: the lab's choice, else what ships. */
    const look = tune || TUNE_LIVE;
    return (
      <main style={S.page}>
        {PORTRAIT_EXPERIENCE.isLab && (
          <aside style={S.tunePanel} aria-label="Lab experiments" data-lab-panel="">
            <button
              className="portrait-style-option"
              type="button"
              aria-expanded={tune.open}
              style={{ ...S.portraitStyleOption(false), alignSelf: "flex-end" }}
              onClick={() => updateTune({ open: !tune.open })}
            >
              {tune.open ? "Hide lab" : "Lab"}
            </button>
            {tune.open && (
            <>
            <div style={S.portraitStyleRow}>
              <span style={S.portraitStyleLabel}>Theme</span>
              <div role="group" aria-label="Color theme" style={S.portraitStyleControl}>
                {LAB_THEME_OPTIONS.map((option) => (
                  <button
                    key={option.value}
                    className="portrait-style-option"
                    type="button"
                    aria-pressed={themeMode === option.value}
                    style={S.portraitStyleOption(themeMode === option.value)}
                    onClick={() => setThemeMode(option.value)}
                  >
                    {option.label}
                  </button>
                ))}
              </div>
            </div>
            <div style={S.portraitStyleRow}>
              <span style={S.portraitStyleLabel}>Avatar</span>
              <div role="group" aria-label="Avatar style" style={S.portraitStyleControl}>
                {PORTRAIT_EXPERIENCE.styles.map((option) => (
                  <button
                    key={option.value}
                    className="portrait-style-option"
                    type="button"
                    aria-pressed={portraitStyle === option.value}
                    style={S.portraitStyleOption(portraitStyle === option.value)}
                    onClick={() => setPortraitStyle(option.value)}
                  >
                    {option.label}
                  </button>
                ))}
              </div>
            </div>
            {themeMode === "dark" && (
              <div style={S.portraitStyleRow}>
                <span style={S.portraitStyleLabel}>Name</span>
                <div role="group" aria-label="Name treatment" style={S.portraitStyleControl}>
                  {LAB_NAME_OPTIONS.map((option) => (
                    <button
                      key={option.value}
                      className="portrait-style-option"
                      type="button"
                      aria-pressed={namePreference === option.value}
                      style={S.portraitStyleOption(namePreference === option.value)}
                      onClick={() => setNamePreference(option.value)}
                    >
                      {option.label}
                    </button>
                  ))}
                </div>
              </div>
            )}
            <div style={S.portraitStyleRow}>
              <span style={S.portraitStyleLabel}>Entrance</span>
              <div role="group" aria-label="Entrance animation" style={S.portraitStyleControl}>
                {LAB_ENTRANCE_OPTIONS.map((option) => (
                  <button
                    key={option.value}
                    className="portrait-style-option"
                    type="button"
                    aria-pressed={entrancePreference === option.value}
                    style={S.portraitStyleOption(entrancePreference === option.value)}
                    onClick={() => {
                      setEntrancePreference(option.value);
                      setEntranceReplay((value) => value + 1);
                    }}
                  >
                    {option.label}
                  </button>
                ))}
                <button
                  className="portrait-style-option"
                  type="button"
                  aria-label="Replay entrance animation"
                  style={S.portraitStyleOption(false)}
                  onClick={() => setEntranceReplay((value) => value + 1)}
                >
                  Replay
                </button>
              </div>
            </div>
            <TuneRow tune={tune} onChange={updateTune} field="display" label="Display" />
            <TuneRow tune={tune} onChange={updateTune} field="body" label="Body" />
            <TuneRow tune={tune} onChange={updateTune} field="space" label="Spacing" />
            <TuneRow tune={tune} onChange={updateTune} field="projects" label="Projects" />
            <TuneRow tune={tune} onChange={updateTune} field="bio" label="Bio" />
            <TuneRow tune={tune} onChange={updateTune} field="cta" label="Bio CTA" />
            <TuneRow tune={tune} onChange={updateTune} field="connect" label="Connect" />
            <button
              className="portrait-style-option"
              type="button"
              style={{ ...S.portraitStyleOption(false), alignSelf: "flex-end" }}
              onClick={() => updateTune({ ...TUNE_LIVE })}
            >
              Reset to live
            </button>
            </>
            )}
          </aside>
        )}
        <div
          key={`${entrancePreference}-${entranceReplay}`}
          className="entrance-preview"
          data-entrance={entrancePreference}
        >
          <Bio
            waveRequest={waveRequest}
            portraitStyle={portraitStyle}
            entranceStyle={entranceStyle(0)}
            magazineName={PORTRAIT_EXPERIENCE.isLab && themeMode === "dark" && namePreference === "magazine"}
            copy={look.bio}
            cta={look.cta}
            onEmailActivate={() => setWaveRequest((value) => value + 1)}
          />
          <WorkSection entranceStyle={entranceStyle(1)} projectsLayout={tune?.projects} />
          {SHOW_KIND_WORDS && <KindWordsSection entranceStyle={entranceStyle(2)} />}
          {SHOW_FAVORITE_THINGS && <FavoritesSection entranceStyle={entranceStyle(2)} />}
          <ConnectSection
            entranceStyle={entranceStyle(2)}
            variant={look.connect}
            onEmailActivate={() => setWaveRequest((value) => value + 1)}
          />
          <Footer entranceStyle={entranceStyle(3)} />
        </div>
      </main>
    );
  }

  window.HomeVariant = HomeVariant;
})();
