// Shared theme + reusable atoms for the personal site.
// Type system: EB Garamond (display + prose) & DM Mono (UI). Black / off-white / single red.

const THEME = {
  dark: { bg: '#0a0a0a', ink: '#f4f1ea', mute: '#8a857c', line: '#1f1d1a', sub: '#2a2724' },
  light: { bg: '#f4f1ea', ink: '#0a0a0a', mute: '#7a766d', line: '#dcd7cc', sub: '#e7e2d6' },
  red: '#ff3b1f',
};

const FONT_SERIF = "'EB Garamond', Georgia, 'Times New Roman', serif";
const FONT_MONO = "'DM Mono', ui-monospace, monospace";

// Essays are withdrawn from the site until they are resubmitted. With this off
// the section is unreachable, not merely hidden: no nav entry, no home-page
// block, no route — /essays and /essays/:id fall through to home. Flip it back
// to true to restore everything; nothing else needs changing here.
const ESSAYS_ENABLED = false;

// Inject site-wide CSS once. Scoped to .pp-page so artboards stay isolated.
if (typeof document !== 'undefined' && !document.getElementById('pp-styles')) {
  const s = document.createElement('style');
  s.id = 'pp-styles';
  s.textContent = `
  @import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,400;1,500&display=swap');

  .pp-page { font-family: ${FONT_MONO}; font-feature-settings: 'ss01','cv11'; line-height:1.45; color:var(--ink); background:var(--bg); width:100%; height:100%; box-sizing:border-box; overflow-y:auto; overflow-x:hidden; position:relative; --pad:56px; }
  .pp-page *, .pp-page *::before, .pp-page *::after { box-sizing:border-box; }
  .pp-page a { color:inherit; text-decoration:none; }
  .pp-page a:hover { color:${THEME.red}; }
  .pp-page .red { color: ${THEME.red}; }
  .pp-page hr.pp-hr { border:0; border-top:1px solid var(--line); margin:0; }
  .pp-page .pp-num { font-variant-numeric: tabular-nums; }

  /* ── display-headline system (Garamond) — accents go italic + accent colour ── */
  .pp-h1 { font-family:${FONT_SERIF}; font-weight:500; letter-spacing:0; line-height:0.98; margin:0; }
  .pp-h1.upper { text-transform:uppercase; letter-spacing:0.005em; }
  .pp-serif { font-family:${FONT_SERIF}; }
  .pp-kick { font-size:11px; color:var(--mute); letter-spacing:0.16em; text-transform:uppercase; }
  .pp-em { font-style:italic; font-weight:500; color:${THEME.red}; }

  /* ── mesh-gradient backdrop for the main page ── */
  .pp-mesh-wrap { position:relative; min-height:100%; }
  .pp-mesh { position:absolute; inset:0; z-index:0; pointer-events:none;
    background-color:#f4f1ea;
    background-image:
      radial-gradient(at 3% 1%,  rgba(255,59,31,0.11)  0px, transparent 45%),
      radial-gradient(at 79% -5%, rgba(255,176,122,0.20) 0px, transparent 50%),
      radial-gradient(at 101% 46%, rgba(150,184,201,0.22) 0px, transparent 52%),
      radial-gradient(at 15% 98%, rgba(255,206,150,0.20) 0px, transparent 50%),
      radial-gradient(at 63% 101%, rgba(212,206,188,0.60) 0px, transparent 55%);
  }
  .pp-page.dark .pp-mesh {
    background-color:#0a0a0a;
    background-image:
      radial-gradient(at 3% 1%,  rgba(255,59,31,0.20)  0px, transparent 45%),
      radial-gradient(at 79% -5%, rgba(150,72,38,0.26) 0px, transparent 50%),
      radial-gradient(at 101% 46%, rgba(44,78,98,0.34) 0px, transparent 52%),
      radial-gradient(at 15% 98%, rgba(92,62,26,0.26) 0px, transparent 50%),
      radial-gradient(at 63% 101%, rgba(26,24,21,0.85) 0px, transparent 55%);
  }

  /* dotted underline that fills red on hover */
  .pp-link { position:relative; display:inline-block; transition: color .15s; cursor:pointer; }
  .pp-link::after { content:''; position:absolute; left:0; right:0; bottom:-2px; height:1px; background-image: linear-gradient(to right, currentColor 50%, transparent 50%); background-size: 4px 1px; opacity:.45; transition: opacity .15s; }
  .pp-link:hover { color: ${THEME.red}; }
  .pp-link:hover::after { opacity: 1; background-image: linear-gradient(to right, ${THEME.red} 50%, transparent 50%); }

  /* scroll reveal */
  .pp-rv { opacity:0; transform: translateY(8px); transition: opacity .6s ease, transform .6s ease; }
  .pp-rv.in { opacity:1; transform: none; }

  /* dense table rows */
  .pp-row { display:grid; align-items:baseline; padding: 14px 0; border-top: 1px solid var(--line); transition: background .15s, padding .15s; cursor:pointer; }
  .pp-row:hover { background: var(--sub); }
  .pp-row:hover .pp-row-title { color: ${THEME.red}; }
  .pp-row:hover .pp-arrow { transform: translateX(4px); color: ${THEME.red}; }
  .pp-row .pp-arrow { transition: transform .18s, color .15s; color: var(--mute); }
  .pp-row .pp-tag { color: var(--mute); }

  /* simple kbd-ish chip */
  .pp-chip { display:inline-block; padding: 2px 6px; border:1px solid var(--line); border-radius:2px; font-size:11px; color:var(--mute); }

  /* image slot — striped placeholder until a real src is given */
  .pp-img { position:relative; background:
      repeating-linear-gradient(135deg, var(--sub) 0 12px, var(--bg) 12px 24px);
      border:1px solid var(--line); overflow:hidden; cursor:zoom-in; }
  .pp-img.has-src { background:none; }
  .pp-img img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block; }
  .pp-img .pp-img-cap { position:absolute; left:10px; bottom:8px; font-size:10px; color:var(--mute); letter-spacing:0.04em; text-transform:uppercase; background:var(--bg); padding:2px 6px; border:1px solid var(--line); }
  .pp-img .pp-img-no { position:absolute; right:10px; top:8px; font-size:10px; color:${THEME.red}; }
  .pp-img.has-src .pp-img-no { text-shadow: 0 0 6px rgba(0,0,0,.5); }
  .pp-img:hover { outline: 1px solid ${THEME.red}; outline-offset: -1px; }
  .pp-img .pp-drop-hint { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:10px; letter-spacing:0.14em; text-transform:uppercase; color:var(--mute); opacity:0; transition:opacity .15s; pointer-events:none; }
  .pp-img:hover .pp-drop-hint { opacity:1; }
  .pp-img.drop-over { outline:2px dashed ${THEME.red}; outline-offset:-2px; }
  .pp-img.drop-over .pp-drop-hint { opacity:1; color:${THEME.red}; }
  .pp-img.pp-editing { cursor:move; }
  .pp-img.pp-editing img { user-select:none; -webkit-user-drag:none; }
  .pp-tools { position:absolute; top:6px; left:6px; display:flex; gap:4px; z-index:5; flex-wrap:wrap; max-width:calc(100% - 12px); }
  .pp-toolbtn { font-family:${FONT_MONO}; font-size:10px; letter-spacing:0.04em; background:var(--bg); border:1px solid var(--line); color:var(--ink); padding:4px 8px; cursor:pointer; line-height:1; }
  .pp-toolbtn:hover { border-color:${THEME.red}; color:${THEME.red}; }
  .pp-crop-hint { position:absolute; right:10px; bottom:8px; font-size:10px; color:var(--mute); letter-spacing:0.06em; text-transform:uppercase; background:var(--bg); padding:2px 6px; border:1px solid var(--line); pointer-events:none; }
  .pp-addframe { display:flex; align-items:center; justify-content:center; aspect-ratio:4/5; width:100%; background:none; border:1px dashed var(--line); color:var(--mute); font-family:${FONT_MONO}; font-size:11px; letter-spacing:0.1em; text-transform:uppercase; cursor:pointer; }
  .pp-addframe:hover { border-color:${THEME.red}; color:${THEME.red}; }
  .pp-editbar { display:flex; justify-content:space-between; align-items:center; gap:16px; padding:10px 14px; border:1px dashed ${THEME.red}; margin-bottom:20px; font-size:11px; color:var(--mute); flex-wrap:wrap; }
  .pp-editinput { font-family:${FONT_MONO}; font-size:11px; width:100%; padding:6px 8px; background:var(--sub); border:1px solid var(--line); color:var(--ink); outline:none; }
  .pp-editinput:focus { border-color:${THEME.red}; }
  textarea.pp-editinput { resize:vertical; line-height:1.5; font-family:${FONT_SERIF}; font-size:16px; }
  .pp-textblock { font-family:${FONT_SERIF}; font-size:18px; line-height:1.5; align-self:center; padding:8px 4px; }
  .pp-essayfig { margin:32px 0; }
  .pp-essayfig figcaption { font-family:${FONT_MONO}; font-size:11px; color:var(--mute); margin-top:8px; }

  /* ── type variants (Tweaks → Typography → Titles) ──
     .t-title marks item/list titles, .t-body marks intro paragraphs.
     type-serif swaps titles to Garamond; type-serifplus also swaps intros. */
  .pp-page.type-serif .t-title, .pp-page.type-serifplus .t-title { font-family:${FONT_SERIF}; letter-spacing:0.005em; }
  .pp-page.type-serifplus .t-body { font-family:${FONT_SERIF}; line-height:1.5 !important; }

  /* essay reading — Garamond prose */
  .pp-prose { font-family: ${FONT_SERIF}; font-size: var(--prose, 20px); line-height: 1.52; }
  .pp-prose p { margin: 0 0 1.1em; }
  .pp-prose em { font-style: italic; }
  .pp-prose a { color:${THEME.red}; border-bottom:1px solid ${THEME.red}; }
  .pp-prose .dropcap::first-letter { float:left; font-size: 3.6em; line-height: .9; padding: .08em .08em 0 0; font-weight: 500; }
  .pp-prose h2 { font-family:inherit; font-size:31px; font-weight:500; margin:36px 0 16px; letter-spacing:-0.005em; }
  .pp-prose h3 { font-family:inherit; font-size:24px; font-weight:500; font-style:italic; margin:28px 0 12px; }
  .pp-prose sup { font-size:0.7em; vertical-align:super; }
  .pp-side { font-family: ${FONT_MONO}; font-size: 12px; color: var(--mute); line-height:1.45; }
  .pp-side b, .pp-side strong { color: var(--ink); font-weight: 500; }

  /* nav */
  .pp-burger { display:none; width:40px; height:40px; flex-direction:column; gap:5px; align-items:center; justify-content:center; background:var(--bg); border:1px solid var(--line); border-radius:2px; cursor:pointer; padding:0; flex:0 0 auto; align-self:center; }
  .pp-burger span { display:block; width:16px; height:1.5px; background:var(--ink); }
  .pp-burger:hover { border-color:${THEME.red}; }
  .pp-menu { position:fixed; inset:0; z-index:75; background:var(--bg); color:var(--ink); display:flex; flex-direction:column; padding:16px 18px calc(20px + env(safe-area-inset-bottom)); overflow:auto; }
  .pp-menu-close { align-self:flex-end; font-family:${FONT_MONO}; font-size:11px; letter-spacing:0.1em; text-transform:uppercase; background:none; border:1px solid var(--line); color:var(--ink); padding:9px 14px; cursor:pointer; }
  .pp-menu-list { display:flex; flex-direction:column; margin-top:18px; }
  .pp-menu-list a { font-family:${FONT_SERIF}; font-size:clamp(30px, 8.5vw, 40px); font-weight:500; text-transform:uppercase; line-height:1; padding:16px 0; border-top:1px solid var(--line); display:flex; align-items:baseline; gap:16px; color:var(--ink); }
  .pp-menu-list a.is-active { color:${THEME.red}; }
  .pp-menu-no { font-family:${FONT_MONO}; font-size:11px; color:var(--mute); font-weight:400; }
  .pp-menu-foot { margin-top:auto; padding-top:22px; border-top:1px solid var(--line); display:flex; justify-content:space-between; align-items:center; gap:16px; }
  .pp-nav a { color: var(--mute); transition: color .15s; }
  .pp-nav a:hover, .pp-nav a.is-active { color: ${THEME.red}; }
  .pp-nav a.is-active::before { content:'· '; color: ${THEME.red}; }
  .pp-wordmark { font-family:${FONT_SERIF}; font-weight:600; color:var(--ink); letter-spacing:0.01em; text-transform:uppercase; font-size:17px; }

  /* language switcher */
  .pp-langbtn { display:inline-flex; align-items:center; gap:7px; font-family:inherit; font-size:12px; color:var(--ink);
    background:var(--bg); border:1px solid var(--line); border-radius:2px; padding:5px 9px; cursor:pointer; transition:border-color .15s; }
  .pp-langbtn:hover { border-color:${THEME.red}; }
  .pp-langmenu { position:absolute; top:calc(100% + 6px); right:0; min-width:158px; z-index:60;
    background:var(--bg); border:1px solid var(--line); border-radius:2px; padding:4px; display:grid; gap:1px;
    box-shadow:0 8px 24px rgba(0,0,0,0.18); }
  .pp-langopt { display:flex; align-items:center; gap:10px; width:100%; text-align:left; font-family:inherit; font-size:12px;
    color:var(--ink); background:none; border:0; padding:7px 9px; cursor:pointer; border-radius:1px; transition:background .12s, color .12s; }
  .pp-langopt:hover { background:var(--sub); }
  .pp-langopt.is-active { color:${THEME.red}; }
  .pp-langopt span:first-child { letter-spacing:0.06em; font-weight:500; }

  /* beating heart */
  @keyframes pp-pulse { 0%,100%{opacity:1} 50%{opacity:.15} }
  .pp-dot { position:relative; display:inline-block; width:12px; height:11px; vertical-align:middle; background:${THEME.red}; animation: pp-pulse 3s ease-in-out infinite;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/%3E%3C/svg%3E") center / contain no-repeat; }

  /* dark/light toggle */
  .pp-theme-btn { position:fixed; top:14px; right:16px; width:22px; height:22px; border-radius:50%; border:1px solid var(--line); background:var(--bg); cursor:pointer; padding:0; z-index:60; }
  .pp-theme-btn::after { content:''; position:absolute; inset:3px 3px 3px 50%; background: var(--ink); border-radius: 0 999px 999px 0; transition: background .15s; }
  .pp-theme-btn:hover { border-color: ${THEME.red}; }

  /* lightbox */
  .pp-lb { position:fixed; inset:0; background: rgba(10,10,10,.94); backdrop-filter: blur(8px); z-index:80; display:flex; align-items:center; justify-content:center; padding: 48px; }
  .pp-lb-close { position:absolute; top:14px; right:16px; color:#f4f1ea; font-size:14px; background:none; border:1px solid #2a2724; padding: 6px 10px; cursor:pointer; font-family:inherit; }
  .pp-lb-nav { position:absolute; top:50%; transform: translateY(-50%); background:none; border:1px solid #2a2724; color:#f4f1ea; width:40px; height:40px; cursor:pointer; font-family:inherit; }
  .pp-lb-prev { left: 24px; }
  .pp-lb-next { right: 24px; }
  .pp-lb-meta { position:absolute; left: 24px; bottom: 18px; color:#8a857c; font-size:11px; }
  .pp-lb-count { position:absolute; right: 24px; bottom: 18px; color:#8a857c; font-size:11px; font-variant-numeric: tabular-nums; }

  /* ── page scaffolding ───────────────────────────────────────── */
  .pp-wrap { padding: 56px var(--pad) 0; }
  .pp-wrap.tight { padding-top: 40px; }
  .pp-nav { position:fixed; top:0; left:0; right:0; z-index:50; background:var(--bg); display:flex; align-items:baseline; justify-content:space-between; padding:24px var(--pad) 18px; border-bottom:1px solid var(--line); gap:24px; flex-wrap:wrap; }
  .pp-foot { border-top:1px solid var(--line); padding:18px var(--pad) 22px; margin-top:64px; display:flex; justify-content:space-between; align-items:baseline; font-size:11px; color:var(--mute); gap:24px; flex-wrap:wrap; }

  /* named grids — each collapses deliberately at breakpoints below */
  .g-bio { display:grid; grid-template-columns:2fr 1fr 1fr; gap:32px; }
  .g-main { display:grid; grid-template-columns:1.4fr 1fr; gap:40px; }
  .g-work { display:grid; grid-template-columns:120px 1fr; gap:32px; }
  .g-workrow { display:grid; grid-template-columns:180px 1fr auto; gap:24px; }
  .g-about { display:grid; grid-template-columns:1.6fr 1fr; gap:56px; }
  .g-cvsec { display:grid; grid-template-columns:160px 1fr; gap:32px; }
  .g-cvrow { display:grid; grid-template-columns:180px 1fr auto; gap:20px; align-items:baseline; }
  .cv-bullets { margin:10px 0 0 180px; padding:0; list-style:none; font-size:13px; line-height:1.55; }
  .g-skills { display:grid; grid-template-columns:repeat(2,1fr); gap:28px; }
  .g-photorow { display:grid; grid-template-columns:80px 1fr 280px; gap:24px; align-items:center; }
  .g-serieshead { display:grid; grid-template-columns:1fr 320px; gap:56px; }
  .g-seriesgrid { display:grid; grid-template-columns:1fr 1fr 1fr; gap:16px; }
  .g-seriesgrid .span-wide { grid-column: span 2; }
  .g-seriesgrid .span-full { grid-column: 1 / -1; }
  .g-colophon { display:grid; grid-template-columns:1fr 1fr; gap:56px; }
  .g-essaysec { display:grid; grid-template-columns:240px 1fr; gap:40px; }
  .g-essayrow { display:grid; grid-template-columns:40px 1fr auto 24px; gap:18px; align-items:baseline; }
  .g-proj { display:grid; grid-template-columns:1.25fr 1fr; gap:48px; align-items:start; }
  .g-contact { display:grid; grid-template-columns:1.4fr 1fr; gap:56px; }
  .g-form { display:grid; grid-template-columns:1fr 1fr; gap:16px; max-width:720px; }
  .pp-sticky { position:sticky; top:76px; align-self:start; }
  .pp-h1-name { white-space:nowrap; }

  /* ── responsive: tablet ─────────────────────────────────────── */
  @media (max-width: 920px) {
    .pp-page { --pad:32px; }
    .g-bio, .g-main, .g-about, .g-proj, .g-contact, .g-serieshead, .g-colophon { grid-template-columns:1fr; gap:28px; }
    .g-seriesgrid { grid-template-columns:1fr 1fr; }
    .g-essaysec { grid-template-columns:200px 1fr; gap:28px; }
    .pp-h1-name { font-size:clamp(48px,10vw,110px) !important; }
  }

  /* ── responsive: phone ──────────────────────────────────────── */
  @media (max-width: 680px) {
    .pp-page { --pad:18px; }
    .pp-wrap { padding-top:36px; }
    .pp-nav { padding-top:12px; padding-bottom:12px; padding-right:52px; gap:10px 14px; align-items:center; }
    .pp-nav-links { display:none !important; }
    .pp-burger { display:flex; }
    .pp-wordmark { font-size:15px; }
    .pp-theme-btn { top:21px; }
    .pp-tools { gap:3px; }
    .pp-toolbtn { padding:6px 9px; }
    .g-work, .g-cvsec, .g-essaysec, .g-skills, .g-form { grid-template-columns:1fr; gap:16px; }
    .g-workrow, .g-cvrow { grid-template-columns:1fr; gap:4px; }
    .cv-bullets { margin-left:0; }
    .g-photorow { grid-template-columns:1fr; gap:12px; }
    .g-photorow > .pr-no { display:none; }
    .g-photorow .pp-img { width:100% !important; }
    .g-seriesgrid { grid-template-columns:1fr; }
    .g-seriesgrid .span-wide, .g-seriesgrid .span-full { grid-column:auto; }
    .g-essayrow { grid-template-columns:1fr; gap:4px; }
    .g-essayrow .er-no, .g-essayrow .arr { display:none; }
    .pp-sticky { position:static; }
    .pp-h1-name { white-space:normal !important; font-size:clamp(38px,12.5vw,66px) !important; }
    .pp-lb { padding:12px !important; }
    .pp-lb-nav { width:34px; height:34px; }
    .pp-lb-prev { left:8px; }
    .pp-lb-next { right:8px; }
    .pp-langmenu { right:auto; left:0; }
  }

  @media print {
    @page { margin:11mm 12mm; }

    /* Scroll-reveal never fires for off-screen content, so force it visible
       or everything below the fold prints as an empty page. */
    .pp-rv { opacity:1 !important; transform:none !important; transition:none !important; }
    /* Screen furniture that means nothing on paper. */
    .pp-nav, .pp-navpad, .pp-theme-btn, .pp-tools, .pp-burger, .pp-menu, .pp-tweaks,
    .pp-addframe, .pp-editinput, .pp-lb, .pp-foot, .cv-dl { display:none !important; }
    /* index.html tints html/body; unpainted, that tint is what shows through
       below the last section as a grey block on the final sheet. */
    html, body { background:#fff !important; }
    .pp-page {
      --bg:#fff; --ink:#111; --mute:#555; --line:#ccc; --sub:#eee;
      background:#fff !important; color:#111 !important;
      /* On screen this is the scroll container. Left as-is the printer only
         gets what fits in one viewport-height box. */
      height:auto !important; min-height:0 !important; overflow:visible !important;
      font-size:9.5pt; line-height:1.32;
    }
    .pp-wrap { padding:0 !important; max-width:none !important; }
    a { text-decoration:none; color:#111 !important; }

    /* The white half-pages between sections came from the section grid.
       Chrome fragments a nested grid poorly: when the whole container will
       not fit on the current sheet it pushes the entire thing to the next
       one and leaves the remainder blank. Printed sections are plain blocks,
       so entries flow continuously and break wherever they run out of page. */
    .g-cvsec { display:block !important; padding:0 !important; }
    .g-cvsec > .pp-sticky {
      display:flex; gap:8px; align-items:baseline;
      padding:3.5mm 0 1.2mm; border-bottom:0.6pt solid #999; margin-bottom:0;
      break-after:avoid; page-break-after:avoid; break-inside:avoid;
    }
    .g-cvsec > .pp-sticky > div { margin:0 !important; font-size:8pt !important; }
    .pp-sticky { position:static !important; }

    /* Condensed spacing throughout: this is a document, not a landing page. */
    .cv-head { margin-bottom:2mm !important; }
    .cv-h1 { font-size:21pt !important; line-height:1 !important; }
    .cv-intro { margin-bottom:0 !important; font-size:8.5pt !important; }
    .cv-entry { padding:1.8mm 0 !important; }
    .cv-entry:first-child { border-top:0 !important; }
    .g-cvrow { grid-template-columns:32mm 1fr auto; gap:5mm; }
    .g-cvrow .t-title { font-size:10.5pt !important; }
    .g-cvrow .pp-num { font-size:8pt !important; padding-left:0 !important; border-left:0 !important; }
    .g-cvrow > div > div:last-child { font-size:8.5pt !important; margin-top:0 !important; }
    .cv-bullets { margin:0.8mm 0 0 32mm !important; font-size:8.5pt; line-height:1.32; }
    .cv-bullets li { padding-left:3.5mm !important; }
    .cv-award { padding:1.2mm 0 !important; gap:5mm !important; font-size:9pt !important;
                grid-template-columns:14mm 1fr !important; }
    .cv-award:first-child { border-top:0 !important; }
    .g-skills { gap:3mm 8mm !important; }
    .g-skills > div > div:first-child { margin-bottom:1.5mm !important; font-size:8pt !important; }
    .pp-chip { padding:0.5px 4px; font-size:8pt !important; }

    /* Keep an entry from being split across a page break. */
    .cv-entry, .cv-award, .g-cvrow, .g-workrow, .cv-bullets li { break-inside:avoid; page-break-inside:avoid; }
    h1, h2, h3 { break-after:avoid; page-break-after:avoid; }
  }
  `;
  document.head.appendChild(s);
}

// ──────────────────────────────────────────────────────────────
// PageShell — wraps every page with theme vars, optional light/dark toggle,
// global scroll-reveal observer, and the router context so a nav click
// navigates within the page rather than reloading it.
// ──────────────────────────────────────────────────────────────
// --------------------------------------------------------------
// Router
//
// Routes are real URLs, so a page can be linked to, shared, bookmarked and
// reached with the browser's back button. Home is "/" rather than "/home" so
// there is one canonical front door; /home redirects to it at the edge.
//
// The site is served as static files, so every one of these paths has to be
// rewritten to index.html in vercel.json or a direct visit 404s before any of
// this runs.
// --------------------------------------------------------------
const PP_ROUTES = ['about', 'cv', 'projects', 'photos', 'contact']
  .concat(ESSAYS_ENABLED ? ['essays'] : []);

function ppPathFor(route, id) {
  if (route === 'series') return '/photos/' + encodeURIComponent(id || 's6');
  if (route === 'essay') return '/essays/' + encodeURIComponent(id || 'e1');
  if (!route || route === 'home') return '/';
  return '/' + route;
}

function ppRouteFrom(pathname) {
  const seg = String(pathname || '/').split('/').filter(Boolean);
  if (!seg.length) return { route: 'home' };
  const head = seg[0];
  const tail = seg[1];
  // A series lives under its index: /photos/s6, /essays/m18.
  if (head === 'photos') return tail ? { route: 'series', id: decodeURIComponent(tail) } : { route: 'photos' };
  if (head === 'essays') {
    if (!ESSAYS_ENABLED) return { route: 'home' };
    return tail ? { route: 'essay', id: decodeURIComponent(tail) } : { route: 'essays' };
  }
  if (PP_ROUTES.indexOf(head) !== -1) return { route: head };
  // Unknown path falls through to home, matching the route switch in Site().
  return { route: 'home' };
}

// Let the browser handle a click it would open in a new tab or window.
function ppModifiedClick(e) {
  return e.defaultPrevented || e.button !== 0 ||
         e.metaKey || e.ctrlKey || e.shiftKey || e.altKey;
}

const RouteCtx = React.createContext({ route: 'home', go: () => {} });

function prefersDark() {
  return typeof window !== 'undefined' && window.matchMedia
    ? window.matchMedia('(prefers-color-scheme: dark)').matches : false;
}

function PageShell({ children, dark = true, withToggle = true, route, setRoute, pageClass = '', styleVars }) {
  // Theme follows the browser's light/dark setting by default; the toggle is a
  // manual override that wins until the system preference next changes.
  const [mode, setMode] = React.useState(() =>
    (typeof window !== 'undefined' && window.matchMedia) ? (prefersDark() ? 'dark' : 'light') : (dark ? 'dark' : 'light'));
  const userSet = React.useRef(false);
  const t = THEME[mode];
  const ref = React.useRef(null);

  // Live-update when the OS/browser theme changes (unless manually overridden).
  React.useEffect(() => {
    if (!(window.matchMedia)) return;
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    const h = (e) => { if (!userSet.current) setMode(e.matches ? 'dark' : 'light'); };
    mq.addEventListener ? mq.addEventListener('change', h) : mq.addListener(h);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', h) : mq.removeListener(h); };
  }, []);

  // Reveal on scroll within this artboard scrollport. IntersectionObserver
  // drives the smooth fade; a fallback reveals any stragglers instantly
  // (transition defeated so opacity can't get stuck at 0) — this also covers
  // the scaled focus overlay, where IO does not fire reliably.
  React.useEffect(() => {
    const root = ref.current;
    if (!root) return;
    const targets = root.querySelectorAll('.pp-rv');
    const io = new IntersectionObserver((es) => {
      es.forEach((e) => { if (e.isIntersecting) e.target.classList.add('in'); });
    }, { root, threshold: 0.05 });
    targets.forEach((el) => io.observe(el));

    const fallback = setTimeout(() => {
      targets.forEach((el) => {
        if (el.classList.contains('in')) return;
        el.style.transition = 'none';
        el.classList.add('in');
        requestAnimationFrame(() => { el.style.transition = ''; });
      });
    }, 450);

    return () => { io.disconnect(); clearTimeout(fallback); };
  }, [route]);

  // Re-fire reveal on route change.
  React.useEffect(() => {
    const root = ref.current; if (!root) return;
    root.scrollTo({ top: 0 });
    root.querySelectorAll('.pp-rv').forEach((el) => el.classList.remove('in'));
  }, [route]);

  const style = {
    '--bg': t.bg,
    '--ink': t.ink,
    '--mute': t.mute,
    '--line': t.line,
    '--sub': t.sub,
    ...styleVars,
  };

  return (
    <RouteCtx.Provider value={{ route, go: setRoute }}>
      <div ref={ref} className={'pp-page ' + mode + (pageClass ? ' ' + pageClass : '')} style={style}>
        {withToggle && (
          <button className="pp-theme-btn" title={mode === 'dark' ? 'Light' : 'Dark'} onClick={() => { userSet.current = true; setMode(mode === 'dark' ? 'light' : 'dark'); }} />
        )}
        {children}
      </div>
    </RouteCtx.Provider>
  );
}

// ──────────────────────────────────────────────────────────────
// Navigation atoms
// ──────────────────────────────────────────────────────────────
function NavLink({ to, children, active }) {
  const { route, go } = React.useContext(RouteCtx);
  const isActive = active !== undefined ? active : route === to;
  return (
    <a href={ppPathFor(to)} className={isActive ? 'is-active' : ''}
       onClick={(e) => { if (ppModifiedClick(e)) return; e.preventDefault(); go(to); }}>{children}</a>
  );
}

function TopNav({ name = 'VLADY OSTROUKHOV' }) {
  const lang = useLang();
  const n = tr('nav', lang);
  const { route, go } = React.useContext(RouteCtx);
  const [menu, setMenu] = React.useState(false);
  const navRef = React.useRef(null);
  const [navH, setNavH] = React.useState(63);
  React.useLayoutEffect(() => {
    const measure = () => { if (navRef.current) setNavH(navRef.current.offsetHeight); };
    measure();
    window.addEventListener('resize', measure);
    return () => window.removeEventListener('resize', measure);
  }, [lang]);
  React.useEffect(() => { setMenu(false); }, [route]);
  React.useEffect(() => {
    if (!menu) return;
    const k = (e) => { if (e.key === 'Escape') setMenu(false); };
    document.addEventListener('keydown', k);
    return () => document.removeEventListener('keydown', k);
  }, [menu]);
  const items = [
    ['about', n.about], ['cv', n.cv], ['projects', n.projects],
    ['photos', n.photographs],
  ].concat(ESSAYS_ENABLED ? [['essays', n.essays]] : []).concat([['contact', n.contact]]);
  return (
    <>
      <div ref={navRef} className="pp-nav">
      <NavLink to="home"><span className="pp-wordmark">{name}</span></NavLink>
      <div className="pp-nav-links" style={{ display:'flex', alignItems:'baseline', gap:22, flexWrap:'wrap' }}>
        <nav style={{ display:'flex', gap:22, fontSize:13, flexWrap:'wrap' }}>
          {items.map(([to, label]) => <NavLink key={to} to={to}>{label}</NavLink>)}
        </nav>
        <span style={{ color:'var(--line)' }}>|</span>
        <LangSwitch />
      </div>
      <button className="pp-burger" aria-label="Menu" onClick={() => setMenu(true)}><span></span><span></span><span></span></button>
      </div>
      <div className="pp-navpad" aria-hidden="true" style={{ height: navH }} />
      {menu && (
        <div className="pp-menu">
          <button className="pp-menu-close" onClick={() => setMenu(false)}>✕ close</button>
          <nav className="pp-menu-list">
            {[['home', n.main], ...items].map(([to, label], i) => (
              <a key={to} href={ppPathFor(to)} className={route === to ? 'is-active' : ''}
                 onClick={(e) => { if (ppModifiedClick(e)) return; e.preventDefault(); go(to); setMenu(false); }}>
                <span className="pp-menu-no">{String(i + 1).padStart(2, '0')}</span>{label}
              </a>
            ))}
          </nav>
          <div className="pp-menu-foot">
            <LangSwitch />
            <a href={'mailto:' + SITE.email} style={{ fontSize:11, color:'var(--mute)' }}>{SITE.email}</a>
          </div>
        </div>
      )}
    </>
  );
}

// ──────────────────────────────────────────────────────────────
// Live clock + location footer. Persists across pages.
// ──────────────────────────────────────────────────────────────
function useNow() {
  const [now, setNow] = React.useState(() => new Date());
  React.useEffect(() => {
    const t = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(t);
  }, []);
  return now;
}

function timeIn(tz, now) {
  try {
    return new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(now);
  } catch { return '--:--:--'; }
}

function Footer({ minimal = false }) {
  const now = useNow();
  const lang = useLang();
  const f = tr('footer', lang);
  return (
    <footer className="pp-foot">
      <div style={{ display:'flex', gap:22, flexWrap:'wrap' }}>
        <span style={{ display:'inline-flex', alignItems:'center', gap:7 }}><a href="https://www.lozano-hemmer.com/pulse_agglomerate.php" target="_blank" rel="noopener noreferrer" aria-label="Pulse" style={{ display:'inline-flex', lineHeight:0, alignItems:'center' }}><span className="pp-dot"></span></a><span className="red" style={{ lineHeight: 1 }}>DONETSK, UA</span></span>
      </div>
      <div style={{ display:'flex', gap:22 }}>
        <span>© {now.getFullYear()} {f.copy}</span>
        <span>{f.ver}</span>
      </div>
    </footer>
  );
}

// ──────────────────────────────────────────────────────────────
// Image slot + Lightbox.
// `src` (project path) shows a real photo; without it you get the striped
// placeholder. `slot` makes the frame droppable — drag an image file onto it
// and it's stored in the browser (see photo-admin.jsx). `pos` shifts the crop
// (object-position); with edit tools on, drag inside the photo to set it.
// ──────────────────────────────────────────────────────────────
function Img({ ratio = '4 / 3', caption, no, onOpen, style, src, alt, pos, slot, onPos, tools }) {
  const editing = React.useContext(PhotoEditCtx);
  const store = usePhotoStore();
  const [over, setOver] = React.useState(false);
  const ref = React.useRef(null);
  const dropped = slot ? store.urls[slot] : null;
  const eff = dropped || src;
  const droppable = !!(slot && editing);   // drops only with edit tools on — never for site visitors
  const canCrop = editing && slot && eff && onPos;

  const handleDrop = (e) => {
    if (!droppable) return;
    e.preventDefault(); e.stopPropagation(); setOver(false);
    const f = e.dataTransfer.files && e.dataTransfer.files[0];
    if (f) store.drop(slot, f);
  };

  const startCrop = (e) => {
    if (!canCrop) return;
    e.preventDefault(); e.stopPropagation();
    const r = ref.current.getBoundingClientRect();
    const [px, py] = ppParsePos(pos);
    const x0 = e.clientX, y0 = e.clientY;
    const mv = (ev) => {
      const nx = ppClamp(px - ((ev.clientX - x0) / r.width) * 100);
      const ny = ppClamp(py - ((ev.clientY - y0) / r.height) * 100);
      onPos(Math.round(nx) + '% ' + Math.round(ny) + '%');
    };
    const up = () => { window.removeEventListener('pointermove', mv); window.removeEventListener('pointerup', up); };
    window.addEventListener('pointermove', mv);
    window.addEventListener('pointerup', up);
  };

  return (
    <div ref={ref}
         className={'pp-img' + (eff ? ' has-src' : '') + (over ? ' drop-over' : '') + (canCrop ? ' pp-editing' : '')}
         style={{ aspectRatio: ratio, ...style }}
         onClick={editing && slot ? undefined : onOpen}
         onDragOver={droppable ? (e) => { e.preventDefault(); setOver(true); } : undefined}
         onDragLeave={droppable ? () => setOver(false) : undefined}
         onDrop={droppable ? handleDrop : undefined}
         onPointerDown={canCrop ? startCrop : undefined}>
      {eff && <img src={eff} alt={alt || caption || ''} loading="lazy" draggable={false} style={pos ? { objectPosition: pos } : undefined} />}
      {no && <span className="pp-img-no">{no}</span>}
      {caption && <span className="pp-img-cap">{caption}</span>}
      {droppable && !eff && <span className="pp-drop-hint">drop a photo here</span>}
      {droppable && eff && over && <span className="pp-drop-hint" style={{ opacity: 1 }}>drop to replace</span>}
      {editing && slot && tools && (
        <div className="pp-tools" onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>{tools}</div>
      )}
      {canCrop && <span className="pp-crop-hint">drag to reframe</span>}
    </div>
  );
}

function Lightbox({ items, index, onClose, onPrev, onNext }) {
  React.useEffect(() => {
    const k = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowLeft') onPrev();
      if (e.key === 'ArrowRight') onNext();
    };
    document.addEventListener('keydown', k);
    return () => document.removeEventListener('keydown', k);
  }, [onClose, onPrev, onNext]);
  if (index == null) return null;
  const it = items[index];
  return (
    <div className="pp-lb" onClick={onClose}>
      <button className="pp-lb-close" onClick={onClose}>esc · close</button>
      <button className="pp-lb-nav pp-lb-prev" onClick={(e) => { e.stopPropagation(); onPrev(); }}>←</button>
      <button className="pp-lb-nav pp-lb-next" onClick={(e) => { e.stopPropagation(); onNext(); }}>→</button>
      {it.src ? (
        <img src={it.src} alt={it.caption || ''} onClick={(e) => e.stopPropagation()}
             style={{ maxWidth:'min(1100px, 92%)', maxHeight:'82vh', objectFit:'contain', border:'1px solid #2a2724' }} />
      ) : (
        <div onClick={(e) => e.stopPropagation()}
             style={{ width:'min(960px, 80%)', aspectRatio: it.ratio || '3/2', background: 'repeating-linear-gradient(135deg, #1a1815 0 14px, #0a0a0a 14px 28px)', border:'1px solid #2a2724', position:'relative' }}>
          <span style={{ position:'absolute', left:14, top:12, color:'#ff3b1f', fontSize:11 }}>{it.no}</span>
          <span style={{ position:'absolute', left:14, bottom:12, color:'#8a857c', fontSize:11, textTransform:'uppercase', letterSpacing:'0.05em' }}>{it.caption}</span>
        </div>
      )}
      <div className="pp-lb-meta">{it.caption}{it.meta ? ' — ' + it.meta : ''}</div>
      <div className="pp-lb-count">{String(index+1).padStart(2,'0')} / {String(items.length).padStart(2,'0')}</div>
    </div>
  );
}

Object.assign(window, { THEME, FONT_SERIF, FONT_MONO, PageShell, TopNav, Footer, NavLink, Img, Lightbox, RouteCtx, useNow, timeIn });
