<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Portfolio</title>
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='18' fill='%23111'/%3E%3Ctext x='50' y='64' font-family='Georgia%2C serif' font-size='36' font-weight='700' fill='white' text-anchor='middle' letter-spacing='1'%3EIAN%3C/text%3E%3C/svg%3E" />
  <meta name="description" content="Photography portfolio" />
  <script>
    /* Apply cached theme before first paint — eliminates color/font flash on repeat visits */
    (function(){try{var c=JSON.parse(localStorage.getItem('pp_theme_cache')||'null');if(!c)return;var r=document.documentElement;if(c.bg)r.style.setProperty('--t-bg',c.bg);if(c.navBg)r.style.setProperty('--t-nav-bg',c.navBg);if(c.text)r.style.setProperty('--t-text',c.text);if(c.accent)r.style.setProperty('--t-accent',c.accent);if(c.accentTint)r.style.setProperty('--t-accent-tint',c.accentTint);if(c.cursorColor)r.style.setProperty('--t-cursor',c.cursorColor);if(c.bodySize)r.style.setProperty('--t-body-size',c.bodySize+'px');if(c.font){var l=document.createElement('link');l.rel='stylesheet';l.href='https://fonts.googleapis.com/css2?family='+c.font.replace(/ /g,'+')+':wght@400;700&display=swap';document.head.appendChild(l);}}catch(e){}})();
  </script>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
  <script src="config.js"></script><!-- optional: edit config.js to set credentials without touching this file -->
  <style>
    /* JS-toggled states Tailwind can't handle declaratively */
    .view { display: none; }
    .view.active { display: block; padding-top: 24px; }

    /* Theme CSS variables — overridden by applySettings() */
    :root {
      --t-bg: #ffffff;
      --t-nav-bg: #ffffff;
      --t-text: #111111;
      --t-sidebar-bg: #ffffff;
      --t-sidebar-text: #888888;
      --t-accent: #fbbf24;
      --t-accent-tint: #feefc8;
      --t-cursor: #111111;
      --t-body-size: 15px;
      --t-line-height: 1.75;
      --t-img-gap: 20px;
      --t-grid-min: 200px;
      --t-card-radius: 0px;
    }
    html { overflow-y: scroll; }
    body { background: var(--t-bg); color: var(--t-text); }

    /* Image grids */
    #photo-grid, #tags-grid {
      display: flex !important;
      flex-wrap: wrap;
      align-items: center;
      justify-content: center;
      gap: 30px;
      padding: 0 40px 0 20px;
    }
    .photo-thumb img { border-radius: var(--t-card-radius); }
    .grid-reveal { opacity: 0; transition: opacity 0.4s ease; }
    .grid-reveal.loaded { opacity: 1; }
    .load-spinner {
      display: flex; align-items: center; justify-content: center;
      padding: 48px 20px; width: 100%;
    }
    .load-spinner::after {
      content: ''; width: 22px; height: 22px;
      border: 2px solid #e8e8e8; border-top-color: #aaa;
      border-radius: 50%; animation: spin 0.7s linear infinite;
    }
    @keyframes spin { to { transform: rotate(360deg); } }
    .seq-img { opacity: 0; transition: opacity 0.3s ease; }
    .seq-img.loaded { opacity: 1; }
    .card img { border-radius: var(--t-card-radius); }
    /* Body font and size */
    body, button, input, textarea, select {
      font-size: var(--t-body-size);
      line-height: var(--t-line-height);
    }
    a { color: var(--t-accent); }
    #zoom { opacity: 0; pointer-events: none; transition: opacity 0.2s; cursor: none; touch-action: none; overscroll-behavior: none; user-select: none; -webkit-user-select: none; }
    #zoom-bg-label { cursor: pointer !important; }
    #zoom.open { opacity: 1; pointer-events: all; }
    #zoom-img-wrap { display: flex; align-items: center; justify-content: center; }
    #zoom-img-wrap img { display: block; max-width: 90vw; max-height: 90vh; object-fit: contain; pointer-events: none; transition: opacity 0.22s ease; }
    #zoom-img-wrap img.zoom-fading { opacity: 0; }
    #zoom-info.open { max-height: 60vh !important; }
    #zoom-info-btn { transition: background 0.2s, color 0.2s; }
    #zoom-info-btn.active { background: rgba(255,255,255,0.9) !important; color: #111 !important; }
    #zoom-cursor {
      position: fixed; pointer-events: none; z-index: 600;
      width: 84px; height: 84px;
      transform: translate(-50%, -50%);
      opacity: 0; transition: opacity 0.15s;
    }
    #zoom-cursor.visible { opacity: 1; }
    #zoom-cursor svg { width: 100%; height: 100%; }
    #search-clear.hidden { display: none; }
    /* Mobile: always show card labels */
    @media (max-width: 600px) { .card-label { opacity: 1 !important; } }

    /* ── MOBILE ─────────────────────────────────────────────────────────────── */
    @media (max-width: 768px) {
      /* Lightbox: no custom cursor on touch devices */
      #zoom { cursor: auto; }
      #zoom-cursor { display: none !important; }

      /* Header: centred breadcrumb, full-width nav trigger */
      #topbar { display: flex !important; justify-content: center; padding: 0 12px; cursor: pointer; }
      #topbar-center { display: none !important; }
      #topbar-right { display: none !important; }
      #topbar-left {
        white-space: nowrap;
        display: flex !important; justify-content: center !important; align-items: center;
        min-width: 0; max-width: 100%;
      }
      #topbar-label-chevron { display: none !important; }
      #topbar-site-name { font-size: 3vw; letter-spacing: 0.06em; flex-shrink: 0; }
      #topbar-sep { font-size: 3vw; flex-shrink: 0; }
      #topbar-label { font-size: 3vw; letter-spacing: 0.03em; padding: 3px 6px; overflow: hidden; text-overflow: ellipsis; min-width: 0; }

      /* Nav dropdown: full width, centred links */
      #nav-dropdown {
        left: 20px !important; right: 20px !important; width: auto !important;
        min-width: unset !important;
        padding: 16px 0 20px;
        box-sizing: border-box;
        text-align: center;
        background: color-mix(in srgb, var(--t-nav-bg) 85%, transparent) !important;
        backdrop-filter: blur(8px);
      }
      .nav-dropdown-item {
        text-align: center !important;
        white-space: normal !important;
        width: 100% !important;
        padding: 20px 0 !important;
        font-size: 16px !important;
      }
      .nav-dropdown-divider {
        display: block !important;
        text-align: center !important;
        width: 100% !important;
      }

      /* Recent Work: tighter vertical spacing, centred titles */
      #view-recent.active { padding-top: 24px !important; }
      .recent-row { margin-bottom: 0 !important; }
      .recent-row-header { padding-bottom: 0 !important; justify-content: center !important; }
      .recent-filmstrip { padding-top: 0 !important; padding-bottom: 8px !important; margin-top: -16px !important; }
      .recent-row-title.visible { opacity: 1 !important; }

      /* Hide scrollbars on horizontal filmstrip */
      .recent-filmstrip { scrollbar-width: none !important; }
      .recent-filmstrip::-webkit-scrollbar { display: none !important; }

      /* Grid: symmetric padding so images centre properly */
      #photo-grid, #tags-grid { padding: 0 16px; }

      /* Tag browser: full-width paragraph-style tag cloud */
      #tagbrowser-tags { width: auto !important; max-width: none !important; padding: 0 16px 24px !important; gap: 6px 10px !important; }
      #tagbrowser-search-wrap { width: auto !important; max-width: none !important; padding: 20px 16px 12px !important; }
      #tagbrowser-meta { width: auto !important; max-width: none !important; padding: 0 16px 12px !important; }
      #tb-mode-toggle { width: auto !important; max-width: none !important; padding: 14px 16px 0 !important; }
      .tb-tag { font-size: 18px !important; }

      /* Footer: collapsible strip — shrink main content bottom gap */
      #main-content { margin-bottom: 44px; }
      #bottombar-handle { display: flex; }
      #bottombar {
        flex-direction: column;
        height: auto !important;
        max-height: 44px;
        overflow: hidden;
        transition: max-height 0.25s ease;
        padding: 0;
        justify-content: flex-start;
        cursor: pointer;
      }
      #bottombar.open { max-height: 900px; cursor: default; }
      #bottombar-handle {
        height: 44px; flex-shrink: 0;
        align-items: center; justify-content: center;
        font-size: 13px; opacity: 0.35; letter-spacing: 1px;
        transition: transform 0.25s ease;
        cursor: pointer;
      }
      #bottombar.open #bottombar-handle { transform: rotate(180deg); }
      #bottombar-left, #bottombar-center, #bottombar-right {
        flex: none !important; width: 100%;
        text-align: center !important; padding: 8px 0;
        font-size: 12px;
      }
      #bottombar-right { padding-bottom: 12px; font-size: 12px; }
      .bottombar-link { font-size: 12px; }

    }
    /* Gallery view toggle */
    .gv-btn {
      font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
      background: none; border: none; cursor: pointer; color: #bbb;
      padding-bottom: 1px; font-family: inherit;
    }
    .gv-btn:hover { color: #111; }
    .gv-btn.active { color: #888; border-bottom: 1px solid #bbb; }
    /* Project row: statement + filmstrip side by side */
    /* Project statement */
    .project-statement {
      max-width: 640px; margin: 0; padding: 36px 28px 36px;
      font-size: 15px; line-height: 1.75; color: #444;
    }
    .project-statement p { margin: 0 0 1.25em; }
    .filmstrip-wrap .project-statement {
      width: 600px; flex-shrink: 0; max-width: none;
      overflow-y: auto; padding: 0 48px 0 20px; box-sizing: border-box;
      height: 100%;
    }
    /* Film strip */
    #view-gallery.active:has(#gallery-filmstrip) {
      display: block;
      min-height: calc(100vh - 88px);
      padding: 0;
      display: flex; flex-direction: column; justify-content: center;
    }
    #view-gallery #gallery-filmstrip {
      flex-shrink: 0;
      width: 100%;
      box-sizing: border-box;
    }
    .filmstrip-wrap {
      overflow-x: auto; overflow-y: hidden;
      display: flex; align-items: center; gap: 80px;
      padding: 60px 0 60px 80px; scrollbar-width: thin; scrollbar-color: #ccc transparent;
      height: min(85vh, 1000px);
      width: 100%; box-sizing: border-box;
      mask-image: linear-gradient(to right, rgba(0,0,0,0.75) 0%, black 3%, black 97%, rgba(0,0,0,0.75) 100%);
      -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,0.75) 0%, black 3%, black 97%, rgba(0,0,0,0.75) 100%);
    }
    .filmstrip-wrap::-webkit-scrollbar { height: 4px; }
    .filmstrip-wrap::-webkit-scrollbar-track { background: transparent; }
    .filmstrip-wrap::-webkit-scrollbar-thumb { background: #333; }
    .filmstrip-item { flex-shrink: 0; cursor: pointer; overflow: hidden; }
    .filmstrip-item img { height: 100%; width: 100%; display: block; object-fit: cover; }
    .filmstrip-item:last-child { margin-right: 80px; }
    /* Lightbox entry */
    .lightbox-entry { display: flex; flex-direction: column; align-items: center;
      justify-content: center; padding: 60px 28px; gap: 20px; }
    .lightbox-cover { max-width: 420px; width: 100%; aspect-ratio: 3/2; overflow: hidden;
      cursor: pointer; position: relative; }
    .lightbox-cover img { width: 100%; height: 100%; object-fit: cover;
      transition: opacity 0.2s; }
    .lightbox-cover-label { position: absolute; inset: 0; display: flex; align-items: center;
      justify-content: center; font-size: 13px; color: rgba(255,255,255,0.7);
      letter-spacing: 0.08em; text-transform: uppercase; pointer-events: none; }
    .lightbox-count { font-size: 12px; color: #555; }
    /* Cards */
    .card {
      position: relative; cursor: pointer; overflow: hidden;
      aspect-ratio: 3/2; background: #f0f0f0;
    }
    .card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
    .card:hover img { transform: scale(1.04); }
    .card-label {
      position: absolute; inset: 0;
      background: linear-gradient(to top, rgba(0,0,0,0.75) 0%, transparent 55%);
      display: flex; flex-direction: column; justify-content: flex-end;
      padding: 18px 20px; opacity: 0; transition: opacity 0.25s;
    }
    .card:hover .card-label { opacity: 1; }
    .card-label-name { font-size: 15px; font-weight: 600; }
    .card-label-meta { font-size: 11px; color: #bbb; margin-top: 3px; }
    .card-folder-badge {
      position: absolute; top: 12px; left: 12px;
      font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
      background: rgba(255,255,255,0.15); color: #fff;
      padding: 3px 7px; backdrop-filter: blur(4px);
    }
    .card-placeholder {
      width: 100%; height: 100%;
      display: flex; align-items: center; justify-content: center;
      font-size: 32px; color: #bbb; background: #f0f0f0;
    }
    .photo-thumb { cursor: pointer; background: #f0f0f0; position: relative; flex: 0 0 auto; height: 220px; overflow: hidden; }
    .photo-thumb img { height: 100%; width: auto; display: block; }
    .filmstrip-item { position: relative; }
    .zoom-tag { font-size: 11px; color: rgba(255,255,255,0.5); cursor: pointer; transition: color 0.15s; }
    .zoom-tag:hover { color: #fff; }
    /* Editor mode */
    .editor-selected { outline: 3px solid #ffe600; outline-offset: -3px; }
    .editor-check { position: absolute; top: 6px; right: 6px; width: 22px; height: 22px;
      background: #ffe600; color: #111; border-radius: 50%; display: flex;
      align-items: center; justify-content: center; font-size: 11px; font-weight: bold;
      z-index: 10; pointer-events: none; }
    /* Page content */
    /* ── RECENT WORK STACK VIEW ─────────────────────────────────────────── */
    #view-recent.active { padding-top: 20px; }
    .recent-row { margin-bottom: 8px; }
    .recent-row-header {
      display: flex; align-items: baseline; justify-content: flex-start; gap: 16px;
      padding: 0 20px; margin-bottom: -20px;
      cursor: pointer;
    }
    .recent-row-title {
      font-size: 13px; font-weight: 400; letter-spacing: 0.04em;
      color: var(--t-text); opacity: 0;
      transform: translateY(6px);
      transition: opacity 0.35s ease, transform 0.35s ease;
      background: color-mix(in srgb, var(--t-nav-bg) 15%, transparent); padding: 3px 10px;
    }
    .recent-row-title.visible {
      opacity: 1;
      transform: translateY(0);
    }
    .recent-row-count { display: none; }
    .recent-row:hover .recent-row-title { opacity: 1; transform: translateY(0); }
    .recent-row:hover .recent-row-count { opacity: 1; }
    .recent-filmstrip {
      overflow-x: auto; overflow-y: hidden;
      display: flex; align-items: center; gap: 60px;
      padding: 0 0 8px 60px;
      height: 300px;
      scrollbar-width: thin; scrollbar-color: #ddd transparent;
      mask-image: linear-gradient(to right, rgba(0,0,0,0.75) 0%, black 3%, black 97%, rgba(0,0,0,0.75) 100%);
      -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,0.75) 0%, black 3%, black 97%, rgba(0,0,0,0.75) 100%);
    }
    .recent-filmstrip::-webkit-scrollbar { height: 3px; }
    .recent-filmstrip::-webkit-scrollbar-track { background: transparent; }
    .recent-filmstrip::-webkit-scrollbar-thumb { background: #ddd; }
    .recent-thumb {
      flex-shrink: 0; cursor: pointer;
      background: #f0f0f0; overflow: hidden;
    }
    .recent-thumb:last-child { margin-right: 60px; }
    .recent-thumb img { height: 100%; width: 100%; object-fit: cover; display: block; }

    /* ── ARCHIVE VIEW ───────────────────────────────────────────────────────── */
    #view-clientwork.active { padding-top: 0; overflow-x: hidden; }
    .archive-search-bar {
      position: sticky; top: 44px; z-index: 100;
      background: var(--t-bg);
      padding: 14px 20px;
      border-bottom: 1px solid #f0f0f0;
    }
    .archive-search-input {
      width: 100%; box-sizing: border-box;
      border: none; border-bottom: 1px solid #ddd;
      background: transparent; outline: none;
      font-family: inherit; font-size: 14px; font-weight: 400;
      letter-spacing: 0.02em; color: var(--t-text);
      padding: 4px 0;
    }
    .archive-search-input::placeholder { color: #bbb; }
    .cw-client-section { margin-bottom: 0; }
    .cw-client-header {
      padding: 20px 20px 8px;
      font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
      color: #aaa;
    }
    .cw-section-label {
      padding: 32px 20px 8px;
      font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
      color: #ccc; border-top: 1px solid #f0f0f0;
    }
    .cw-section-label:first-child { border-top: none; padding-top: 20px; }
    .cw-row { border-bottom: 1px solid #f0f0f0; }
    .cw-row-header {
      display: flex; align-items: baseline; gap: 16px;
      padding: 7px 20px; cursor: pointer; width: 100%;
    }
    .cw-row-header:hover .cw-row-title { opacity: 0.5; }
    .cw-row-title { font-size: 14px; font-weight: 400; color: var(--t-text); flex: 1; transition: opacity 0.15s; }
    .cw-row-count { font-size: 11px; color: #bbb; flex-shrink: 0; }
    .cw-row.open { background: var(--t-accent-tint); }
    .cw-expand { overflow: hidden; max-height: 0; transition: max-height 0.35s ease; }
    .cw-expand.open { max-height: 500px; }
    .cw-filmstrip {
      overflow-x: auto; overflow-y: hidden;
      display: flex; align-items: stretch; gap: 16px;
      padding: 16px 20px 20px;
      height: 240px;
      scrollbar-width: thin; scrollbar-color: #ddd transparent;
      mask-image: linear-gradient(to right, rgba(0,0,0,0.75) 0%, black 3%, black 97%, rgba(0,0,0,0.75) 100%);
      -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,0.75) 0%, black 3%, black 97%, rgba(0,0,0,0.75) 100%);
    }
    .cw-filmstrip::-webkit-scrollbar { height: 3px; }
    .cw-filmstrip::-webkit-scrollbar-thumb { background: #ddd; }
    .cw-thumb { flex-shrink: 0; cursor: pointer; background: #f0f0f0; overflow: hidden; }
    .cw-thumb img { height: 100%; width: 100%; object-fit: cover; display: block; }
    .idx-tags-section { padding: 32px 20px 48px; border-top: 1px solid #f0f0f0; }
    .idx-tags-label { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #ccc; margin-bottom: 16px; }
    .idx-tags-list { display: flex; flex-wrap: wrap; gap: 8px 12px; }
    .idx-tag-btn { font-size: 13px; color: #888; background: none; border: none; cursor: pointer; padding: 0; transition: color 0.15s; }
    .idx-tag-btn:hover { color: var(--t-text); }
    #tags-filter-bar {
      padding: 16px 28px 8px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
    }
    .tfb-tag {
      font-size: 12px; padding: 4px 10px; cursor: pointer; border: 1px solid #ddd;
      background: none; font-family: inherit; letter-spacing: 0.03em; transition: all 0.15s;
      color: #888;
    }
    .tfb-tag:hover { color: var(--t-text); border-color: #aaa; }
    .tfb-tag.active {
      background: color-mix(in srgb, var(--t-nav-bg) 10%, transparent); border-color: color-mix(in srgb, var(--t-nav-bg) 40%, transparent);
      color: var(--t-text); font-weight: 500;
    }
    .tfb-tag.active::after { content: ' ×'; opacity: 0.6; }
    .tb-tag {
      font-size: 15px; font-weight: 300; color: #bbb; cursor: pointer;
      background: none; border: none; padding: 0; font-family: inherit;
      letter-spacing: 0.01em; transition: color 0.2s, opacity 0.2s; line-height: 1.1;
    }
    .tb-tag:hover { color: var(--t-text); }
    .tb-tag.active { color: #111; background: var(--t-nav-bg); font-weight: 400; padding: 2px 12px 4px; border-radius: 2px; }
    .tb-tag.active::after { content: ' ×'; opacity: 0.5; font-weight: 300; font-size: 18px; }
    .tb-tag.faded { color: #e8e8e8; cursor: pointer; }
    .tb-tag.faded:hover { color: #ccc; }
    #tagbrowser-tags.filtering .tb-tag:not(.active):not(.faded) { color: #111; font-weight: 400; }
    #tagbrowser-meta { font-size: 11px; color: #bbb; padding: 0 40px 16px; width: 75vw; max-width: 1400px; margin: 0 auto; min-height: 20px; letter-spacing: 0.04em; }
    #tagbrowser-results { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 4px; padding: 0 20px 80px; max-width: 1400px; margin: 0 auto; }
    #tb-mode-toggle { display: flex; gap: 24px; padding: 18px 40px 0; width: 75vw; max-width: 1400px; margin: 0 auto; }
    .tb-mode-btn { background: none; border: none; border-bottom: 1px solid transparent; font-family: inherit; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: #ccc; cursor: pointer; padding: 0 0 6px; transition: color 0.15s, border-color 0.15s; }
    .tb-mode-btn:hover { color: #888; }
    .tb-mode-btn.active { color: #111; border-bottom-color: #111; }
    #tb-archive-panel { padding: 24px 40px 80px; width: 75vw; max-width: 1400px; margin: 0 auto; box-sizing: border-box; }
    #tb-archive-panel .cw-row-header { padding-left: 0; padding-right: 0; }
    #tb-archive-panel .cw-client-header { padding-left: 0; padding-right: 0; }
    #tb-archive-panel .cw-section-label { padding-left: 0; padding-right: 0; }
    #tb-archive-panel .cw-filmstrip { padding-left: 0; padding-right: 0; }

    /* Story blocks */
    .story-quote { border-left: 3px solid #ccc; padding: 8px 0 8px 20px; font-style: italic; color: #555; margin: 16px 0 32px; }
    .story-caption { font-size: 12px; color: #aaa; margin-top: 4px; margin-bottom: 24px; }
    .story-divider { border: none; border-top: 1px solid #ddd; margin: 24px 0; }
    .story-spacer-sm { height: 16px; }
    .story-spacer-md { height: 40px; }
    .story-spacer-lg { height: 80px; }

    .page-content-wrap {
      max-width: 680px; margin: 0 auto; padding: 48px 28px;
      font-size: 15px; line-height: 1.8; color: #444;
    }
    .page-content-wrap figure { margin: 24px 0; }
    .page-content-wrap figcaption { font-size: 12px; color: #aaa; margin-top: 6px; }
    .page-content-wrap img { max-width: 100%; height: auto; }
    .page-content-wrap p { margin: 0 0 1em; }
    /* Blog */
    .post-list-grid.grid-mode { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px,1fr)); gap: 1px; padding: 1px; }
    .post-list-grid.list-mode { display: flex; flex-direction: column; width: 100%; margin: 0; padding: 24px 20px; gap: 0; }
    .post-list-item { border-bottom: 1px solid #e5e5e5; }
    .post-list-row { display: flex; align-items: center; gap: 16px; padding: 16px 0; cursor: pointer; width: 100%; }
    .post-list-row:hover .post-list-title { text-decoration: underline; }
    .post-list-title { font-size: 15px; font-weight: 600; color: #111; flex: 1; transition: none; letter-spacing: 0; }
    .post-list-date { font-size: 11px; color: #bbb; flex-shrink: 0; }
    .post-list-chevron { flex-shrink: 0; opacity: 0.5; transition: transform 0.2s ease; }
    .post-list-item.open .post-list-chevron { transform: rotate(180deg); }
    .post-list-expand { overflow: hidden; max-height: 0; transition: max-height 0.2s ease-out; }
    .post-list-expand.open { max-height: 4000px; transition: max-height 0.35s ease-in; }
    .post-list-expand-inner { padding: 0 0 16px; font-size: 14px; line-height: 1.75; color: #555; max-width: 600px; }
    .post-card { background: #f0f0f0; cursor: pointer; position: relative; overflow: hidden; }
    .post-card-img { width: 100%; aspect-ratio: 3/2; object-fit: cover; display: block; transition: transform 0.4s ease; }
    .post-card:hover .post-card-img { transform: scale(1.03); }
    .post-card-placeholder { width: 100%; aspect-ratio: 3/2; background: #e8e8e8; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #bbb; }
    .post-card-body { padding: 16px 18px 18px; background: #fff; }
    .post-card-title { font-size: 15px; font-weight: 600; color: #111; margin-bottom: 4px; }
    .post-card-meta { font-size: 11px; color: #bbb; }
    /* Individual post */
    .post-view-wrap { max-width: 720px; margin: 0 auto; padding: 40px 28px 80px; }
    .post-view-date { font-size: 12px; color: #bbb; margin-bottom: 28px; }
    .post-view-tags { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 28px; }
    .post-view-tag { font-size: 11px; color: #aaa; cursor: pointer; transition: color 0.15s; }
    .post-view-tag:hover { color: #111; }
    /* Content blocks */
    .post-block-text { font-size: 15px; line-height: 1.8; color: #333; margin-bottom: 28px; }
    .post-block-text p { margin: 0 0 1em; }
    .post-block-image { margin-bottom: 28px; }
    .post-block-image img { max-width: 100%; display: block; cursor: pointer; }
    .post-block-image figcaption { font-size: 12px; color: #aaa; margin-top: 6px; }
    .post-block-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 3px; margin-bottom: 28px; }
    .post-block-gallery img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; cursor: pointer; }

    /* ── TOP BAR ────────────────────────────────────────────────────────── */
    #topbar {
      position: fixed; top: 0; left: 0; right: 0;
      height: 44px; background: var(--t-accent-tint);
      display: grid; grid-template-columns: 1fr auto 1fr;
      align-items: center; padding: 0 20px; z-index: 300;
    }
    #topbar-center {
      display: flex; justify-content: center;
    }
    #topbar-left {
      display: flex; align-items: center; gap: 0;
      user-select: none; color: var(--t-text);
    }
    #topbar-site-name {
      font-weight: 700; font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase;
      cursor: pointer;
    }
    #topbar-sep {
      font-weight: 400; font-size: 13px; letter-spacing: 0.02em; margin: 0 4px;
    }
    #topbar-label {
      font-weight: 400; font-size: 13px; letter-spacing: 0.04em;
      background: var(--t-nav-bg); padding: 3px 10px; transition: background 0.2s;
      cursor: pointer;
    }
    #topbar-label-chevron.open { transform: rotate(180deg); }
    #topbar-right {
      display: flex; align-items: center; gap: 12px; justify-content: flex-end;
    }
    #topbar-search-btn {
      background: none; border: none; cursor: pointer; padding: 4px 10px;
      color: #aaa; transition: color 0.15s; display: flex; align-items: center; gap: 5px;
      font-family: inherit; font-size: 13px; letter-spacing: 0.04em;
    }
    #topbar-search-btn:hover { color: #111; }
    #topbar-search-cycle { font-size: 12px; transition: opacity 0.4s; min-width: 80px; text-align: left; }
    .topbar-divider { color: #ddd; font-size: 13px; font-weight: 300; user-select: none; }
    #gallery-view-toggle { display: none; align-items: center; gap: 8px; }
    #topbar-editor-btn {
      background: none; border: none; cursor: pointer; padding: 0;
      font-size: 13px; font-weight: 400; letter-spacing: 0.04em;
      color: #aaa; transition: color 0.15s; font-family: inherit;
    }
    #topbar-editor-btn:hover { color: #111; }
    #topbar-editor-btn.active { color: #ffe600; }

    /* ── NAV DROPDOWN ───────────────────────────────────────────────────── */
    #nav-dropdown {
      display: none;
      position: fixed; top: 44px; left: 0;
      background: var(--t-nav-bg);
      z-index: 301;
      padding: 12px 16px 16px;
      min-width: 220px;
      max-height: calc(100vh - 60px);
      overflow-y: auto;
    }
    #nav-dropdown.open { display: block; }
    .nav-dropdown-item {
      display: block; width: 100%; text-align: left;
      background: none; border: none; cursor: pointer;
      font-size: 13px; font-weight: 400; letter-spacing: 0.04em;
      color: var(--t-text); padding: 6px 0; line-height: 1.3;
      font-family: inherit; white-space: nowrap;
      transition: opacity 0.15s;
    }
    .nav-dropdown-item:hover { opacity: 0.6; }
    .nav-dropdown-item.current { font-weight: 400; }
    .nav-dropdown-divider {
      display: block; border: none; margin: 6px 0;
      font-size: 13px; font-weight: 700; letter-spacing: 0.04em;
      color: #111; cursor: default; pointer-events: none;
      padding: 0;
    }

    /* ── BOTTOM BAR ─────────────────────────────────────────────────────── */
    #bottombar {
      position: fixed; bottom: 0; left: 0; right: 0;
      height: 44px; background: var(--t-accent-tint);
      display: flex; align-items: center; justify-content: space-between;
      padding: 0 20px; z-index: 300;
    }
    #bottombar-left { flex: 1; }
    #bottombar-center { flex: 1; text-align: center; }
    #bottombar-right {
      flex: 1; text-align: right;
      font-size: 13px; letter-spacing: 0.04em; font-weight: 400;
      color: var(--t-text);
    }
    .bottombar-link {
      background: none; border: none; cursor: pointer;
      font-size: 13px; letter-spacing: 0.04em; font-weight: 400;
      color: var(--t-text); transition: opacity 0.15s; font-family: inherit; padding: 0;
    }
    .bottombar-link:hover { opacity: 0.5; }
    @media (min-width: 769px) {
      #bottombar-handle { display: none; }
      /* Zoom: desktop uses column layout with metadata below image, no info button */
      #zoom { flex-direction: column; }
      #zoom-img-wrap img { max-height: 75vh; }
      #zoom-info-btn { display: none !important; }
      #zoom-info {
        position: static !important;
        max-height: none !important;
        overflow: visible !important;
        transition: none !important;
        background: none !important;
      }
      #zoom-info > div {
        background: none !important;
        padding: 0 !important;
        text-align: center;
      }
      #zoom-gallery-label { color: #bbb !important; margin-top: 20px; margin-bottom: 0 !important; }
      #zoom-title { color: #888 !important; margin-bottom: 0 !important; }
      #zoom-caption { color: #888 !important; }
      #zoom-tags { justify-content: center; }
      .zoom-tag { color: #aaa !important; }
      .zoom-tag:hover { color: #111 !important; }
    }

    /* ── MAIN CONTENT ───────────────────────────────────────────────────── */
    #main-content {
      margin-top: 44px;
      margin-bottom: 44px;
    }

    /* Editor mode: bottom bar shifts up above the editor dl bar */
    body.editor-mode-on #bottombar {
      bottom: 56px;
    }
  </style>
</head>
<body class="bg-white text-[#111] min-h-screen" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">

<!-- TOP BAR -->
<header id="topbar">
  <div id="topbar-left" onclick="if(window.innerWidth<=768)toggleNavDropdown()">
    <span id="topbar-site-name" onclick="if(window.innerWidth>768){goHome();event.stopPropagation()}">Ian Allen</span>
    <span id="topbar-sep"> / </span>
    <span id="topbar-label" onclick="if(window.innerWidth>768){toggleNavDropdown();event.stopPropagation()}" style="display:inline-flex;align-items:center;gap:5px;">
      <span id="topbar-label-text">Recent Work</span>
      <svg id="topbar-label-chevron" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.5;transition:transform 0.2s ease;flex-shrink:0;" class="desktop-only"><polyline points="6 9 12 15 18 9"/></svg>
    </span>
  </div>
  <div id="topbar-center">
    <button id="topbar-search-btn" title="Search (press /)">
      Search <span id="topbar-search-cycle"></span>
    </button>
  </div>
  <div id="topbar-right">
    <div id="gallery-view-toggle">
      <span class="topbar-divider">|</span>
      <button class="gv-btn" data-mode="grid">Grid</button>
      <span style="color:#ddd;font-size:13px;">/</span>
      <button class="gv-btn" data-mode="filmstrip">Film Strip</button>
      <span style="color:#ddd;font-size:13px;">/</span>
      <button class="gv-btn" data-mode="lightbox">Lightbox</button>
      <span class="topbar-divider">|</span>
    </div>
    <button id="topbar-editor-btn">Editor Mode</button>
  </div>
</header>

<!-- NAV DROPDOWN -->
<nav id="nav-dropdown" role="navigation" aria-label="Site navigation">
  <!-- populated by buildNavDropdown() after data loads -->
</nav>

<!-- EDITOR ACCESS MODAL -->
<div id="editor-modal" class="hidden fixed inset-0 z-[600] flex items-center justify-center" style="background:rgba(0,0,0,0.7)">
  <div class="w-full max-w-sm mx-4 p-8" style="background:#1a1a1a;border:1px solid #2a2a2a">
    <h3 class="text-white font-semibold text-base mb-2">Editor Access</h3>
    <p class="text-neutral-400 text-sm leading-relaxed mb-5">This mode lets you browse and select images to download for presentation or reference. Files are downloaded as a zip with photographer credit embedded in the filename.<br><br>To request access, contact <a href="/cdn-cgi/l/email-protection#f49d9a929bb49d959a959898919a849c9b809bda979b99" class="text-neutral-300 underline"><span class="__cf_email__" data-cfemail="a9c0c7cfc6e9c0c8c7c8c5c5ccc7d9c1c6ddc687cac6c4">[email&#160;protected]</span></a>.</p>
    <input id="editor-code-input" type="password" placeholder="Access code"
      class="w-full bg-[#111] border border-[#333] text-white text-sm px-3.5 py-2 outline-none focus:border-[#555] mb-1 placeholder:text-neutral-600"
      style="font-family:inherit" />
    <p id="editor-code-error" class="text-[11px] text-red-400 mb-3 h-4"></p>
    <div class="flex gap-2 justify-end">
      <button id="editor-modal-cancel" class="text-sm text-neutral-500 hover:text-white transition-colors bg-transparent border-none cursor-pointer px-3 py-2" style="font-family:inherit">Cancel</button>
      <button id="editor-modal-submit" class="text-sm font-semibold px-5 py-2 border-none cursor-pointer" style="background:#ffe600;color:#111;font-family:inherit">Enter</button>
    </div>
  </div>
</div>

<!-- SEARCH MODAL -->
<div id="search-modal" class="hidden fixed inset-0 z-[600] flex items-start justify-center pt-24" style="background:rgba(0,0,0,0.5)">
  <div class="w-full max-w-md mx-4 p-6" style="background:#fff;border:1px solid #e0e0e0">
    <div class="flex items-center gap-3 mb-1">
      <svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round"><circle cx="8.5" cy="8.5" r="5.5"/><line x1="13" y1="13" x2="18" y2="18"/></svg>
      <input id="search-input" type="text" placeholder="Search by tag or keyword…"
        class="flex-1 text-sm text-[#111] outline-none placeholder:text-[#bbb]"
        style="font-family:inherit;border:none;background:transparent" />
      <button id="search-clear" class="hidden text-xs text-neutral-400 hover:text-[#111] transition-colors bg-transparent border-none cursor-pointer" style="font-family:inherit">clear</button>
    </div>
    <div class="border-t border-[#e8e8e8] mt-3 pt-3 flex justify-between items-center">
      <span class="text-[11px] text-[#bbb]">Press Enter to search, Esc to close</span>
      <button id="search-modal-cancel" class="text-xs text-neutral-400 hover:text-[#111] transition-colors bg-transparent border-none cursor-pointer" style="font-family:inherit">Cancel</button>
    </div>
  </div>
</div>


<!-- MAIN CONTENT -->
<main id="main-content">

<!-- HOME: top-level folders + galleries -->
<div class="view active" id="view-home">
  <div class="text-center py-20 text-[#bbb] text-sm" id="home-loading">Loading…</div>
  <div class="grid gap-0.5 p-0.5" style="grid-template-columns:repeat(auto-fill,minmax(260px,1fr))" id="home-grid"></div>
  <div id="home-story" style="display:none;max-width:860px;margin:0 auto;padding:0 28px;"></div>
</div>

<!-- RECENT WORK: stacked horizontal filmstrips -->
<div class="view" id="view-recent"></div>

<!-- CLIENT WORK: accordion list -->
<div class="view" id="view-clientwork"></div>

<!-- FOLDER: galleries inside a folder -->
<div class="view" id="view-folder">
  <div class="grid gap-0.5 p-0.5" style="grid-template-columns:repeat(auto-fill,minmax(260px,1fr))" id="folder-grid"></div>
</div>

<!-- GALLERY: photo thumbnails -->
<div class="view" id="view-gallery">
  <div class="text-center py-20 text-[#444] text-sm" id="gallery-loading" style="display:none">Loading…</div>
  <div id="photo-grid"></div>
</div>

<!-- TAG BROWSER -->
<div class="view" id="view-tagbrowser">
  <div id="tb-mode-toggle">
    <button class="tb-mode-btn active" id="tb-mode-tags">Tags</button>
    <button class="tb-mode-btn" id="tb-mode-archive">Archive</button>
  </div>
  <!-- Tags panel -->
  <div id="tb-tags-panel">
    <div id="tagbrowser-search-wrap" style="padding:24px 40px 16px;width:75vw;max-width:1400px;margin:0 auto;">
      <div style="display:flex;align-items:center;gap:12px;border-bottom:1px solid #e0e0e0;padding-bottom:14px;">
        <svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="#999" stroke-width="2.2" stroke-linecap="round"><circle cx="8.5" cy="8.5" r="5.5"/><line x1="13" y1="13" x2="18" y2="18"/></svg>
        <input id="tagbrowser-input" type="text" placeholder="" style="flex:1;border:none;outline:none;font-size:17px;font-family:inherit;background:transparent;color:#111;" />
      </div>
      <div style="font-size:11px;color:#ccc;margin-top:8px;">Press Enter to search</div>
    </div>
    <div id="tagbrowser-tags" style="padding:0 40px 24px;width:75vw;max-width:1400px;margin:0 auto;display:flex;flex-wrap:wrap;gap:8px 14px;align-items:baseline;"></div>
    <div id="tagbrowser-meta"></div>
    <div id="tagbrowser-results"></div>
  </div>
  <!-- Archive panel -->
  <div id="tb-archive-panel" style="display:none;"></div>
</div>

<!-- TAG RESULTS -->
<div class="view" id="view-tags">
  <div id="tags-filter-bar"></div>
  <div class="px-7 pt-2 pb-3 text-xs text-neutral-400" id="tags-meta"></div>
  <div id="tags-grid"></div>
</div>

<!-- PAGE -->
<div class="view" id="view-page">
  <div id="page-content" class="page-content-wrap"></div>
</div>

<!-- BLOG LIST -->
<div class="view" id="view-blog">
  <div style="display:flex;align-items:center;justify-content:flex-end;padding:16px 24px 0;gap:6px;">
    <button id="blog-view-list-btn" style="font-size:13px;font-weight:400;letter-spacing:0.04em;text-transform:uppercase;background:none;border:none;cursor:pointer;color:#111;border-bottom:2px solid #111;padding-bottom:1px;">List</button>
    <span style="color:#ccc;font-size:13px;">/</span>
    <button id="blog-view-grid-btn" style="font-size:13px;font-weight:400;letter-spacing:0.04em;text-transform:uppercase;background:none;border:none;cursor:pointer;color:#bbb;padding-bottom:1px;">Grid</button>
  </div>
  <div id="post-list-grid"></div>
</div>

<!-- BLOG POST -->
<div class="view" id="view-post">
  <div class="post-view-wrap">
    <button onclick="openBlog()" style="background:none;border:none;cursor:pointer;font-size:13px;color:#bbb;padding:0;margin-bottom:24px;display:block;font-family:inherit;">← Back to List</button>
    <div id="post-view-content"></div>
    <div id="post-view-nav" style="display:flex;justify-content:space-between;margin-top:48px;padding-top:20px;border-top:1px solid #f0f0f0;"></div>
  </div>
</div>

</main>

<!-- ZOOM -->
<div id="zoom" class="fixed inset-0 z-[500] flex items-center justify-center" style="background:#f4ff61;backdrop-filter:blur(2px)">
  <button id="zoom-close" style="position:absolute;top:12px;right:12px;width:44px;height:44px;background:rgba(0,0,0,0.12);border:none;border-radius:50%;cursor:pointer;font-size:26px;color:#555;line-height:1;padding:0;display:flex;align-items:center;justify-content:center;z-index:2;" title="Close">×</button>
  <div style="position:absolute;top:12px;left:12px;z-index:2;" onclick="event.stopPropagation()">
    <label for="zoom-bg-picker" id="zoom-bg-label" title="Change background color" style="width:44px;height:44px;background:rgba(0,0,0,0.12);border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><g fill="none" stroke="#555" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10"/><path d="M12 16a4 4 0 1 1 0-8a4 4 0 0 1 0 8m0-14v6m0 8v6M2 12h6m8 0h6M4.929 4.929L9.172 9.17m5.656 5.659l4.243 4.242m-14.142 0l4.243-4.242m5.656-5.658l4.243-4.242"/></g></svg></label>
    <input type="color" id="zoom-bg-picker" value="#f4ff61" style="position:absolute;width:44px;height:44px;opacity:0;top:0;left:0;cursor:pointer;" />
  </div>
  <button style="display:none" id="zoom-prev">‹</button>
  <div id="zoom-img-wrap">
    <img id="zoom-img" src="" alt="" />
  </div>
  <button style="display:none" id="zoom-next">›</button>
  <!-- Info button -->
  <button id="zoom-info-btn" style="position:absolute;bottom:16px;right:16px;width:44px;height:44px;background:rgba(0,0,0,0.12);border:none;border-radius:50%;cursor:pointer;font-size:17px;font-style:italic;font-family:Georgia,serif;color:#555;line-height:1;padding:0;display:flex;align-items:center;justify-content:center;z-index:2;" title="Info">i</button>
  <!-- Info overlay -->
  <div id="zoom-info" style="position:absolute;bottom:0;left:0;right:0;max-height:0;overflow:hidden;transition:max-height 0.3s ease;z-index:1;" onclick="event.stopPropagation()">
    <div style="padding:24px 24px 36px;background:rgba(0,0,0,0.88);">
      <div id="zoom-gallery-label" style="font-size:11px;color:rgba(255,255,255,0.4);letter-spacing:0.06em;text-transform:uppercase;cursor:pointer;margin-bottom:6px;"></div>
      <div id="zoom-title" style="font-size:15px;color:#fff;text-decoration:underline;text-underline-offset:3px;margin-bottom:4px;"></div>
      <div id="zoom-caption" style="font-size:13px;color:rgba(255,255,255,0.65);margin-bottom:10px;"></div>
      <div id="zoom-tags" style="display:flex;flex-wrap:wrap;gap:6px;"></div>
    </div>
  </div>
</div>
<div id="zoom-cursor">
  <svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
    <polygon id="zoom-cursor-arrow" points="14,4 24,22 4,22" fill="var(--t-cursor)"/>
  </svg>
</div>

<!-- EDITOR BAR -->
<div id="editor-dl-bar" class="hidden fixed bottom-0 left-0 right-0 z-[400] items-center gap-4 px-7 py-3.5 border-t border-[#333]" style="background:#111">
  <div class="flex items-center gap-4 flex-1">
    <span class="text-xs font-semibold tracking-widest uppercase" style="color:#ffe600">Editor</span>
    <button id="editor-clear-btn" class="text-xs text-neutral-500 hover:text-white transition-colors bg-transparent border-none cursor-pointer underline" style="font-family:inherit">Clear</button>
  </div>
  <span id="editor-dl-count" class="text-sm text-neutral-400 flex-1 text-center">Click images to select</span>
  <div class="flex items-center gap-3 flex-1 justify-end">
    <button id="editor-dl-btn" class="font-semibold text-sm px-5 py-2 border-none cursor-pointer disabled:opacity-40" style="background:#ffe600;color:#111;font-family:inherit">Download (0)</button>
    <button id="editor-exit-btn" class="text-xs text-neutral-500 hover:text-white transition-colors bg-transparent border-none cursor-pointer" style="font-family:inherit">Exit Editor Mode</button>
  </div>
</div>

<!-- BOTTOM BAR -->
<footer id="bottombar">
  <div id="bottombar-handle" onclick="const b=document.getElementById('bottombar');const open=b.getAttribute('data-open')==='1';b.setAttribute('data-open',open?'0':'1');b.style.height=open?'':'auto';b.style.maxHeight=open?'':'900px';this.style.transform=open?'':'rotate(180deg)'">⌃</div>
  <div id="bottombar-left">
    <button id="bottombar-about-btn" class="bottombar-link">About / Contact</button>
  </div>
  <div id="bottombar-center">
    <button id="bottombar-blog-btn" class="bottombar-link">News</button>
  </div>
  <div id="bottombar-right">
    © <span id="copyright-year"></span> Ian Allen
  </div>
</footer>

<!-- Compatibility stubs: applySettings() references these IDs -->
<img id="site-logo" src="" alt="" style="display:none;position:absolute;pointer-events:none;width:0;height:0;" />
<div id="site-name" style="display:none;position:absolute;pointer-events:none;width:0;height:0;"></div>

<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script>
  document.getElementById('copyright-year').textContent = new Date().getFullYear();

  const _cfg = window.PORTFOLIO_CONFIG || {};
  const SUPABASE_URL  = _cfg.supabaseUrl  || 'https://qlbcgpbfpkkumvznvejs.supabase.co';
  const SUPABASE_ANON = _cfg.supabaseAnon || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFsYmNncGJmcGtrdW12em52ZWpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1Mjk1ODUsImV4cCI6MjA4ODEwNTU4NX0.BzIHuf1Anb16cmwx7cqXMwEXO3R3kuzVDNjsVWP0wVE';
  const WORKER_URL    = _cfg.workerUrl    || 'https://portfolio-worker.long-sun-9086.workers.dev';
  const sb = supabase.createClient(SUPABASE_URL, SUPABASE_ANON);

  const EDITOR_CODE = 'ianphoto'; // change this to your preferred passcode

  let allGalleries  = [];
  let allPages      = [];
  let allPosts      = [];
  let currentImages = [];
  let currentGallery = null;
  let currentGalleryMode = 'grid';
  let _inCloseZoom = false;
  let _inRenderGalleryMode = false;
  let currentFolder  = null;
  let zoomIndex      = 0;
  let activeTag      = null;
  let activeTags     = new Set();
  let tagReturnGallery = null; // gallery to return to when all tags cleared
  let tagReturnImgId  = null;  // image id to reopen zoom on return
  let editorMode     = false;
  const editorSelected = new Map(); // id → img
  let introMode       = 'story';
  let introGridSource = 'recent-work';
  let introTagSeed    = '';
  let navOrder        = [];
  let introBlocks     = [];
  let introStyle      = {};
  let currentLabel = 'Recent Work'; // tracks current breadcrumb label
  let recentWorkIds = [];
  let clientWorkIds = [];
  let hiddenNavIds  = [];

  // ── BREADCRUMB / TOP BAR LABEL ────────────────────────────────────────────
  function setTopbarLabel(label) {
    currentLabel = label || '';
    const textEl = document.getElementById('topbar-label-text') || document.getElementById('topbar-label');
    textEl.textContent = currentLabel;
    const inSearch = currentLabel.startsWith('Search') || currentLabel.startsWith('Archive');
    document.getElementById('topbar-search-btn').style.display = inSearch ? 'none' : '';
  }

  // Phone detection: smallest screen dimension ≤ 430px (iPhone max; iPads start at 744px)
  const isPhone = () => Math.min(window.screen.width, window.screen.height) <= 430;

  // ── NAV DROPDOWN ──────────────────────────────────────────────────────────
  let navDropdownOpen = false;

  function toggleNavDropdown() {
    navDropdownOpen ? closeNavDropdown() : openNavDropdown();
  }

  function openNavDropdown() {
    navDropdownOpen = true;
    const label    = document.getElementById('topbar-label');
    const dropdown = document.getElementById('nav-dropdown');
    const rect     = label.getBoundingClientRect();
    dropdown.style.left = (rect.left - 16) + 'px';
    dropdown.scrollTop = 0;
    dropdown.classList.add('open');
    document.getElementById('topbar-label-chevron')?.classList.add('open');
  }

  function closeNavDropdown() {
    navDropdownOpen = false;
    document.getElementById('nav-dropdown').classList.remove('open');
    document.getElementById('topbar-label-chevron')?.classList.remove('open');
  }

  // Close dropdown when clicking outside
  document.addEventListener('click', e => {
    if (!navDropdownOpen) return;
    const topbarLeft = document.getElementById('topbar-left');
    const dropdown   = document.getElementById('nav-dropdown');
    if (!topbarLeft.contains(e.target) && !dropdown.contains(e.target)) {
      closeNavDropdown();
    }
  });

  // Close dropdown on Escape (handled globally below alongside other Esc handlers)

  function buildNavDropdown() {
    const dropdown = document.getElementById('nav-dropdown');
    dropdown.innerHTML = '';

    const isNavHidden = id => hiddenNavIds.includes(String(id));

    function makeNavItem(label, onClick, isCurrent) {
      const btn = document.createElement('button');
      btn.className = 'nav-dropdown-item' + (isCurrent ? ' current' : '');
      btn.textContent = label;
      btn.addEventListener('click', () => { closeNavDropdown(); onClick(); });
      return btn;
    }
    function makeDivider() {
      const span = document.createElement('span');
      span.className = 'nav-dropdown-divider';
      span.textContent = '————————————';
      return span;
    }

    // Build ordered list of all eligible nav items
    const topGalleries = allGalleries.filter(g => g.parent_id === null && g.type !== 'archive' && g.type !== 'client');
    const allPossibleIds = [
      'recent-work',
      ...topGalleries.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)).map(g => String(g.id)),
      ...allPages.map(pg => 'page:' + pg.id),
      'search'
    ];

    let ordered;
    if (navOrder && navOrder.length) {
      const seen = new Set();
      ordered = [];
      navOrder.forEach(id => { if (id.startsWith('spacer:') || allPossibleIds.includes(id)) { ordered.push(id); seen.add(id); } });
      allPossibleIds.forEach(id => { if (!seen.has(id)) ordered.push(id); });
    } else {
      ordered = allPossibleIds;
    }

    ordered.forEach(id => {
      if (id.startsWith('spacer:')) {
        dropdown.appendChild(makeDivider());

      } else if (isNavHidden(id)) {
        return;

      } else if (id === 'recent-work') {
        const isCurrent = document.getElementById('view-recent')?.classList.contains('active');
        dropdown.appendChild(makeNavItem('Recent Work', openRecentWork, isCurrent));

      } else if (id === 'search') {
        const isCurrent = document.getElementById('view-tagbrowser')?.classList.contains('active');
        dropdown.appendChild(makeNavItem('Search', openTagBrowser, isCurrent));

      } else if (id.startsWith('page:')) {
        const pgId = id.slice(5);
        const pg = allPages.find(p => String(p.id) === pgId);
        if (!pg) return;
        dropdown.appendChild(makeNavItem(pg.title || pg.slug, () => openPage && openPage(pg), false));

      } else {
        const g = allGalleries.find(x => String(x.id) === id);
        if (!g) return;
        if (isFolder(g)) {
          const kids = childrenOf(g.id).filter(child => !isNavHidden(child.id));
          if (!kids.length) return;
          kids.forEach(child => {
            const isCurrent = currentGallery && currentGallery.id === child.id;
            dropdown.appendChild(makeNavItem(child.name, () => openGallery(child), isCurrent));
          });
        } else {
          const isCurrent = currentGallery && currentGallery.id === g.id;
          dropdown.appendChild(makeNavItem(g.name, () => openGallery(g), isCurrent));
        }
      }
    });
  }

  // ── BOTTOM BAR WIRING ─────────────────────────────────────────────────────
  function wireBottomBar() {
    const aboutBtn = document.getElementById('bottombar-about-btn');
    const blogBtn  = document.getElementById('bottombar-blog-btn');

    // Wire About/Contact: look for a page whose slug contains "about"
    const aboutPage = allPages.find(pg => pg.slug && pg.slug.toLowerCase().includes('about'));
    if (aboutPage) {
      aboutBtn.addEventListener('click', () => openPage(aboutPage));
    } else {
      // No about page — make it non-interactive
      aboutBtn.style.cursor = 'default';
      aboutBtn.style.color  = '#ccc';
    }

    // Wire Blog
    if (allPosts.length) {
      blogBtn.addEventListener('click', openBlog);
    } else {
      blogBtn.style.cursor = 'default';
      blogBtn.style.color  = '#ccc';
    }
  }

  // ── EDITOR SELECTION ──────────────────────────────────────────────────────
  function toggleEditorSelect(img, el) {
    if (editorSelected.has(img.id)) {
      editorSelected.delete(img.id);
      el.classList.remove('editor-selected');
      el.querySelector('.editor-check')?.remove();
    } else {
      editorSelected.set(img.id, img);
      el.classList.add('editor-selected');
      const check = document.createElement('div');
      check.className = 'editor-check';
      check.textContent = '✓';
      el.appendChild(check);
    }
    updateEditorBar();
  }

  function updateEditorBar() {
    const n = editorSelected.size;
    document.getElementById('editor-dl-count').textContent =
      n === 0 ? 'Click images to select' : `${n} image${n !== 1 ? 's' : ''} selected`;
    document.getElementById('editor-dl-btn').textContent = `Download (${n})`;
    document.getElementById('editor-dl-btn').disabled = n === 0;
  }

  function showDownloadPanel(entries) {
    document.getElementById('dl-panel')?.remove();
    const panel = document.createElement('div');
    panel.id = 'dl-panel';
    panel.style.cssText = 'position:fixed;inset:0;z-index:600;background:rgba(0,0,0,0.85);overflow-y:auto;padding:40px 24px 100px;box-sizing:border-box;';
    const inner = document.createElement('div');
    inner.style.cssText = 'max-width:900px;margin:0 auto;';
    inner.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;">
      <div>
        <div style="color:#fff;font-size:18px;font-weight:600;margin-bottom:6px;">Save Your Selection</div>
        <div style="color:#aaa;font-size:13px;">Right-click (or long-press) each image → <strong style="color:#fff">Save Image As…</strong> to download at full resolution.</div>
      </div>
      <button id="dl-panel-close" style="background:none;border:1px solid #444;color:#aaa;cursor:pointer;font-size:13px;padding:8px 16px;font-family:inherit;">Close</button>
    </div>`;
    const grid = document.createElement('div');
    grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;';
    entries.forEach(img => {
      const cell = document.createElement('div');
      cell.style.cssText = 'background:#111;border-radius:2px;overflow:hidden;';
      const original = img.filename || img.url.split('/').pop().split('?')[0];
      const dot  = original.lastIndexOf('.');
      const name = dot > 0 ? original.slice(0, dot) : original;
      const ext  = dot > 0 ? original.slice(dot) : '.jpg';
      const saveName = `${name}_IanAllen${ext}`;
      cell.innerHTML = `<img src="${img.url}" style="width:100%;display:block;cursor:context-menu;" title="Right-click → Save Image As…" alt="${img.title || ''}">
        <div style="padding:8px 10px;font-size:11px;color:#666;word-break:break-all;">${saveName}</div>`;
      grid.appendChild(cell);
    });
    inner.appendChild(grid);
    panel.appendChild(inner);
    document.body.appendChild(panel);
    panel.querySelector('#dl-panel-close').addEventListener('click', () => panel.remove());
  }

  async function downloadSelected() {
    const btn = document.getElementById('editor-dl-btn');
    btn.disabled = true;
    const entries = [...editorSelected.values()];
    const galleryName = currentGallery ? currentGallery.name.replace(/[^a-zA-Z0-9]/g, '_') : 'Selection';

    const zip = new JSZip();
    let added = 0;
    for (let i = 0; i < entries.length; i++) {
      const img = entries[i];
      btn.textContent = `Fetching ${i + 1}/${entries.length}…`;
      try {
        const proxyUrl = `${WORKER_URL}/proxy?filename=${encodeURIComponent(img.url)}`;
        const resp = await fetch(proxyUrl, { mode: 'cors', credentials: 'omit' });
        if (!resp.ok) throw new Error(resp.status);
        const blob = await resp.blob();
        if (blob.size < 100) throw new Error('empty');
        const original = img.filename || img.url.split('/').pop().split('?')[0];
        const dot  = original.lastIndexOf('.');
        const name = dot > 0 ? original.slice(0, dot) : original;
        const ext  = dot > 0 ? original.slice(dot) : '.jpg';
        zip.file(`${name}_IanAllen${ext}`, blob);
        added++;
      } catch(e) { console.warn('CORS blocked, will use panel fallback', img.url); }
    }

    if (!added) {
      // CORS blocked — show save panel
      updateEditorBar();
      btn.disabled = false;
      showDownloadPanel(entries);
      return;
    }

    btn.textContent = 'Zipping…';
    const content = await zip.generateAsync({ type: 'blob' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(content);
    a.download = `IanAllen_${galleryName}.zip`;
    a.click();
    URL.revokeObjectURL(a.href);
    updateEditorBar();
    btn.disabled = false;
  }

  function activateEditorMode() {
    editorMode = true;
    document.getElementById('editor-dl-bar').classList.remove('hidden');
    document.getElementById('editor-dl-bar').style.display = 'flex';
    document.getElementById('topbar-editor-btn').classList.add('active');
    document.body.classList.add('editor-mode-on');
    document.body.style.paddingBottom = '64px';
  }

  // ── EDITOR ACCESS MODAL ───────────────────────────────────────────────────
  document.getElementById('topbar-editor-btn').addEventListener('click', () => {
    if (editorMode) return; // already active
    document.getElementById('editor-code-input').value = '';
    document.getElementById('editor-code-error').textContent = '';
    document.getElementById('editor-modal').style.display = 'flex';
  });
  document.getElementById('editor-modal-cancel').addEventListener('click', () => {
    document.getElementById('editor-modal').style.display = 'none';
  });
  function submitEditorCode() {
    const code = document.getElementById('editor-code-input').value;
    if (code === EDITOR_CODE) {
      sessionStorage.setItem('editorAuth', '1');
      document.getElementById('editor-modal').style.display = 'none';
      activateEditorMode();
    } else {
      document.getElementById('editor-code-error').textContent = 'Incorrect code — contact info@ianallenphoto.com';
      document.getElementById('editor-code-input').value = '';
    }
  }
  document.getElementById('editor-modal-submit').addEventListener('click', submitEditorCode);
  document.getElementById('editor-code-input').addEventListener('keydown', e => {
    if (e.key === 'Enter') submitEditorCode();
  });
  document.getElementById('editor-modal').addEventListener('click', e => {
    if (e.target === document.getElementById('editor-modal')) document.getElementById('editor-modal').style.display = 'none';
  });

  document.getElementById('editor-clear-btn').addEventListener('click', () => {
    editorSelected.clear();
    document.querySelectorAll('.editor-selected').forEach(el => {
      el.classList.remove('editor-selected');
      el.querySelector('.editor-check')?.remove();
    });
    updateEditorBar();
  });

  document.getElementById('editor-dl-btn').addEventListener('click', downloadSelected);

  document.getElementById('editor-exit-btn').addEventListener('click', () => {
    sessionStorage.removeItem('editorAuth');
    location.href = location.pathname + location.search.replace(/[?&]editor=1/, '').replace(/^&/, '?');
  });

  // ── SEARCH MODAL ──────────────────────────────────────────────────────────
  const searchInput = document.getElementById('search-input');
  const searchClear = document.getElementById('search-clear');
  const searchModal = document.getElementById('search-modal');

  let searchCycleTimer = null;
  function startSearchCycle() {
    clearInterval(searchCycleTimer);
    const tags = [...(window._allSiteTags || [])];
    if (!tags.length) return;
    const cycleEl = document.getElementById('topbar-search-cycle');
    let i = Math.floor(Math.random() * tags.length);
    function next() {
      if (cycleEl) cycleEl.textContent = '#' + tags[i % tags.length];
      searchInput.setAttribute('placeholder', '#' + tags[i % tags.length]);
      i++;
    }
    next();
    searchCycleTimer = setInterval(next, 2200);
  }

  function openSearchModal() {
    searchModal.classList.remove('hidden');
    searchModal.style.display = 'flex';
    searchInput.value = '';
    searchClear.classList.add('hidden');
    startSearchCycle();
    setTimeout(() => searchInput.focus(), 50);
  }
  function closeSearchModal() {
    clearInterval(searchCycleTimer);
    document.getElementById('topbar-search-cycle').textContent = '';
    searchModal.classList.add('hidden');
    searchModal.style.display = '';
  }

  document.getElementById('topbar-search-btn').addEventListener('click', openSearchModal);
  document.getElementById('search-modal-cancel').addEventListener('click', closeSearchModal);
  searchModal.addEventListener('click', e => { if (e.target === searchModal) closeSearchModal(); });

  searchInput.addEventListener('keydown', e => {
    if (e.key === 'Enter') {
      const val = searchInput.value.trim();
      if (val) { closeSearchModal(); smartSearch(val); }
    }
    if (e.key === 'Escape') closeSearchModal();
  });
  searchInput.addEventListener('input', () => {
    searchClear.classList.toggle('hidden', !searchInput.value);
  });
  searchClear.addEventListener('click', () => {
    searchInput.value = '';
    searchClear.classList.add('hidden');
  });

  // Global keyboard shortcuts
  document.addEventListener('keydown', e => {
    // Escape: close search, zoom, or nav dropdown
    if (e.key === 'Escape') {
      if (!searchModal.classList.contains('hidden')) { closeSearchModal(); return; }
      if (document.getElementById('zoom').classList.contains('open')) { closeZoom(); return; }
      if (navDropdownOpen) { closeNavDropdown(); return; }
    }
    // '/' opens search when not focused in an input
    if (e.key === '/' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
      e.preventDefault();
      openSearchModal();
    }
  });

  function performSearch(query) {
    textSearchToArchive(query.trim());
  }

  async function searchByTag(tag) {
    if (activeTags.has(tag)) return;
    if (!activeTags.size) tagReturnGallery = currentGallery;
    activeTags.add(tag);
    activeTag = tag;
    const inTagBrowser = document.getElementById('view-tagbrowser').classList.contains('active');
    const tagsTabActive = document.getElementById('tb-mode-tags')?.classList.contains('active');
    if (inTagBrowser && window._tbTagsLoaded) {
      if (!tagsTabActive) showTbMode('tags');
      await renderTagBrowserResults();
    } else {
      await openTagBrowser({ keepActiveTags: true });
    }
  }

  function updateTagCloudStates(relatedTags) {
    const container = document.getElementById('tagbrowser-tags');
    const isFiltering = relatedTags && relatedTags.size > 0;
    container.classList.toggle('filtering', isFiltering);
    document.querySelectorAll('.tb-tag').forEach(btn => {
      const tag = btn.dataset.tag;
      btn.classList.remove('active', 'faded');
      if (activeTags.has(tag)) {
        btn.classList.add('active');
      } else if (isFiltering && !relatedTags.has(tag)) {
        btn.classList.add('faded');
      }
    });
  }

  function clearTagBrowserResults() {
    document.getElementById('tagbrowser-meta').textContent = '';
    document.getElementById('tagbrowser-results').innerHTML = '';
    document.getElementById('tagbrowser-tags').classList.remove('filtering');
    updateTagCloudStates(null);
    startTagCycle(document.getElementById('tagbrowser-input'), window._tbAllTags || []);
  }

  async function renderTagBrowserResults() {
    const tags = [...activeTags];
    setTopbarLabel('Search · ' + tags.map(t => '#' + t).join(' '));
    setURL({ tags: tags.join(',') });
    clearInterval(tagCycleTimer);
    const _ti = document.getElementById('tagbrowser-input');
    if (_ti) _ti.setAttribute('placeholder', '#');
    clearSearchInputs();
    const meta = document.getElementById('tagbrowser-meta');
    const grid = document.getElementById('tagbrowser-results');
    meta.textContent = 'Searching…';
    grid.innerHTML = '';

    const { data } = await sb.from('images')
      .select('*')
      .contains('tags', tags)
      .not('hidden', 'is', true)
      .order('created_at', { ascending: false });

    const results = data || [];
    currentImages = results;
    currentGallery = null;

    const relatedTags = new Set();
    results.forEach(img => (img.tags || []).forEach(t => relatedTags.add(t)));
    updateTagCloudStates(relatedTags);

    meta.textContent = results.length ? `${results.length} image${results.length !== 1 ? 's' : ''}` : '';
    if (!results.length) {
      grid.innerHTML = '<div style="padding:48px 0;color:#ccc;font-size:14px;text-align:center;">No images found.</div>';
      return;
    }
    results.forEach((img, i) => {
      grid.appendChild(makeThumb(img, i, () => editorMode ? toggleEditorSelect(img, grid.children[i]) : openZoom(i)));
    });
    revealWhenLoaded(grid);
  }

  // ── INIT ──────────────────────────────────────────────────────────────────
  (async () => {
    await loadAll();
    const p = new URLSearchParams(location.search);
    const folderSlug  = p.get('folder');
    const gallerySlug = p.get('gallery');
    const tagParam    = p.get('tags') || p.get('tag');
    const imgId       = p.get('img');
    const pageSlug    = p.get('page');
    const blogParam   = p.get('blog');
    const postParam   = p.get('post');

    // editor mode
    if (p.get('editor') === '1') {
      const authed = sessionStorage.getItem('editorAuth') === '1';
      if (authed) {
        activateEditorMode();
      } else {
        const code = window.prompt('Enter editor access code:');
        if (code === EDITOR_CODE) {
          sessionStorage.setItem('editorAuth', '1');
          activateEditorMode();
        }
      }
    }

    if (blogParam) { openBlog(); return; }
    if (postParam) {
      const { data: postData } = await sb.from('posts').select('*').eq('slug', postParam).single();
      if (postData) { await openPost(postData); return; }
    }

    if (pageSlug === 'index' || pageSlug === 'client-work') { openIndex(); return; }
    if (pageSlug === 'search') { openTagBrowser(); return; }
    if (pageSlug) {
      const pg = allPages.find(pg => pg.slug === pageSlug);
      if (pg) { openPage(pg); return; }
    }
    if (tagParam) {
      const tagList = tagParam.split(',').map(t => t.trim()).filter(Boolean);
      for (const t of tagList) await searchByTag(t);
      return;
    }
    if (gallerySlug) {
      const g = allGalleries.find(g => g.slug === gallerySlug);
      if (g) { await openGallery(g, imgId); return; }
    }
    if (folderSlug) {
      const f = allGalleries.find(g => g.slug === folderSlug);
      if (f) { openFolder(f); return; }
    }
    // Default landing — delegate to goHome() which reads introMode/introGridSource/introTagSeed
    goHome();
  })();

  // Blend a hex color toward white at the given opacity (0–1)
  function hexToTint(hex, opacity) {
    const h = hex.replace('#', '');
    const r = parseInt(h.slice(0,2), 16);
    const g = parseInt(h.slice(2,4), 16);
    const b = parseInt(h.slice(4,6), 16);
    const tr = Math.round(r + (255 - r) * (1 - opacity));
    const tg = Math.round(g + (255 - g) * (1 - opacity));
    const tb = Math.round(b + (255 - b) * (1 - opacity));
    return `rgb(${tr},${tg},${tb})`;
  }

  // ── SETTINGS ──────────────────────────────────────────────────────────────
  function applySettings(s) {
    if (!s || !Object.keys(s).length) return;
    introMode       = s.introMode       || 'story';
    introGridSource = s.introGridSource || 'recent-work';
    introTagSeed    = s.introTagSeed    || '';
    navOrder        = s.navOrder        || [];
    introBlocks   = migrateIntroBlocks(s.introBlocks || []);
    introStyle    = s.introStyle  || {};
    recentWorkIds = s.recentWorkIds || [];
    clientWorkIds = s.clientWorkIds || [];
    hiddenNavIds  = s.hiddenNavIds  || [];

    // Site name override
    if (s.siteName) {
      document.getElementById('topbar-site-name').textContent = s.siteName;
    }

    // Logo / site-name stubs (kept for applySettings compatibility)
    const logo     = document.getElementById('site-logo');
    const siteName = document.getElementById('site-name');
    if (s.logoUrl) {
      logo.src = s.logoUrl;
      logo.style.display = 'block';
      if (siteName) siteName.style.display = 'none';
    } else {
      if (logo) logo.style.display = 'none';
      if (siteName) siteName.style.display = '';
    }

    const root = document.documentElement;
    if (s.bg)          root.style.setProperty('--t-bg',           s.bg);
    if (s.navBg)       root.style.setProperty('--t-nav-bg',       s.navBg);
    if (s.text)        root.style.setProperty('--t-text',         s.text);
    if (s.sidebarBg)   root.style.setProperty('--t-sidebar-bg',   s.sidebarBg);
    if (s.sidebarText) root.style.setProperty('--t-sidebar-text', s.sidebarText);
    if (s.accentColor) {
      root.style.setProperty('--t-accent', s.accentColor);
      root.style.setProperty('--t-accent-tint', hexToTint(s.accentColor, (s.accentTint ?? 25) / 100));
    }
    if (s.cursorColor) root.style.setProperty('--t-cursor', s.cursorColor);
    if (s.bodySize)    root.style.setProperty('--t-body-size',    s.bodySize + 'px');
    if (s.lineHeight)  root.style.setProperty('--t-line-height',  s.lineHeight);
    if (s.imgGap)      root.style.setProperty('--t-img-gap',      s.imgGap + 'px');
    if (s.gridMin)     root.style.setProperty('--t-grid-min',     s.gridMin + 'px');
    if (s.cardRadius)  root.style.setProperty('--t-card-radius',  s.cardRadius + 'px');

    // Load font
    const fontName = s.fontCustom || (s.font !== 'system' ? s.font : '');
    if (fontName) {
      const slug = fontName.replace(/ /g, '+');
      const existingLink = document.getElementById('google-font-link');
      if (existingLink) existingLink.remove();
      const link = document.createElement('link');
      link.id   = 'google-font-link';
      link.rel  = 'stylesheet';
      link.href = `https://fonts.googleapis.com/css2?family=${slug}:wght@400;700&display=swap`;
      link.onerror = () => { link.remove(); };
      link.onload = () => {
        document.fonts.load(`16px '${fontName}'`).then(fonts => {
          if (fonts.length > 0) {
            root.style.setProperty('--t-font', `'${fontName}'`);
            document.body.style.fontFamily = `'${fontName}', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`;
          }
        }).catch(() => {});
      };
      document.head.appendChild(link);
    }

    // Cache theme for instant application on next load (eliminates FOUC)
    try {
      const cache = {};
      if (s.bg)          cache.bg          = s.bg;
      if (s.navBg)       cache.navBg       = s.navBg;
      if (s.text)        cache.text        = s.text;
      if (s.accentColor) { cache.accent = s.accentColor; cache.accentTint = hexToTint(s.accentColor, (s.accentTint ?? 25) / 100); }
      if (s.cursorColor) cache.cursorColor = s.cursorColor;
      if (s.bodySize)    cache.bodySize    = s.bodySize;
      if (fontName)      cache.font        = fontName;
      localStorage.setItem('pp_theme_cache', JSON.stringify(cache));
    } catch(e) {}
  }

  // ── DATA ──────────────────────────────────────────────────────────────────
  async function loadAll() {
    const [galleriesRes, pagesRes, postsRes, settingsRes] = await Promise.all([
      sb.from('galleries').select('*, images(count)').order('sort_order', { ascending: true }),
      sb.from('pages').select('*').order('sort_order', { ascending: true }),
      sb.from('posts').select('id, title, slug, cover_image_url, tags, published_at').order('published_at', { ascending: false }),
      sb.from('settings').select('data').eq('id', 1).single()
    ]);
    allGalleries = galleriesRes.data || [];
    allPages     = pagesRes.data || [];
    allPosts     = postsRes.data || [];
    applySettings(settingsRes.data?.data || {});
    document.getElementById('home-loading').style.display = 'none';
    renderHome();
    buildNavDropdown();
    wireBottomBar();
  }

  function isFolder(g) { return allGalleries.some(o => o.parent_id === g.id); }
  function childrenOf(id) { return allGalleries.filter(g => g.parent_id === id); }
  function topLevel() { return allGalleries.filter(g => g.parent_id === null); }

  // ── HOME ──────────────────────────────────────────────────────────────────
  function renderHome() {
    const grid  = document.getElementById('home-grid');
    const story = document.getElementById('home-story');

    if (introMode === 'story' && introBlocks.length) {
      grid.style.display  = 'none';
      story.style.display = 'block';
      renderHomeStory(story);
    } else {
      grid.style.display  = '';
      story.style.display = 'none';
      grid.innerHTML = '';
      const items = topLevel().filter(g => !g.type || g.type === 'portfolio');
      if (!items.length) {
        grid.innerHTML = '<div class="text-center py-16 text-[#444] text-sm col-span-full">No galleries yet.</div>';
        return;
      }
      items.forEach(g => grid.appendChild(makeCard(g)));
    }
  }

  // ── RECENT WORK STACK ─────────────────────────────────────────────────────
  async function openRecentWork() {
    currentGallery = null;
    currentFolder  = null;
    setTopbarLabel('Recent Work');
    document.title = 'Recent Work — Portfolio';
    setURL({});
    buildNavDropdown();
    document.getElementById('gallery-view-toggle').style.display = 'none';

    const container = document.getElementById('view-recent');
    container.innerHTML = '<div style="padding:40px 20px;font-size:13px;color:#bbb;">Loading…</div>';
    showView('view-recent');

    // Get galleries: curated list if set, otherwise all non-folder galleries
    const allNonFolders = allGalleries.filter(g => !isFolder(g) && g.type !== 'archive');
    const galleries = recentWorkIds.length
      ? recentWorkIds.map(id => allNonFolders.find(g => g.id === id)).filter(Boolean)
      : allNonFolders;

    if (!galleries.length) {
      container.innerHTML = '<div style="padding:40px 20px;font-size:13px;color:#bbb;">No galleries to show.</div>';
      return;
    }

    // Fetch images for all galleries in one query
    const ids = galleries.map(g => g.id);
    const { data: allImgs } = await sb.from('images').select('id,url,thumb_url,title,caption,gallery_id,sort_order,tags')
      .in('gallery_id', ids).not('hidden', 'is', true).order('sort_order', { ascending: true });

    const byGallery = {};
    (allImgs || []).forEach(img => {
      if (!byGallery[img.gallery_id]) byGallery[img.gallery_id] = [];
      byGallery[img.gallery_id].push(img);
    });
    // Re-sort per gallery client-side — multi-gallery ORDER BY can interleave rows with equal sort_order values
    Object.values(byGallery).forEach(arr => arr.sort((a, b) => (a.sort_order ?? 999999) - (b.sort_order ?? 999999)));

    container.innerHTML = '';

    galleries.forEach(g => {
      const imgs = byGallery[g.id] || [];
      const row = document.createElement('div');
      row.className = 'recent-row';

      const header = document.createElement('div');
      header.className = 'recent-row-header';
      const titleEl = document.createElement('span');
      titleEl.className = 'recent-row-title';
      titleEl.textContent = g.name;
      titleEl.style.cursor = 'pointer';
      titleEl.addEventListener('click', (e) => { e.stopPropagation(); openGallery(g); });
      const countEl = document.createElement('span');
      countEl.className = 'recent-row-count';
      countEl.textContent = `${imgs.length} image${imgs.length !== 1 ? 's' : ''}`;
      header.appendChild(titleEl);
      header.appendChild(countEl);
      header.addEventListener('click', () => openGallery(g));
      row.appendChild(header);

      const strip = document.createElement('div');
      strip.className = 'recent-filmstrip';

      if (!imgs.length) {
        strip.innerHTML = '<span style="font-size:13px;color:#bbb;padding:0 4px;">No images yet</span>';
      } else {
        imgs.forEach((img, i) => {
          const thumb = document.createElement('div');
          thumb.className = 'recent-thumb';
          const imgEl = document.createElement('img');
          imgEl.src = img.thumb_url || img.url;
          imgEl.alt = img.title || img.caption || '';
          imgEl.loading = 'lazy';
          imgEl.addEventListener('load', () => {
            const ratio = imgEl.naturalWidth / imgEl.naturalHeight;
            const h = ratio < 0.85 ? 240 : ratio > 1.2 ? 150 : 190;
            thumb.style.height = h + 'px';
            thumb.style.width  = (h * ratio) + 'px';
            // scroll to focus/cover image (or centre) on mobile once all images loaded
            if (window.matchMedia('(hover: none)').matches &&
                strip.querySelectorAll('img[src]').length === imgs.length &&
                [...strip.querySelectorAll('img')].every(im => im.complete)) {
              const focusIdx = g.cover_image_url
                ? imgs.findIndex(im => im.url === g.cover_image_url || im.thumb_url === g.cover_image_url)
                : -1;
              const thumbs = strip.querySelectorAll('.recent-thumb');
              const target = focusIdx >= 0 ? thumbs[focusIdx] : null;
              if (target) {
                strip.scrollLeft = target.offsetLeft - (strip.clientWidth - target.offsetWidth) / 2;
              }
            }
          });
          thumb.appendChild(imgEl);
          thumb.addEventListener('click', () => {
            currentGallery = g; currentImages = imgs;
            editorMode ? toggleEditorSelect(img, thumb) : openZoom(i);
          });
          strip.appendChild(thumb);
        });
        revealWhenLoaded(strip, true);
        setupCenterHighlight(strip, '.recent-thumb');
      }

      row.appendChild(strip);
      container.appendChild(row);
    });

    // Mobile: reveal title when row scrolls into centre of viewport
    if (window.matchMedia('(hover: none)').matches) {
      const rows = container.querySelectorAll('.recent-row');
      // First row is already visible on load — show it immediately
      rows[0]?.querySelector('.recent-row-title')?.classList.add('visible');
      const obs = new IntersectionObserver((entries) => {
        entries.forEach(e => {
          e.target.querySelector('.recent-row-title')?.classList.toggle('visible', e.isIntersecting);
        });
      }, { rootMargin: '-45% 0px -27% 0px', threshold: 0 });
      rows.forEach((row, i) => { if (i > 0) obs.observe(row); });
    }
  }

  // ── SHARED ARCHIVE RENDERING ──────────────────────────────────────────────
  function populateArchiveContainer(container) {
    const PAGE_SIZE = 60;

    function renderGalleryRow(g) {
      const count = (g.images?.[0]?.count) ?? 0;
      const row = document.createElement('div');
      row.className = 'cw-row';
      row.dataset.galleryId = g.id;

      const header = document.createElement('div');
      header.className = 'cw-row-header';
      header.innerHTML = `<span class="cw-row-title">${g.name}</span><span class="cw-row-count">${count} image${count !== 1 ? 's' : ''}</span>`;

      const expand = document.createElement('div');
      expand.className = 'cw-expand';
      const strip = document.createElement('div');
      strip.className = 'cw-filmstrip';
      expand.appendChild(strip);

      let loaded = false;
      let allImages = [];
      let pageOffset = 0;
      let sentinelObserver = null;

      function appendThumbs(imgs) {
        imgs.forEach((img, i) => {
          const globalIdx = pageOffset - imgs.length + i;
          const thumb = document.createElement('div');
          thumb.className = 'cw-thumb';
          const imgEl = document.createElement('img');
          imgEl.src = img.thumb_url || img.url; imgEl.alt = img.title || img.caption || ''; imgEl.loading = 'lazy';
          imgEl.addEventListener('load', () => {
            thumb.style.width = (240 * imgEl.naturalWidth / imgEl.naturalHeight) + 'px';
          });
          thumb.appendChild(imgEl);
          thumb.addEventListener('click', () => {
            if (editorMode) { toggleEditorSelect(img, thumb); return; }
            currentGallery = g; currentImages = allImages; openZoom(globalIdx);
          });
          strip.appendChild(thumb);
        });
      }

      async function loadNextPage() {
        const { data: imgs } = await sb.from('images')
          .select('id,url,title,caption,gallery_id,sort_order,tags')
          .eq('gallery_id', g.id)
          .order('sort_order', { ascending: true })
          .range(pageOffset, pageOffset + PAGE_SIZE - 1);
        if (!imgs?.length) { if (sentinelObserver) sentinelObserver.disconnect(); return; }
        allImages.push(...imgs);
        pageOffset += imgs.length;
        appendThumbs(imgs);
        // If we got a full page, add/move sentinel to end for next load
        if (imgs.length === PAGE_SIZE) {
          let sentinel = strip.querySelector('.filmstrip-sentinel');
          if (!sentinel) {
            sentinel = document.createElement('div');
            sentinel.className = 'filmstrip-sentinel';
            sentinel.style.cssText = 'width:1px;height:100%;flex-shrink:0;';
            sentinelObserver = new IntersectionObserver(entries => {
              if (entries[0].isIntersecting) loadNextPage();
            }, { root: strip, threshold: 0 });
            sentinelObserver.observe(sentinel);
          }
          strip.appendChild(sentinel);
        } else {
          if (sentinelObserver) sentinelObserver.disconnect();
        }
        if (pageOffset === PAGE_SIZE) { // first load
          revealWhenLoaded(strip, true);
          setupCenterHighlight(strip, '.cw-thumb');
        }
      }

      header.addEventListener('click', async () => {
        const isOpen = expand.classList.contains('open');
        container.querySelectorAll('.cw-expand.open').forEach(el => el.classList.remove('open'));
        container.querySelectorAll('.cw-row.open').forEach(el => el.classList.remove('open'));
        if (isOpen) return;
        expand.classList.add('open');
        row.classList.add('open');
        if (!loaded) {
          loaded = true;
          showSpinner(strip);
          strip.innerHTML = '';
          await loadNextPage();
          if (!allImages.length) strip.innerHTML = '<span style="font-size:13px;color:#bbb;">No images yet</span>';
        }
      });

      row.appendChild(header);
      row.appendChild(expand);
      return row;
    }

    function makeSectionLabel(text) {
      const el = document.createElement('div');
      el.className = 'cw-section-label';
      el.textContent = text;
      return el;
    }

    const folders    = allGalleries.filter(g => isFolder(g));
    const portfolios = allGalleries.filter(g => !isFolder(g) && (!g.type || g.type === 'portfolio'));
    const projects   = allGalleries.filter(g => g.type === 'project' && !g.parent_id);
    const clients    = allGalleries.filter(g => g.type === 'client' && !g.parent_id);

    if (portfolios.length || folders.length) {
      folders.forEach(folder => {
        const children = allGalleries.filter(g => !isFolder(g) && g.parent_id === folder.id);
        if (!children.length) return;
        const section = document.createElement('div');
        section.className = 'cw-client-section';
        const clientHeader = document.createElement('div');
        clientHeader.className = 'cw-client-header';
        clientHeader.textContent = folder.name;
        section.appendChild(clientHeader);
        children.forEach(g => section.appendChild(renderGalleryRow(g)));
        container.appendChild(section);
      });
      portfolios.filter(g => !g.parent_id).forEach(g => container.appendChild(renderGalleryRow(g)));
    }
    if (projects.length) {
      container.appendChild(makeSectionLabel('Projects'));
      projects.forEach(g => container.appendChild(renderGalleryRow(g)));
    }
    if (clients.length) {
      container.appendChild(makeSectionLabel('Client Work'));
      clients.forEach(g => container.appendChild(renderGalleryRow(g)));
    }
    const archives = allGalleries.filter(g => g.type === 'archive' && !g.parent_id && g.slug !== '_archive' && !hiddenNavIds.includes(String(g.id)));
    if (archives.length) {
      container.appendChild(makeSectionLabel('Archive'));
      archives.forEach(g => container.appendChild(renderGalleryRow(g)));
    }
  }

  // ── INDEX ─────────────────────────────────────────────────────────────────
  async function openIndex() {
    currentGallery = null;
    currentFolder  = null;
    tagReturnGallery = null;
    tagReturnImgId  = null;
    setTopbarLabel('Archive');
    document.title = 'Archive — Portfolio';
    setURL({ page: 'index' });
    buildNavDropdown();
    document.getElementById('gallery-view-toggle').style.display = 'none';

    const container = document.getElementById('view-clientwork');
    container.innerHTML = '';
    showView('view-clientwork');

    // ── Sticky search bar
    const searchBar = document.createElement('div');
    searchBar.className = 'archive-search-bar';
    const searchInput = document.createElement('input');
    searchInput.type = 'text';
    searchInput.className = 'archive-search-input';
    searchInput.placeholder = 'Search by title, description or tag…';
    searchInput.addEventListener('keydown', e => {
      if (e.key === 'Enter') {
        const q = searchInput.value.trim();
        if (q) textSearchToArchive(q);
      }
    });
    searchBar.appendChild(searchInput);
    container.appendChild(searchBar);

    populateArchiveContainer(container);

    // ── Tags (fetched from images)
    const tagsSection = document.createElement('div');
    tagsSection.className = 'idx-tags-section';
    tagsSection.innerHTML = '<div class="idx-tags-label">Tags</div><div class="idx-tags-list" style="color:#bbb;font-size:13px;">Loading…</div>';
    container.appendChild(tagsSection);

    const { data: tagRows } = await sb.from('images').select('tags').not('tags', 'is', null);
    const tagSet = new Set();
    (tagRows || []).forEach(r => (r.tags || []).forEach(t => tagSet.add(t)));
    allPosts.forEach(p => (p.tags || []).forEach(t => tagSet.add(t)));
    window._allSiteTags = [...tagSet].sort();

    const tagsList = document.createElement('div');
    tagsList.className = 'idx-tags-list';
    if (tagSet.size) {
      [...tagSet].sort().forEach(tag => {
        const btn = document.createElement('button');
        btn.className = 'idx-tag-btn';
        btn.textContent = '#' + tag;
        btn.addEventListener('click', () => searchByTag(tag));
        tagsList.appendChild(btn);
      });
    } else {
      tagsList.textContent = 'No tags yet';
      tagsList.style.cssText = 'color:#bbb;font-size:13px;';
    }
    tagsSection.querySelector('.idx-tags-list').replaceWith(tagsList);
  }

  // Keep old name as alias so any other callers still work
  const openClientWork = openIndex;

  // ── TAG BROWSER ───────────────────────────────────────────────────────────
  async function openTagBrowser({ keepActiveTags = false } = {}) {
    currentGallery = null; currentFolder = null;
    if (!keepActiveTags) {
      activeTags.clear(); activeTag = null;
      tagReturnGallery = null; tagReturnImgId = null;
    }
    setTopbarLabel('Search');
    document.title = 'Search — Portfolio';
    setURL({ page: 'search' });
    buildNavDropdown();
    // Reset to Tags mode on fresh open
    if (!keepActiveTags) {
      document.getElementById('tb-tags-panel').style.display = '';
      document.getElementById('tb-archive-panel').style.display = 'none';
      document.getElementById('tb-archive-panel').dataset.rendered = '';
      document.getElementById('tb-mode-tags')?.classList.add('active');
      document.getElementById('tb-mode-archive')?.classList.remove('active');
    }
    document.getElementById('gallery-view-toggle').style.display = 'none';
    showView('view-tagbrowser');

    if (!keepActiveTags || !window._tbTagsLoaded) {
      const container = document.getElementById('tagbrowser-tags');
      container.innerHTML = '<span style="font-size:13px;color:#ccc;">Loading tags…</span>';

      // Paginate through all images — Supabase default caps at 1000 rows per request
      let allTagRows = [], from = 0;
      while (true) {
        const { data } = await sb.from('images').select('tags')
          .not('tags', 'is', null).not('tags', 'eq', '{}')
          .range(from, from + 999);
        if (!data?.length) break;
        allTagRows = allTagRows.concat(data);
        if (data.length < 1000) break;
        from += 1000;
      }
      const tagCount = {};
      allTagRows.forEach(r => (r.tags || []).forEach(t => { tagCount[t] = (tagCount[t] || 0) + 1; }));
      window._tbAllTags = Object.keys(tagCount).sort();
      window._tbTagRows = allTagRows;
      window._tbTagsLoaded = true;

      container.innerHTML = '';
      window._tbAllTags.forEach(tag => {
        const btn = document.createElement('button');
        btn.className = 'tb-tag';
        btn.dataset.tag = tag;
        btn.textContent = '#' + tag;
        btn.addEventListener('click', () => {
          stopTagSelectCycle();
          if (btn.classList.contains('faded')) { activeTags.clear(); searchByTag(tag); return; }
          if (activeTags.has(tag)) {
            activeTags.delete(tag);
            if (!activeTags.size) {
              if (tagReturnGallery) { openGallery(tagReturnGallery, tagReturnImgId); tagReturnImgId = null; }
              else { clearTagBrowserResults(); }
            } else {
              renderTagBrowserResults();
            }
          } else {
            if (!activeTags.size) tagReturnGallery = currentGallery;
            activeTags.add(tag); activeTag = tag;
            renderTagBrowserResults();
          }
        });
        container.appendChild(btn);
      });

      // Wire up search input
      const input = document.getElementById('tagbrowser-input');
      input.value = '';
      input.onkeydown = e => {
        stopTagSelectCycle();
        if (e.key === 'Enter') { const q = input.value.trim(); if (q) smartSearch(q); }
      };

      // Wire up mode toggle buttons (once)
      document.getElementById('tb-mode-tags').onclick = () => showTbMode('tags');
      document.getElementById('tb-mode-archive').onclick = () => showTbMode('archive');
    }

    if (activeTags.size) {
      await renderTagBrowserResults();
    } else if (window._tbAllTags?.length) {
      startTagSelectCycle(window._tbAllTags);
    } else {
      clearTagBrowserResults();
    }
  }

  let tagSelectCycleTimer = null;
  function startTagSelectCycle(tags) {
    clearInterval(tagSelectCycleTimer);
    let lastPick = [];
    async function pickNext() {
      const picked = pickValidTagCombo(tags, window._tbTagRows || [], lastPick);
      lastPick = picked;
      activeTags.clear();
      picked.forEach(t => activeTags.add(t));
      activeTag = picked[0];
      await renderTagBrowserResults();
    }
    pickNext();
    tagSelectCycleTimer = setInterval(pickNext, 4000);
  }
  function pickValidTagCombo(tags, tagRows, exclude) {
    // 1st tag: random, avoiding last round's picks
    const pool = tags.filter(t => !exclude.includes(t));
    const src = pool.length ? pool : [...tags];
    const first = src[Math.floor(Math.random() * src.length)];
    const picked = [first];
    // try adding a 2nd tag (always attempt), 3rd tag (50% chance)
    const maxExtra = Math.random() < 0.5 ? 2 : 1;
    for (let extra = 0; extra < maxExtra; extra++) {
      // images matching all already-picked tags
      const matching = tagRows.filter(r => picked.every(t => (r.tags || []).includes(t)));
      if (!matching.length) break;
      // rank remaining tags by how many matching images they appear on
      const counts = {};
      matching.forEach(r => (r.tags || []).forEach(t => {
        if (!picked.includes(t)) counts[t] = (counts[t] || 0) + 1;
      }));
      const ranked = Object.entries(counts).sort((a, b) => b[1] - a[1]);
      if (!ranked.length) break;
      // pick from top 5 with some randomness to avoid always picking the same combo
      const topN = ranked.slice(0, 5);
      picked.push(topN[Math.floor(Math.random() * topN.length)][0]);
    }
    return picked;
  }
  function stopTagSelectCycle() {
    clearInterval(tagSelectCycleTimer);
    tagSelectCycleTimer = null;
  }

  function showTbMode(mode) {
    document.getElementById('tb-mode-tags').classList.toggle('active', mode === 'tags');
    document.getElementById('tb-mode-archive').classList.toggle('active', mode === 'archive');
    document.getElementById('tb-tags-panel').style.display = mode === 'tags' ? '' : 'none';
    const archivePanel = document.getElementById('tb-archive-panel');
    archivePanel.style.display = mode === 'archive' ? '' : 'none';
    if (mode === 'archive') {
      activeTags.clear(); activeTag = null;
      clearTagBrowserResults();
      setTopbarLabel('Archive');
      if (!archivePanel.dataset.rendered) {
        renderTbArchive(archivePanel);
        archivePanel.dataset.rendered = '1';
      }
    } else {
      const label = activeTags.size ? 'Search · ' + [...activeTags].map(t => '#' + t).join(' ') : 'Search';
      setTopbarLabel(label);
    }
  }

  function renderTbArchive(panel) {
    panel.innerHTML = '';
    const wrap = document.createElement('div');
    wrap.style.cssText = 'margin-bottom:32px;';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;align-items:center;gap:12px;border-bottom:1px solid #e0e0e0;padding-bottom:14px;';
    row.innerHTML = '<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="#999" stroke-width="2.2" stroke-linecap="round"><circle cx="8.5" cy="8.5" r="5.5"/><line x1="13" y1="13" x2="18" y2="18"/></svg>';
    const input = document.createElement('input');
    input.type = 'text';
    input.id = 'tb-archive-input';
    input.placeholder = 'Search titles, descriptions or tags…';
    input.style.cssText = 'flex:1;border:none;outline:none;font-size:17px;font-family:inherit;background:transparent;color:#111;';
    input.addEventListener('input', () => archiveSearch(panel, input.value));
    input.addEventListener('keydown', e => {
      if (e.key === 'Escape') { input.value = ''; filterArchiveRows(panel, ''); }
    });
    row.appendChild(input);
    wrap.appendChild(row);
    panel.appendChild(wrap);
    populateArchiveContainer(panel);
  }

  function _setArchiveGalleryListVisible(panel, visible) {
    panel.querySelectorAll('.cw-row, .cw-section-label, .cw-client-section, .cw-client-header').forEach(el => {
      el.style.display = visible ? '' : 'none';
    });
  }

  function searchQueryVariants(q) {
    // Normalize to "and" form, then produce &, +, and plain variants
    const base = q.replace(/\s*[&+]\s*/g, ' and ').replace(/\s+/g, ' ').trim();
    const withAmp  = base.replace(/\band\b/gi, '&');
    const withPlus = base.replace(/\band\b/gi, '+');
    const forms = [...new Set([q, base, withAmp, withPlus])].filter(Boolean);
    // Add singular/plural stems so "portraits" matches "portrait" and vice versa
    const extra = new Set();
    forms.forEach(f => wordStems(f).forEach(s => extra.add(s)));
    return [...new Set([...forms, ...extra])].filter(Boolean);
  }

  function wordStems(q) {
    const l = q.toLowerCase();
    const out = new Set([l]);
    if (l.endsWith('ies') && l.length > 4)      { out.add(l.slice(0, -3) + 'y'); }
    else if (l.endsWith('ses') && l.length > 4)  { out.add(l.slice(0, -2)); }
    else if (l.endsWith('es') && l.length > 3)   { out.add(l.slice(0, -2)); out.add(l.slice(0, -1)); }
    else if (l.endsWith('s')  && l.length > 2)   { out.add(l.slice(0, -1)); }
    if (!l.endsWith('s'))                         { out.add(l + 's'); }
    return [...out];
  }

  let _archiveSearchTimer = null;
  async function archiveSearch(panel, query) {
    const q = query.trim();
    let resultsGrid = panel.querySelector('.tb-archive-results');
    if (!resultsGrid) {
      resultsGrid = document.createElement('div');
      resultsGrid.className = 'tb-archive-results';
      resultsGrid.style.cssText = 'display:flex;flex-wrap:wrap;justify-content:center;gap:12px 8px;margin-top:8px;';
      panel.appendChild(resultsGrid);
    }
    clearTimeout(_archiveSearchTimer);
    if (!q) {
      resultsGrid.innerHTML = '';
      resultsGrid.style.display = 'none';
      _setArchiveGalleryListVisible(panel, true);
      return;
    }
    _archiveSearchTimer = setTimeout(async () => {
      _setArchiveGalleryListVisible(panel, false);
      resultsGrid.style.display = 'flex';
      resultsGrid.innerHTML = '<span style="font-size:13px;color:#bbb;padding:24px 0;">Searching…</span>';
      const variants = searchQueryVariants(q);
      const tagVariants = wordStems(q);
      const orFilter = variants.flatMap(v => [`title.ilike.%${v}%`, `description.ilike.%${v}%`]).join(',');
      const [{ data: byText }, { data: byTag }] = await Promise.all([
        sb.from('images').select('*').or(orFilter).not('hidden', 'is', true).order('created_at', { ascending: false }),
        sb.from('images').select('*').overlaps('tags', tagVariants).not('hidden', 'is', true).order('created_at', { ascending: false }),
      ]);
      const seen = new Set();
      const results = [...(byText || []), ...(byTag || [])].filter(img => {
        if (seen.has(img.id)) return false;
        seen.add(img.id); return true;
      });
      currentImages = results;
      currentGallery = null;
      resultsGrid.innerHTML = '';
      if (!results.length) {
        resultsGrid.innerHTML = '<span style="font-size:13px;color:#bbb;padding:24px 0;">No results found.</span>';
        return;
      }
      results.forEach((img, i) => {
        const wrap = document.createElement('div');
        wrap.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:4px;';
        wrap.appendChild(makeThumb(img, i, () => openZoom(i)));
        if (img.title) {
          const label = document.createElement('div');
          label.textContent = img.title;
          label.style.cssText = 'font-size:11px;color:#999;padding:2px 4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;text-align:center;letter-spacing:0.02em;';
          wrap.appendChild(label);
        }
        resultsGrid.appendChild(wrap);
      });
      revealWhenLoaded(resultsGrid);
    }, 350);
  }

  function clearSearchInputs() {
    const tagInput = document.getElementById('tagbrowser-input');
    if (tagInput) tagInput.value = '';
    const archiveInput = document.getElementById('tb-archive-input');
    if (archiveInput) { archiveInput.value = ''; archiveSearch(document.getElementById('tb-archive-panel'), ''); }
  }

  function smartSearch(query) {
    const q = query.trim().replace(/^#/, '');
    const tagMatch = (window._allSiteTags || window._tbAllTags || []).find(t => t.toLowerCase() === q.toLowerCase());
    if (tagMatch) searchByTag(tagMatch);
    else textSearchToArchive(q);
  }

  function textSearchToArchive(query) {
    currentGallery = null; currentFolder = null;
    buildNavDropdown();
    document.getElementById('gallery-view-toggle').style.display = 'none';
    showView('view-tagbrowser');
    // Load tag cloud in background so Tags tab works if the user switches
    if (!window._tbTagsLoaded) openTagBrowser({ keepActiveTags: true });
    // Ensure toggle buttons are wired
    document.getElementById('tb-mode-tags').onclick = () => showTbMode('tags');
    document.getElementById('tb-mode-archive').onclick = () => showTbMode('archive');
    showTbMode('archive');
    setTopbarLabel('Archive');
    setURL({ search: query });
    const archiveInput = document.getElementById('tb-archive-input');
    if (archiveInput) archiveInput.value = query;
    archiveSearch(document.getElementById('tb-archive-panel'), query);
  }

  let tagCycleTimer = null;
  function startTagCycle(input, tags) {
    clearInterval(tagCycleTimer);
    if (!tags.length) return;
    let i = 0;
    function next() {
      input.setAttribute('placeholder', '#' + tags[i % tags.length]);
      i++;
    }
    next();
    tagCycleTimer = setInterval(next, 2200);
  }

  function _nextRowId() { return String(Date.now()) + '-' + Math.random().toString(36).slice(2); }

  function migrateIntroBlocks(blocks) {
    if (!Array.isArray(blocks)) return [];
    const result = [];
    blocks.forEach(b => {
      if (!b.type) {
        // Old layout-based format
        if (b.layout === 'two-col') {
          const rowId = _nextRowId();
          const slotToBlock = (slot, sid) => {
            if (!slot) return null;
            if (slot.type === 'image') return { id: sid || _nextRowId(), type: 'image', rowId, colSpan: 6, src: slot.src || '', galleryId: slot.galleryId || null, caption: '' };
            if (slot.size === 'heading') return { id: sid || _nextRowId(), type: 'heading', rowId, colSpan: 6, text: slot.text || '' };
            return { id: sid || _nextRowId(), type: 'text', rowId, colSpan: 6, text: slot.text || '' };
          };
          const left = slotToBlock(b.left, b.id ? b.id + '-l' : null);
          const right = slotToBlock(b.right, b.id ? b.id + '-r' : null);
          if (left) result.push(left);
          if (right) result.push(right);
        } else if (b.layout === 'full' && b.content) {
          const slot = b.content;
          const rowId = _nextRowId();
          if (slot.type === 'image') result.push({ id: b.id || _nextRowId(), type: 'image', rowId, colSpan: 12, src: slot.src || '', galleryId: slot.galleryId || null, caption: '' });
          else if (slot.size === 'heading') result.push({ id: b.id || _nextRowId(), type: 'heading', rowId, colSpan: 12, text: slot.text || '' });
          else result.push({ id: b.id || _nextRowId(), type: 'text', rowId, colSpan: 12, text: slot.text || '' });
        }
      } else if (b.type === 'two-col') {
        // New-ish format with two-col — split into two blocks
        const rowId = _nextRowId();
        const slotToBlock = (slot, sid) => {
          if (!slot) return null;
          if (slot.type === 'image') return { id: sid || _nextRowId(), type: 'image', rowId, colSpan: 6, src: slot.src || '', galleryId: slot.galleryId || null, caption: '' };
          if (slot.size === 'heading') return { id: sid || _nextRowId(), type: 'heading', rowId, colSpan: 6, text: slot.text || '' };
          return { id: sid || _nextRowId(), type: 'text', rowId, colSpan: 6, text: slot.text || '' };
        };
        const left = slotToBlock(b.left, b.id ? b.id + '-l' : null);
        const right = slotToBlock(b.right, b.id ? b.id + '-r' : null);
        if (left) result.push(left);
        if (right) result.push(right);
      } else {
        // Already new format — ensure rowId and colSpan
        if (!b.rowId) b.rowId = _nextRowId();
        if (b.colSpan == null) b.colSpan = b.type === 'spacer' ? 3 : 12;
        result.push(b);
      }
    });
    return result;
  }

  function renderHomeStory(container) {
    container.innerHTML = '';
    const bg          = introStyle.bg           || '';
    const headingSize = introStyle.headingSize  || '38';
    const bodySize    = introStyle.bodySize     || '16';
    const headingColor= introStyle.headingColor || 'var(--t-text)';
    const bodyColor   = introStyle.bodyColor    || '#444';
    document.getElementById('view-home').style.background = bg;
    container.dataset.headingSize  = headingSize;
    container.dataset.bodySize     = bodySize;
    container.dataset.headingColor = headingColor;
    container.dataset.bodyColor    = bodyColor;

    // Group blocks by rowId, preserving order
    const rows = [];
    const seenRows = new Map();
    introBlocks.forEach(block => {
      if (seenRows.has(block.rowId)) {
        rows[seenRows.get(block.rowId)].push(block);
      } else {
        seenRows.set(block.rowId, rows.length);
        rows.push([block]);
      }
    });

    rows.forEach(row => {
      if (row.length === 1) {
        const el = makeStoryBlockEl(row[0], false);
        if (el) container.appendChild(el);
      } else {
        const rowEl = document.createElement('div');
        rowEl.style.cssText = 'display:grid;grid-template-columns:repeat(12,1fr);gap:24px;margin-bottom:32px;align-items:start;';
        row.forEach(block => {
          const wrap = document.createElement('div');
          wrap.style.gridColumn = `span ${block.colSpan || 12}`;
          const el = makeStoryBlockEl(block, true);
          if (el) wrap.appendChild(el);
          rowEl.appendChild(wrap);
        });
        container.appendChild(rowEl);
      }
    });
  }

  function makeStoryBlockEl(block, isCol) {
    const hSize  = document.getElementById('home-story').dataset.headingSize  || '38';
    const bSize  = document.getElementById('home-story').dataset.bodySize     || '16';
    const hColor = document.getElementById('home-story').dataset.headingColor || 'var(--t-text)';
    const bColor = document.getElementById('home-story').dataset.bodyColor    || '#444';

    if (block.type === 'heading') {
      const el = document.createElement('div');
      const sz = isCol ? Math.round(parseInt(hSize) * 0.75) : parseInt(hSize);
      el.style.cssText = `font-size:${sz}px;font-weight:700;line-height:1.2;margin-bottom:24px;color:${hColor};`;
      el.innerHTML = (block.text || '').replace(/\n(?![^<]*>)/g, '<br>');
      return el;
    }
    if (block.type === 'text') {
      const el = document.createElement('div');
      el.style.cssText = `font-size:${bSize}px;line-height:1.75;color:${bColor};margin-bottom:20px;`;
      el.innerHTML = (block.text || '').replace(/\n(?![^<]*>)/g, '<br>');
      return el;
    }
    if (block.type === 'quote') {
      const el = document.createElement('blockquote');
      el.className = 'story-quote';
      el.style.cssText += `font-size:${bSize}px;`;
      el.innerHTML = (block.text || '').replace(/\n(?![^<]*>)/g, '<br>');
      return el;
    }
    if (block.type === 'image') {
      return makeStorySlotEl({ type: 'image', src: block.src, galleryId: block.galleryId, caption: block.caption }, isCol);
    }
    if (block.type === 'divider') {
      const el = document.createElement('hr');
      el.className = 'story-divider';
      return el;
    }
    if (block.type === 'spacer') {
      const el = document.createElement('div');
      el.style.height = `${block.height || 40}px`;
      return el;
    }
    return null;
  }

  function makeStorySlotEl(slot, isCol) {
    if (!slot) return null;

    const wrap = document.createElement('div');
    wrap.style.flex = '1';

    if (slot.type === 'empty') {
      return wrap;
    }

    if (slot.type === 'text') {
      const container = document.getElementById('home-story');
      const hSize  = container.dataset.headingSize  || '38';
      const bSize  = container.dataset.bodySize     || '16';
      const hColor = container.dataset.headingColor || 'var(--t-text)';
      const bColor = container.dataset.bodyColor    || '#444';
      const div = document.createElement('div');
      const text = (slot.text || '').replace(/\n(?![^<]*>)/g, '<br>');
      if (slot.size === 'heading') {
        const sz = isCol ? Math.round(parseInt(hSize) * 0.75) : parseInt(hSize);
        div.style.cssText = `font-size:${sz}px;font-weight:700;line-height:1.2;margin-bottom:24px;color:${hColor};`;
      } else {
        div.style.cssText = `font-size:${bSize}px;line-height:1.75;color:${bColor};margin-bottom:20px;`;
      }
      div.innerHTML = text;
      wrap.appendChild(div);
      return wrap;
    }

    if (slot.type === 'image') {
      let src = slot.src || '';
      let galleryTarget = null;
      if (slot.galleryId) {
        const g = allGalleries.find(x => x.id === slot.galleryId);
        if (g && g.cover_image_url) {
          src = g.cover_image_url;
          galleryTarget = g;
        } else {
          return null;
        }
      }
      if (!src) return null;

      const imgEl = document.createElement('img');
      imgEl.src = src;
      imgEl.alt = galleryTarget ? galleryTarget.name : '';
      imgEl.loading = 'lazy';
      if (isCol) {
        imgEl.style.cssText = 'width:100%;object-fit:cover;max-height:360px;display:block;margin-bottom:24px;cursor:pointer;';
      } else {
        imgEl.style.cssText = 'max-width:100%;display:block;margin-bottom:24px;object-fit:cover;max-height:500px;cursor:pointer;';
      }

      imgEl.addEventListener('click', () => {
        if (galleryTarget) {
          openGallery(galleryTarget);
        } else {
          currentImages = [{ url: src, title: '', caption: '', tags: [] }];
          currentGallery = null;
          openZoom(0);
        }
      });

      wrap.appendChild(imgEl);
      if (!isCol && slot.caption) {
        const cap = document.createElement('div');
        cap.className = 'story-caption';
        cap.textContent = slot.caption;
        wrap.appendChild(cap);
      }
      return wrap;
    }

    return null;
  }

  function goHome() {
    currentFolder = null; currentGallery = null; currentImages = []; activeTag = null; activeTags = new Set();
    searchInput.value = '';
    buildNavDropdown();
    document.title = 'Portfolio';
    setURL({});
    closeZoom();
    if (introMode === 'grid') {
      if (introGridSource === 'recent-work') {
        openRecentWork();
      } else {
        const target = allGalleries.find(g => String(g.id) === String(introGridSource));
        if (target) {
          if (isFolder(target) || target.type === 'portfolio' || target.type === 'folder') openFolder(target);
          else openGallery(target);
        } else {
          openRecentWork();
        }
      }
    } else if (introMode === 'tags') {
      openTagBrowser();
      if (introTagSeed) setTimeout(() => searchByTag(introTagSeed), 100);
    } else {
      showView('view-home');
      setTopbarLabel('Recent Work');
      renderHome();
    }
  }

  // ── FOLDER ────────────────────────────────────────────────────────────────
  function openFolder(f) {
    currentFolder = f;
    const grid = document.getElementById('folder-grid');
    grid.innerHTML = '';
    const kids = childrenOf(f.id);
    if (!kids.length) {
      grid.innerHTML = '<div class="text-center py-16 text-[#444] text-sm col-span-full">No galleries in this folder yet.</div>';
    } else {
      kids.forEach(g => grid.appendChild(makeCard(g)));
    }
    showView('view-folder');
    setTopbarLabel(f.name);
    buildNavDropdown();
    document.title = `${f.name} — Portfolio`;
    setURL({ folder: f.slug });
  }

  // ── GALLERY ───────────────────────────────────────────────────────────────
  async function openGallery(g, autoImgId = null, forceMode = null) {
    currentGallery = g;

    showView('view-gallery');
    document.getElementById('gallery-loading').style.display = 'block';
    document.getElementById('photo-grid').innerHTML = '';

    // clear any previous statement / filmstrip / project-row / toggle
    document.getElementById('gallery-statement')?.remove();
    document.getElementById('gallery-filmstrip')?.remove();
    document.getElementById('gallery-lightbox-entry')?.remove();
    document.getElementById('project-row')?.remove();
    document.getElementById('gallery-view-toggle').style.display = 'none';

    setTopbarLabel(g.name);
    buildNavDropdown();
    document.title = `${g.name} — Portfolio`;
    setURL({ gallery: g.slug });

    const [{ data: direct }, { data: sharedRows }] = await Promise.all([
      sb.from('images').select('*').eq('gallery_id', g.id).not('hidden', 'is', true).order('sort_order', { ascending: true }),
      sb.from('gallery_images').select('sort_order, images(*)').eq('gallery_id', g.id)
    ]);

    document.getElementById('gallery-loading').style.display = 'none';
    const directIds = new Set((direct || []).map(i => i.id));
    const sharedImgs = (sharedRows || [])
      .filter(r => r.images && !r.images.hidden && !directIds.has(r.images.id))
      .map(r => ({ ...r.images, sort_order: r.sort_order }));
    currentImages = [...(direct || []), ...sharedImgs]
      .filter(img => !img.hidden)
      .sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));

    const galleryView = document.getElementById('view-gallery');
    const grid        = document.getElementById('photo-grid');
    const naturalMode = forceMode || (g.type === 'project' ? (g.display_mode || 'filmstrip') : (g.display_mode || 'grid'));
    const mode        = editorMode ? 'grid' : naturalMode;

    if (!currentImages.length) {
      grid.innerHTML = '<div class="text-center py-16 text-[#444] text-sm col-span-full">No images yet.</div>';
      return;
    }

    // Show view toggle only when not in editor mode
    const toggleBar = document.getElementById('gallery-view-toggle');
    if (!editorMode) {
      toggleBar.style.display = 'flex';
      toggleBar.querySelectorAll('.gv-btn').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.mode === naturalMode);
        btn.onclick = () => {
          toggleBar.querySelectorAll('.gv-btn').forEach(b => b.classList.remove('active'));
          btn.classList.add('active');
          renderGalleryMode(btn.dataset.mode);
        };
      });
    }

    renderGalleryMode(mode);

    if (autoImgId) {
      const idx = currentImages.findIndex(i => i.id === autoImgId);
      if (idx >= 0) openZoom(idx);
    }
  }

  function renderGalleryMode(mode) {
    const galleryView = document.getElementById('view-gallery');
    const grid        = document.getElementById('photo-grid');
    const isProject   = currentGallery?.type === 'project';
    _inRenderGalleryMode = true;

    // Clear previous mode output
    document.getElementById('gallery-filmstrip')?.remove();
    document.getElementById('gallery-statement')?.remove();
    document.getElementById('project-row')?.remove();
    grid.innerHTML = '';
    grid.style.display = '';
    closeZoom();          // must see old currentGalleryMode, so set new one after
    currentGalleryMode = mode;

    if (mode === 'filmstrip' && isProject) {
      // Project: statement as first item inside the scrolling filmstrip
      grid.style.display = 'none';
      const strip = document.createElement('div');
      strip.id = 'gallery-filmstrip';
      strip.className = 'filmstrip-wrap';
      if (currentGallery.statement) {
        const stmt = document.createElement('div');
        stmt.className = 'project-statement';
        stmt.innerHTML = currentGallery.statement.replace(/\n(?![^<]*>)/g, '<br>');
        strip.appendChild(stmt);
      }
      currentImages.forEach((img, i) => {
        const item = document.createElement('div');
        item.className = 'filmstrip-item';
        const imgEl = document.createElement('img');
        imgEl.src = (isPhone() ? img.thumb_url : null) || img.url; imgEl.alt = img.title || img.caption || ''; imgEl.loading = 'lazy';
        imgEl.addEventListener('load', () => {
          const ratio = imgEl.naturalWidth / imgEl.naturalHeight;
          const max = window.innerHeight * 0.75;
          const h = Math.min(ratio < 0.85 ? 800 : ratio > 1.2 ? 540 : 660, max);
          item.style.height = h + 'px';
          item.style.width  = (h * ratio) + 'px';
        });
        item.appendChild(imgEl);
        if (editorSelected.has(img.id)) { item.classList.add('editor-selected'); const c = document.createElement('div'); c.className = 'editor-check'; c.textContent = '✓'; item.appendChild(c); }
        item.addEventListener('click', () => editorMode ? toggleEditorSelect(img, item) : openZoom(i));
        strip.appendChild(item);
      });
      galleryView.appendChild(strip);
      revealWhenLoaded(strip, true);
      setupCenterHighlight(strip, '.filmstrip-item');
    } else if (mode === 'filmstrip') {
      grid.style.display = 'none';
      const strip = document.createElement('div');
      strip.id = 'gallery-filmstrip';
      strip.className = 'filmstrip-wrap';
      currentImages.forEach((img, i) => {
        const item = document.createElement('div');
        item.className = 'filmstrip-item';
        const imgEl = document.createElement('img');
        imgEl.src = (isPhone() ? img.thumb_url : null) || img.url; imgEl.alt = img.title || img.caption || ''; imgEl.loading = 'lazy';
        imgEl.addEventListener('load', () => {
          const ratio = imgEl.naturalWidth / imgEl.naturalHeight;
          const max = window.innerHeight * 0.75;
          const h = Math.min(ratio < 0.85 ? 800 : ratio > 1.2 ? 540 : 660, max);
          item.style.height = h + 'px';
          item.style.width  = (h * ratio) + 'px';
        });
        item.appendChild(imgEl);
        if (editorSelected.has(img.id)) { item.classList.add('editor-selected'); const c = document.createElement('div'); c.className = 'editor-check'; c.textContent = '✓'; item.appendChild(c); }
        item.addEventListener('click', () => editorMode ? toggleEditorSelect(img, item) : openZoom(i));
        strip.appendChild(item);
      });
      galleryView.appendChild(strip);
      revealWhenLoaded(strip, true);
      setupCenterHighlight(strip, '.filmstrip-item');
    } else if (mode === 'lightbox') {
      grid.style.display = 'none';
      // Project statement above lightbox
      if (isProject && currentGallery.statement) {
        const stmt = document.createElement('div');
        stmt.id = 'gallery-statement';
        stmt.className = 'project-statement';
        stmt.innerHTML = currentGallery.statement.replace(/\n(?![^<]*>)/g, '<br>');
        grid.before(stmt);
      }
      openZoom(0);
    } else {
      // Grid mode — project statement above grid
      if (isProject && currentGallery.statement) {
        const stmt = document.createElement('div');
        stmt.id = 'gallery-statement';
        stmt.className = 'project-statement';
        stmt.innerHTML = currentGallery.statement.replace(/\n(?![^<]*>)/g, '<br>');
        grid.before(stmt);
      }
      currentImages.forEach((img, i) => {
        grid.appendChild(makeThumb(img, i, () => editorMode ? toggleEditorSelect(img, grid.children[i]) : openZoom(i)));
      });
      revealWhenLoaded(grid);
    }
    _inRenderGalleryMode = false;
  }

  function galleryBack() {
    if (currentGallery?.parent_id) {
      const parent = allGalleries.find(p => p.id === currentGallery.parent_id);
      if (parent) { openFolder(parent); return; }
    }
    goHome();
  }

  // ── PAGE ──────────────────────────────────────────────────────────────────
  function openPage(page) {
    currentGallery = null; currentFolder = null; activeTag = null; activeTags = new Set();
    showView('view-page');
    const el = document.getElementById('page-content');
    const raw = page.content || '';
    el.innerHTML = raw.replace(/\n(?![^<]*>)/g, '<br>');
    setTopbarLabel(page.title || page.slug);
    buildNavDropdown();
    document.title = `${page.title || page.slug} — Portfolio`;
    setURL({ page: page.slug });
  }

  // ── BLOG ──────────────────────────────────────────────────────────────────
  function openBlog() {
    currentGallery = null; currentFolder = null; activeTag = null; activeTags = new Set();
    showView('view-blog');
    setTopbarLabel('News');
    buildNavDropdown();
    document.title = 'News — Portfolio';
    setURL({ blog: '1' });
    renderPostList();
  }

  let blogViewMode = 'list';

  function setBlogViewMode(mode) {
    blogViewMode = mode;
    const listBtn = document.getElementById('blog-view-list-btn');
    const gridBtn = document.getElementById('blog-view-grid-btn');
    listBtn.style.color = mode === 'list' ? '#111' : '#bbb';
    listBtn.style.borderBottom = mode === 'list' ? '2px solid #111' : 'none';
    gridBtn.style.color = mode === 'grid' ? '#111' : '#bbb';
    gridBtn.style.borderBottom = mode === 'grid' ? '2px solid #111' : 'none';
    renderPostList();
  }

  document.getElementById('blog-view-list-btn').addEventListener('click', () => setBlogViewMode('list'));
  document.getElementById('blog-view-grid-btn').addEventListener('click', () => setBlogViewMode('grid'));

  function renderPostList() {
    const grid = document.getElementById('post-list-grid');
    grid.innerHTML = '';
    grid.className = blogViewMode === 'grid' ? 'post-list-grid grid-mode' : 'post-list-grid list-mode';

    allPosts.forEach((post, index) => {
      const date = post.published_at ? new Date(post.published_at).toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' }) : '';
      if (blogViewMode === 'list') {
        const item = document.createElement('div');
        item.className = 'post-list-item';

        const row = document.createElement('div');
        row.className = 'post-list-row';
        row.innerHTML = `<span class="post-list-title">${post.title}</span><span class="post-list-date">${date}</span><svg class="post-list-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;

        const expand = document.createElement('div');
        expand.className = 'post-list-expand';
        const inner = document.createElement('div');
        inner.className = 'post-list-expand-inner';
        inner.innerHTML = '<p style="color:#bbb;font-size:13px">Loading…</p>';
        expand.appendChild(inner);

        let loaded = false;
        const openPost = async () => {
          expand.classList.add('open');
          item.classList.add('open');
          if (!loaded) {
            loaded = true;
            const { data } = await sb.from('posts').select('*').eq('id', post.id).single();
            if (data) inner.innerHTML = renderPostBlocks(data);
            else inner.innerHTML = '<p>Could not load post.</p>';
          }
        };

        row.addEventListener('click', async () => {
          const isOpen = expand.classList.contains('open');
          document.querySelectorAll('.post-list-expand.open').forEach(el => el.classList.remove('open'));
          document.querySelectorAll('.post-list-item.open').forEach(el => el.classList.remove('open'));
          if (isOpen) return;
          openPost();
        });

        item.appendChild(row);
        item.appendChild(expand);
        grid.appendChild(item);

        // Auto-open the first (most recent) post
        if (index === 0) openPost();
      } else {
        const card = document.createElement('div');
        card.className = 'post-card';
        card.innerHTML = post.cover_image_url
          ? `<img class="post-card-img" src="${post.cover_image_url}" alt="${post.title}" loading="lazy" />`
          : `<div class="post-card-placeholder">No image</div>`;
        card.innerHTML += `<div class="post-card-body"><div class="post-card-title">${post.title}</div><div class="post-card-meta">${date}</div></div>`;
        card.addEventListener('click', () => openPost(post));
        grid.appendChild(card);
      }
    });
  }

  function renderPostBlocks(data) {
    let blocks = [];
    try { blocks = JSON.parse(data.content || '[]'); } catch(e) {}
    let html = '';
    blocks.forEach(b => {
      if (b.type === 'text') {
        html += `<div class="post-block-text">${b.html || ''}</div>`;
      } else if (b.type === 'image' && b.url) {
        html += `<figure class="post-block-image"><img src="${b.url}" alt="${b.caption || ''}" loading="lazy" />${b.caption ? `<figcaption>${b.caption}</figcaption>` : ''}</figure>`;
      } else if (b.type === 'gallery') {
        html += `<div class="post-block-gallery">`;
        (b.images || []).forEach(img => { html += `<img src="${img.url}" alt="${img.caption || ''}" loading="lazy" />`; });
        html += `</div>`;
      }
    });
    return html;
  }

  async function openPost(post) {
    currentGallery = null; currentFolder = null; activeTag = null; activeTags = new Set();
    showView('view-post');
    setTopbarLabel(post.title);
    buildNavDropdown();
    document.title = `${post.title} — Portfolio`;
    setURL({ post: post.slug });

    const contentEl = document.getElementById('post-view-content');
    contentEl.innerHTML = '<p style="color:#bbb;font-size:13px">Loading…</p>';

    const { data } = await sb.from('posts').select('*').eq('id', post.id).single();
    if (!data) { contentEl.innerHTML = '<p>Post not found.</p>'; return; }

    const date = data.published_at ? new Date(data.published_at).toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' }) : '';

    // collect images for lightbox
    let blocks = [];
    try { blocks = JSON.parse(data.content || '[]'); } catch(e) {}
    const postImages = [];
    blocks.forEach(b => {
      if (b.type === 'image' && b.url) postImages.push({ url: b.url, caption: b.caption || '', title: '', tags: [] });
      if (b.type === 'gallery') (b.images || []).forEach(img => postImages.push({ url: img.url, caption: img.caption || '', title: '', tags: [] }));
    });

    let html = `<p class="post-view-date">${date}</p>` + renderPostBlocks(data);

    if (data.tags?.length) {
      html += `<div class="post-view-tags">` + data.tags.map(t => `<span class="post-view-tag">#${t}</span>`).join('') + `</div>`;
    }

    contentEl.innerHTML = html;

    if (postImages.length) {
      currentImages = postImages;
      currentGallery = null;
      contentEl.querySelectorAll('img').forEach((img, idx) => {
        img.style.cursor = 'zoom-in';
        img.addEventListener('click', () => openZoom(idx));
      });
    }

    // prev / next navigation
    const navEl = document.getElementById('post-view-nav');
    navEl.innerHTML = '';
    const idx  = allPosts.findIndex(p => p.id === post.id);
    const prev = allPosts[idx - 1];
    const next = allPosts[idx + 1];
    const btnStyle = 'background:none;border:none;cursor:pointer;font-size:13px;color:#bbb;padding:0;font-family:inherit;';
    if (prev) {
      const b = document.createElement('button');
      b.style.cssText = btnStyle + 'text-align:left;';
      b.innerHTML = `<span style="display:block;font-size:10px;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:3px;">← Previous</span><span style="color:#111;">${prev.title}</span>`;
      b.addEventListener('click', () => openPost(prev));
      navEl.appendChild(b);
    } else {
      navEl.appendChild(document.createElement('span'));
    }
    if (next) {
      const b = document.createElement('button');
      b.style.cssText = btnStyle + 'text-align:right;';
      b.innerHTML = `<span style="display:block;font-size:10px;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:3px;">Next →</span><span style="color:#111;">${next.title}</span>`;
      b.addEventListener('click', () => openPost(next));
      navEl.appendChild(b);
    }
  }

  // ── PHOTO THUMB ───────────────────────────────────────────────────────────
  function showSpinner(parent) {
    const el = document.createElement('div');
    el.className = 'load-spinner';
    parent.appendChild(el);
    return el;
  }

  // Fade in a container once all imgs loaded (sequential=true reveals each img as it arrives)
  function setupCenterHighlight(strip, itemSel) {
    function update() {
      const sr = strip.getBoundingClientRect();
      const sc = sr.left + sr.width / 2;
      let closest = null, closestDist = Infinity;
      strip.querySelectorAll(itemSel).forEach(el => {
        const r = el.getBoundingClientRect();
        const dist = Math.abs(r.left + r.width / 2 - sc);
        if (dist < closestDist) { closestDist = dist; closest = el; }
      });
      strip.querySelectorAll(itemSel + '.strip-center').forEach(el => el.classList.remove('strip-center'));
      if (closest) closest.classList.add('strip-center');
    }
    strip.addEventListener('scroll', update, { passive: true });
    // Run after images have had a chance to size themselves
    requestAnimationFrame(() => requestAnimationFrame(update));
  }

  function revealWhenLoaded(container, sequential = false) {
    const imgs = Array.from(container.querySelectorAll('img'));
    if (!imgs.length) return;

    if (sequential) {
      // Each img fades in individually as it loads
      imgs.forEach(img => {
        img.classList.add('seq-img');
        const reveal = () => img.classList.add('loaded');
        if (img.complete) reveal();
        else { img.addEventListener('load', reveal, { once: true }); img.addEventListener('error', reveal, { once: true }); }
      });
    } else {
      // Whole container fades in once all imgs loaded
      container.classList.add('grid-reveal');
      let pending = imgs.length;
      const done = () => { if (--pending <= 0) container.classList.add('loaded'); };
      imgs.forEach(img => {
        if (img.complete) done();
        else { img.addEventListener('load', done, { once: true }); img.addEventListener('error', done, { once: true }); }
      });
    }
  }

  function makeThumb(imgData, i, onClick) {
    const thumb = document.createElement('div');
    thumb.className = 'photo-thumb';
    if (imgData.thumb_width && imgData.thumb_height) {
      const ratio = imgData.thumb_width / imgData.thumb_height;
      const h = ratio < 0.85 ? 320 : ratio > 1.2 ? 200 : 260;
      thumb.style.height = h + 'px';
      thumb.style.width  = (h * ratio) + 'px';
    }
    const imgEl = document.createElement('img');
    imgEl.alt = imgData.title || imgData.caption || '';
    imgEl.src = imgData.thumb_url || imgData.url;
    if (editorSelected.has(imgData.id)) {
      thumb.classList.add('editor-selected');
      const c = document.createElement('div'); c.className = 'editor-check'; c.textContent = '✓';
      thumb.appendChild(c);
    }
    thumb.appendChild(imgEl);
    thumb.addEventListener('click', onClick);
    return thumb;
  }

  // ── CARD ──────────────────────────────────────────────────────────────────
  function makeCard(g) {
    const folder = isFolder(g);
    const count  = folder
      ? `${childrenOf(g.id).length} galler${childrenOf(g.id).length !== 1 ? 'ies' : 'y'}`
      : `${g.images?.[0]?.count ?? 0} image${(g.images?.[0]?.count ?? 0) !== 1 ? 's' : ''}`;

    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = g.cover_image_url
      ? `<img src="${g.cover_image_url}" alt="${g.name}" loading="lazy" />`
      : `<div class="card-placeholder">${folder ? '📁' : '🖼'}</div>`;
    card.innerHTML += `
      ${folder ? '<div class="card-folder-badge">Series</div>' : ''}
      <div class="card-label">
        <div class="card-label-name">${g.name}</div>
        <div class="card-label-meta">${count}</div>
      </div>`;
    card.addEventListener('click', () => folder ? openFolder(g) : openGallery(g));
    return card;
  }

  // ── ZOOM ──────────────────────────────────────────────────────────────────
  function openZoom(index) {
    zoomIndex = index;
    renderZoom();
    document.getElementById('zoom').classList.add('open');
    document.body.style.overflow = 'hidden';
  }

  function renderZoom(direction) {
    const img = currentImages[zoomIndex];
    const imgEl = document.getElementById('zoom-img');
    // Clean up any stray extra images left from previous sessions
    document.querySelectorAll('#zoom-img-wrap img:not(#zoom-img)').forEach(el => el.remove());
    // Close info panel on image change
    if (direction) {
      document.getElementById('zoom-info').classList.remove('open');
      document.getElementById('zoom-info-btn').classList.remove('active');
    }

    const doUpdate = () => {
      imgEl.src = (isPhone() ? img.thumb_url : null) || img.url;
      imgEl.alt = img.title || img.caption || '';
      if (direction && isPhone()) {
        imgEl.classList.remove('zoom-fading');
      }
    };

    if (direction && isPhone()) {
      imgEl.classList.add('zoom-fading');
      imgEl.addEventListener('transitionend', doUpdate, { once: true });
    } else {
      doUpdate();
    }
    document.getElementById('zoom-title').textContent   = img.title || '';
    document.getElementById('zoom-caption').textContent = img.description || img.caption || '';
    const galleryLabel = document.getElementById('zoom-gallery-label');
    const sourceGallery = currentGallery || (img.gallery_id && allGalleries.find(g => g.id === img.gallery_id));
    if (sourceGallery && !currentGallery) {
      galleryLabel.textContent = sourceGallery.name;
      galleryLabel.onclick = () => { closeZoom(); openGallery(sourceGallery); };
    } else {
      galleryLabel.textContent = '';
      galleryLabel.onclick = null;
    }
    document.getElementById('zoom-prev').disabled = zoomIndex === 0;
    document.getElementById('zoom-next').disabled = zoomIndex === currentImages.length - 1;

    const tagsEl = document.getElementById('zoom-tags');
    tagsEl.innerHTML = '';
    (img.tags || []).forEach(tag => {
      const span = document.createElement('span');
      span.className = 'zoom-tag';
      span.textContent = '#' + tag;
      span.addEventListener('click', () => {
        const returnImgId = currentImages[zoomIndex]?.id;
        closeZoom();
        tagReturnImgId = returnImgId;
        searchByTag(tag);
      });
      tagsEl.appendChild(span);
    });

    if (currentGallery) setURL({ gallery: currentGallery.slug, img: img.id });
    else if (activeTag)  setURL({ tag: activeTag, img: img.id });
  }

  function closeZoom() {
    const wasOpen = document.getElementById('zoom').classList.contains('open');
    document.getElementById('zoom').classList.remove('open');
    document.getElementById('zoom-img').src = '';
    document.body.style.overflow = '';
    if (_inRenderGalleryMode) return; // called from within renderGalleryMode — just close overlay, no re-render
    if (wasOpen && currentGallery && currentGallery.display_mode === 'lightbox') {
      renderGalleryMode('grid');
      document.querySelectorAll('.gv-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === 'grid'));
      setURL({ gallery: currentGallery.slug });
    } else if (currentGallery) {
      // If user toggled into lightbox mode, re-render grid on close
      if (currentGalleryMode === 'lightbox' && !_inCloseZoom) {
        _inCloseZoom = true;
        currentGalleryMode = 'grid';
        renderGalleryMode('grid');
        document.querySelectorAll('.gv-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === 'grid'));
        _inCloseZoom = false;
      }
      setURL({ gallery: currentGallery.slug });
    }
  }

  // ── ZOOM BACKGROUND COLOR PICKER ──────────────────────────────────────────
  (function() {
    const zoomEl   = document.getElementById('zoom');
    const picker   = document.getElementById('zoom-bg-picker');
    const label    = document.getElementById('zoom-bg-label');
    const LS_KEY   = 'zoom-bg-color';
    const DEFAULT  = '#f4ff61';

    function applyColor(hex) {
      zoomEl.style.background = hex;
      picker.value = hex;
    }

    applyColor(localStorage.getItem(LS_KEY) || DEFAULT);

    label.addEventListener('click', e => { e.stopPropagation(); picker.click(); });
    picker.addEventListener('input',  e => applyColor(e.target.value));
    picker.addEventListener('change', e => localStorage.setItem(LS_KEY, e.target.value));
  })();

  document.getElementById('zoom-close').addEventListener('click', closeZoom);
  document.getElementById('zoom-prev').addEventListener('click', () => { if (zoomIndex > 0) { zoomIndex--; renderZoom(); } });
  document.getElementById('zoom-next').addEventListener('click', () => { if (zoomIndex < currentImages.length - 1) { zoomIndex++; renderZoom(); } });

  // ── FULL-SCREEN ZONE NAVIGATION ───────────────────────────────────────────
  const zoomEl     = document.getElementById('zoom');
  const zoomCursor = document.getElementById('zoom-cursor');
  const zoomArrow  = document.getElementById('zoom-cursor-arrow');

  function zoomZone(e) {
    const x = e.clientX / window.innerWidth;
    const y = e.clientY / window.innerHeight;
    if (y < 0.18) return 'up';
    return x < 0.5 ? 'left' : 'right';
  }

  zoomEl.addEventListener('mousemove', e => {
    zoomCursor.style.left = e.clientX + 'px';
    zoomCursor.style.top  = e.clientY + 'px';
    if (e.target.closest('#zoom-tags') || e.target.closest('#zoom-close') || e.target.closest('#zoom-bg-label') || e.target.closest('#zoom-bg-picker') || e.target.closest('#zoom-info-btn') || e.target.closest('#zoom-info')) { zoomCursor.classList.remove('visible'); return; }
    zoomCursor.classList.add('visible');
    const zone = zoomZone(e);
    // up=0°(pointing up), right=90°, left=270°
    const rot = zone === 'up' ? 0 : zone === 'right' ? 90 : 270;
    zoomCursor.style.transform = `translate(-50%, -50%) rotate(${rot}deg)`;
    // dim arrow if at boundary (no prev/next)
    const atEdge = (zone === 'left' && zoomIndex === 0) || (zone === 'right' && zoomIndex === currentImages.length - 1);
    zoomArrow.style.opacity = atEdge ? '0.2' : '1';
  });

  zoomEl.addEventListener('mouseleave', () => zoomCursor.classList.remove('visible'));

  zoomEl.addEventListener('click', e => {
    if (e.target.closest('#zoom-tags') || e.target.closest('#zoom-info-btn') || e.target.closest('#zoom-info')) return;
    const zone = zoomZone(e);
    if (zone === 'up') { closeZoom(); return; }
    if (zone === 'left'  && zoomIndex > 0)                        { zoomIndex--; renderZoom(); }
    if (zone === 'right' && zoomIndex < currentImages.length - 1) { zoomIndex++; renderZoom(); }
  });

  document.addEventListener('keydown', e => {
    if (!document.getElementById('zoom').classList.contains('open')) return;
    if (e.key === 'ArrowLeft'  && zoomIndex > 0)                        { zoomIndex--; renderZoom(); }
    if (e.key === 'ArrowRight' && zoomIndex < currentImages.length - 1) { zoomIndex++; renderZoom(); }
    if (e.key === 'Escape') closeZoom();
  });

  let touchStartX = null, touchStartY = null, dragAxis = null;
  const zoomImgEl = () => document.getElementById('zoom-img');

  document.getElementById('zoom').addEventListener('touchstart', e => {
    touchStartX = e.touches[0].clientX;
    touchStartY = e.touches[0].clientY;
    dragAxis = null;
    zoomImgEl().style.transition = 'none';
  }, { passive: true });

  document.getElementById('zoom').addEventListener('touchmove', e => {
    e.preventDefault();
    if (touchStartX === null) return;
    const dx = e.touches[0].clientX - touchStartX;
    const dy = e.touches[0].clientY - touchStartY;
    const adx = Math.abs(dx), ady = Math.abs(dy);
    if (!dragAxis && Math.max(adx, ady) > 8) dragAxis = adx >= ady ? 'x' : 'y';
    if (!dragAxis) return;
    const delta = dragAxis === 'x' ? dx : dy;
    const goNext = delta < 0;
    const atEnd = goNext ? zoomIndex >= currentImages.length - 1 : zoomIndex <= 0;
    const damp = atEnd ? 0.08 : 0.25; // heavy resistance at boundary
    zoomImgEl().style.transform = dragAxis === 'x'
      ? `translateX(${dx * damp}px)`
      : `translateY(${dy * damp}px)`;
  }, { passive: false });

  document.getElementById('zoom').addEventListener('touchend', e => {
    if (touchStartX === null) return;
    const dx = e.changedTouches[0].clientX - touchStartX;
    const dy = e.changedTouches[0].clientY - touchStartY;
    touchStartX = touchStartY = null;
    const imgEl = zoomImgEl();
    const delta = dragAxis === 'x' ? dx : dy;
    const adx = Math.abs(dx), ady = Math.abs(dy);
    const goNext = delta < 0;
    const atEnd = dragAxis && (goNext ? zoomIndex >= currentImages.length - 1 : zoomIndex <= 0);

    if (dragAxis && Math.max(adx, ady) >= 40 && !atEnd) {
      // Slide out then navigate
      const outX = dragAxis === 'x' ? (dx < 0 ? -window.innerWidth * 0.6 : window.innerWidth * 0.6) : 0;
      const outY = dragAxis === 'y' ? (dy < 0 ? -window.innerHeight * 0.5 : window.innerHeight * 0.5) : 0;
      imgEl.style.transition = 'transform 0.18s ease-in, opacity 0.18s ease-in';
      imgEl.style.transform = `translate(${outX}px,${outY}px)`;
      imgEl.style.opacity = '0';
      setTimeout(() => {
        imgEl.style.transition = 'none';
        imgEl.style.transform = '';
        imgEl.style.opacity = '0';
        if (goNext) { zoomIndex++; renderZoom(); }
        else        { zoomIndex--; renderZoom(); }
        requestAnimationFrame(() => requestAnimationFrame(() => {
          imgEl.style.transition = 'opacity 0.4s ease';
          imgEl.style.opacity = '';
        }));
      }, 180);
    } else if (atEnd && Math.max(adx, ady) >= 20) {
      // Slide toward boundary then spring back to signal end of list
      const overshoot = dragAxis === 'x' ? `translateX(${goNext ? -55 : 55}px)` : `translateY(${goNext ? -45 : 45}px)`;
      imgEl.style.transition = 'transform 0.18s ease-out';
      imgEl.style.transform = overshoot;
      setTimeout(() => {
        imgEl.style.transition = 'transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)';
        imgEl.style.transform = '';
      }, 180);
    } else {
      // Snap back with a slight spring
      imgEl.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)';
      imgEl.style.transform = '';
    }
    dragAxis = null;
  }, { passive: true });

  // Info button — use touchend so it works through touch-action:none on #zoom
  document.getElementById('zoom-info-btn').addEventListener('touchend', e => {
    e.stopPropagation();
    e.preventDefault();
    const panel = document.getElementById('zoom-info');
    const btn   = document.getElementById('zoom-info-btn');
    panel.classList.toggle('open');
    btn.classList.toggle('active', panel.classList.contains('open'));
  }, { passive: false });

  // ── VIEW SWITCH ───────────────────────────────────────────────────────────
  function showView(id) {
    if (id !== 'view-tagbrowser') { clearInterval(tagCycleTimer); stopTagSelectCycle(); }
    document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
    document.getElementById(id).classList.add('active');
    window.scrollTo(0, 0);
    if (id !== 'view-home') document.getElementById('view-home').style.background = '';
    closeNavDropdown();
  }

  // ── URL ───────────────────────────────────────────────────────────────────
  function setURL(params) {
    const p = new URLSearchParams();
    if (editorMode)     p.set('editor',  '1');
    if (params.folder)  p.set('folder',  params.folder);
    if (params.gallery) p.set('gallery', params.gallery);
    if (params.tags)    p.set('tags',    params.tags);
    else if (params.tag) p.set('tags',  params.tag);
    if (params.img)     p.set('img',     params.img);
    if (params.page)    p.set('page',    params.page);
    if (params.blog)    p.set('blog',    params.blog);
    if (params.post)    p.set('post',    params.post);
    const qs = p.toString();
    history.replaceState(null, '', qs ? '?' + qs : location.pathname);
  }
</script>
</body>
</html>
