/* SPARK profile stylesheet — extracted from profile.html (build k).
   Content is a pure move from the inline <style> block; the split was
   verified byte-for-byte lossless at extraction time. */

        /* --- VERTICAL SPACING DRAG HANDLE --- */
        .spacing-drag-handle {
            background: rgba(0,0,0,0.8);
            border: 1px solid #555;
            color: white;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            padding: 4px 8px;
            cursor: ns-resize; /* Shows the up/down arrow cursor */
            user-select: none;
            display: inline-flex;
            align-items: center;
            gap: 5px;
            transition: all 0.2s;
        }
        .spacing-drag-handle:hover {
            background: black;
            border-color: white;
        }
        body.is-spacing-active {
            cursor: ns-resize !important; /* Locks cursor while dragging */
        }
        /* --- PANNING UX HINTS --- */
        .pan-hint {
            position: absolute;
            top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0,0,0,0.7);
            color: white;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            padding: 8px 12px;
            border: 1px dashed #777;
            pointer-events: none; /* Lets clicks pass through to the image! */
            opacity: 0;
            transition: opacity 0.2s;
            z-index: 15;
            white-space: nowrap;
            letter-spacing: 1px;
        }
        /* Only show hint in Layout mode, on supported grids, on hover */
        body.is-adjusting-layout .portfolio-hero-grid .work-item:hover .pan-hint,
        body.is-adjusting-layout .portfolio-square-grid .work-item:hover .pan-hint {
            opacity: 1;
        }
        /* Hide it while the user is actively dragging */
        body.is-panning-active .pan-hint {
            opacity: 0 !important;
        }
        /* --- THUMBNAIL PANNING CSS --- */
        body.is-adjusting-layout .work-media {
            cursor: move; /* Shows the pan/crosshair cursor */
            pointer-events: auto; /* Re-enable clicks so we can drag */
        }
        /* Makes sure the pan math is perfect */
        .portfolio-hero-grid .work-media, 
        .portfolio-square-grid .work-media { 
            object-fit: cover; 
            transform-origin: center center;
            will-change: object-position;
        }
        /* --- NEW GALLERY TEMPLATES --- */
        
        /* 1. Bento Box (Hero Grid) */
        .portfolio-hero-grid {
            display: grid !important;
            align-items: stretch !important; 
            grid-template-columns: repeat(var(--tile-cols, 4), 1fr);
            grid-auto-rows: 250px; 
            gap: var(--layout-gap, 15px);
            width: 100%;
        }
        .portfolio-hero-grid .work-item { margin: 0 !important; width: 100% !important; height: 100%; }
        /* Low column counts (Aug 9 2026, v2): the first pass spanned
           every second tile across both columns, which collapsed the
           whole grid into full-width strips. At two columns the pattern
           is now a hero every third piece (two columns and two rows),
           singles filling around it; at one column everything is a
           single cell. */
        .portfolio-hero-grid.tile-cols-2 .work-item { grid-column: span 1 !important; grid-row: span 1 !important; }
        .portfolio-hero-grid.tile-cols-2 .work-item:nth-child(3n + 1) { grid-column: span 2 !important; grid-row: span 2 !important; }
        .portfolio-hero-grid.tile-cols-1 .work-item { grid-column: span 1 !important; grid-row: span 1 !important; }
        .portfolio-hero-grid.tile-cols-1, .portfolio-hero-grid.tile-cols-2 { grid-auto-rows: 320px; }
        .portfolio-hero-grid .media-container, .portfolio-hero-grid .work-media { height: 100%; width: 100%; object-fit: cover !important; }
        
        /* The Math for the Bento Pattern (When viewing normally) */
        .portfolio-hero-grid:not(.has-add-card) .work-item:nth-child(4n + 1) { grid-column: span 2; grid-row: span 2; } 
        .portfolio-hero-grid:not(.has-add-card) .work-item:nth-child(4n + 2) { grid-column: span 2; grid-row: span 1; } 
        .portfolio-hero-grid:not(.has-add-card) .work-item:nth-child(4n + 3), 
        .portfolio-hero-grid:not(.has-add-card) .work-item:nth-child(4n + 4) { grid-column: span 1; grid-row: span 1; } 

        /* The Offset Math (When the Add Card is present as the 1st item!) */
        .portfolio-hero-grid.has-add-card .work-item:nth-child(4n + 2) { grid-column: span 2; grid-row: span 2; } /* Shifted +1 */
        .portfolio-hero-grid.has-add-card .work-item:nth-child(4n + 3) { grid-column: span 2; grid-row: span 1; } /* Shifted +1 */
        .portfolio-hero-grid.has-add-card .work-item:nth-child(4n + 4), 
        .portfolio-hero-grid.has-add-card .work-item:nth-child(4n + 5) { grid-column: span 1; grid-row: span 1; } /* Shifted +1 */

        /* The Add Card inside Bento Box (Forces it to take up a full neat row at the top) */
        .portfolio-hero-grid .add-card { grid-column: span 4 !important; min-height: 120px !important; margin: 0 !important; }

        /* Auto-hero promotion neutralised (Jun 2026). The bento pattern above
           promoted every nth-child(4n + 1) tile to a 2x2 block, which blew up
           the first piece on every profile and looked especially wrong on a
           single first upload (the lone tile filled the whole grid). Tiles now
           render uniform. Deliberate per-post width still comes from each
           post's own colSpan in masonry mode — this only kills the automatic
           promotion, not user-chosen widths. !important so it wins over the
           higher-specificity nth-child rules without needing to delete them. */
        .portfolio-hero-grid .work-item { grid-column: span 1 !important; grid-row: span 1 !important; }

        /* 2. Classic Square Grid */
        .portfolio-square-grid {
            display: grid !important;
            align-items: stretch !important; 
            grid-template-columns: repeat(var(--tile-cols, 5), 1fr);
            gap: var(--layout-gap, 15px);
            width: 100%;
        }
        .portfolio-square-grid .work-item { margin: 0 !important; width: 100% !important; aspect-ratio: 1; }
        .portfolio-square-grid .media-container, .portfolio-square-grid .work-media { height: 100%; width: 100%; object-fit: cover !important; }
        
        /* Fixes the Add Card so it becomes a perfect square too */
        .portfolio-square-grid .add-card { aspect-ratio: 1; min-height: 0 !important; height: 100%; margin: 0 !important; }

        /* The about-trigger is mobile-only. Default to hidden so it never appears on
           desktop. Mobile @media block below sets `display: block !important` to override.
           Defined HERE (before media queries) so the cascade works correctly — placing
           this rule after the @media block would let it always win at equal specificity. */
        .mobile-about-trigger { display: none; }

        /* Bento mobile override removed (Aug 10 2026): flattening the
           hero grid to one square column on phones both broke the look
           Felix wanted kept identical to desktop AND starved the lazy
           images of real dimensions, so tiles sat unloaded until the
           lightbox forced a fetch. The desktop pattern now renders at
           every width. */
        /* --- NATIVE DRAG RESIZE HANDLE --- */
        .resize-handle {
            position: absolute;
            bottom: 0;
            right: 0;
            width: 30px;
            height: 30px;
            cursor: se-resize;
            z-index: 30;
            display: none;
        }
        body.is-adjusting-layout .resize-handle {
            display: block; /* Only show in Layout Mode! */
        }
        .resize-handle::after {
            content: '';
            position: absolute;
            right: 6px;
            bottom: 6px;
            width: 10px;
            height: 10px;
            border-right: 2px solid white;
            border-bottom: 2px solid white;
            box-shadow: 1px 1px 0 rgba(0,0,0,0.5);
        }
        :root {
    /* User-themable extras for the Appearance editor */
    --bg-image: none;
    --bg-overlay-opacity: 0.4;
    --layout-gap: 1.5rem;
    --layout-page-padding: 2rem;
    --accent: var(--text);
    --accent-hover: var(--accent);
}

        * { margin: 0; padding: 0; box-sizing: border-box; }

        /* Page colour lives on <html> here (not body) because the film
           grain rides html::before on profile pages — an opaque body
           background would paint over it. Visually identical, and the
           overscroll rubber-band shows the same colour. */
        html { background-color: var(--bg); }
        body {
            background-color: transparent;
            color: var(--text);
            font-family: var(--font-sans);
            line-height: 1.5;
            overflow-x: hidden;
        }

        /* Film grain (Jul 2026, replaces the dotted grid). Everywhere else
           the grain arrives from tokens.css on body::before — but on
           profile pages body::before belongs to the user's custom
           background image (rule below), so the grain rides html::before
           instead. Same tile, same opacity as tokens.css. The customiser
           mirrors data-dots-off / data-has-bg onto <html> so the user
           toggle and photo-background suppression can reach it. */
        html::before {
            content: '';
            position: fixed;
            inset: 0;
            z-index: -1;
            pointer-events: none;
            /* User-adjustable via Customise Page > Background (Jul 2026).
               spark-customise.js sets --grain-opacity from the theme. */
            opacity: var(--grain-opacity, 0.09);
            background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23n)"/></svg>');
        }

        /* Background image + overlay (rendered via pseudo-elements).
           Both are display:none until a user uploads an image — the
           --bg-image-display var flips them on. NOTE: tokens.css also
           declares body::before (the global grain); this rule loads later
           and wins, but tokens' opacity: 0.09 would leak through and
           render the user's background at 9% — the explicit opacity: 1
           below cancels it. Profile grain lives on html::before above. */
        body::before {
            content: '';
            position: fixed;
            inset: 0;
            z-index: -2;
            opacity: 1; /* cancels tokens.css grain opacity leak */
            background-image: var(--bg-image);
            background-size: cover;
            background-position: center;
            background-attachment: fixed;
            pointer-events: none;
            display: var(--bg-image-display, none);
        }
        body::after {
            content: '';
            position: fixed;
            inset: 0;
            z-index: -1;
            background: var(--bg);
            opacity: var(--bg-overlay-opacity);
            pointer-events: none;
            display: var(--bg-image-display, none);
        }
        /* User toggle (Jun 11 2026, retextured Jul 2026): Customise Page >
           Background turns the film grain off. The attribute is mirrored
           onto <html> by spark-customise.js because the grain lives on
           html::before. */
        html[data-dots-off]::before { display: none; }
        /* Suppress the grain when a photo background is in use — texture
           over a user's image fights with it, same call as the old dots. */
        html[data-has-bg]::before { display: none; }
        /* Heading font — applied to the profile name and section titles.
           Falls back to font-main when not customised because --font-sans
           defaults to the same value as --font-sans in :root. */
        h1, h2, h3, h4, h5, h6,
        #p-name, .section-title, .feed-title {
            /* Jun 11 2026: headings get their own themable var. Previously
               applyThemeObject set --font-sans twice (heading then body),
               so the "Heading font" picker silently did nothing. */
            font-family: var(--font-heading, var(--font-sans));
        }

        /* --- CUSTOM NAME IMAGE (May 8) --------------------------------
           The profile owner can upload a custom graphic (typography /
           logotype / signature) via the appearance editor's Type tab.
           Two states managed by data-name-mode on #topbar-identity:

             default                → text shown, image hidden
             data-name-mode="image" → image shown, text hidden
                                       (text stays in DOM for a11y/SEO)

           Image height capped at 80px on desktop, 60px on mobile, with
           object-fit:contain so weird aspect ratios don't crop awkwardly.
           Width auto so wider logotypes get more horizontal space.
           ============================================================== */
        #p-name-image {
            display: none;
            max-height: 80px;
            max-width: 100%;
            width: auto;
            object-fit: contain;
            object-position: left center;
        }
        #topbar-identity[data-name-mode="image"] #p-name {
            /* Hide the text node visually but keep it readable by screen
               readers and crawlers. Standard accessible-hide pattern. */
            position: absolute;
            width: 1px;
            height: 1px;
            padding: 0;
            margin: -1px;
            overflow: hidden;
            clip: rect(0, 0, 0, 0);
            white-space: nowrap;
            border: 0;
        }
        #topbar-identity[data-name-mode="image"] #p-name-image {
            display: block;
        }
        @media (max-width: 768px) {
            #p-name-image { max-height: 60px; }
        }

        /* --- NAVIGATION --- */
        nav { 
            padding: 1.5rem 2rem; 
            display: flex; 
            justify-content: space-between; 
            align-items: center; 
            border-bottom: 1px solid var(--border);
            background: var(--bg);
            position: sticky;
            top: 0;
            z-index: 100;
            /* Hide-on-scroll-down, show-on-scroll-up — same pattern as
               featured.html. Scrolling down past a small threshold slides
               the nav up out of view for a more immersive read; scrolling
               up (or near the top) brings it back. Driven by onNavScroll()
               toggling body.nav-hidden. */
            transform: translateY(0);
            transition: transform 0.3s ease;
        }
        body.nav-hidden nav {
            transform: translateY(-100%);
        }
        .nav-left { display: flex; align-items: center; } 
        .nav-right { display: flex; align-items: center; gap: 20px; }

        .logo { font-size: 1.2rem; font-weight: 700; text-decoration: none; color: var(--text, white); text-transform: uppercase; margin-right: 40px;}
        
        
        .nav-links a { margin-right: 20px; color: var(--text-muted); text-decoration: none; font-size: 0.9rem; }
        .nav-links a:hover, .nav-links a.active { color: var(--text, white); }

        .btn-auth {
            background-color: white;
            color: black;
            font-family: var(--font-mono);
            font-size: 0.8rem;
            font-weight: 700;
            text-transform: uppercase;
            text-decoration: none;
            padding: 0.6rem 1.2rem;
            border: 1px solid white;
            transition: all 0.2s ease;
            display: inline-block;
            cursor: pointer;
        }
        .btn-auth:hover { background-color: transparent; color: white; }

        /* --- NO AUTH WALL ---
           Logged-out visitors hitting their own /profile.html (no ?user=
           param, no username path slug) get this card instead of the
           empty "Your Name / Your Discipline" placeholder template that
           used to leak through. Matches the visual pattern from
           messages.html for consistency across logged-out states. */
        .login-wall {
            display: none;  /* flipped to flex by init() when logged-out + own-profile */
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: calc(100vh - 80px);
            text-align: center;
            padding: 2rem;
        }
        .login-wall h2 { font-size: 2rem; margin-bottom: 1rem; }
        .login-wall p {
            color: var(--text-muted, #888);
            margin-bottom: 2rem;
            max-width: 420px;
            line-height: 1.5;
        }

/* --- GHOST TEXT PLACEHOLDERS --- */
        [placeholder]:empty::before {
            content: attr(placeholder);
            color: #555;
            pointer-events: none;
            cursor: text;
        }
        #p-bio[placeholder]:empty::before {
            display: block; /* Prevents the multiline bio box from collapsing */
        }

        /* --- NOTIFICATION STYLES --- */
        .notif-wrapper { position: relative; display: flex; align-items: center; }
        
        .btn-notif {
            background: transparent;
            border: none;
            color: var(--text-muted);
            cursor: pointer;
            position: relative;
            padding: 8px;
            transition: color 0.2s;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .btn-notif:hover { color: white; }
        
        .notif-dot {
            position: absolute;
            top: 6px;
            right: 6px;
            width: 6px;
            height: 6px;
            background: #ff3b30;
            border-radius: 50%;
            display: none; 
            pointer-events: none;
        }
        .notif-dot.active { display: block; }

        .notif-dropdown { 
            position: absolute; top: 100%; right: 0; margin-top: 10px; width: 320px; 
            background: #1a1a1a; border: 1px solid var(--border); display: none; 
            flex-direction: column; box-shadow: 0 10px 40px rgba(0,0,0,0.8); z-index: 200; 
            /* NEW: Scroll limits */
            max-height: 380px; /* ~3.5 items deep */
            overflow-y: auto; 
        }
        .notif-dropdown.show { display: flex; }
        .notif-header { 
            padding: 15px; border-bottom: 1px solid var(--border); 
            font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase; 
            color: var(--text-muted); background: #151515; 
            /* NEW: Keeps header pinned to the top while scrolling */
            position: sticky; top: 0; z-index: 10; 
        }

        .notif-item {
            padding: 15px;
            border-bottom: 1px solid #222;
            cursor: pointer;
            transition: background 0.2s;
            display: flex;
            gap: 12px;
            align-items: start;
        }
        .notif-item:hover { background: #222; }
        .notif-item:last-child { border-bottom: none; }
        
        .notif-icon { 
            font-size: 1rem; 
            color: white;
            background: #333;
            width: 24px;
            height: 24px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 50%;
            font-family: serif; 
        }
        .notif-content { flex: 1; }
        .notif-text { font-size: 0.85rem; color: #ddd; line-height: 1.4; display: block; margin-bottom: 4px; }
        .notif-time { font-family: var(--font-mono); font-size: 0.65rem; color: #666; }

        /* --- LAYOUT --- */
        .layout-grid {
            display: grid;
            grid-template-columns: 400px 1fr;
            min-height: 100vh;
            border-left: 1px solid var(--border);
            border-right: 1px solid var(--border);
            max-width: 1600px;
            margin: 0 auto;
            transition: grid-template-columns 0.3s ease, opacity 0.18s ease-out;
        }

        /* --- LOADING STATE ---
           Hide the profile content until either loadProfile() or
           loadReadOnlyProfile() has populated the DOM. Prevents the
           flash-of-default-content where placeholder copy ("Your Name",
           "Your Discipline", "City, Country", "Write a short bio…")
           paints first and then gets overwritten when Supabase resolves.

           Header + dotted background stay visible during the brief load
           — only the profile body is held back, so users see a clean
           page rather than a mid-loaded ghost.

           Reveal is triggered by removing 'profile-loading' from <body>
           at the end of each loader. A 3s setTimeout safety net (in the
           inline script just after <body>) clears the class regardless,
           so a slow Supabase or unhandled error never strands the user
           on a blank page. */
        body.profile-loading .layout-grid {
            opacity: 0;
        }

        /* Smooth the reveal of the whole layout when profile-loading clears.
           Previously this was an instant opacity 0→1 pop the moment the
           class was removed, which read as a "flash." 400ms ease-in is
           slow enough to feel deliberate, fast enough that nobody waits. */
        .layout-grid {
            transition: opacity 0.4s ease-in;
        }

        /* --- PORTFOLIO IMAGE CASCADE ---
           Each .work-media image fades in over 0.6s with a 70ms stagger
           between adjacent items, producing a left-to-right cascade as
           the portfolio renders. Pure CSS — runs reliably regardless of
           how fast individual images load over the network (the old
           onload-based approach gave no stagger because thumbnails came
           back near-simultaneously and snapped in together).

           The animation runs whenever .work-media enters the DOM, which
           includes re-renders triggered by edits. The body.is-editing /
           is-adjusting-layout selectors below kill the animation during
           those interactions so the page doesn't re-cascade every time
           the user clicks edit — only the initial load gets the effect. */
        /* Progressive-load fade-in (Stage 1, May 14 2026).
           Tiles with width/height in their data get reserved space via
           aspect-ratio. The tile sits at opacity 0; the .is-loaded class
           is added by JS when its <img> fires the load event. Top tiles
           (which start downloading first) fade in first, then the rest
           cascade down as their bytes arrive. No big "flash" because
           the layout was already correct before any image was visible. */
        .work-item.has-progressive-load {
            aspect-ratio: var(--tile-aspect);
        }
        .work-item.has-progressive-load .work-media,
        .work-item.has-progressive-load .media-container > img,
        .work-item.has-progressive-load .media-container > div > img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        .work-item.has-progressive-load {
            opacity: 0;
            transition: opacity 0.45s ease-out;
        }
        .work-item.has-progressive-load.is-loaded {
            opacity: 1;
        }
        /* Skip the fade entirely for editing modes — feels jarring if
           every save replays the entrance. Matches the existing pattern. */
        body.is-editing .work-item.has-progressive-load,
        body.is-adjusting-layout .work-item.has-progressive-load {
            opacity: 1 !important;
            transition: none !important;
        }

        /* Filter hide (Stage 2, May 14 2026).
           When applyFilter() toggles .is-filtered-out on a tile, it
           disappears from the masonry. display:none is what removes it
           from the flex flow — the remaining items in that column shift
           up automatically with no JS layout work.
           We DON'T animate the hide because:
             1. display:none can't transition, so any "fade" would need
                opacity tricks + height collapsing — visually mushy.
             2. Instant disappear feels more responsive than animation
                when the user has explicitly clicked a filter button.
           The matching items that REMAIN visible already had .is-loaded,
           so they don't replay the fade-in. Net effect on filter click:
           non-matching tiles vanish, matching tiles stay put — fastest
           possible perceived response. */
        .work-item.is-filtered-out {
            display: none !important;
        }

        /* Old position-staggered fade. Keep for tiles WITHOUT progressive
           load (data without width/height — older entries) so they still
           get a fade-in instead of appearing instantly. */
        .work-item:not(.has-progressive-load) .work-media {
            opacity: 0;
            animation: workMediaFadeIn 0.6s ease-out forwards;
        }
        @keyframes workMediaFadeIn {
            from { opacity: 0; }
            to   { opacity: 1; }
        }
        body.is-editing .work-item:not(.has-progressive-load) .work-media,
        body.is-adjusting-layout .work-item:not(.has-progressive-load) .work-media {
            animation: none !important;
            opacity: 1 !important;
        }
        /* Stagger 70ms per item up to the 20th. Items past 20 use the
           same delay as item 20 so a long portfolio doesn't make the
           last image wait 5+ seconds. */
        .work-item:nth-child(1)  .work-media { animation-delay: 0ms; }
        .work-item:nth-child(2)  .work-media { animation-delay: 70ms; }
        .work-item:nth-child(3)  .work-media { animation-delay: 140ms; }
        .work-item:nth-child(4)  .work-media { animation-delay: 210ms; }
        .work-item:nth-child(5)  .work-media { animation-delay: 280ms; }
        .work-item:nth-child(6)  .work-media { animation-delay: 350ms; }
        .work-item:nth-child(7)  .work-media { animation-delay: 420ms; }
        .work-item:nth-child(8)  .work-media { animation-delay: 490ms; }
        .work-item:nth-child(9)  .work-media { animation-delay: 560ms; }
        .work-item:nth-child(10) .work-media { animation-delay: 630ms; }
        .work-item:nth-child(11) .work-media { animation-delay: 700ms; }
        .work-item:nth-child(12) .work-media { animation-delay: 770ms; }
        .work-item:nth-child(13) .work-media { animation-delay: 840ms; }
        .work-item:nth-child(14) .work-media { animation-delay: 910ms; }
        .work-item:nth-child(15) .work-media { animation-delay: 980ms; }
        .work-item:nth-child(16) .work-media { animation-delay: 1050ms; }
        .work-item:nth-child(17) .work-media { animation-delay: 1120ms; }
        .work-item:nth-child(18) .work-media { animation-delay: 1190ms; }
        .work-item:nth-child(19) .work-media { animation-delay: 1260ms; }
        .work-item:nth-child(n+20) .work-media { animation-delay: 1330ms; }

        /* --- COLLAPSIBLE SIDEBAR STATES --- */
        /* When collapsed: sidebar width becomes 0, main takes everything */
        .layout-grid.sidebar-collapsed {
            grid-template-columns: 0 1fr;
        }
        .layout-grid.sidebar-collapsed .profile-sidebar {
            transform: translateX(-100%);
            opacity: 0;
            pointer-events: none;
            padding-left: 0;
            padding-right: 0;
        }
        .profile-sidebar {
            transition: transform 0.3s ease, opacity 0.3s ease, padding 0.3s ease;
        }

        /* Invisible hover zone on the left edge — reveals expand arrow */
        .sidebar-hover-zone {
            position: fixed;
            top: 0;
            left: 0;
            width: 24px;
            height: 100vh;
            z-index: 40;
            display: none;
        }
        .layout-grid.sidebar-collapsed ~ .sidebar-hover-zone {
            display: block;
        }

        /* The arrow button that pops up on hover */
        .sidebar-toggle-arrow {
            position: fixed;
            top: 50%;
            left: 16px;
            transform: translateY(-50%);
            width: 32px;
            height: 64px;
            background: #aaa;
            border: 1px solid var(--border);
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #1a1a1a;
            cursor: pointer;
            z-index: 50;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.2s ease, background 0.2s ease, left 0.3s ease;
            font-family: var(--font-mono);
        }
        .sidebar-toggle-arrow:hover {
            background: #2a2a2a;
            color: white;
        }
        .sidebar-toggle-arrow svg {
            width: 14px;
            height: 14px;
        }
        /* Show arrow when sidebar collapsed AND hover zone is active */
        .sidebar-hover-zone:hover ~ .sidebar-toggle-arrow,
        .sidebar-toggle-arrow:hover,
        .sidebar-toggle-arrow.force-visible {
            opacity: 1;
            pointer-events: auto;
        }
        /* Collapse arrow pinned to sidebar's right edge when expanded (owner can quickly hide) */
        .sidebar-collapse-btn {
            position: absolute;
            top: 12px;
            right: 12px;
            width: 28px;
            height: 28px;
            background: #1a1a1a;
            border: 1px solid var(--border);
            border-radius: 50%;
            display: none;
            align-items: center;
            justify-content: center;
            color: #888;
            cursor: pointer;
            z-index: 10;
            transition: background 0.2s ease, color 0.2s ease;
        }
        .profile-sidebar:hover .sidebar-collapse-btn {
            display: flex;
        }
        .sidebar-collapse-btn:hover {
            background: #2a2a2a;
            color: white;
        }
        .sidebar-collapse-btn svg {
            width: 12px;
            height: 12px;
        }
        /* Hide collapse affordance while editing — sidebar must stay open */
        body.is-editing .sidebar-collapse-btn {
            display: none !important;
        }
        /* EXCEPTION: when the user revealed the sidebar via "Add Additional
           Information" on an empty profile, they need a way to dismiss it
           again, so show the collapse button (on hover, like normal) even
           though edit mode is active. */
        body.is-editing.sidebar-info-revealed .profile-sidebar:hover .sidebar-collapse-btn {
            display: flex !important;
        }

        /* When sidebar is on the right side of the page, mirror the collapse button
           to the sidebar's INNER edge (which is now the left edge) so it doesn't get
           clipped by the screen edge. */
        .layout-grid.layout-right .sidebar-collapse-btn {
            right: auto;
            left: 12px;
        }
        .layout-grid.layout-right .sidebar-collapse-btn svg {
            transform: rotate(180deg);
        }

        /* --- PROFILE TOPBAR (always visible identity band above main content) --- */
        .profile-topbar {
            display: flex;
            align-items: center;
            gap: 1.5rem;
            padding: 2rem 2rem 1.5rem;
            border-bottom: 1px solid var(--border);
            position: relative;
        }
        /* ============================================================
           HERO COVER — cycling cover behind the profile header.
           A separate control from the page Background. Five layouts,
           driven by body[data-cover-layout]. Disabled by default
           (hidden unless body has data-cover-on). Each .hero-cover-slide
           is a cross-fading image; only one is .is-active at a time.
           ============================================================ */
        .hero-cover { display: none; }
        body[data-cover-on] .hero-cover {
            display: block;
            position: absolute;
            inset: 0;
            overflow: hidden;
            z-index: 0;
        }
        body[data-cover-on] .profile-topbar > .topbar-avatar,
        body[data-cover-on] .profile-topbar > .topbar-identity,
        body[data-cover-on] .profile-topbar > .topbar-actions {
            position: relative;
            z-index: 2;
        }
        /* CONTRAST OVER COVERS (Jun 11 2026). The action buttons sat
           directly on the cover photo with transparent backgrounds —
           illegible on busy imagery. Detecting luminance and flipping
           text colour fails on mixed light/dark images, so instead every
           button gets a frosted dark scrim: guaranteed contrast on any
           cover, no detection needed. The primary Upload button is
           already solid and stays as-is. */
        body[data-cover-on] .profile-topbar .topbar-actions .btn:not(.btn-primary) {
            background: rgba(8, 8, 8, 0.55);
            -webkit-backdrop-filter: blur(10px);
            backdrop-filter: blur(10px);
            border-color: rgba(255, 255, 255, 0.45);
            color: #fff;
        }
        body[data-cover-on] .profile-topbar .topbar-actions .btn:not(.btn-primary):hover {
            background: rgba(0, 0, 0, 0.78);
            border-color: #fff;
        }
        body[data-cover-on] .profile-topbar .topbar-actions .icon-btn {
            background: rgba(8, 8, 8, 0.55);
            -webkit-backdrop-filter: blur(10px);
            backdrop-filter: blur(10px);
            color: #fff;
        }
        /* Identity text gets a soft shadow lift for the same reason —
           subtle, but keeps role/location readable on light covers. */
        body[data-cover-on] .profile-topbar > .topbar-identity {
            text-shadow: 0 1px 12px rgba(0, 0, 0, 0.65);
        }
        .hero-cover-track { position: absolute; inset: 0; }
        .hero-cover-slide {
            position: absolute; inset: 0;
            background-size: cover; background-position: center;
            opacity: 0; transition: opacity 1.1s ease;
        }
        .hero-cover-slide.is-active { opacity: 1; }
        /* Only the active slide takes clicks (to open the shown work in the
           lightbox); inactive slides underneath must not intercept. */
        .hero-cover-slide { pointer-events: none; }
        body[data-cover-on] .hero-cover-slide.is-active { pointer-events: auto; cursor: pointer; }
        body.is-editing .hero-cover-slide.is-active { cursor: default; }
        .hero-cover-scrim { position: absolute; inset: 0; pointer-events: none; }
        .hero-cover-dots {
            position: absolute; display: flex; gap: 5px; z-index: 3;
            bottom: 14px; right: 18px;
        }
        .hero-cover-dots span {
            width: 5px; height: 3px; border-radius: 2px;
            background: rgba(255,255,255,0.4); transition: width 0.3s, background 0.3s;
        }
        .hero-cover-dots span.is-active { width: 14px; background: #fff; }

        /* When a cover is on, the header text reads light over the image.
           Applies to every layout except split (2), whose text sits on the
           page background beside the cover, so it keeps theme colours.
           RENUMBERED (Jul 22 2026): the scheme is now label-true, matching
           the Customise picker: 1 full-bleed, 2 split cover-left,
           3 centred, 4 tall cinematic, 5 framed / inset. Legacy value 7
           (the old framed strip) stays styled inline in profile.html for
           profiles that saved it under the old numbering. This block is
           the SINGLE home for cover layouts: the old inline picker-true
           set in profile.html is deleted. */
        body[data-cover-on]:not([data-cover-layout="2"]) .profile-topbar h1 {
            color: #fff;
        }
        body[data-cover-on]:not([data-cover-layout="2"]) .profile-topbar .topbar-role,
        body[data-cover-on]:not([data-cover-layout="2"]) .profile-topbar .topbar-meta,
        body[data-cover-on]:not([data-cover-layout="2"]) .profile-topbar .topbar-ig a {
            color: rgba(255, 255, 255, 0.78);
        }

        /* 1 — Full-bleed banner: the classic band, name bottom-left,
           gradient up from the bottom. Unchanged from the original. */
        body[data-cover-on][data-cover-layout="1"] .profile-topbar { min-height: 480px; align-items: flex-end; }
        /* Width-proportional cover height (Aug 11 2026): a fixed 480px
           band on a wide viewport letterboxes a 16:9 cover at up to 8:1
           and object-fit cover then shows a sliver of the frame — the
           "super zoomed" cover at low page zoom / on big monitors. The
           band now grows with the viewport width, capped so tall
           windows keep the grid above the fold. Desktop only; phones
           keep their existing height. */
        @media (min-width: 901px) {
            body[data-cover-on][data-cover-layout="1"] .profile-topbar {
                min-height: clamp(480px, 42vw, 82vh);
            }
        }
        body[data-cover-on][data-cover-layout="1"] .hero-cover-scrim {
            background: linear-gradient(to top, rgba(0,0,0,0.78), rgba(0,0,0,0.05));
        }

        /* 2 — Split, cover left: the cover owns the left 46%, the details
           stack in a column on the right over the page background. The
           column stack is deliberate: the old row version squeezed the
           name to a sliver (the Jun split-fix saga). */
        body[data-cover-on][data-cover-layout="2"] .profile-topbar {
            min-height: 460px;
            padding: 2.5rem 2.5rem 2.5rem calc(46% + 2.5rem);
            flex-direction: column;
            flex-wrap: nowrap;
            justify-content: center;
            align-items: flex-start;
        }
        body[data-cover-on][data-cover-layout="2"] .hero-cover { right: 54%; }
        body[data-cover-on][data-cover-layout="2"] .hero-cover-scrim { background: none; }
        body[data-cover-on][data-cover-layout="2"] .hero-cover-dots { bottom: 12px; left: 14px; right: auto; }
        body[data-cover-on][data-cover-layout="2"] .topbar-identity { flex: 0 0 auto; width: auto; min-width: 0; }
        body[data-cover-on][data-cover-layout="2"] .topbar-actions {
            flex: 0 0 auto;
            width: auto;
            margin: 1.25rem 0 0 0;
            justify-content: flex-start;
            flex-wrap: wrap;
        }
        body[data-cover-on][data-cover-layout="2"] .profile-topbar h1 {
            font-size: clamp(1.75rem, 4vw, 3.25rem);
            line-height: 1.05;
        }

        /* 3 — Centred: avatar, name and actions centred over the cover. */
        body[data-cover-on][data-cover-layout="3"] .profile-topbar {
            min-height: 480px;
            padding: 3rem 2.5rem;
            flex-direction: column;
            flex-wrap: nowrap;
            align-items: center;
            justify-content: center;
            text-align: center;
        }
        body[data-cover-on][data-cover-layout="3"] .hero-cover-scrim { background: rgba(0, 0, 0, 0.4); }
        body[data-cover-on][data-cover-layout="3"] .hero-cover-dots { bottom: 12px; left: 50%; right: auto; transform: translateX(-50%); }
        body[data-cover-on][data-cover-layout="3"] .topbar-identity { flex: 0 0 auto; width: auto; align-items: center; text-align: center; }
        body[data-cover-on][data-cover-layout="3"] .topbar-actions {
            flex: 0 0 auto;
            width: auto;
            margin: 1.25rem 0 0 0;
            justify-content: center;
            flex-wrap: wrap;
        }

        /* 4 — Tall cinematic: a genuinely tall full-bleed hero, name
           bottom-left over a strong bottom-up scrim. */
        body[data-cover-on][data-cover-layout="4"] .profile-topbar {
            min-height: 72vh;
            padding: 2.5rem;
            align-items: flex-end;
            align-content: flex-end;
            flex-wrap: wrap;
        }
        body[data-cover-on][data-cover-layout="4"] .hero-cover-scrim {
            background: linear-gradient(to top, rgba(0,0,0,0.85), rgba(0,0,0,0.12) 55%, rgba(0,0,0,0));
        }
        body[data-cover-on][data-cover-layout="4"] .hero-cover-dots { bottom: 14px; left: 50%; right: auto; transform: translateX(-50%); }
        body[data-cover-on][data-cover-layout="4"] .topbar-actions { flex: 0 0 auto; width: auto; margin-left: auto; }

        /* 5 — Framed / inset: the cover floats in a rounded frame with a
           margin; the base .hero-cover overflow clips the slides to the
           radius. Soft scrim keeps the name readable inside the frame. */
        body[data-cover-on][data-cover-layout="5"] .profile-topbar {
            min-height: 460px;
            padding: 3rem;
            align-items: flex-end;
        }
        body[data-cover-on][data-cover-layout="5"] .hero-cover {
            top: 1.25rem;
            right: 1.25rem;
            bottom: 1.25rem;
            left: 1.25rem;
            border-radius: 16px;
            border: 1px solid rgba(255, 255, 255, 0.16);
        }
        body[data-cover-on][data-cover-layout="5"] .hero-cover-scrim {
            background: linear-gradient(to top, rgba(0,0,0,0.7), rgba(0,0,0,0) 55%);
        }
        body[data-cover-on][data-cover-layout="5"] .hero-cover-dots { bottom: 26px; right: 30px; }
        body[data-cover-on][data-cover-layout="5"] .topbar-actions { flex: 0 0 auto; width: auto; margin-left: auto; }

        @media (max-width: 900px) {
            body[data-cover-on][data-cover-layout="2"] .profile-topbar { padding: 2rem 1rem; min-height: 320px; }
            body[data-cover-on][data-cover-layout="2"] .hero-cover { right: 0; }
            body[data-cover-on][data-cover-layout="3"] .profile-topbar { min-height: 340px; padding: 2rem 1rem; }
            body[data-cover-on][data-cover-layout="4"] .profile-topbar { min-height: 52vh; }
            body[data-cover-on][data-cover-layout="5"] .hero-cover { top: 0.75rem; right: 0.75rem; bottom: 0.75rem; left: 0.75rem; border-radius: 12px; }
        }

        .profile-topbar .topbar-avatar {
            flex-shrink: 0;
            width: 80px;
            height: 80px;
            border-radius: 50%;
            overflow: hidden;
            background: #1a1a1a;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #555;
            font-size: 2rem;
            border: 1px solid var(--border);
        }
        .profile-topbar .topbar-avatar img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        .profile-topbar .topbar-identity {
            flex: 1;
            min-width: 0;
        }
        .profile-topbar h1 {
            font-size: clamp(1.8rem, 4vw, 2.6rem);
            font-weight: 700;
            line-height: 1;
            margin: 0 0 8px 0;
            letter-spacing: -0.02em;
            /* Render the name in whatever case the data carries — i.e. Title
               Case for "Dmytro Pochkun", "Felix Welsh", etc. We previously
               forced text-transform: lowercase as a brutalist aesthetic
               choice, but it made the names feel impersonal. Title Case
               headline against the mono ALL-CAPS role line below it keeps
               enough contrast to retain the editorial/brutalist hierarchy. */
        }
        .profile-topbar .topbar-role {
            font-family: var(--font-mono);
            font-size: 0.68rem;
            /* Jul 15 2026: theme-mixed instead of hardcoded #aaa, and
               quieted with the rest of the mono labels. */
            color: color-mix(in srgb, var(--text, #ffffff) 62%, transparent);
            text-transform: uppercase;
            letter-spacing: 0.14em;
            margin-right: 18px;
        }
        .profile-topbar .topbar-meta {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 14px;
            margin-top: 8px;
            font-family: var(--font-mono);
            font-size: 0.64rem;
            color: color-mix(in srgb, var(--text, #ffffff) 55%, transparent);
            text-transform: uppercase;
            letter-spacing: 0.12em;
        }
        /* Instagram handle beneath the location (Jul 22 2026): quiet mono
           link derived from the member's socials; brightens on hover and
           flips light over covers alongside the meta line. */
        .profile-topbar .topbar-ig { margin-top: 4px; }
        .profile-topbar .topbar-ig a {
            font-family: var(--font-mono);
            font-size: 0.72rem;
            letter-spacing: 0.08em;
            color: color-mix(in srgb, var(--text) 72%, transparent);
            text-decoration: none;
        }
        .profile-topbar .topbar-ig a:hover {
            color: var(--text);
            text-decoration: underline;
        }
        .profile-topbar .topbar-meta .meta-dot {
            display: inline-block;
            width: 3px;
            height: 3px;
            border-radius: 50%;
            background: #555;
        }
        .profile-topbar .topbar-actions {
            display: flex;
            gap: 10px;
            flex-shrink: 0;
        }
        .profile-topbar .topbar-actions .btn {
            padding: 10px 18px;
            font-size: 0.75rem;
            white-space: nowrap;
        }

        /* Empty-sidebar notice (visible only when owner views own empty sidebar) */
        .sidebar-empty-hint {
            display: none;
            padding: 16px;
            margin-top: 8px;
            border: 1px dashed #333;
            border-radius: 4px;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            color: #777;
            text-align: center;
            line-height: 1.5;
        }
        /* Retired (Jun 11 2026): the reveal rule below used to surface
           this nag in edit mode, duplicating the setup checklist's
           "write a short bio" prompt. One prompt per task — the
           checklist owns onboarding now. JS still toggles
           .has-empty-sections; it just no longer reveals anything. */
        /* body.is-editing .sidebar-empty-hint.has-empty-sections { display: block; } */

        /* When sidebar is collapsed, the feed-tabs no longer need the sidebar's border-left */
        .layout-grid.sidebar-collapsed main {
            border-left: none;
        }

        /* === MOBILE — TABLET & PHONE (Cosmos-style 2-col, inline sidebar) === */
        /* Strategy: drop the slide-in drawer entirely on mobile. The sidebar (bio,
           skills, links, etc.) renders inline BELOW the work feed using flex-order,
           matching the IG profile model: visitors see work first, scroll for context.
           This eliminates the drawer-state bugs, the awkward toggle-arrow on touch,
           the body-scroll-lock fragility, and the half-height-drawer iOS issue.
           It also collapses the layout to a single vertical column, so any masonry
           overflow can't push the page wider than viewport. */
        @media (max-width: 900px) {
            /* Layout: vertical stack, feed first, sidebar after */
            .layout-grid,
            .layout-grid.sidebar-collapsed,
            .layout-grid.layout-right {
                display: flex !important;
                flex-direction: column !important;
                grid-template-columns: 1fr !important; /* belt-and-braces vs grid override */
            }
            .layout-grid main { order: 1; }
            .layout-grid .profile-sidebar { order: 2; }

            /* Sidebar: regular inline content, not a drawer.
               Override !important rules from desktop layout-right and any stragglers. */
            .profile-sidebar {
                position: relative !important;
                top: auto !important;
                left: auto !important;
                width: 100% !important;
                max-width: 100% !important;
                height: auto !important;
                transform: none !important;
                /* CRITICAL: also neutralize the desktop drawer-collapse rule that fires
                   when initSidebarState() adds `sidebar-collapsed` to layout-grid on mobile
                   (legacy from the old drawer model). Without these overrides the sidebar
                   stays opacity:0 / pointer-events:none even when our accordion expands it. */
                opacity: 1 !important;
                pointer-events: auto !important;
                z-index: auto !important;
                border-right: none !important;
                border-bottom: none !important;
                border-top: 1px solid var(--border) !important;
                box-shadow: none !important;
                padding: 2rem 1rem !important;
            }

            /* Hide drawer-only affordances on mobile — they don't apply when sidebar is inline */
            .sidebar-toggle-arrow,
            .sidebar-hover-zone,
            .sidebar-backdrop,
            .profile-sidebar .sidebar-collapse-btn { display: none !important; }

            /* Topbar: tighter padding so the feed appears as soon as possible above the fold */
            .profile-topbar {
                flex-wrap: wrap;
                padding: 1.25rem 1rem 1rem;
                gap: 1rem;
            }
            .profile-topbar .topbar-avatar { width: 64px; height: 64px; }
            .profile-topbar h1 {
                /* Floor lowered to 1.25rem so "morganzaky" doesn't clip on narrow phones */
                font-size: clamp(1.25rem, 6.5vw, 2rem);
            }
            .profile-topbar .topbar-actions {
                width: 100%;
                margin-top: 4px;
            }
            .profile-topbar .topbar-actions .btn {
                flex: 1;
                text-align: center;
            }

            /* 2-COL FEED (Cosmos-style spacing). The 2-col count is forced in JS too;
               this is the visual side. 12px outer padding + 12px gap matches Cosmos's
               mobile feed proportions — distinct visible breathing room between tiles
               without becoming an editorial-style spaced grid. */
            .portfolio-feed {
                padding: 12px !important;
                gap: 12px !important;
            }
            .masonry-col {
                gap: 12px !important;
                min-width: 0 !important;
                max-width: 100% !important;       /* SAFETY: never let a column expand past its flex share */
            }
            .work-item {
                width: 100% !important;
                max-width: 100% !important;       /* SAFETY: never let a tile push beyond its column */
                border-width: 0 !important;        /* tighter Cosmos look — borders eat space at small sizes */
            }
            /* SAFETY: any media inside a tile is constrained to tile width regardless of
               its intrinsic dimensions. This is the rule that actually fixes the horizontal
               overflow Morgan was seeing — large source images were forcing parent flex
               columns to their natural widths, pushing the page past viewport. */
            .work-item img,
            .work-item video,
            .work-item iframe,
            .work-item .work-media,
            .work-item .media-container,
            .work-item picture { max-width: 100% !important; height: auto; }

            /* Pills: smaller, with right-edge fade hint for horizontal scroll */
            .portfolio-filters {
                padding: 0.5rem 1rem !important;
                gap: 6px !important;
                /* Native momentum scroll on iOS */
                -webkit-overflow-scrolling: touch;
                scroll-behavior: smooth;
            }
            .portfolio-filter-btn {
                padding: 4px 10px !important;
                font-size: 0.6rem !important;
                flex-shrink: 0; /* never compress when the row needs to scroll */
            }
            .feed-tabs { padding: 1rem 1rem 0.5rem; }

            /* "About / Skills / Experience" trigger — mobile accordion control.
               JS moves the sidebar to right after this button on mobile (and back to its
               original position on desktop), turning it into an in-place expandable panel
               instead of a scroll-target. Tap toggles `body.about-expanded` which animates
               the sidebar height + padding via the transition rules below. */
            .mobile-about-trigger {
                display: block !important;
                width: calc(100% - 2rem);
                margin: 0.25rem 1rem 0.75rem 1rem;
                padding: 12px 14px;
                background: transparent;
                border: 1px solid var(--border);
                color: color-mix(in srgb, var(--text, #ffffff) 65%, transparent);
                font-family: var(--font-mono);
                font-size: 0.7rem;
                text-transform: uppercase;
                letter-spacing: 0.08em;
                cursor: pointer;
                text-align: center;
                transition: background 0.2s, color 0.2s, border-color 0.2s;
            }
            .mobile-about-trigger:active,
            body.about-expanded .mobile-about-trigger {
                background: color-mix(in srgb, var(--text, #ffffff) 7%, transparent);
                color: var(--text, #ffffff);
                border-color: var(--text, #ffffff);
            }
            /* Arrow rotates ↓ → ↑ when expanded. Lives in a span inside the button. */
            .mobile-about-trigger .toggle-icon {
                display: inline-block;
                transition: transform 0.3s ease;
            }
            body.about-expanded .mobile-about-trigger .toggle-icon {
                transform: rotate(180deg);
            }

            /* Inline About section on mobile — uses a CLONED container (.mobile-about-container)
               populated by JS from the original sidebar's content. The original sidebar stays
               in its layout-grid position but is hidden on mobile (rule below). This avoids
               DOM-move fragility we hit before — clones are bulletproof.

               Animation: uses the modern grid-template-rows: 0fr → 1fr trick so the section
               smoothly rolls out from collapsed to its actual content height (not a fixed
               max-height ceiling, which animates jankily for variable-height content). The
               container is always display:grid; only the row size + padding + border colors
               animate. Border width stays at 1px solid transparent so there's no layout
               shift when the border colors fade in. Requires a single inner wrapper child
               with min-height:0 so the grid row can actually compress to zero. */
            .profile-sidebar {
                display: none !important;
            }
            .mobile-about-container {
                display: grid;
                grid-template-rows: 0fr;
                transition: grid-template-rows 0.35s ease,
                            padding 0.35s ease,
                            border-color 0.35s ease,
                            margin 0.35s ease;
                padding: 0 1rem;
                border-top: 1px solid transparent;
                border-bottom: 1px solid transparent;
                margin-bottom: 0;
            }
            .mobile-about-container > .mobile-about-inner {
                min-height: 0;          /* critical: lets the grid row collapse to 0 */
                overflow: hidden;       /* clips content while it animates */
                display: flex;
                flex-direction: column;
                gap: 1.5rem;
            }
            body.about-expanded .mobile-about-container {
                grid-template-rows: 1fr;
                padding: 1.5rem 1rem;
                border-top-color: var(--border);
                border-bottom-color: var(--border);
                margin-bottom: 0.5rem;
            }
            /* Make sure cloned section labels and content keep their look inline.
               The original sidebar styles already target these classes so most of it
               just works; this is for bits that depended on the sidebar wrapper. */
            .mobile-about-container .section-label {
                display: block;
                font-family: var(--font-mono);
                font-size: 0.68rem;
                font-weight: 400;
                color: color-mix(in srgb, var(--text, #ffffff) 62%, transparent);
                letter-spacing: 0.14em;
                margin-bottom: 0.5rem;
            }
            .mobile-about-container .bio-text {
                /* Jul 14 2026: was hardcoded white, unreadable on light
                   themes; mirrors the desktop .bio-text rule now. */
                color: var(--text, #ffffff);
                line-height: 1.5;
                font-size: 0.9rem;
            }

            /* Hard belt: prevent any phantom horizontal scroll regardless of what overflows */
            html, body { max-width: 100vw; overflow-x: hidden; }
        }

        /* === PHONE-ONLY FINE TUNING (≤ 600px) === */
        @media (max-width: 600px) {
            nav { padding: 1rem; }
            .profile-topbar {
                padding: 1rem 1rem 0.75rem;
                gap: 0.75rem;
            }
            .profile-topbar .topbar-avatar { width: 56px; height: 56px; }
            .profile-topbar .topbar-meta { gap: 8px; font-size: 0.65rem; }
            .profile-topbar .topbar-role { font-size: 0.65rem; }
            .portfolio-filter-btn {
                padding: 4px 9px !important;
                font-size: 0.58rem !important;
            }

            /* Keep the same 12px Cosmos-style spacing on phones — no further tightening.
               Earlier values (4px) made the grid feel cramped per user feedback. */
            .portfolio-feed { padding: 12px !important; gap: 12px !important; }
            .masonry-col { gap: 12px !important; }
            .narrow-row { gap: 12px !important; }
            .addcards-row { gap: 12px !important; }
        }

        /* =================================================================
           COMPACT MOBILE HEADER (≤768px) — added 2026-05-02
           -----------------------------------------------------------------
           Goal: shrink the profile header from ~900px (eating most of an iPhone
           viewport on first paint) to ~110px so the work grid surfaces above
           the fold. Three structural changes vs the existing 900px block:

           1. Topbar uses a fixed three-column grid (avatar | identity | actions)
              instead of flex-wrapping. Everything stays on one row.
           2. EDIT PROFILE / SHARE text buttons become icon-only (pencil + share
              glyph) so they fit alongside the avatar without wrapping. The
              renderTopbarActions JS swaps text → icons for the owner case;
              visitor CTAs (Connect / Message) keep their text since they're
              the primary action, not chrome.
           3. Tab bar (My Work / Credited In) + category pills become sticky
              under the page chrome once the user scrolls past the header.
              Persistent navigation while browsing the work grid, mirroring
              the Instagram / Behance mobile pattern.

           Identity stack switches from inline (role · location · connections)
           to a vertical stack: name / role / location / connections with
           tight 1.15 line-height — total ~95px tall.

           Block placed AFTER the 600px and 900px blocks so its rules win on
           specificity-tied cascades, while leaving desktop completely alone.
           ================================================================= */
        @media (max-width: 768px) {
            /* --- TOPBAR LAYOUT — fixed three-column row, no wrap. --- */
            .profile-topbar {
                display: grid !important;
                grid-template-columns: auto 1fr auto;
                align-items: center;
                gap: 12px !important;
                padding: 14px 14px 10px !important;
                /* Flex-wrap from the 900px block would otherwise push actions
                   onto a new row; grid kills the wrap entirely. */
                flex-wrap: nowrap !important;
            }

            /* Smaller avatar — 48px reads as identity glyph, not hero. */
            .profile-topbar .topbar-avatar {
                width: 48px !important;
                height: 48px !important;
            }

            /* --- IDENTITY STACK — vertical, tight line-height. --- */
            .topbar-identity {
                min-width: 0;          /* lets grid child truncate cleanly */
                display: flex;
                flex-direction: column;
                gap: 2px;              /* name → role → meta line */
                line-height: 1.15;
            }
            .profile-topbar h1 {
                /* Floor 1.05rem so a long handle still fits in the column.
                   Cap at 1.35rem — anything bigger pushes connections off-row. */
                font-size: clamp(1.05rem, 5.2vw, 1.35rem) !important;
                line-height: 1.15 !important;
                margin: 0 !important;
                /* Truncate long names with ellipsis instead of wrapping into
                   another row that would push the page taller again. */
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            .profile-topbar .topbar-role {
                font-size: 0.65rem !important;
                line-height: 1.2 !important;
                color: #888;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            /* Meta line — location · connections. Stays inline because they
               fit together; ellipsis catches edge cases (long city names etc). */
            .profile-topbar .topbar-meta {
                font-size: 0.65rem !important;
                gap: 6px !important;
                line-height: 1.2 !important;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }

            /* --- ACTIONS CLUSTER — icon-only buttons. ---
               topbar-actions stops being full-width (override the 900px-block
               `width: 100%` rule that pushed actions onto a wrapped row).
               Icons sit inline, right of the identity column. */
            .profile-topbar .topbar-actions {
                width: auto !important;
                margin-top: 0 !important;
                display: flex;
                gap: 6px;
                flex-shrink: 0;
            }
            /* Icon-only button styling. Class applied by renderTopbarActions
               on mobile via window.matchMedia detection (added in JS below). */
            .profile-topbar .topbar-actions .icon-btn {
                width: 36px;
                height: 36px;
                padding: 0 !important;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                background: transparent;
                border: 1px solid var(--border);
                color: #ccc;
                cursor: pointer;
                flex: 0 0 auto !important;   /* override 900px-block flex:1 */
                border-radius: 0;            /* hard-edge per Spark style */
                transition: border-color 0.15s, color 0.15s, background 0.15s;
            }
            .profile-topbar .topbar-actions .icon-btn:hover,
            .profile-topbar .topbar-actions .icon-btn:active {
                border-color: white;
                color: white;
                background: rgba(255,255,255,0.04);
            }
            .profile-topbar .topbar-actions .icon-btn svg {
                width: 16px;
                height: 16px;
                stroke: currentColor;
                fill: none;
                stroke-width: 1.6;
            }
            /* --- Connect button (mobile) ---
               Becomes an icon-btn in three of four states: + (none), … (pending),
               ✓ (connected). Only "Accept" stays as a text button — inbound
               requests are notification-grade events that warrant the visual
               weight. updateProfileConnectButton swaps the right markup +
               classes per state; we just need the styling for each variant.

               icon-btn--primary: the "Connect" CTA. Inverted from the standard
               grey-bordered icon-btn — white background with the icon stroked
               in the page background colour, matching the old "Message" white
               CTA. Higher visual weight than other icons in the row to draw
               taps (this is the primary growth lever).

               Accept fallback: 'Accept' is short enough that a tiny text
               button fits without breaking the row. Green-tinted via inline
               style on the element itself (set in updateProfileConnectButton). */
            .profile-topbar .topbar-actions .icon-btn--primary {
                /* Jul 14 2026: inverted from the THEME, not hardcoded white.
                   On light themes the old white-on-white left the '+' icon
                   invisible; text-on-bg inversion holds contrast on any
                   palette (dark themes render exactly as before). */
                background: var(--text, #ffffff) !important;
                border-color: var(--text, #ffffff) !important;
                color: var(--bg, #0d0d0d) !important;
            }
            .profile-topbar .topbar-actions .icon-btn--primary:hover,
            .profile-topbar .topbar-actions .icon-btn--primary:active {
                background: #f0f0f0 !important;
                border-color: #f0f0f0 !important;
                color: var(--bg) !important;
            }
            .profile-topbar .topbar-actions #btn-connect-profile.btn {
                /* When state is 'received' (Accept) the button is a small
                   text button. Keep it compact alongside the icon row. */
                padding: 8px 12px !important;
                font-size: 0.6rem !important;
                letter-spacing: 0.05em !important;
                flex: 0 0 auto !important;
                white-space: nowrap;
            }
            /* Visitor primary CTA fallback (covers any other .btn-primary
               that might appear in the topbar). Compresses to fit. */
            .profile-topbar .topbar-actions .btn-primary {
                padding: 8px 14px !important;
                font-size: 0.65rem !important;
                flex: 0 0 auto !important;
            }

            /* About-trigger: slight tightening for shorter total stack. */
            .mobile-about-trigger {
                margin: 0.4rem 1rem 0.6rem 1rem !important;
                padding: 10px 14px !important;
                font-size: 0.65rem !important;
            }

            /* --- STICKY TAB + FILTER BAR ---
               After the user scrolls past the header, the tabs row pins to
               the top of the viewport so My Work / Credited In + category
               pills are always one tap away. Background matches body so the
               row reads as chrome, not a floating overlay.

               Both elements get position:sticky with cascading top values
               (0 for tabs, then ~44px for pills) so they pin one beneath
               the other in DOM order. Total stack ≈ 70px. No JS needed. */
            .feed-tabs {
                position: sticky;
                top: 0;
                z-index: 30;
                background: var(--bg);
                /* 1px shadow line — chrome separator without layout cost. */
                box-shadow: 0 1px 0 var(--border);
                padding: 12px 1rem 8px !important;
                margin: 0 !important;
            }
            .portfolio-filters {
                position: sticky;
                top: var(--feed-tabs-h, 37px);  /* measured by
                                         syncFeedTabsStack (Aug 10 2026):
                                         the tabs wrap to two lines on
                                         narrow phones, so a hardcoded
                                         offset let the rows cross */
                z-index: 29;
                background: var(--bg);
                box-shadow: 0 1px 0 var(--border);
                padding: 0.5rem 1rem !important;
                margin: 0 !important;
            }

            /* Work grid: small top breathing room so first row of tiles
               isn't kissing the sticky pills. */
            .portfolio-feed {
                padding-top: 14px !important;
            }
        }
        /* === END COMPACT MOBILE HEADER === */

        /* --- SIDEBAR --- */
        .profile-sidebar {
            border-right: 1px solid var(--border);
            /* Tighter top padding (Jul 2026) so the Message/Email row sits
               right at the top of the column now the nav strip is gone. */
            padding: 1.5rem 2rem 3rem;
            position: sticky;
            top: 0;
            height: 100vh;
            overflow-y: auto;
            background: var(--bg);
            display: flex;
            flex-direction: column;
            gap: 2rem; 
            /* Jul 2026: the top nav bar is gone (replaced by the floating
               SPARK button), so the old 85px reservation is obsolete — it
               left a page-background band above the sidebar. The sidebar
               now runs to the very top; Get in touch is the first thing. */
            transition: top 0.3s ease, height 0.3s ease, transform 0.3s ease, opacity 0.3s ease, padding 0.3s ease;
        }
        body.nav-hidden .profile-sidebar {
            top: 0;
            height: 100vh;
        }

        /* NEW: Container to put Avatar next to Name */
        .header-main-info {
            display: flex;
            align-items: center; 
            justify-content: space-between;
            gap: 15px; 
            margin-bottom: 1.5rem; 
        }
        
        .profile-text-info {
            display: flex;
            flex-direction: column; 
            flex: 1;
            min-width: 0;
            container-type: inline-size; /* NEW: Tells the container to measure its own width for font scaling */
        }

        .avatar-container {
            width: 90px;
            height: 90px;
            border-radius: 50%;
            overflow: hidden;
            background: #2a2a2a;
            position: relative;
            cursor: pointer; 
            transition: all 0.3s ease;
            flex-shrink: 0;
        }
        .avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }
        .avatar-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: bold; color: #555; font-family: var(--font-mono); background: #2a2a2a; }
        .avatar-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: none; flex-direction: column; align-items: center; justify-content: center; font-family: var(--font-mono); font-size: 0.6rem; color: #aaa; text-transform: uppercase; z-index: 5; opacity: 0; transition: opacity 0.2s ease; pointer-events: none; }
        .avatar-overlay svg { margin-bottom: 5px; color: #777; }
        body.is-editing .avatar-overlay { display: flex; }
        .avatar-container:hover .avatar-overlay { opacity: 1; }

        .profile-header h1 { 
            font-size: 2.8rem; /* Start massive by default */
            font-weight: 500; 
            letter-spacing: -1.5px; 
            color: white; 
            line-height: 1.05; 
            margin-bottom: 0.3rem; 
            margin-top: 0; 
            text-align: left; 
            width: 100%;
            /* Safety fallbacks */
            overflow-wrap: break-word; 
            word-break: break-word;
        }
        .profile-role { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; display: block; margin-bottom: 0; text-align: left;}
        .location { display: flex; align-items: center; font-size: 0.9rem; color: var(--text); gap: 8px; }
        .location::before { content: ''; display: block; width: 6px; height: 6px; background: var(--text); }
        
        .connections-display {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-top: 15px;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            color: var(--text-muted);
            width: max-content;
        }
        .conn-clickable { cursor: pointer; transition: color 0.2s; }
        .conn-clickable:hover { color: white; }
        
        .mutual-count { display: none; }
        .mutual-count.show { display: inline-block; }

        .btn {
            flex: 1; padding: 12px 0; text-align: center; border: 1px solid var(--border);
            color: var(--text); text-decoration: none; font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase; transition: all 0.2s; background: transparent; cursor: pointer;
            display: flex; align-items: center; justify-content: center;
        }
        .btn:hover { background: var(--text); color: var(--bg); border-color: var(--text); }
        .btn-primary { border: 1px solid var(--text); background: var(--text); color: var(--bg); }
        
        .btn-sent { background: #333 !important; color: #aaa !important; border: 1px solid #333 !important; cursor: default; }

        /* Jul 14 2026: themed. Was a fixed dark block (#222) that sat like
           a bruise on light custom themes; surface/border/text track the
           palette now, on any background. */
        /* Jul 15 2026: these used background: var(--surface), which custom
           themes can resolve to a near-white — with the theme's white text
           on top, [ Edit Profile ] became white-on-white (unreadable on
           e.g. a pink theme). Two-part fix:
           1. The base tool buttons are now ghosts on the page background,
              with border and text mixed from the theme's own text colour,
              so contrast is guaranteed on every palette.
           2. #btn-edit (the always-visible one) inverts: text-on-bg. A
              white box gets the background colour as its text — the same
              pattern as the topbar Connect button. Type quieted to match
              the Jul 15 label pass. */
        .edit-mode-btn { 
            background: transparent;
            border: 1px solid color-mix(in srgb, var(--text, #ffffff) 30%, transparent);
            color: color-mix(in srgb, var(--text, #ffffff) 72%, transparent); 
            width: 100%; padding: 10px; margin-top: auto; cursor: pointer;
            font-family: var(--font-mono); font-size: 0.7rem; font-weight: 400;
            letter-spacing: 0.12em; text-transform: uppercase;
        }
        .edit-mode-btn:hover { border-color: var(--text, white); color: var(--text, white); }
        #btn-edit.edit-mode-btn {
            background: var(--text, #ffffff);
            border-color: var(--text, #ffffff);
            color: var(--bg, #121212);
        }
        #btn-edit.edit-mode-btn:hover {
            background: color-mix(in srgb, var(--text, #ffffff) 85%, var(--bg, #121212));
            color: var(--bg, #121212);
        }
        
        .save-btn {
            background: white; color: black; border: 1px solid white; 
            width: 100%; padding: 10px; font-weight: 700; cursor: pointer;
            font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase;
            display: none; 
        }
        
        .btn-discard {
            background: transparent; color: #888; border: 1px solid transparent;
            width: 100%; padding: 10px; font-weight: 700; cursor: pointer;
            font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase;
            display: none; margin-top: 10px; transition: all 0.2s;
        }
        .btn-discard:hover { color: #fff; border-color: #444; }
        /* Retired from view (Jun 11 2026): the sidebar Save/Discard pair
           duplicated the sticky save bar — two simultaneous save controls
           is one more thing to parse, and the sticky bar is always in
           sight by design. Elements stay in the DOM (toggleEditMode and
           downstream code write to them by ID); !important outranks the
           inline display they receive, so they simply never paint. */
        #btn-save, #btn-discard { display: none !important; }

        /* ============================================================
           STICKY SAVE BAR (May 27 2026, revised)
           ------------------------------------------------------------
           Top-of-viewport bar visible only in edit mode. Houses the
           primary [ Save Changes ] and [ Discard ] actions plus a
           live label. The existing sidebar buttons remain as a
           redundancy. Sits *flush below* the sticky nav — the offset
           is driven by --ssb-top which is set in JS from the nav's
           measured height (syncStickySaveBarOffsets), so the bar
           never overlaps the nav even if the nav's padding changes.
           ============================================================ */
        .sticky-save-bar {
            position: fixed;
            top: var(--ssb-top, 80px);
            left: 0; right: 0;
            z-index: 99;             /* < nav (100) so any micro-overlap hides under the nav */
            background: #0d0d0d;
            border-bottom: 1px solid #fff;
            padding: 10px 24px;
            display: none;
            align-items: center;
            justify-content: space-between;
            gap: 16px;
            font-family: var(--font-mono);
        }
        body.is-editing .sticky-save-bar { display: flex; }
        /* Push layout-grid content (not the nav) down to clear the
           save bar in edit mode. Hardcoded 52px matches the save
           bar's natural height; sufficient since the bar's height
           doesn't change responsively. */
        body.is-editing .layout-grid { padding-top: 52px; }
        .ssb-label {
            font-size: 0.78rem;
            text-transform: uppercase;
            letter-spacing: 0.12em;
            color: #fff;
        }
        /* Autosave status — quiet, lowercase, muted (Jun 11 2026). Sits
           inline after "Editing" e.g. "Editing · Saved". Never shouts. */
        .ssb-autosave-status {
            text-transform: none;
            letter-spacing: 0.02em;
            color: #8a8a8a;
            font-size: 0.72rem;
        }
        /* Calmer edit chrome (Jun 11 2026): the pulsing red dot read as
           an alarm / recording indicator, and a follow-up pass removed
           the reassurance sentence too — every extra word on a sticky
           bar is one more thing competing for attention on page entry.
           The bar is now: one word of state, two actions. */
        .ssb-actions {
            display: flex;
            gap: 10px;
            align-items: center;
        }
        .ssb-btn {
            font-family: var(--font-mono);
            font-size: 0.78rem;
            text-transform: uppercase;
            letter-spacing: 0.08em;
            padding: 9px 16px;
            border: 1px solid;
            cursor: pointer;
            transition: all 0.15s;
        }
        .ssb-save {
            background: #fff;
            color: #000;
            border-color: #fff;
            font-weight: 700;
        }
        .ssb-save:hover { background: #ccc; border-color: #ccc; }
        /* Discard at rest is a quiet text action — the red treatment made
           the whole bar feel dangerous before the user had done anything.
           The confirm() dialog remains the destructive-action safety net. */
        .ssb-discard {
            background: transparent;
            color: #888;
            border-color: transparent;
        }
        .ssb-discard:hover { color: #fff; text-decoration: underline; text-underline-offset: 3px; }
        @media (max-width: 600px) {
            .sticky-save-bar { padding: 8px 12px; }
            .ssb-label { font-size: 0.68rem; }
            .ssb-btn { padding: 7px 11px; font-size: 0.68rem; }
        }

        /* ============================================================
           SETUP CHECKLIST BANNER (May 27 2026)
           ------------------------------------------------------------
           Banner above the feed-tabs in edit mode. Lists the basic
           profile setup tasks with live ✓/✗ state. Clicking a row
           scrolls to + focuses the relevant field. Auto-hides when
           all items complete. Owner-only.
           ============================================================ */
        .setup-checklist {
            margin: 1.5rem 2rem 0 2rem;
            padding: 1rem 1.25rem;
            border: 1px solid var(--border);
            background: #0a0a0a;
            display: none;
            font-family: var(--font-mono);
        }
        body.is-editing .setup-checklist.has-incomplete { display: block; }
        .sc-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            margin-bottom: 1rem;
        }
        .sc-title {
            font-size: 0.78rem;
            text-transform: uppercase;
            letter-spacing: 0.12em;
            color: #fff;
        }
        .sc-progress {
            font-size: 0.72rem;
            color: #888;
            letter-spacing: 0.08em;
        }
        .sc-progress strong { color: #fff; }
        .sc-bar {
            height: 2px;
            background: #1f1f1f;
            margin-bottom: 1rem;
            overflow: hidden;
        }
        .sc-bar-fill {
            height: 100%;
            background: #fff;
            transition: width 0.3s ease;
        }
        /* Calmer checklist (Jun 11 2026): completed items are no longer
           rendered at all — finished tasks don't need screen space. What
           remains is a single compact line of next actions, so a user
           who is 4/5 done sees ONE quiet pill, not a six-box wall. */
        .sc-grid {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }
        .sc-row {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 8px 14px;
            border: 1px solid #2a2a2a;
            background: transparent;
            color: #ccc;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            text-align: left;
            cursor: pointer;
            transition: all 0.15s;
        }
        .sc-row:hover { border-color: #fff; color: #fff; }
        .sc-row .sc-arrow { color: #555; transition: color 0.15s; }
        .sc-row:hover .sc-arrow { color: #fff; }
        @media (max-width: 720px) {
            .setup-checklist { margin: 1rem 1rem 0 1rem; padding: 0.85rem 1rem; }
            .sc-grid { flex-direction: column; align-items: stretch; }
        }

        /* Brief highlight when the checklist scrolls a field into view —
           gives the user visual confirmation of which field was targeted. */
        .sc-focus-flash {
            animation: sc-flash 1.6s ease-out;
        }
        @keyframes sc-flash {
            0%   { box-shadow: 0 0 0 3px rgba(255,255,255,0.6); }
            100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); }
        }

        .privacy-toggle-wrapper {
            display: none; 
            align-items: center;
            gap: 10px;
            margin-top: 1rem;
            padding: 10px;
            border: 1px solid #2a2a2a;
            color: #aaa;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            text-transform: uppercase;
            cursor: pointer;
        }
        .privacy-toggle-wrapper:hover { border-color: white; color: white; }
        .privacy-toggle-wrapper input { cursor: pointer; }

        /* ============================================================
           PRIVATE PROFILE — locked state for non-connected visitors.
           ============================================================
           When a profile owner has set privacyMode: true and the visitor
           is not the owner and not a connected user, loadReadOnlyProfile
           applies body[data-private-locked] which:
             - hides every section except identity (name, role, location,
               bio, connect button)
             - shows a single .private-lock-card explaining why
             - keeps the connect button visible so the visitor can
               request access

           Note: this is CLIENT-SIDE gating. A determined viewer with
           browser dev tools could still inspect the raw_profile_data
           in the network tab. True server-side enforcement requires
           an RLS policy + a viewer-aware Postgres function — deferred
           post-EF. This is a meaningful first layer of privacy and
           matches what most consumer apps actually ship.
           ============================================================ */
        body[data-private-locked] #portfolio-feed-container,
        body[data-private-locked] #experience-container,
        body[data-private-locked] #education-container,
        body[data-private-locked] #skills-container,
        body[data-private-locked] #links-container,
        body[data-private-locked] #showreel-container,
        body[data-private-locked] #portfolio-display-container,
        body[data-private-locked] #saved-jobs-container,
        body[data-private-locked] #applications-container,
        body[data-private-locked] .feed-toggle-row,
        body[data-private-locked] #btn-archived-view,
        body[data-private-locked] .private-section,
        body[data-private-locked] #appearance-controls,
        body[data-private-locked] #privacy-toggle-container {
            display: none !important;
        }
        .private-lock-card {
            display: none;
            /* Take the full vertical space the right column has — no
               more floating-panel-with-gaps look. min-height matches
               the typical visible viewport area below the topbar. */
            min-height: 60vh;
            margin: 0;
            padding: 3rem 1.5rem;
            text-align: center;
            background: transparent;
            /* Centre the contents vertically as well as horizontally
               so the lock icon/title/text/button sit as a coherent
               block in the middle of the empty space rather than
               clinging to the top edge. */
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
        body[data-private-locked] .private-lock-card { display: flex; }
        .private-lock-icon {
            font-size: 2rem;
            margin-bottom: 0.75rem;
            opacity: 0.6;
        }
        .private-lock-title {
            font-family: var(--font-mono);
            font-size: 0.85rem;
            text-transform: uppercase;
            letter-spacing: 1.5px;
            color: var(--text);
            margin-bottom: 0.5rem;
        }
        .private-lock-text {
            font-size: 0.85rem;
            color: var(--text-muted);
            max-width: 320px;
            margin: 0 auto;
            line-height: 1.5;
        }
        /* Wrapper around the connect button — centres horizontally
           regardless of intrinsic button width. The inline-block button
           rule below also keeps the button auto-width so a long label
           like "Sign in to connect" doesn't stretch full-width awkwardly. */
        .private-lock-action {
            margin-top: 1.5rem;
            display: flex;
            justify-content: center;
        }
        #private-lock-connect-btn {
            display: inline-block;
            min-width: 0;
            width: auto;
        }
        /* Owner-facing badge — visible always so the owner can confirm
           privacy is on. Sits in the topbar area near the connect /
           edit buttons. Shown regardless of viewer because both the
           owner AND visitors should know the profile is private. */
        .privacy-mode-badge {
            display: none;
            align-items: center;
            gap: 6px;
            font-family: var(--font-mono);
            font-size: 0.65rem;
            text-transform: uppercase;
            letter-spacing: 1px;
            color: var(--text-muted);
            padding: 4px 10px;
            border: 1px solid var(--border);
            border-radius: 14px;
        }
        body[data-profile-private] .privacy-mode-badge {
            display: inline-flex;
        }

        /* Jul 14 2026: derived from --text instead of --text-muted. Custom
           themes only set bg/text/accent, so muted stayed a dark-theme grey
           and section titles washed out on light backgrounds. Mixing from
           the theme's own text colour tracks every palette automatically.
           Jul 15 2026: quieted with the feed tabs — smaller, regular
           weight, tracked out; the sidebar labels are captions, not
           headings. */
        .section-label { font-family: var(--font-mono); font-size: 0.68rem; font-weight: 400; letter-spacing: 0.14em; color: color-mix(in srgb, var(--text, #ffffff) 62%, transparent); margin-bottom: 1rem; display: block; border-bottom: 1px solid var(--border); padding-bottom: 5px; }
        /* Modal cards are a fixed dark surface, not themed: keep their
           labels readable regardless of the page palette. */
        .app-modal-card .section-label { color: #bbbbbb; }
        
        .section-label-btn { 
            font-family: var(--font-mono); font-size: 0.7rem; color: #bbbbbb; 
            margin-bottom: 1rem; display: block; border-bottom: 1px solid var(--border); 
            padding-bottom: 5px; cursor: pointer; transition: color 0.2s;
        }
        .section-label-btn:hover { color: white; border-bottom-color: white; }

        .bio-text { font-size: 0.95rem; color: var(--text, #ccc); min-height: 20px; }

        .availability-status {
            font-family: var(--font-mono);
            font-size: 0.85rem;
            color: var(--text);
            display: flex;
            flex-direction: column; 
            gap: 5px;
        }

        /* When the owner enters edit mode, force-show the availability
           section so they can fill it in. The load logic hides this
           section when empty (so visitors don't see a confusing empty
           label, and so the owner doesn't see a section they didn't
           know about). The override is gated on body.is-editing only,
           so visitors and the owner-in-view-mode still get the
           hide-when-empty behaviour. !important is needed because the
           load logic sets display:none inline on the parent. */
        body.is-editing #p-availability-section {
            display: block !important;
        }

        .timeline-item { margin-bottom: 1.5rem; padding-left: 1rem; border-left: 1px solid var(--border); position: relative; }
        .timeline-item:last-child { margin-bottom: 0; }
        .timeline-role { font-weight: 600; font-size: 1rem; color: var(--text, white); display: block; }
        .timeline-company { font-size: 0.9rem; color: color-mix(in srgb, var(--text, #ffffff) 72%, transparent); display: block; margin-bottom: 4px; }
        /* Jul 14 2026: was hardcoded #555, invisible on light themes. */
        .timeline-date { font-family: var(--font-mono); font-size: 0.7rem; color: color-mix(in srgb, var(--text, #ffffff) 55%, transparent); }
        
        .status-tag { font-family: var(--font-mono); font-size: 0.65rem; padding: 2px 6px; border: 1px solid #444; color: #aaa; text-transform: uppercase; display: inline-block; margin-top: 4px; }
        .app-link { color: var(--text, white); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; }
        .app-link:hover { border-bottom-color: var(--text, white); }

        .timeline-remove { position: absolute; right: 0; top: 0; color: #ff3b30; font-size: 0.7rem; font-family: var(--font-mono); cursor: pointer; display: none; text-transform: uppercase; }
        .add-timeline-btn { display: none; width: 100%; text-align: center; border: 1px solid #2a2a2a; padding: 8px; font-family: var(--font-mono); font-size: 0.7rem; color: #777; cursor: pointer; margin-top: 10px; text-transform: uppercase; }
        .add-timeline-btn:hover { border-color: white; color: white; }

        .skills-grid { display: flex; flex-wrap: wrap; gap: 8px; }
        .skill-tag { font-size: 0.75rem; padding: 4px 8px; border: 1px solid var(--border); color: var(--text-muted); cursor: default; }
        .add-skill-btn { display: none; padding: 4px 8px; border: 1px solid #2a2a2a; color: #777; font-size: 0.75rem; cursor: pointer; font-family: monospace; }
        .add-skill-btn:hover { border-color: white; color: white; }

        .links-list { display: flex; flex-direction: column; gap: 10px; }
        .link-item { position: relative; display: flex; flex-direction: column; }
        .link-label { font-size: 0.9rem; color: var(--text); text-decoration: none; display: inline-block; cursor: pointer; }
        .link-label:hover { text-decoration: underline; }
        .link-url { font-family: var(--font-mono); font-size: 0.7rem; color: #555; display: none; margin-top: 2px; }
        
        .add-link-btn { display: none; width: 100%; text-align: center; border: 1px solid #2a2a2a; padding: 8px; font-family: var(--font-mono); font-size: 0.7rem; color: #777; cursor: pointer; margin-top: 10px; text-transform: uppercase; }
        .add-link-btn:hover { border-color: white; color: white; }
        .link-remove { position: absolute; right: 0; top: 0; color: #ff3b30; font-size: 0.7rem; font-family: var(--font-mono); cursor: pointer; display: none; text-transform: uppercase; }

        body.is-editing .timeline-remove { display: block; }
        body.is-editing .add-timeline-btn { display: block; }
        body.is-editing .add-skill-btn { display: inline-block; }
        body.is-editing .skill-tag { cursor: text; border-style: dashed; }
        /* Calmer delete affordance (Jun 11 2026): the old hover flooded
           the whole tag dark red before the user had expressed any intent.
           A strikethrough + muted red border says "clicking removes this"
           without sounding an alarm. */
        body.is-editing .skill-tag:hover { border-color: #ff3b30; color: #ff3b30; text-decoration: line-through; cursor: pointer; } 
        body.is-editing .link-url { display: block; border-bottom: 1px dashed #444; }
        body.is-editing .add-link-btn { display: block; }
        body.is-editing .link-remove { display: block; }
        body.is-editing .link-label { pointer-events: none; text-decoration: none; cursor: text; }
        body.is-editing .status-tag { border-color: white; cursor: text; border-style: dashed; }
        
        /* ============================================================
           DARKROOM DEVELOP REVEAL (Jun 11 2026)
           ------------------------------------------------------------
           Opt-in scroll treatment (Customise Page > Layout > Scroll
           reveal). Work arrives soft and desaturated, then "develops"
           to full colour and sharpness as it enters the viewport —
           like a print coming up in the tray. Pure CSS transition
           driven by a one-shot IntersectionObserver; costs nothing
           after each tile has developed. Disabled wholesale under
           prefers-reduced-motion.
           ============================================================ */
        body[data-reveal="develop"] .portfolio-feed .work-item img,
        body[data-reveal="develop"] .portfolio-feed .work-item video {
            filter: grayscale(1) blur(7px) brightness(0.82);
            transform: scale(1.015);
            transition: filter 1.05s ease, transform 1.05s ease;
        }
        body[data-reveal="develop"] .portfolio-feed .work-item.is-developed img,
        body[data-reveal="develop"] .portfolio-feed .work-item.is-developed video {
            filter: none;
            transform: none;
        }
        @media (prefers-reduced-motion: reduce) {
            body[data-reveal="develop"] .portfolio-feed .work-item img,
            body[data-reveal="develop"] .portfolio-feed .work-item video {
                filter: none; transform: none; transition: none;
            }
        }

        /* Edit mode no longer dashes the work-item frame (Jun 11 2026):
           dashed = "missing" in this design language, and applying it to
           every existing piece made a finished portfolio look provisional
           the moment you entered edit mode. Editability cues live on the
           title underline and hover chrome instead. */
        body.is-editing .work-item { cursor: default; }
        /* INLINE DRAG-TO-REORDER: cursor affordance for tiles when in edit mode.
           Desktop/fine-pointer only (mobile uses the reorder modal). Suppressed in
           adjust-layout mode, which has its own purpose. JS gating must match. */
        @media (hover: hover) and (pointer: fine) {
            body.is-editing:not(.is-adjusting-layout) .masonry-col .work-item { cursor: grab; }
            body.is-editing:not(.is-adjusting-layout) .masonry-col .work-item.sortable-drag { cursor: grabbing; }
            body.is-editing:not(.is-adjusting-layout) .masonry-col .work-item.sortable-ghost { opacity: 0.35; }
        }
        /* --- NEW: VISUAL REFINEMENT --- */
        .spacing-controls { display: none; } /* Hide push/pull buttons by default */
        body.is-adjusting-layout .spacing-controls { display: flex; } /* Show ONLY in Layout mode */
        body.is-adjusting-layout .add-card { display: none !important; } /* Also hide add-card */
        body.is-adjusting-layout .work-item { border-style: solid; border-color: rgba(255,255,255,0.05); } /* Subtle clean border */
        /* --- VISUAL POLISH FOR LAYOUT MODE --- */
        body.is-adjusting-layout .add-card { display: none !important; } 
        body.is-adjusting-layout .work-item { border-style: solid; border-color: rgba(255,255,255,0.05); }

        /* REORDER MODE (desktop): hides add-cards and the new-folder card so the user
           can focus on rearranging existing tiles via inline drag. The drag itself is
           already enabled in edit mode (CSS rule earlier in this file gates `cursor:grab`
           on `body.is-editing:not(.is-adjusting-layout)`) — reorder mode just removes
           visual distractions. Mobile users can't trigger this mode (toggleReorderMode
           routes them to the legacy reorder modal instead, since touch drag isn't wired
           up in the inline grid). */
        body.is-reorder-mode .add-card,
        body.is-reorder-mode .add-folder-card { display: none !important; }
        body.is-reorder-mode .work-item { border-style: solid; border-color: rgba(255,255,255,0.08); }

        /* --- APPEARANCE SETTINGS --- */
        @media (min-width: 901px) {
            .layout-grid.layout-right { grid-template-columns: 1fr 400px; }
            .layout-grid.layout-right .profile-sidebar { order: 2; border-right: none; border-left: 1px solid var(--border); }
            .layout-grid.layout-right main { order: 1; }
            /* Collapsed + right-aligned */
            .layout-grid.layout-right.sidebar-collapsed { grid-template-columns: 1fr 0; }
            .layout-grid.layout-right.sidebar-collapsed .profile-sidebar { transform: translateX(100%); }
            /* Flip the hover zone to the right edge in layout-right */
            .layout-grid.layout-right.sidebar-collapsed ~ .sidebar-hover-zone { left: auto; right: 0; }
            .layout-grid.layout-right.sidebar-collapsed ~ .sidebar-toggle-arrow { left: auto; right: 32px; border-radius: 4px; }
            .layout-grid.layout-right.sidebar-collapsed ~ .sidebar-toggle-arrow svg { transform: rotate(180deg); }
        }

        /* Retired from view (Jun 11 2026): the [ DESIGN SETTINGS ] block
           duplicated the Customise Page panel's territory — gallery
           template + re-balance now live in its Layout tab. The block
           and its #grid-select / #rebalance-btn stay in the DOM because
           ~15 code paths read them by ID; they're just never painted. */
        .appearance-settings { display: none !important; }
        .appearance-settings label { font-family: var(--font-mono); font-size: 0.7rem; color: #aaa; text-transform: uppercase; margin-bottom: -5px; }
        .appearance-select { background: #222; border: 1px solid #444; color: white; padding: 6px; font-family: var(--font-mono); font-size: 0.8rem; width: 100%; outline: none; cursor: pointer; }
        .appearance-select:hover { border-color: white; }

        .feed-tabs {
            padding: 1.75rem 2rem 1rem 2rem;
            display: flex;
            gap: 2rem;
            border-bottom: 1px solid var(--border);
        }
        /* Quieted Jul 15 2026: the Jul 14 readability bump (0.9rem/700)
           made the tabs compete with the work; Courier's bold is also a
           muddy double-strike cut. Smaller, regular weight, tracked out.
           Hierarchy now comes from colour and the underline, not mass.
           line-height is pinned to 1 because the compact mobile header's
           sticky pill offset depends on this row's height.
           If Courier ever feels too soft here, a sharper cut with the
           same voice, scoped to these labels only:
           font-family: ui-monospace, 'SF Mono', Menlo, Consolas, var(--font-mono); */
        .feed-tab {
            background: transparent;
            border: none;
            color: color-mix(in srgb, var(--text, #ffffff) 55%, transparent);
            font-family: var(--font-mono);
            font-size: 0.72rem;
            font-weight: 400;
            letter-spacing: 0.14em;
            line-height: 1;
            text-transform: uppercase;
            cursor: pointer;
            padding-bottom: 5px;
            position: relative;
        }
        .feed-tab:hover { color: var(--text, white); }
        /* Feed tools trigger (Jun 11 2026) — right-aligned on the tabs
           row, edit-mode + owner only. Quiet at rest; takes on the active
           mode's colour when it's acting as an exit button. */
        .feed-tools-btn {
            display: none;
            margin-left: auto;
            background: transparent;
            border: 1px solid #2a2a2a;
            color: #888;
            font-family: var(--font-mono);
            font-size: 0.72rem;
            letter-spacing: 0.08em;
            text-transform: uppercase;
            padding: 5px 12px;
            cursor: pointer;
            transition: all 0.15s;
        }
        .feed-tools-btn:hover { border-color: #fff; color: #fff; }
        body.is-editing .feed-tools-btn { display: inline-flex; align-items: center; }
        body.is-previewing .feed-tools-btn { display: none !important; }
        .feed-tools-btn.is-exit-adjust { border-color: #ff3b30; color: #ff3b30; }
        .feed-tools-btn.is-exit-archived { border-color: #f39c12; color: #f39c12; }
        /* Two grid-tool buttons sit together at the right of the tabs row.
           The group carries the right-alignment so the visible button stays
           put even when its sibling is hidden mid-mode. (Jun 2026) */
        .feed-tools-group { margin-left: auto; display: flex; gap: 8px; align-items: center; }
        .feed-tools-group .feed-tools-btn { margin-left: 0; }
        .feed-tools-btn svg {
            width: 13px; height: 13px;
            stroke: currentColor; fill: none; stroke-width: 2;
            stroke-linecap: round; stroke-linejoin: round;
            margin-right: 6px;
        }
        /* The sidebar homes of these tools are retired from view — the
           legacy buttons stay in the DOM (toggleLayoutMode etc. mutate
           them by ID for state) but never paint. */
        #layout-toggle-btn, #btn-reorder, #btn-archived-view { display: none !important; }
        .feed-tab.active { color: var(--text, white); }
        .feed-tab.active::after {
            /* Underline matches active tab colour — flips with theme.
               Hairline (1px) since the Jul 15 type quieting. */
            content: ''; position: absolute; bottom: -1px; left: 0; width: 100%; height: 1px;
            background: var(--text, white);
        }

        .portfolio-filters {
            padding: 1.5rem 2rem;
            display: none; 
            gap: 10px;
            overflow-x: auto;
            border-bottom: 1px solid var(--border);
            margin-bottom: 0; /* Changed so it doesn't double-stack with the feed padding */
            white-space: nowrap;
        }
        .portfolio-filter-btn {
            background: transparent;
            /* Hairline derives from the theme text; the var(--border)
               line stands as the fallback where color-mix is
               unsupported. */
            border: 1px solid var(--border);
            border-color: color-mix(in srgb, var(--text, #ffffff) 22%, transparent);
            color: color-mix(in srgb, var(--text, #ffffff) 62%, transparent);
            padding: 8px 14px;
            border-radius: 20px;
            font-family: var(--font-mono);
            font-size: 0.62rem;
            font-weight: 400;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            cursor: pointer;
            transition: all 0.2s;
        }
        .portfolio-filter-btn:hover, .portfolio-filter-btn.active {
            /* Active state — uses theme text colour so it stays
               readable on light themes (white-on-cream is invisible).
               The faint translucent fill is text-main with low alpha
               via colour-mix, so it works on dark and light themes
               alike — falls back to a neutral rgba on browsers without
               colour-mix support. */
            border-color: var(--text, white);
            color: var(--text, white);
            background: rgba(127,127,127,0.08);
        }

        .portfolio-feed { 
            padding: 2rem; /* Now it ALWAYS has a 2rem gap at the top! */
            display: flex;
            flex-direction: column;  /* feed stacks rows vertically */
            /* Theme layout slider — falls back to 2rem when no theme set.
               Same fallback used in .masonry-col below. */
            gap: var(--layout-gap, 2rem);
        }
        .masonry-col {
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: var(--layout-gap, 2rem);
            min-width: 0; 
        }

        /* Drop-target sentinel injected at the bottom of every masonry-col
           in startNewNarrowRow. Does nothing in viewing mode (display:none
           via the default state). In edit mode it expands to a fixed
           height so SortableJS sees real DOM space to drop a tile into,
           even when the column is much shorter than its neighbour. Without
           this, dragging from a tall column into a short one required
           hovering the cursor over an existing tile in the short column —
           dropping into the empty space below the last tile silently
           failed because there was no DOM target there.
           Pointer-events stays auto so SortableJS hit-testing works.
           Background is transparent so the masonry rhythm isn't disturbed. */
        .masonry-col-droppad {
            display: none;
        }
        body.is-editing .masonry-col-droppad {
            display: block;
            min-height: 200px;
            width: 100%;
            background: transparent;
        }
        /* During an active drag, gently outline the droppad so the user
           sees where they're aiming. Sortable adds .sortable-drag to the
           element being dragged on body (we hook off body.is-editing
           plus the Sortable global drag state via .is-dragging-tile,
           toggled in the Sortable callbacks below). */
        body.is-editing.is-dragging-tile .masonry-col-droppad {
            border: 1px dashed rgba(255, 255, 255, 0.15);
            border-radius: 0;
        }

        /* ----------------------------------------------------------------------
           NARROW + WIDE ROWS
           The portfolio feed used to be a single flex row with N masonry columns.
           That made wide items (colSpan > 1) impossible — a single DOM element
           can't occupy multiple columns simultaneously.

           New model: the feed is a vertical stack of "rows," each containing
           either (a) a horizontal flex of masonry-cols [.narrow-row] or (b) a
           single wide item [.wide-row]. Wide items break out of column flow,
           taking a fraction of the feed width corresponding to their colSpan.
           Items render in user-defined sequence; consecutive narrow items
           collapse into one .narrow-row, each wide item gets its own row.
           Trade-off: column heights "reset" at every wide break — accepted
           because wide items are user-chosen feature breaks, not the default.
        ---------------------------------------------------------------------- */
        .narrow-row {
            display: flex;
            align-items: flex-start;
            /* Honours the Customise Page grid-gap setting (Jun 11 2026).
               Previously hardcoded 2rem, so "Grid gap" only changed
               vertical spacing — columns stayed 2rem apart. */
            gap: var(--layout-gap, 2rem);
            width: 100%;
        }
        .wide-row {
            display: flex;
            justify-content: flex-start;
            width: 100%;
        }
        .wide-row > .work-item {
            margin: 0;  /* override createWorkItemDOM's auto-margin centring */
        }

        /* ----------------------------------------------------------------------
           PINNED ADD-CARDS ROW
           The three "add" affordances (Add Photo/Video, Embed Video, New Folder)
           always sit in a fixed row at the top of the feed when in edit mode.
           They DO NOT participate in the masonry packer and DO NOT shift when
           the user uploads new work — they stay in place. Each card takes 1/N
           of the feed width (matching column width below).
        ---------------------------------------------------------------------- */
        .addcards-row {
            display: flex;
            align-items: flex-start;
            gap: 2rem;
            width: 100%;
        }
        .addcards-row > * {
            flex: 1;
            min-width: 0;
        }

        /* --- FOLDERS --- */
        .folder-card {
            position: relative;
            background: #1a1a1a;
            border: 1px solid var(--border);
            overflow: hidden;
            cursor: pointer;
            min-height: 280px;
            transition: border-color 0.2s;
        }
        .folder-card:hover { border-color: white; }
        .folder-card .folder-cover {
            display: block;
            width: 100%;
            height: auto;
            min-height: 280px;
            object-fit: cover;
            transition: filter 0.25s ease;
        }
        .folder-card:hover .folder-cover { filter: brightness(0.55); }
        .folder-card .folder-placeholder {
            width: 100%;
            aspect-ratio: 1;
            min-height: 280px;
            display: flex; align-items: center; justify-content: center;
            font-family: var(--font-sans);
            font-size: 5rem; font-weight: 700; color: #333;
            background: repeating-linear-gradient(45deg, #1a1a1a, #1a1a1a 10px, #222 10px, #222 20px);
        }
        .folder-card .folder-label {
            position: absolute;
            bottom: 0; left: 0; right: 0;
            padding: 18px 18px 16px;
            background: linear-gradient(to top, rgba(0,0,0,0.92) 0%, rgba(0,0,0,0.7) 50%, transparent 100%);
            color: white;
            pointer-events: none;
        }
        /* Sections, not folders (Jul 10 2026): the label block reads like
           the entrance to a page of a personal site. Kicker, display
           title, optional one-line description, piece count. */
        .folder-card .folder-kicker {
            font-family: var(--font-mono);
            font-size: 0.6rem;
            letter-spacing: 0.22em;
            text-transform: uppercase;
            color: #999;
            margin-bottom: 6px;
        }
        .folder-card .folder-title {
            font-family: var(--font-sans);
            font-size: 1.5rem;
            font-weight: 700;
            line-height: 1.15;
            margin-bottom: 4px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .folder-card .folder-desc {
            font-family: var(--font-sans);
            font-size: 0.8rem;
            color: #bbb;
            line-height: 1.4;
            margin-bottom: 6px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .folder-card .folder-count {
            font-family: var(--font-mono);
            font-size: 0.65rem;
            color: #ccc;
            text-transform: uppercase;
            letter-spacing: 0.08em;
        }

        /* Title placement variants (Jul 10 2026). Default is bottom-left;
           top positions flip the scrim gradient, centre uses a soft full
           overlay so the title stays legible on any cover. */
        .folder-card.fl-pos-bc .folder-label { text-align: center; }
        .folder-card.fl-pos-tl .folder-label,
        .folder-card.fl-pos-tc .folder-label {
            top: 0; bottom: auto;
            background: linear-gradient(to bottom, rgba(0,0,0,0.92) 0%, rgba(0,0,0,0.7) 50%, transparent 100%);
            padding: 16px 18px 22px;
        }
        .folder-card.fl-pos-tc .folder-label { text-align: center; }
        .folder-card.fl-pos-c .folder-label {
            top: 0; bottom: 0;
            display: flex; flex-direction: column;
            align-items: center; justify-content: center;
            text-align: center;
            background: rgba(0,0,0,0.4);
        }
        .folder-card.fl-face-mono .folder-title {
            font-family: var(--font-mono);
            font-weight: 500;
            letter-spacing: 0.06em;
            text-transform: uppercase;
            font-size: 1.2rem;
        }

        /* On the Spark grid the engine gives the card an explicit height:
           the cover must fill it, and long labels clip instead of spill. */
        .portfolio-grid2d .folder-card { min-height: 0; overflow: hidden; }
        /* Owner-chosen tile shape (Aug 10 2026): the cover fills and
           crops to the chosen box; without the class, Original keeps
           the cover's natural flow exactly as before. */
        .folder-card.fl-fixed-ar { min-height: 0; }
        .folder-card.fl-fixed-ar .folder-cover {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        .folder-card.fl-fixed-ar .folder-placeholder {
            position: absolute;
            inset: 0;
            height: 100%;
            aspect-ratio: auto;
        }
        .portfolio-grid2d .folder-card .folder-cover { height: 100%; min-height: 0; object-fit: cover; }
        .portfolio-grid2d .folder-card .folder-placeholder { height: 100%; min-height: 0; aspect-ratio: auto; }
        .folder-card.is-filtered-out { display: none !important; }

        /* Card edit affordance (Jul 11 2026), sibling of the delete ✕. */
        .folder-card .folder-edit {
            position: absolute;
            top: 10px; left: 48px;
            background: rgba(0, 0, 0, 0.75);
            color: white;
            border: 1px solid var(--border);
            width: 28px; height: 28px;
            font-size: 0.85rem;
            cursor: pointer;
            z-index: 5;
            transition: all 0.2s;
        }
        .folder-card .folder-edit:hover { background: white; color: black; }

        /* Section terminal tabs + in-section main-grid toggle
           (Jul 11 2026 rebuild). */
        .sec-tabs {
            display: flex;
            border: 1px solid var(--border);
            margin-bottom: 1.25rem;
        }
        .sec-tab {
            flex: 1;
            background: transparent;
            border: none;
            border-right: 1px solid var(--border);
            color: #888;
            font-family: var(--font-mono);
            font-size: 0.72rem;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            padding: 10px;
            cursor: pointer;
            transition: all 0.15s;
        }
        .sec-tab:last-child { border-right: none; }
        .sec-tab.is-on { background: white; color: black; }
        .sec-main-toggle {
            position: absolute;
            bottom: 10px; left: 10px;
            z-index: 6;
            background: rgba(0, 0, 0, 0.78);
            border: 1px solid var(--border);
            color: #999;
            font-family: var(--font-mono);
            font-size: 0.58rem;
            letter-spacing: 0.1em;
            padding: 5px 8px;
            cursor: pointer;
            transition: all 0.15s;
        }
        .sec-main-toggle.is-on { color: white; border-color: #666; }
        .sec-main-toggle:hover { background: white; color: black; border-color: white; }

        /* Section page container (Jul 18 2026): full-bleed. The 1240px
           centred cap made section grids render as a cramped middle
           column, nothing like the main grid. The wrapper now spans the
           same width as the root feed, so the engine measures the same
           clientWidth and tiles come out identical to the main grid.
           Alignment and title styling still sit on this class. Values
           are explicit (not deleted) to beat the duplicate rule in
           profile.html's inline styles if the two files ever deploy
           out of step. */
        .section-page {
            max-width: none;
            margin-left: 0;
            margin-right: 0;
        }

        /* Folder terminal (Aug 9 2026 landscape rebuild): wide two-pane
           card, preview left, controls right, nothing scrolls on
           desktop. The preview is interactive: it is the cover surface
           (click to add, drag to pan), so pointer events are ON for the
           box and OFF for its children, letting the box own the drag. */
        .folder-terminal {
            max-width: 2000px;
            width: 96%;
            padding: 2.5rem 3rem;
            max-height: 92vh;
        }
        .folder-terminal-body {
            display: flex;
            gap: 3rem;
            align-items: flex-start;
        }
        .folder-terminal-left { flex: 0 0 46%; min-width: 0; }
        .folder-terminal-right { flex: 1; min-width: 0; }
        .folder-terminal .modal-input { margin-bottom: 10px; }
        .folder-cover-hint {
            font-family: var(--font-mono);
            font-size: 0.62rem;
            color: #777;
            margin: 8px 0 10px;
        }
        .folder-mini-label {
            font-family: var(--font-mono);
            font-size: 0.66rem;
            color: #888;
            min-width: 64px;
        }
        /* Folder colour pickers (Aug 11 2026). */
        .sl-colour {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            font-family: var(--font-mono);
            font-size: 0.62rem;
            color: #999;
            cursor: pointer;
        }
        .sl-colour input[type=color] {
            width: 30px;
            height: 24px;
            padding: 0;
            border: 1px solid var(--border);
            background: transparent;
            cursor: pointer;
        }
        /* Fixed-aspect folder cards must not inherit packer-measured
           heights in the FLOWING templates (Aug 11 2026): there,
           aspect-ratio is the authority. The Spark Grid is excluded
           (Aug 12 2026): its engine sets explicit tile heights, and
           overriding them collapsed folder cards to hairline strips. */
        .portfolio-feed:not(.portfolio-grid2d):not(.ff-doors):not(.ff-doors-h):not(.ff-banners):not(.ff-contents) .folder-card.fl-fixed-ar { height: auto !important; }
        /* Columns row dims behind non-grid folder templates (Aug 10
           2026): only the grid subset reads the count, so behind a
           contact sheet or runway the chips would be lying. */
        .sl-row-dim { opacity: 0.35; pointer-events: none; }
        .folder-live-preview {
            width: 100%;
            height: auto;
            aspect-ratio: 4 / 5;
            max-height: 56vh;
            margin-bottom: 0;
            cursor: grab;
            touch-action: none;
        }
        .folder-live-preview:active { cursor: grabbing; }
        .folder-live-preview * { pointer-events: none; }
        .folder-live-preview[data-span="2"] { aspect-ratio: 16 / 10; }
        .folder-live-preview[data-span="3"] { aspect-ratio: 21 / 10; }
        .folder-live-preview .folder-cover { height: 100%; min-height: 0; object-fit: cover; }
        .folder-live-preview .folder-placeholder { height: 100%; min-height: 0; aspect-ratio: auto; }
        @media (max-width: 760px) {
            .folder-terminal { padding: 1.5rem; max-height: 88vh; }
            .folder-terminal-body { flex-direction: column; gap: 1.25rem; }
            .folder-terminal-left { flex: none; width: 100%; }
            .folder-live-preview { max-height: 34vh; }
        }
        .folder-card .folder-delete {
            position: absolute;
            top: 10px; left: 10px;
            background: rgba(220, 40, 40, 0.9);
            color: white;
            border: none;
            width: 30px; height: 30px;
            border-radius: 50%;
            cursor: pointer;
            display: none;
            align-items: center; justify-content: center;
            font-size: 0.9rem;
            z-index: 5;
        }
        body.is-editing .folder-card .folder-delete { display: flex; }
        .folder-card .folder-delete:hover { background: #ff3b30; }

        .add-folder-card {
            position: relative;
            background: transparent;
            border: 1px solid #2a2a2a;
            cursor: pointer;
            min-height: 220px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            transition: border-color 0.2s, background 0.2s, color 0.2s;
            color: #666;
            width: 100%;
        }
        .add-folder-card:hover { border-color: white; color: white; background: #1a1a1a; }
        .add-folder-card .plus { font-size: 3rem; line-height: 1; margin-bottom: 10px; }
        .add-folder-card .lbl { font-family: monospace; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }

        .folder-breadcrumb {
            padding: 1rem 2rem;
            display: flex;
            align-items: center;
            gap: 12px;
            border-bottom: 1px solid var(--border);
            font-family: var(--font-mono);
            font-size: 0.75rem;
        }
        .folder-breadcrumb .back-btn {
            background: none;
            border: 1px solid var(--border);
            color: #aaa;
            padding: 6px 12px;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            text-transform: uppercase;
            letter-spacing: 0.1em;
            cursor: pointer;
            transition: background 0.15s, color 0.15s, border-color 0.15s;
        }
        .folder-breadcrumb .back-btn:hover { background: white; color: black; border-color: white; }
        .folder-breadcrumb .crumb-title { color: white; font-size: 0.95rem; text-transform: none; font-family: var(--font-sans); font-weight: 600; letter-spacing: 0; }
        .folder-breadcrumb .crumb-count { color: #777; margin-left: auto; }

        /* Section header (Jul 10 2026): replaces the breadcrumb strip when
           inside a folder. Reads like the top of a section page on a
           personal site: back link, display title, optional description,
           mono piece count, owner edit affordance. */
        .folder-section-header {
            padding: 28px 0 20px;
            border-bottom: 1px solid var(--border);
            margin-bottom: 20px;
        }
        .folder-section-header .fsh-back {
            background: transparent;
            border: 1px solid var(--border);
            color: #999;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            letter-spacing: 0.1em;
            text-transform: uppercase;
            padding: 7px 12px;
            cursor: pointer;
            transition: all 0.2s;
        }
        .folder-section-header .fsh-back:hover { background: white; color: black; border-color: white; }
        .folder-section-header .fsh-heading-row {
            display: flex;
            align-items: baseline;
            gap: 16px;
            margin-top: 18px;
        }
        .folder-section-header .fsh-title {
            font-family: var(--font-heading, var(--font-sans));
            font-size: clamp(1.7rem, 4vw, 2.6rem);
            font-weight: 700;
            line-height: 1.1;
            color: var(--text);
            margin: 0;
        }
        .folder-section-header .fsh-edit {
            background: transparent;
            border: none;
            color: #777;
            font-family: var(--font-mono);
            font-size: 0.68rem;
            letter-spacing: 0.08em;
            cursor: pointer;
            padding: 4px 0;
            transition: color 0.2s;
        }
        .folder-section-header .fsh-edit:hover { color: white; }
        .folder-section-header .fsh-desc {
            font-family: var(--font-sans);
            font-size: 0.95rem;
            color: #aaa;
            line-height: 1.55;
            max-width: 640px;
            margin: 10px 0 0;
        }
        .folder-section-header .fsh-meta {
            font-family: var(--font-mono);
            font-size: 0.65rem;
            color: #777;
            letter-spacing: 0.1em;
            text-transform: uppercase;
            margin-top: 12px;
        }
        @media (max-width: 900px) {
            .folder-section-header { padding: 20px 0 16px; }
            .folder-section-header .fsh-heading-row { flex-wrap: wrap; gap: 10px; margin-top: 14px; }
        }
        /* Section page layout variants (Jul 11 2026 rebuild): classes
           sit on the .section-page wrapper. */
        .section-page.fsh-align-center .folder-section-header { text-align: center; }
        .section-page.fsh-align-center .fsh-heading-row { justify-content: center; }
        .section-page.fsh-align-center .fsh-desc { margin-left: auto; margin-right: auto; }
        .section-page.fsh-face-mono .fsh-title {
            font-family: var(--font-mono);
            font-weight: 500;
            letter-spacing: 0.06em;
            text-transform: uppercase;
            font-size: clamp(1.4rem, 3.2vw, 2.1rem);
        }

        .folder-modal-cover-preview {
            width: 100%;
            aspect-ratio: 16/9;
            background: #111;
            margin-bottom: 1rem;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #555;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            overflow: hidden;
            border: 1px dashed #333;
        }
        .folder-modal-cover-preview img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        /* Work picker (Aug 9 2026): a grid of thumbnails, click to
           toggle in or out of the folder. The checkbox is invisible but
           still carries the state, so save logic reads it unchanged. */
        .folder-assign-list {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
            gap: 6px;
            padding: 8px;
            max-height: 220px;
            overflow-y: auto;
            border: 1px solid #2a2a2a;
            background: #0e0e0e;
        }
        .folder-assign-empty {
            grid-column: 1 / -1;
            padding: 16px;
            color: #555;
            font-family: var(--font-mono);
            font-size: 0.72rem;
            text-align: center;
        }
        .folder-assign-thumb {
            position: relative;
            aspect-ratio: 1;
            cursor: pointer;
            display: block;
        }
        .folder-assign-thumb input[type=checkbox] {
            position: absolute;
            opacity: 0;
            pointer-events: none;
        }
        .folder-assign-thumb img {
            width: 100%; height: 100%;
            object-fit: cover;
            display: block;
            background: #222;
            border: 1px solid transparent;
            filter: grayscale(35%);
            opacity: 0.7;
            transition: opacity 0.15s, filter 0.15s, border-color 0.15s;
        }
        .folder-assign-thumb:hover img { opacity: 1; }
        .folder-assign-thumb.is-in img {
            opacity: 1;
            filter: none;
            border-color: white;
        }
        .folder-assign-thumb.is-locked { cursor: not-allowed; }
        .folder-assign-thumb.is-locked img { opacity: 0.22; filter: grayscale(100%); }
        .folder-assign-thumb .assign-tick {
            position: absolute;
            top: 4px; right: 4px;
            width: 16px; height: 16px;
            background: white;
            color: black;
            font-size: 11px;
            line-height: 16px;
            text-align: center;
            display: none;
        }
        .folder-assign-thumb.is-in .assign-tick { display: block; }
        /* Cover-from-work (Aug 11 2026): a small corner action on each
           picker thumb sets that piece as the folder cover. */
        .folder-assign-thumb .assign-cover-btn {
            position: absolute;
            left: 4px;
            bottom: 4px;
            font-family: var(--font-mono);
            font-size: 0.5rem;
            letter-spacing: 0.08em;
            text-transform: uppercase;
            background: rgba(0, 0, 0, 0.72);
            color: #eee;
            border: 1px solid rgba(255, 255, 255, 0.35);
            padding: 2px 5px;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.15s;
            pointer-events: auto;
        }
        .folder-assign-thumb:hover .assign-cover-btn { opacity: 1; }
        @media (hover: none) { .folder-assign-thumb .assign-cover-btn { opacity: 1; } }
        .folder-assign-thumb .assign-cover-btn:hover { border-color: white; color: white; }
        /* Pieces with no visual (audio, embeds, PDFs without a thumb):
           a mono type label instead of a broken image icon. */
        .folder-assign-thumb .assign-noimg {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%; height: 100%;
            background: #191919;
            border: 1px solid #2a2a2a;
            color: #666;
            font-family: var(--font-mono);
            font-size: 0.58rem;
            letter-spacing: 0.08em;
        }
        .folder-assign-thumb.is-in .assign-noimg { border-color: white; color: #ccc; }

        .work-item { 
            position: relative; 
            background: #1a1a1a; 
            border: 1px solid var(--border); 
            overflow: hidden; 
            cursor: pointer; 
            transition: border-color 0.3s; 
            display: block;
            width: 100%;
            /* Theme layout slider — falls back to 0 (Spark default
               brutalist hard edges). Inheriting via var means the
               radius can be tuned per-profile in the appearance editor. */
            border-radius: var(--media-radius, 0);
        }
        .work-item:hover { border-color: white; }
        
        .media-container { position: relative; width: 100%; background: #000; }
        .work-media { width: 100%; height: auto; display: block; }

        .carousel-nav {
            position: absolute; top: 50%; transform: translateY(-50%);
            background: rgba(0,0,0,0.5); color: white; border: none;
            width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
            display: none; align-items: center; justify-content: center;
            font-size: 1rem; z-index: 10; transition: background 0.2s;
        }
        .carousel-nav:hover { background: rgba(0,0,0,0.8); }
        .work-item:hover .carousel-nav { display: flex; }
        .carousel-prev { left: 10px; }
        .carousel-next { right: 10px; }
        
        /* Instagram-style tile dots (Jul 14 2026): replaces the corner
           counter. White with a soft shadow reads on any image. */
        .carousel-dots {
            position: absolute;
            bottom: 8px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 4px;
            z-index: 2;
            pointer-events: none;
        }
        .carousel-dots span {
            width: 5px;
            height: 5px;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.45);
            box-shadow: 0 0 3px rgba(0, 0, 0, 0.45);
            transition: background 0.2s;
        }
        .carousel-dots span.active { background: #ffffff; }

        .carousel-counter {
            position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.6);
            color: white; font-family: var(--font-mono); font-size: 0.7rem;
            padding: 2px 6px; border-radius: 4px; pointer-events: none; z-index: 10;
        }

        .work-overlay { position: absolute; bottom: 0; left: 0; width: 100%; padding: 1rem; background: linear-gradient(to top, rgba(0,0,0,0.9), transparent); opacity: 0; transition: opacity 0.3s; pointer-events: none; }
        .work-item:hover .work-overlay { opacity: 1; }

        /* PROJECT BADGE (May 26 2026).
           Small mono-caps badge in the top-left corner of project tiles
           — a deliberate visual signal that this isn't just a regular
           post but a deeper full project with its own URL. Pointer events
           off so it doesn't intercept clicks; the tile click handler
           routes to the project URL regardless. */
        .work-project-badge {
            position: absolute;
            top: 10px;
            left: 10px;
            background: rgba(0, 0, 0, 0.8);
            border: 1px solid rgba(255, 255, 255, 0.25);
            color: #fff;
            font-family: var(--font-mono);
            font-size: 0.6rem;
            text-transform: uppercase;
            letter-spacing: 0.12em;
            padding: 4px 10px;
            pointer-events: none;
            z-index: 15;
            backdrop-filter: blur(6px);
            -webkit-backdrop-filter: blur(6px);
        }
        
        .work-title { font-size: 0.9rem; color: white; font-weight: 500; pointer-events: auto; line-height: 1.4; }
        .work-title a { color: white; text-decoration: underline; }
        .work-title span { display: inline-block; }

        .delete-btn { position: absolute; top: 10px; right: 10px; background: red; color: white; width: 24px; height: 24px; display: none; align-items: center; justify-content: center; font-size: 12px; cursor: pointer; border: none; z-index: 20; }
        body.is-editing .delete-btn { display: flex; }
        body.is-editing .work-media { opacity: 0.8; }
        body.is-editing .work-title { cursor: text; border-bottom: 1px dashed #999; display: inline-block; min-width: 50px; }

        .add-card { 
            min-height: 220px; border: 2px dashed #444; display: flex; flex-direction: column; 
            align-items: center; justify-content: center; cursor: pointer; color: #666; transition: 0.2s; width: 100%;
        }
        .add-card:hover { border-color: white; color: white; background: #1a1a1a; }
        /* Dashed is reserved for the single primary next action (add work).
           Secondary affordances in the row sit back with a quiet solid
           border so the page has one obvious door. (Jun 11 2026) */
        .add-card.secondary { border: 1px solid #2a2a2a; }
        .add-card.secondary:hover { border-color: white; }

        /* ---------------------------------------------------------------
           EMPTY-STATE ONBOARDING HERO
           Shown above the work feed when the owner is viewing their OWN
           empty profile (no work uploaded yet). Disappears the moment any
           work is uploaded. The CTA inside it just clicks the existing
           hidden file input — same upload path as the add-card, so we
           don't fork the upload flow.
           Brutalist: hard borders, mono caption, no rounded corners, no
           gradients, no illustrations. Matches the rest of the site.
        --------------------------------------------------------------- */
        /* Public preview: owner views their page as a visitor. Hide every
           owner/edit affordance so what's left is exactly the visitor view.
           is-previewing is never set at the same time as is-editing. */
        body.is-previewing #topbar-actions,
        body.is-previewing .empty-state-hero,
        body.is-previewing .sticky-save-bar,
        body.is-previewing .setup-checklist,
        body.is-previewing .avatar-overlay,
        body.is-previewing .sidebar-empty-hint,
        body.is-previewing .add-card,
        body.is-previewing .work-add-tile,
        /* View count is an owner-only metric — Preview shows the visitor's
           view, so it must hide here too (Jun 11 2026). */
        body.is-previewing #p-view-count,
        body.is-previewing #meta-dot-views {
            display: none !important;
        }
        body.is-previewing [contenteditable] {
            -webkit-user-modify: read-only;
            user-modify: read-only;
        }

        .empty-state-hero {
            border: 2px dashed #555;
            padding: 60px 40px;
            margin-bottom: 2rem;
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            gap: 24px;
            background: #0a0a0a;
        }
        .empty-state-hero .es-eyebrow {
            font-family: var(--font-mono);
            font-size: 0.7rem;
            letter-spacing: 0.2em;
            color: #777;
            text-transform: uppercase;
        }
        .empty-state-hero .es-headline {
            font-size: clamp(1.6rem, 3vw, 2.4rem);
            line-height: 1.1;
            color: white;
            margin: 0;
            font-weight: 600;
            max-width: 720px;
        }
        .empty-state-hero .es-sub {
            font-family: var(--font-mono);
            font-size: 0.85rem;
            color: #aaa;
            line-height: 1.5;
            max-width: 640px;
        }
        .empty-state-hero .es-cta-row {
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
            margin-top: 8px;
        }
        .empty-state-hero .es-cta {
            display: inline-block;
            padding: 14px 22px;
            font-family: var(--font-mono);
            font-size: 0.8rem;
            letter-spacing: 0.1em;
            text-transform: uppercase;
            cursor: pointer;
            transition: 0.15s;
            border: 1px solid white;
            background: white;
            color: black;
        }
        .empty-state-hero .es-cta:hover { background: transparent; color: white; }
        .empty-state-hero .es-cta.secondary {
            background: transparent;
            color: white;
            border-color: #555;
        }
        .empty-state-hero .es-cta.secondary:hover { border-color: white; }
        /* Preview of the masonry payoff inside the empty state. CSS-only ghost
           tiles (no fake images, so it can't feel like fake content) arranged
           in two columns like the real grid, dimmed and clearly labelled as a
           preview. Shows the visual result before the user has done anything. */
        .empty-state-hero .es-preview {
            width: 100%;
            margin-top: 24px;
            padding-top: 20px;
            border-top: 1px solid #222;
        }
        .empty-state-hero .es-preview-label {
            font-family: var(--font-mono);
            font-size: 0.65rem;
            letter-spacing: 0.15em;
            text-transform: uppercase;
            color: #555;
            margin-bottom: 14px;
        }
        .empty-state-hero .es-preview-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
            opacity: 0.5;
            max-width: 460px;
            pointer-events: none;
        }
        .empty-state-hero .es-pcol { display: flex; flex-direction: column; gap: 10px; }
        .empty-state-hero .es-ghost {
            width: 100%;
            background: linear-gradient(135deg, #1c1c1c 0%, #262626 50%, #1c1c1c 100%);
            background-size: 200% 200%;
            animation: esGhostShimmer 3.5s ease-in-out infinite;
            border: 1px solid #2a2a2a;
        }
        .empty-state-hero .es-ghost-video {
            display: flex; align-items: center; justify-content: center;
        }
        .empty-state-hero .es-ghost-video .es-play {
            width: 0; height: 0;
            border-style: solid;
            border-width: 9px 0 9px 15px;
            border-color: transparent transparent transparent #555;
        }
        @keyframes esGhostShimmer {
            0%   { background-position: 0% 50%; }
            50%  { background-position: 100% 50%; }
            100% { background-position: 0% 50%; }
        }
        @media (max-width: 700px) {
            .empty-state-hero .es-preview-grid { max-width: 100%; }
        }
        .empty-state-hero .es-checklist {
            display: flex;
            flex-direction: column;
            gap: 8px;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            color: #888;
            margin-top: 8px;
            padding-top: 20px;
            border-top: 1px solid #222;
            width: 100%;
        }
        .empty-state-hero .es-checklist .es-check-row {
            display: flex;
            gap: 12px;
            align-items: center;
        }
        .empty-state-hero .es-checklist .es-check-row .es-bullet {
            width: 14px;
            height: 14px;
            border: 1px solid #555;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }
        .empty-state-hero .es-checklist .es-check-row.done .es-bullet {
            background: white;
            border-color: white;
            color: black;
        }
        /* Three-piece progress strip (Jun 2026) */
        .es-progress { display: flex; align-items: center; justify-content: center; gap: 12px; margin: 16px 0 20px; flex-wrap: wrap; }
        .es-prog-dots { display: inline-flex; gap: 8px; }
        .es-prog-dot { width: 32px; height: 4px; border-radius: 2px; background: rgba(255,255,255,0.18); transition: background 0.35s ease; }
        .es-prog-dot.filled { background: #fff; }
        .es-prog-count { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; color: #888; }

        /* ===== First-run setup wizard (Jun 2026) ===== */
        /* Full-page setup wizard (Jul 25 2026). Previously a boxed modal
           over a blurred page; it is now a full-viewport canvas. The
           progress line rides the very top of the screen, the step
           content sits centre-stage at a larger scale, and the footer
           pins to the bottom. Steps crossfade with a slight vertical
           drift (see .spark-wiz-main transition classes below); the
           preview card deliberately does NOT animate between steps, so
           it reads as one persistent object being built. */
        .spark-wiz-overlay { position: fixed; inset: 0; z-index: 1900; background: var(--bg, #121212); display: flex; align-items: stretch; justify-content: center; padding: 0; overflow-y: auto; opacity: 0; animation: wizOverlayIn 0.45s ease forwards; }
        .spark-wiz-overlay::before { content: ''; position: fixed; inset: 0; background: radial-gradient(ellipse 70% 55% at 50% 0%, rgba(255,255,255,0.045), transparent 70%); pointer-events: none; }
        /* Film grain (Jul 26 2026): same tile and opacity as the site's
           html::before grain, which the opaque overlay would otherwise
           paint over. The card lifts above it via z-index. */
        .spark-wiz-overlay::after { content: ''; position: fixed; inset: 0; pointer-events: none; opacity: var(--grain-opacity, 0.09); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23n)"/></svg>'); }
        @keyframes wizOverlayIn { to { opacity: 1; } }
        .spark-wiz-card { width: 100%; max-width: none; background: transparent; border: none; border-radius: 0; overflow: visible; box-shadow: none; display: flex; flex-direction: column; min-height: 100%; position: relative; z-index: 1; }
        .spark-wiz-progress { position: fixed; top: 0; left: 0; right: 0; height: 3px; background: #1b1b1f; z-index: 3; }
        .spark-wiz-progress-fill { height: 100%; width: 0%; background: #fff; transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1); }
        .spark-wiz-body { flex: 1 1 auto; display: flex; gap: 64px; align-items: center; justify-content: center; padding: 72px 48px 16px; width: 100%; max-width: 1060px; margin: 0 auto; box-sizing: border-box; }
        .spark-wiz-main { flex: 1 1 60%; min-width: 0; max-width: 560px; opacity: 1; transform: none; transition: opacity 0.22s ease, transform 0.22s ease; }
        .spark-wiz-main.wiz-out { opacity: 0; transform: translateY(-12px); }
        .spark-wiz-main.wiz-in { opacity: 0; transform: translateY(14px); transition: none; }
        @media (prefers-reduced-motion: reduce) {
            .spark-wiz-overlay { animation: none; opacity: 1; }
            .spark-wiz-main, .spark-wiz-main.wiz-out, .spark-wiz-main.wiz-in { transition: none; transform: none; opacity: 1; }
        }
        .spark-wiz-preview { flex: 0 0 250px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
        .spark-wiz-step-label { font-family: var(--font-mono, monospace); font-size: 0.62rem; letter-spacing: 0.12em; text-transform: uppercase; color: #777; margin-bottom: 18px; }
        .spark-wiz-headline { font-size: 2.1rem; font-weight: 600; color: #fff; margin: 0 0 10px; letter-spacing: -0.015em; }
        .spark-wiz-sub { font-size: 0.95rem; color: #999; margin: 0 0 24px; line-height: 1.55; max-width: 46ch; }
        @media (max-width: 720px) {
            .spark-wiz-body { flex-direction: column; gap: 32px; align-items: stretch; padding: 56px 22px 12px; }
            .spark-wiz-preview { flex: none; width: 100%; max-width: 300px; margin: 0 auto; }
            .spark-wiz-headline { font-size: 1.5rem; }
        }
        .spark-wiz-input { width: 100%; background: #161618; border: 1px solid #2e2e32; border-radius: 8px; color: #fff; padding: 13px 14px; font-size: 0.95rem; outline: none; box-sizing: border-box; }
        .spark-wiz-input:focus { border-color: #666; }
        .spark-wiz-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
        .spark-wiz-chip { background: transparent; border: 1px solid #3a3a3e; color: #bbb; padding: 8px 13px; border-radius: 999px; font-size: 0.8rem; cursor: pointer; transition: all 0.15s ease; }
        .spark-wiz-chip:hover { border-color: #666; color: #fff; }
        .spark-wiz-chip.on { background: #fff; color: #000; border-color: #fff; }
        .spark-wiz-addrow { margin-bottom: 12px; }
        .spark-wiz-selected { display: flex; flex-wrap: wrap; gap: 6px; }
        .spark-wiz-seltag { font-family: var(--font-mono, monospace); font-size: 0.72rem; background: #1c1c20; border: 1px solid #2e2e32; color: #ddd; padding: 5px 9px; border-radius: 6px; }
        .spark-wiz-seltag .x { cursor: pointer; color: #888; margin-left: 4px; }
        .spark-wiz-seltag .x:hover { color: #f87171; }
        .spark-wiz-photo-btn { background: transparent; border: 1px dashed #444; color: #ddd; padding: 14px 18px; border-radius: 8px; font-family: var(--font-mono, monospace); font-size: 0.8rem; cursor: pointer; }
        .spark-wiz-photo-btn:hover { border-color: #888; color: #fff; }
        .spark-wiz-thumbs { display: flex; gap: 10px; margin-bottom: 12px; }
        .spark-wiz-thumb { width: 64px; height: 64px; border-radius: 8px; background-size: cover; background-position: center; background-color: #1a1a1e; }
        .spark-wiz-thumb-ghost { border: 1px dashed #3a3a3e; background-color: transparent; }
        /* Mini portfolio grid, four-slot edition (Jul 26 2026): slots run
           wide / single / single / wide across two rows — the exact shape
           the editorial upload default gives a fresh profile. Tiles fill
           in upload order; new tiles pop in; the whole grid is a click
           target for upload. Video files render a real <video> frame. */
        .spark-wiz-minigrid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr); gap: 8px; width: 100%; max-width: 430px; aspect-ratio: 3 / 2; margin-bottom: 14px; cursor: pointer; }
        .spark-wiz-minitile { position: relative; border-radius: 8px; background-color: #141417; background-size: cover; background-position: center; overflow: hidden; transition: transform 0.2s ease; }
        .spark-wiz-minitile:nth-child(1) { grid-column: 1 / 3; grid-row: 1; }
        .spark-wiz-minitile:nth-child(2) { grid-column: 3; grid-row: 1; }
        .spark-wiz-minitile:nth-child(3) { grid-column: 1; grid-row: 2; }
        .spark-wiz-minitile:nth-child(4) { grid-column: 2 / 4; grid-row: 2; }
        .spark-wiz-minitile video { width: 100%; height: 100%; object-fit: cover; display: block; }
        .spark-wiz-minigrid:hover .spark-wiz-minitile-ghost { border-color: #666; color: #ccc; }
        /* Tile tools (Jul 26 2026): remove / replace / add-image on
           filled tiles. Hover-revealed with a pointer; always shown on
           touch, where hover doesn't exist. */
        .spark-wiz-minitile .wiz-tile-tools { position: absolute; top: 6px; right: 6px; display: none; gap: 4px; z-index: 2; }
        .spark-wiz-minitile:hover .wiz-tile-tools { display: flex; }
        @media (hover: none) { .spark-wiz-minitile .wiz-tile-tools { display: flex; } }
        .wiz-tile-tools button { width: 24px; height: 24px; border-radius: 6px; border: none; background: rgba(10, 10, 12, 0.82); color: #fff; font-size: 0.85rem; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }
        .wiz-tile-tools button:hover { background: #fff; color: #000; }
        .spark-wiz-minitile.wiz-tile-busy { opacity: 0.55; pointer-events: none; }
        .spark-wiz-toast { position: fixed; left: 50%; bottom: 34px; transform: translate(-50%, 12px); background: #17171b; border: 1px solid #2c2c30; border-radius: 12px; color: #ddd; font-size: 0.85rem; line-height: 1.45; padding: 12px 18px; max-width: 440px; text-align: center; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5); opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; z-index: 5; pointer-events: none; }
        .spark-wiz-toast.is-on { opacity: 1; transform: translate(-50%, 0); }
        .spark-wiz-minitile-ghost { border: 1px dashed #3a3a3e; background-color: transparent; display: flex; align-items: center; justify-content: center; color: #555; font-family: var(--font-mono, monospace); font-size: 1.1rem; transition: border-color 0.15s ease, color 0.15s ease; }
        .wiz-tile-new { animation: wizTilePop 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
        @keyframes wizTilePop { from { opacity: 0; transform: scale(0.82); } to { opacity: 1; transform: scale(1); } }
        @media (prefers-reduced-motion: reduce) { .wiz-tile-new { animation: none; } }
        .spark-wiz-prog-count { font-family: var(--font-mono, monospace); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.08em; color: #888; margin-bottom: 16px; }
        .spark-wiz-footer { display: flex; align-items: center; justify-content: space-between; padding: 16px 48px 40px; width: 100%; max-width: 1060px; margin: 0 auto; box-sizing: border-box; }
        @media (max-width: 720px) { .spark-wiz-footer { padding: 14px 22px 28px; } }
        /* Profile tour coachmarks (Jul 26 2026). The spotlight's giant
           box-shadow provides the page dim with a cutout over the
           anchor; it captures no pointer events, so highlighted
           buttons stay clickable for the click-to-advance steps. */
        #spark-tour { position: fixed; inset: 0; z-index: 9500; pointer-events: none; }
        #spark-tour .spark-tour-spot { position: fixed; border-radius: 10px; box-shadow: 0 0 0 9999px rgba(6, 6, 8, 0.78), 0 0 0 2px rgba(255, 255, 255, 0.85); pointer-events: none; transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease; }
        #spark-tour .spark-tour-shade { position: fixed; background: rgba(6, 6, 8, 0.78); border-radius: 6px; pointer-events: none; }
        #spark-tour .spark-tour-spot--flush { border-radius: 0; box-shadow: 0 0 0 9999px rgba(6, 6, 8, 0.78); }
        #spark-tour .spark-tour-card { position: fixed; pointer-events: auto; background: #101013; border: 1px solid #2c2c30; border-radius: 12px; padding: 14px 16px; box-sizing: border-box; box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55); transition: top 0.25s ease, left 0.25s ease; }
        #spark-tour .spark-tour-label { font-family: var(--font-mono, monospace); font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; color: #777; margin-bottom: 8px; }
        #spark-tour .spark-tour-text { font-size: 0.88rem; color: #ddd; line-height: 1.5; }
        #spark-tour .spark-tour-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; }
        #spark-tour .spark-tour-skip { background: none; border: none; color: #888; font-size: 0.8rem; cursor: pointer; padding: 6px 2px; }
        #spark-tour .spark-tour-skip:hover { color: #fff; }
        #spark-tour .spark-tour-next { background: #fff; color: #000; border: none; border-radius: 8px; padding: 8px 18px; font-size: 0.82rem; font-weight: 600; cursor: pointer; }
        #spark-tour .spark-tour-next:hover { opacity: 0.85; }
        @media (prefers-reduced-motion: reduce) {
            #spark-tour .spark-tour-spot, #spark-tour .spark-tour-card { transition: none; }
        }
        /* Studio welcome card (Jul 26 2026): the landing moment after
           the wizard, offering the tour. Backdrop click dismisses. */
        #spark-tour-welcome { position: fixed; inset: 0; z-index: 9500; background: rgba(6, 6, 8, 0.72); display: flex; align-items: center; justify-content: center; animation: wizOverlayIn 0.35s ease; }
        #spark-tour-welcome .stw-card { background: #101013; border: 1px solid #2c2c30; border-radius: 14px; padding: 34px 38px; max-width: 420px; width: calc(100% - 48px); box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6); text-align: center; }
        #spark-tour-welcome .stw-title { font-size: 1.6rem; font-weight: 600; color: #fff; letter-spacing: -0.01em; }
        #spark-tour-welcome .stw-sub { font-size: 0.92rem; color: #999; line-height: 1.55; margin: 12px 0 24px; }
        #spark-tour-welcome .stw-go { background: #fff; color: #000; border: none; border-radius: 10px; padding: 12px 26px; font-size: 0.9rem; font-weight: 600; cursor: pointer; }
        #spark-tour-welcome .stw-go:hover { opacity: 0.85; }
        @media (prefers-reduced-motion: reduce) { #spark-tour-welcome { animation: none; } }
        .spark-wiz-footer-right { display: flex; gap: 10px; align-items: center; }
        .spark-wiz-back, .spark-wiz-skip { background: transparent; border: none; color: #888; font-size: 0.85rem; cursor: pointer; padding: 10px 6px; }
        .spark-wiz-back:hover, .spark-wiz-skip:hover { color: #fff; }
        .spark-wiz-next { background: #fff; color: #000; border: none; padding: 11px 28px; border-radius: 8px; font-size: 0.9rem; font-weight: 600; cursor: pointer; }
        .spark-wiz-next:hover { opacity: 0.85; }
        .spark-wiz-pcard { width: 100%; background: linear-gradient(160deg,#1a1a1f,#101013); border: 1px solid #2a2a2e; border-radius: 12px; padding: 22px 16px; text-align: center; box-sizing: border-box; }
        .spark-wiz-pavatar { width: 64px; height: 64px; border-radius: 50%; background: #26262b; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; overflow: hidden; color: #666; font-size: 1.4rem; }
        .spark-wiz-pavatar img { width: 100%; height: 100%; object-fit: cover; }
        .spark-wiz-pname { color: #fff; font-weight: 600; font-size: 1rem; word-break: break-word; }
        .spark-wiz-prole { color: #bbb; font-size: 0.82rem; margin-top: 2px; word-break: break-word; }
        .spark-wiz-ploc { color: #777; font-size: 0.72rem; margin-top: 6px; font-family: var(--font-mono, monospace); }
        .spark-wiz-pchips { display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; margin-top: 12px; }
        .spark-wiz-pchip { font-size: 0.66rem; background: #232327; color: #ccc; padding: 3px 8px; border-radius: 5px; }
        .spark-wiz-preview-cap { font-family: var(--font-mono, monospace); font-size: 0.58rem; letter-spacing: 0.1em; text-transform: uppercase; color: #555; }
        /* Boost-your-profile meter — sits under the action-button row */
        #topbar-actions-stack { display: flex; flex-direction: column; align-items: stretch; gap: 10px; flex-shrink: 0; }
        body[data-cover-on] .profile-topbar > #topbar-actions-stack { position: relative; z-index: 2; }

        /* ============================================================
           UPLOAD MENU (Jun 11 2026)
           ------------------------------------------------------------
           The three in-feed add tiles (Add Photo/Video, Embed Video,
           New Folder) moved here — into a small menu that wheels out
           of the topbar Upload button. The feed is now purely the
           user's work; adding things is one named door in the header.
           Items stagger in with a soft slide (no flash); honours
           prefers-reduced-motion.
           ============================================================ */
        .upload-menu {
            position: fixed;
            z-index: 1500;
            min-width: 230px;
            background: #0d0d0d;
            border: 1px solid #2a2a2a;
            padding: 6px;
            display: none;
            flex-direction: column;
            gap: 2px;
            box-shadow: 0 12px 32px rgba(0,0,0,0.55);
        }
        .upload-menu.is-open { display: flex; }
        .upload-menu .um-item {
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 11px 12px;
            background: transparent;
            border: none;
            color: #ccc;
            font-family: var(--font-mono);
            font-size: 0.78rem;
            letter-spacing: 0.05em;
            text-transform: uppercase;
            text-align: left;
            cursor: pointer;
            opacity: 0;
            transform: translateY(-6px);
            transition: opacity 0.22s ease, transform 0.22s ease,
                        background 0.15s ease, color 0.15s ease;
        }
        .upload-menu.is-open .um-item { opacity: 1; transform: translateY(0); }
        /* The wheel-out stagger — each row arrives a beat after the last. */
        .upload-menu.is-open .um-item:nth-child(1) { transition-delay: 0s; }
        .upload-menu.is-open .um-item:nth-child(2) { transition-delay: 0.05s; }
        .upload-menu.is-open .um-item:nth-child(3) { transition-delay: 0.1s; }
        .upload-menu .um-item:hover { background: #1a1a1a; color: #fff; }
        .upload-menu .um-item svg {
            width: 16px; height: 16px; flex-shrink: 0;
            stroke: currentColor; fill: none; stroke-width: 2;
        }
        @media (prefers-reduced-motion: reduce) {
            .upload-menu .um-item { transition: background 0.15s ease, color 0.15s ease; transform: none; opacity: 1; }
        }

        /* Quiet toast (Jun 11 2026) — replaces the blocking
           alert("Profile Saved to Database!") with a small confirmation
           that arrives, breathes, and leaves. */
        .spark-toast {
            position: fixed;
            left: 50%;
            bottom: 28px;
            transform: translateX(-50%) translateY(8px);
            z-index: 3000;
            background: #fff;
            color: #000;
            font-family: var(--font-mono);
            font-size: 0.78rem;
            letter-spacing: 0.08em;
            text-transform: uppercase;
            padding: 10px 18px;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.25s ease, transform 0.25s ease;
            box-shadow: 0 6px 24px rgba(0,0,0,0.4);
        }
        .spark-toast.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }
        @media (prefers-reduced-motion: reduce) {
            .spark-toast { transition: opacity 0.25s ease; transform: translateX(-50%); }
            .spark-toast.is-visible { transform: translateX(-50%); }
        }
        body.is-editing #spark-boost-meter, body.is-previewing #spark-boost-meter { display: none !important; }
        .spark-boost-meter { cursor: pointer; margin: 0; padding: 11px 14px; border: 1px solid #2a2a2e; border-radius: 10px; background: #0e0e10; transition: border-color 0.15s ease; }
        .spark-boost-meter:hover { border-color: #444; }
        .spark-boost-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
        .spark-boost-label { font-family: var(--font-mono, monospace); font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: #ddd; }
        .spark-boost-pct { font-family: var(--font-mono, monospace); font-size: 0.8rem; color: #4ade80; font-weight: 600; }
        .spark-boost-bar { height: 5px; background: #232327; border-radius: 3px; overflow: hidden; }
        .spark-boost-fill { height: 100%; background: #4ade80; border-radius: 3px; transition: width 0.4s ease; }
        .spark-boost-next { font-family: var(--font-mono, monospace); font-size: 0.66rem; color: #888; margin-top: 8px; }
        @media (max-width: 720px) {
            .spark-wiz-body { flex-direction: column-reverse; gap: 18px; padding: 24px 20px 8px; }
            .spark-wiz-preview { flex: none; }
            .spark-wiz-footer { padding: 12px 20px 22px; }
            .spark-wiz-headline { font-size: 1.25rem; }
        }
        .empty-state-hero .es-checklist .es-check-row.done {
            color: #555;
            text-decoration: line-through;
        }
        @media (max-width: 700px) {
            .empty-state-hero { padding: 32px 20px; }
        }

        .showreel-wrapper {
            width: 100%; border: 1px solid var(--border); background: #000;
            min-height: 100px; display: flex; align-items: center; justify-content: center;
        }
        .showreel-video { width: 100%; display: block; }
        /* Negative empty states retired from view (Jun 11 2026): the
           "No Showreel Selected" / "No Portfolio Uploaded" boxes told the
           owner what they lack; the [ + Add ... ] buttons below them are
           the same affordance framed as an invitation. The elements stay
           in the DOM — collectAndSaveData uses the "No Portfolio" string
           as an is-empty sentinel — they're just never painted. The
           wrapper also collapses so no empty 100px black box remains. */
        .showreel-empty-state { display: none; }
        .showreel-wrapper:has(.showreel-empty-state) { display: none; }
        body:not(.is-editing) #showreel-section-container:has(.showreel-empty-state) { display: none; }
        
        .portfolio-empty-state { display: none; }
        body:not(.is-editing) #portfolio-section-container:has(.portfolio-empty-state) { display: none; }

        /* ============================================================
           HELP GUIDE (Jun 11 2026)
           ------------------------------------------------------------
           "?" slide-over: a short, owner-facing guide to setting up a
           profile, written in brand voice. Same panel mechanics as the
           Customise Page editor (right slide-over, z 9000 tier).
           Static content — no data dependencies, safe everywhere.
           ============================================================ */
        #help-guide-panel {
            position: fixed; top: 0; right: 0; bottom: 0;
            width: 400px; max-width: 92vw;
            background: #0c0c0c; border-left: 1px solid #2a2a2a;
            color: #ccc;
            z-index: 9000;
            transform: translateX(100%);
            /* visibility delays until the slide-out finishes, then the
               panel stops being painted at all. (Jun 11 2026 perf fix) */
            visibility: hidden;
            transition: transform 0.25s ease-out, visibility 0s linear 0.25s;
            display: flex; flex-direction: column;
            overflow: hidden;
            box-shadow: -4px 0 24px rgba(0,0,0,0.5);
        }
        #help-guide-panel.is-open {
            transform: translateX(0);
            visibility: visible;
            transition: transform 0.25s ease-out, visibility 0s;
        }
        /* PERF (Jun 11 2026): the sketch animations only run while the
           panel is actually open. They are infinite loops on ~12 nodes
           animating stroke/fill properties — left running while the
           panel sat off-screen they dragged every frame of the page. */
        #help-guide-panel:not(.is-open) .hg-sk-check,
        #help-guide-panel:not(.is-open) .hg-sk-tile,
        #help-guide-panel:not(.is-open) .hg-sk-pulse,
        #help-guide-panel:not(.is-open) .hg-sk-swatch,
        #help-guide-panel:not(.is-open) .hg-sk-fly {
            animation-play-state: paused;
        }
        @media (prefers-reduced-motion: reduce) {
            #help-guide-panel { transition: none; }
        }
        .hg-header {
            display: flex; align-items: center; justify-content: space-between;
            padding: 1.1rem 1.4rem; border-bottom: 1px solid #2a2a2a; flex-shrink: 0;
        }
        .hg-title { font-family: var(--font-mono); font-size: 0.85rem; letter-spacing: 0.1em; text-transform: uppercase; color: #fff; }
        .hg-close { background: transparent; border: none; color: #888; font-size: 1.1rem; cursor: pointer; padding: 4px 8px; }
        .hg-close:hover { color: #fff; }
        .hg-body { overflow-y: auto; padding: 1.4rem; font-size: 0.85rem; line-height: 1.65; }
        .hg-body .hg-section { margin-bottom: 1.8rem; }
        .hg-body .hg-label {
            display: block; font-family: var(--font-mono); font-size: 0.7rem;
            letter-spacing: 0.1em; text-transform: uppercase; color: #fff;
            border-bottom: 1px solid #2a2a2a; padding-bottom: 6px; margin-bottom: 10px;
        }
        /* Animated sketches (Jun 11 2026): one small looping illustration
           per section — monochrome line-work in currentColor, gentle and
           slow so they read as sketches, not adverts. All animation is
           decorative and disabled under prefers-reduced-motion. */
        .hg-sketch {
            display: block;
            width: 150px;
            height: 76px;
            margin: 2px auto 14px;
            color: #777;
            overflow: visible;
        }
        .hg-sketch rect, .hg-sketch circle, .hg-sketch path, .hg-sketch line, .hg-sketch polyline {
            fill: none;
            stroke: currentColor;
            stroke-width: 1.5;
            stroke-linecap: round;
            stroke-linejoin: round;
        }
        .hg-sketch .hg-fill { fill: currentColor; stroke: none; opacity: 0.22; }
        /* 01 — checkmark draws itself, rests, redraws */
        .hg-sk-check {
            stroke-dasharray: 26;
            stroke-dashoffset: 26;
            animation: hg-draw 4s ease-in-out infinite;
        }
        @keyframes hg-draw {
            0%        { stroke-dashoffset: 26; }
            25%, 80%  { stroke-dashoffset: 0; opacity: 1; }
            95%, 100% { stroke-dashoffset: 26; opacity: 0; }
        }
        /* 02 — grid tiles arrive one by one, hold, restart */
        .hg-sk-tile {
            opacity: 0;
            animation: hg-tile-in 6s ease-out infinite;
        }
        .hg-sk-tile:nth-child(1) { animation-delay: 0s; }
        .hg-sk-tile:nth-child(2) { animation-delay: 0.35s; }
        .hg-sk-tile:nth-child(3) { animation-delay: 0.7s; }
        .hg-sk-tile:nth-child(4) { animation-delay: 1.05s; }
        .hg-sk-tile:nth-child(5) { animation-delay: 1.4s; }
        .hg-sk-tile:nth-child(6) { animation-delay: 1.75s; }
        @keyframes hg-tile-in {
            0%       { opacity: 0; transform: translateY(4px); }
            8%, 82%  { opacity: 1; transform: translateY(0); }
            92%, 100%{ opacity: 0; transform: translateY(0); }
        }
        /* 03 — play button ring breathes outward */
        .hg-sk-pulse {
            transform-origin: 37px 38px;
            animation: hg-pulse 3s ease-out infinite;
        }
        @keyframes hg-pulse {
            0%   { transform: scale(0.8); opacity: 0.9; }
            70%  { transform: scale(1.35); opacity: 0; }
            100% { transform: scale(1.35); opacity: 0; }
        }
        /* 04 — swatches take turns lighting up. The fill must be set at
           this specificity: inline presentation attributes lose to the
           generic .hg-sketch circle { fill:none } rule above. */
        .hg-sketch .hg-sk-swatch { fill: currentColor; stroke: currentColor; animation: hg-swatch 4.5s ease-in-out infinite; }
        .hg-sk-swatch:nth-of-type(1) { animation-delay: 0s; }
        .hg-sk-swatch:nth-of-type(2) { animation-delay: 1.5s; }
        .hg-sk-swatch:nth-of-type(3) { animation-delay: 3s; }
        @keyframes hg-swatch {
            0%, 100% { fill-opacity: 0.15; }
            15%      { fill-opacity: 0.85; }
            33%      { fill-opacity: 0.15; }
        }
        /* 05 — share arrow lifts away and returns */
        .hg-sk-fly { animation: hg-fly 3.6s ease-in-out infinite; }
        @keyframes hg-fly {
            0%, 20%  { transform: translate(0, 0); opacity: 1; }
            55%      { transform: translate(14px, -14px); opacity: 0; }
            56%      { transform: translate(-8px, 8px); opacity: 0; }
            80%, 100%{ transform: translate(0, 0); opacity: 1; }
        }
        @media (prefers-reduced-motion: reduce) {
            .hg-sk-check { animation: none; stroke-dashoffset: 0; }
            .hg-sk-tile { animation: none; opacity: 1; }
            .hg-sk-pulse { animation: none; opacity: 0; }
            .hg-sk-swatch { animation: none; fill-opacity: 0.5; }
            .hg-sk-fly { animation: none; }
        }
        .hg-body p { margin: 0 0 0.8rem 0; color: #aaa; }
        .hg-body strong { color: #e0e0e0; font-weight: 600; }
        .hg-kbd {
            font-family: var(--font-mono); font-size: 0.72rem; color: #e0e0e0;
            border: 1px solid #2a2a2a; padding: 1px 6px; white-space: nowrap;
        }
        /* "?" trigger in the sticky save bar — quiet circle, owner-only
           by construction (the bar itself is edit-mode-only). */
        .ssb-help {
            width: 26px; height: 26px;
            display: inline-flex; align-items: center; justify-content: center;
            background: transparent; border: 1px solid #2a2a2a; border-radius: 50%;
            color: #888; font-family: var(--font-mono); font-size: 0.75rem;
            cursor: pointer; transition: all 0.15s;
        }
        .ssb-help:hover { border-color: #fff; color: #fff; }

        .modal-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.85); display: none; align-items: center; justify-content: center;
            z-index: 2000; backdrop-filter: blur(5px);
        }
        .app-modal-card {
            background: #1a1a1a; border: 1px solid var(--border);
            padding: 3rem; width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto; position: relative;
        }
        .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; border-bottom: 1px solid #333; padding-bottom: 1rem; }
        .modal-title { font-size: 1.5rem; color: white; }
        .close-modal { font-size: 2rem; cursor: pointer; color: #777; line-height: 1; }
        .close-modal:hover { color: white; }

        .modal-input {
            width: 100%; background: #222; border: 1px solid #444; color: white; 
            padding: 10px; font-family: var(--font-mono); margin-bottom: 1rem;
        }
        /* Two-pane Add/Edit Work modal: media on the left, all fields on the
           right so you see the work and fill its details without scrolling
           past a tall image. Collapses to a single column on narrow screens. */
        #uploadModal .app-modal-card {
            max-width: 920px !important;
            max-height: 92vh;
            padding: 2rem 2.25rem;
        }
        /* Folder terminal width (Aug 9 2026): the generic .app-modal-card
           rule above caps modals at 600px and sits later in the cascade
           than .folder-terminal, so it silently won. Same id + important
           convention as #uploadModal to settle it. */
        #folderModal .app-modal-card {
            max-width: 2000px !important;
            width: 96% !important;
            max-height: 92vh;
            padding: 2.5rem 3rem;
        }
        .upload-2pane {
            display: flex;
            gap: 28px;
            align-items: flex-start;
        }
        .upload-pane-left {
            flex: 0 0 42%;
            position: sticky;
            top: 0;
            align-self: flex-start;
        }
        .upload-pane-left .preview-container {
            min-height: 220px;
        }
        .upload-pane-left .modal-preview-img {
            max-height: 60vh;
        }
        .upload-pane-right {
            flex: 1 1 auto;
            min-width: 0;
            max-height: 82vh;
            overflow-y: auto;
            padding-right: 4px;
        }
        @media (max-width: 768px) {
            #uploadModal .app-modal-card { max-width: 500px !important; }
            #folderModal .app-modal-card { max-width: 500px !important; padding: 1.5rem; }
            .upload-2pane { flex-direction: column; gap: 0; }
            .upload-pane-left { position: static; flex-basis: auto; width: 100%; }
            .upload-pane-right { max-height: none; overflow: visible; width: 100%; }
        }

        .cat-cb-label {
            background: #222; border: 1px solid #444; color: #ccc; padding: 6px 12px; 
            font-family: var(--font-mono); font-size: 0.75rem; cursor: pointer; transition: all 0.2s;
            display: flex; align-items: center; gap: 6px; text-transform: uppercase;
        }
        .cat-cb-label:hover { border-color: white; color: white; background: rgba(255,255,255,0.05); }
        .cat-cb-label input { cursor: pointer; accent-color: #fff; }

        /* ============================================================
           UPGRADE-TO-FULL-PROJECT MODAL (May 27 2026)
           ------------------------------------------------------------
           Tiny modal launched from the lightbox sidebar's "Make This
           a Project" button. Asks for a full project name, shows live
           URL preview as the user types, then promotes the work-item
           to a project and redirects to project.html?edit=1.
           ============================================================ */
        .upgrade-modal-card {
            background: #1a1a1a;
            border: 1px solid var(--border);
            padding: 2.5rem;
            width: 90%;
            max-width: 480px;
            position: relative;
        }
        .upgrade-modal-card .modal-header { margin-bottom: 1.75rem; }
        .upgrade-modal-card .modal-title {
            font-size: 0.85rem;
            font-family: var(--font-mono);
            text-transform: uppercase;
            letter-spacing: 0.12em;
            color: #fff;
        }
        .upgrade-name-input {
            width: 100%;
            background: #222;
            border: 1px solid #444;
            color: white;
            padding: 12px 14px;
            font-family: var(--font-mono);
            font-size: 0.85rem;
            margin-bottom: 1.25rem;
        }
        .upgrade-name-input:focus {
            outline: none;
            border-color: #fff;
        }
        .upgrade-url-label {
            font-family: var(--font-mono);
            font-size: 0.65rem;
            text-transform: uppercase;
            letter-spacing: 0.1em;
            color: #666;
            margin-bottom: 0.5rem;
        }
        .upgrade-url-preview {
            font-family: var(--font-mono);
            font-size: 0.78rem;
            color: #aaa;
            background: #0a0a0a;
            border: 1px solid #2a2a2a;
            padding: 10px 12px;
            margin-bottom: 1.75rem;
            word-break: break-all;
            min-height: 1.5em;
        }
        .upgrade-url-preview .upgrade-url-slug { color: #fff; }
        .upgrade-url-preview.is-empty { color: #555; font-style: italic; }
        .upgrade-modal-actions {
            display: flex;
            justify-content: space-between;
            gap: 12px;
        }
        .upgrade-btn {
            background: transparent;
            border: 1px solid #444;
            color: #ccc;
            padding: 10px 18px;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            text-transform: uppercase;
            letter-spacing: 0.08em;
            cursor: pointer;
            transition: all 0.15s;
        }
        .upgrade-btn:hover:not(:disabled) {
            border-color: #fff;
            color: #fff;
        }
        .upgrade-btn-primary {
            background: #fff;
            color: #000;
            border-color: #fff;
        }
        .upgrade-btn-primary:hover:not(:disabled) {
            background: #ccc;
            border-color: #ccc;
        }
        .upgrade-btn:disabled {
            opacity: 0.35;
            cursor: not-allowed;
        }
        .upgrade-modal-error {
            color: #ff6464;
            font-family: var(--font-mono);
            font-size: 0.72rem;
            margin-top: 0.75rem;
            min-height: 1em;
        }

        /* ============================================================
           PROJECT EDITOR (May 26 2026)
           ------------------------------------------------------------
           Adds project-only fields to the upload modal: post-type radio,
           template picker (4 horizontally-scrolling cards), brief, slug,
           client. Hidden by default; revealed when the "Project" radio
           is selected.
           ============================================================ */

        /* Post-type radio: Regular Post / Project segmented toggle */
        .post-type-toggle {
            display: flex;
            gap: 8px;
            margin-bottom: 1.5rem;
            padding: 4px;
            background: #0a0a0a;
            border: 1px solid #2a2a2a;
        }
        .post-type-option {
            flex: 1;
            padding: 10px;
            text-align: center;
            cursor: pointer;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            color: #777;
            text-transform: uppercase;
            letter-spacing: 0.08em;
            transition: all 0.15s;
            border: 1px solid transparent;
            user-select: none;
        }
        .post-type-option:hover { color: #ccc; }
        .post-type-option.is-active {
            background: #fff;
            color: #000;
        }
        .post-type-option input { display: none; }
        .post-type-hint {
            font-size: 0.7rem;
            color: #666;
            font-family: var(--font-mono);
            text-transform: uppercase;
            letter-spacing: 0.05em;
            margin: -1rem 0 1.5rem;
        }

        /* Project-only fields container — hidden until user selects Project */
        .project-fields { display: none; }
        .project-fields.is-active { display: block; }

        /* Template picker: 4 horizontally-scrolling cards */
        .template-picker {
            display: flex;
            gap: 10px;
            margin-bottom: 1.5rem;
            overflow-x: auto;
            overflow-y: hidden;
            scroll-snap-type: x mandatory;
            -webkit-overflow-scrolling: touch;
            scrollbar-width: thin;
            padding-bottom: 6px;
        }
        .template-picker::-webkit-scrollbar { height: 4px; }
        .template-picker::-webkit-scrollbar-thumb { background: #444; }
        .template-card {
            flex: 0 0 140px;
            scroll-snap-align: start;
            background: #0a0a0a;
            border: 1px solid #2a2a2a;
            cursor: pointer;
            transition: border-color 0.15s, background 0.15s;
            display: flex;
            flex-direction: column;
            padding: 0;
            overflow: hidden;
        }
        .template-card:hover { border-color: #555; }
        .template-card.is-selected {
            border-color: #fff;
            background: #1a1a1a;
        }
        /* SVG thumbnail at the top of each card */
        .template-thumb {
            width: 100%;
            height: 100px;
            background: #050505;
            display: flex;
            align-items: center;
            justify-content: center;
            border-bottom: 1px solid #1a1a1a;
        }
        .template-thumb svg { display: block; }
        .template-card-info {
            padding: 8px 10px 10px;
            display: flex;
            flex-direction: column;
            gap: 3px;
        }
        .template-card-name {
            font-family: var(--font-mono);
            font-size: 0.7rem;
            color: #fff;
            letter-spacing: 0.05em;
            text-transform: uppercase;
        }
        .template-card-desc {
            font-size: 0.65rem;
            color: #666;
            line-height: 1.35;
            font-family: var(--font-mono);
        }

        /* Slug input + availability indicator */
        .slug-row {
            display: flex;
            align-items: center;
            gap: 0;
            margin-bottom: 0.4rem;
            border: 1px solid #2a2a2a;
            background: #0a0a0a;
        }
        .slug-row:focus-within { border-color: #555; }
        .slug-prefix {
            padding: 0 10px;
            color: #666;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            white-space: nowrap;
        }
        .slug-input {
            flex: 1;
            background: transparent;
            border: none;
            outline: none;
            padding: 10px 0;
            color: #fff;
            font-family: var(--font-mono);
            font-size: 0.8rem;
        }
        .slug-status {
            font-family: var(--font-mono);
            font-size: 0.65rem;
            text-transform: uppercase;
            letter-spacing: 0.08em;
            margin-bottom: 1.5rem;
            min-height: 14px;
        }
        .slug-status.checking { color: #888; }
        .slug-status.ok { color: #74cf5b; }
        .slug-status.error { color: #ff6b6b; }
        .slug-status.suggestion { color: #ffbe40; }

        /* Cover index picker — small thumbnails of the post's assets,
           one is the "cover" with a highlight ring. Tap to switch. */
        .cover-picker {
            display: flex;
            gap: 8px;
            margin-bottom: 1.5rem;
            overflow-x: auto;
            padding-bottom: 4px;
        }
        .cover-picker-item {
            flex: 0 0 70px;
            height: 70px;
            background: #0a0a0a;
            border: 2px solid #2a2a2a;
            cursor: pointer;
            position: relative;
            overflow: hidden;
            transition: border-color 0.15s;
        }
        .cover-picker-item:hover { border-color: #555; }
        .cover-picker-item.is-cover {
            border-color: #fff;
        }
        .cover-picker-item.is-cover::after {
            content: 'COVER';
            position: absolute;
            top: 4px; left: 4px;
            background: #fff;
            color: #000;
            font-family: var(--font-mono);
            font-size: 0.55rem;
            padding: 2px 4px;
            letter-spacing: 0.08em;
        }
        .cover-picker-item img,
        .cover-picker-item video {
            width: 100%; height: 100%;
            object-fit: cover;
            display: block;
        }
        .cover-picker-empty {
            color: #555;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            padding: 8px 0;
        }

        /* --- UPGRADED: FULL-WIDTH PREVIEW PANEL --- */
        .preview-container { 
            display: flex; 
            flex-direction: column; /* Stack previews vertically for full width */
            gap: 10px; 
            width: 100%; /* Fill the entire modal width */
            margin-bottom: 1rem; 
            padding-bottom: 10px; 
            border-bottom: 1px solid #333; 
            /* Removed old horizontal swiping logic */
        }
        
        .preview-thumb { 
            width: 100%; 
            height: auto; 
            /* Removed max-height so portrait photos can naturally expand to full width! */
            object-fit: contain; 
            background: transparent; /* <-- Removes the black boxes completely! */
            border-radius: 4px;
        }

        /* ----------------------------------------------------------------------
           DRAGGABLE IMAGE PREVIEW (multi-image posts)
           When a post has 2+ images, each image gets wrapped in a sortable
           tile so users can drag to reorder. The tile shows a position badge
           ([1], [2], …) and a subtle hint that this is grabbable. We don't
           apply this to single-image posts (no point) or to audio/video posts
           (those have a special primary-asset-plus-cover relationship that
           reordering would scramble).
        ---------------------------------------------------------------------- */
        .preview-image-tile {
            position: relative;
            cursor: grab;
            border-radius: 4px;
            overflow: hidden;
            transition: transform 0.15s ease, box-shadow 0.15s ease;
        }
        .preview-image-tile:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
        }
        .preview-image-tile:active { cursor: grabbing; }
        .preview-image-tile.sortable-ghost {
            opacity: 0.3;
        }
        .preview-image-tile.sortable-drag {
            cursor: grabbing;
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
        }
        .preview-image-tile .preview-thumb { display: block; }
        .preview-image-tile .preview-pos-badge {
            position: absolute;
            top: 8px;
            left: 8px;
            background: rgba(0, 0, 0, 0.75);
            color: white;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            letter-spacing: 0.1em;
            padding: 4px 8px;
            border-radius: 2px;
            pointer-events: none;
            backdrop-filter: blur(4px);
        }
        .preview-image-tile .preview-drag-hint {
            position: absolute;
            top: 8px;
            right: 8px;
            background: rgba(0, 0, 0, 0.75);
            color: white;
            font-family: var(--font-mono);
            font-size: 0.65rem;
            letter-spacing: 0.1em;
            padding: 4px 8px;
            border-radius: 2px;
            pointer-events: none;
            text-transform: uppercase;
            opacity: 0;
            transition: opacity 0.15s;
        }
        .preview-image-tile:hover .preview-drag-hint { opacity: 1; }
        /* New (unsaved) image tiles. Visually distinguished with a thin
           white outline so the user can see at a glance which images are
           "about to be added" vs already saved. The × remove button lets
           the user back out of an accidental file pick before saving. */
        .preview-image-tile.is-new {
            outline: 2px solid white;
            outline-offset: -2px;
        }
        .preview-image-tile .preview-new-badge {
            position: absolute;
            bottom: 8px;
            left: 8px;
            background: white;
            color: black;
            font-family: var(--font-mono);
            font-size: 0.65rem;
            letter-spacing: 0.1em;
            padding: 4px 8px;
            border-radius: 2px;
            pointer-events: none;
            text-transform: uppercase;
            font-weight: 700;
        }
        .preview-image-tile .preview-remove-btn {
            position: absolute;
            top: 8px;
            right: 8px;
            background: rgba(255, 59, 48, 0.95);
            color: white;
            border: none;
            width: 26px;
            height: 26px;
            border-radius: 50%;
            font-size: 14px;
            font-weight: 700;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0;
            line-height: 1;
            z-index: 5;
        }
        .preview-image-tile .preview-remove-btn:hover { background: rgb(255, 59, 48); }
        .preview-reorder-hint {
            font-family: var(--font-mono);
            font-size: 0.7rem;
            color: #888;
            letter-spacing: 0.1em;
            text-transform: uppercase;
            text-align: center;
            padding: 8px 0 4px;
        }

        /* ----------------------------------------------------------------------
           POST WIDTH PICKER (1 col / 2 cols / 3 cols)
           Three pill-style toggle buttons in the upload/edit modal letting the
           user choose how many columns the post spans on the profile grid.
           Mirrors the visual language of the [filter pill] style used for
           categories — brutalist mono caps, hard borders, no gradients.
        ---------------------------------------------------------------------- */
        .width-picker {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .width-pill {
            background: transparent;
            border: 1px solid #444;
            color: #888;
            padding: 8px 14px;
            font-family: var(--font-mono);
            font-size: 0.75rem;
            letter-spacing: 0.1em;
            text-transform: uppercase;
            cursor: pointer;
            transition: all 0.15s ease;
            border-radius: 999px;
        }
        .width-pill:hover {
            color: white;
            border-color: #666;
        }
        .width-pill.active {
            background: white;
            color: black;
            border-color: white;
        }
        /* Visual mini-diagram pills: a 3-cell mini grid showing how much
           width the post takes, plus a label. Makes the 1/2/3-col choice
           instantly understandable instead of an abstract number. */
        .width-pill--diagram {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 7px 12px;
        }
        .width-pill--diagram .wp-diagram {
            display: inline-flex;
            gap: 2px;
            width: 26px;
        }
        .width-pill--diagram .wp-cell {
            flex: 1;
            height: 14px;
            border-radius: 2px;
            border: 1px solid currentColor;
            opacity: 0.35;
        }
        .width-pill--diagram .wp-cell.wp-on {
            background: currentColor;
            opacity: 1;
        }
        .width-pill--diagram .wp-label {
            font-size: 0.72rem;
        }
        .wp-hint {
            font-family: var(--font-mono);
            font-size: 0.68rem;
            line-height: 1.5;
            color: #777;
            margin: 0 0 1.5rem 0;
            max-width: 46ch;
        }
        .modal-preview-img { width: 100%; max-height: 250px; object-fit: contain; margin-bottom: 1rem; background: #000; display: none; } 

        .credits-input-group { display: flex; gap: 10px; margin-bottom: 10px; position: relative; align-items: center; }
        .credits-input-group .modal-input { margin-bottom: 0; }
        .add-credit-btn {
            background: #333; border: 1px solid #444; color: white; cursor: pointer;
            padding: 0 15px; height: 42px; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; font-family: var(--font-mono); text-transform: uppercase;
        }
        .add-credit-btn:hover { border-color: white; background: #444; }

        .tag-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 1rem; }
        .tag-row { background: #222; padding: 8px 12px; border: 1px solid #333; display: flex; justify-content: space-between; align-items: center; font-size: 0.8rem; }
        .tag-info { display: flex; gap: 5px; color: #ccc; }
        .tag-role { color: #777; font-family: var(--font-mono); }
        .tag-remove { cursor: pointer; color: #aaa; font-weight: bold; }
        .tag-remove:hover { color: white; }

        .search-results {
            background: #222; border: 1px solid #444; max-height: 150px; overflow-y: auto;
            position: absolute; width: 60%; top: 100%; left: 0; z-index: 100; display: none;
        }
        .search-item { padding: 10px; cursor: pointer; font-size: 0.9rem; border-bottom: 1px solid #333; }
        .search-item:hover { background: #333; color: white; }
        .search-item-invite { background: #181818; font-size: 0.85rem; }
        .search-item-invite:hover { background: #222; }

        /* INVITE BY EMAIL — viral hook UI */
        .tag-by-email-panel { background: #161616; border: 1px solid #333; padding: 14px; margin: 10px 0 14px 0; }
        .invite-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase; color: #888; letter-spacing: 0.5px; }
        .invite-panel-header strong { color: #fff; font-weight: 500; }
        .invite-close { color: #555; cursor: pointer; font-size: 1.1rem; padding: 0 6px; }
        .invite-close:hover { color: #fff; }
        .invite-panel-body .modal-input { margin-bottom: 8px; }
        .invite-microcopy { font-size: 0.72rem; color: #666; line-height: 1.4; margin: 4px 0 12px 0; font-family: var(--font-mono); }
        .invite-confirm-btn { width: 100%; }
        .tag-invite-badge { display: inline-block; margin-left: 8px; font-family: var(--font-mono); font-size: 0.6rem; text-transform: uppercase; color: #c9a961; border: 1px solid #4a3d1a; padding: 1px 6px; letter-spacing: 0.5px; }
        /* Verified credits — the moat made visible (Jun 11 2026). */
        .tag-verified-badge { display: inline-block; margin-left: 8px; font-family: var(--font-mono); font-size: 0.6rem; text-transform: uppercase; color: #4ade80; border: 1px solid #1f5132; padding: 1px 6px; letter-spacing: 0.5px; }
        .tag-pending-badge { display: inline-block; margin-left: 8px; font-family: var(--font-mono); font-size: 0.6rem; text-transform: uppercase; color: #888; border: 1px solid #3a3a3a; padding: 1px 6px; letter-spacing: 0.5px; }
        .tag-declined-badge { display: inline-block; margin-left: 8px; font-family: var(--font-mono); font-size: 0.6rem; text-transform: uppercase; color: #b06a6a; border: 1px solid #4a2a2a; padding: 1px 6px; letter-spacing: 0.5px; }
        .credit-verified { color: #4ade80; font-weight: 700; margin-left: 3px; cursor: default; }
        .lb-credit-item .credit-verified { font-size: 0.85em; }

        .lightbox { 
            position: fixed; top: 0; left: 0; width: 100%; height: 100%; 
            background-color: rgba(0, 0, 0, 0.95); display: none; align-items: stretch; justify-content: space-between; 
            z-index: 2000; backdrop-filter: blur(10px);
        }
        .lightbox-media-wrapper { 
            flex: 1; display: flex; align-items: center; justify-content: center; position: relative;
            padding: 40px; pointer-events: auto; cursor: pointer; perspective: 1000px; overflow: hidden;
        }
        #lightbox-container { z-index: 10; transition: transform 0.3s; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; pointer-events: none; }
        .lightbox-content { max-width: 100%; max-height: 100%; object-fit: contain; box-shadow: 0 0 50px rgba(0,0,0,0.5); pointer-events: auto; cursor: default; }

        .lightbox-preview-next {
            position: absolute; top: 50%; right: 5%; transform: translateY(-50%) translateX(20%) rotateY(-25deg) scale(0.7); 
            opacity: 0; z-index: 5; pointer-events: none; transition: all 0.4s ease; max-height: 70%; max-width: 40%;
            display: flex; align-items: center; justify-content: center; box-shadow: -10px 0 30px rgba(0,0,0,0.5);
        }
        .lightbox-preview-next img, .lightbox-preview-next video { max-height: 100%; max-width: 100%; object-fit: contain; border: 1px solid rgba(255,255,255,0.1); }

        .lb-arrow-specific {
            position: absolute; top: 50%; right: 8%; transform: translateY(-50%); width: auto; height: auto; border-radius: 0;
            background: transparent; border: none; display: block; backdrop-filter: none; font-size: 3rem; color: #333;
            padding: 20px; cursor: pointer; z-index: 2020; transition: color 0.2s; user-select: none;
        }
        .lb-arrow-specific:hover { background: transparent; color: white; border: none; }

        .lightbox-info-sidebar { 
            width: 400px; background: #121212; border-left: 1px solid var(--border); display: flex;
            flex-direction: column; padding: 40px; overflow-y: auto; z-index: 2010;
        }
        .lb-title { font-size: 1.8rem; color: white; margin-bottom: 15px; letter-spacing: -0.5px; line-height: 1.1; }
        .lb-title a { color: white; text-decoration: underline; }
        /* Date of work — small mono caps placard between title and
           description. Mimics the layout of a museum/gallery wall label.
           Hidden by inline style if either dateMonth or dateYear is empty. */
        .lb-date {
            font-family: var(--font-mono);
            font-size: 0.75rem;
            color: #888;
            text-transform: uppercase;
            letter-spacing: 0.1em;
            margin-bottom: 18px;
            margin-top: -6px;
        }
        .lb-description { font-size: 0.95rem; color: #aaa; line-height: 1.6; margin-bottom: 30px; padding-bottom: 30px; border-bottom: 1px solid #222; white-space: pre-wrap; }
        .lb-credits-section-label { font-family: var(--font-mono); font-size: 0.7rem; color: #555; text-transform: uppercase; margin-bottom: 15px; display: block; }
        .lb-credits { font-family: var(--font-mono); font-size: 0.85rem; color: #ccc; }
        /* Lightbox edit button — appears at the top of the sidebar when
           the current slide belongs to the viewing user's own work. Mirrors
           the .edit-mode-btn aesthetic (mono caps, dark border) so it
           reads as an admin/edit affordance, not a primary CTA. */
        .lb-edit-btn {
            background: #222; border: 1px solid #444; color: #aaa;
            padding: 10px 16px; margin-bottom: 24px; cursor: pointer;
            font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase;
            letter-spacing: 0.05em; align-self: flex-start;
        }
        .lb-edit-btn:hover { border-color: white; color: white; }
        .lb-credit-item { display: block; margin-bottom: 12px; }
        .lb-credit-role { color: #555; font-weight: normal; text-transform: uppercase; margin-right: 8px; letter-spacing: 0.5px; font-size: 0.75rem; }
        .lb-credit-name { color: #aaa; text-decoration: none; border-bottom: 1px dotted #444; transition: all 0.2s; }
        .lb-credit-name:hover { border-bottom-style: solid; color: white; }

        .lb-nav { position: absolute; top: 50%; transform: translateY(-50%); font-size: 3rem; color: #333; cursor: pointer; padding: 20px; user-select: none; transition: color 0.2s; z-index: 2001; }
        .lb-nav:hover { color: white; }
        .lb-prev { left: 20px; }
        .lb-next { right: 20px; }

        .close-lb { position: absolute; top: 20px; right: 20px; color: #555; font-size: 2rem; cursor: pointer; z-index: 2020; }
        .close-lb:hover { color: white; }

        footer { padding: 4rem 2rem; text-align: center; border-top: 1px solid var(--border); background: var(--bg); position: relative; }
        /* Jul 14 2026: follows the theme's text colour (was fixed near-black,
           which vanished into dark themes and clashed on colourful ones). */
        .footer-logo { font-size: 5rem; font-weight: 800; color: var(--text, #1a1a1a); letter-spacing: -5px; line-height: 1; margin-bottom: 2rem; }
        .footer-links a { color: var(--text-muted); text-decoration: none; margin: 0 15px; font-size: 0.8rem; font-family: var(--font-mono); text-transform: uppercase; }
        .footer-links a:hover { color: white; }

        @media (max-width: 1100px) {
            .lightbox { flex-direction: column; }
            .lightbox-info-sidebar { width: 100%; height: auto; border-left: none; border-top: 1px solid #222; padding: 24px 20px; }
            /* Bigger image (Jul 10 2026): 60vh with 40px padding left the
               artwork small in a sea of black. 72dvh + 10px padding gives
               the piece the room it deserves; dvh tracks the real iOS
               viewport as the toolbar collapses (vh fallback first). */
            .lightbox-media-wrapper { height: 72vh; height: 72dvh; flex: none; padding: 10px; }
            .lightbox-preview-next, .lb-arrow-specific { display: none; }
            /* Arrows centre on the IMAGE, not the screen: half the media
               area height. Keep in sync with the wrapper height above.
               Slight lift + shadow so they read over bright artwork. */
            .lb-nav { top: 36vh; top: 36dvh; font-size: 2.4rem; padding: 14px 10px; color: #bbb; text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8); }
            .lb-prev { left: 6px; }
            .lb-next { right: 6px; }
        }

        @media (max-width: 900px) {
            /* NOTE: this block previously contained conflicting .profile-sidebar overrides
               (position:relative; top:0; height:auto) which fought with the mobile drawer
               styles above and produced the half-height drawer bug on iOS. Those rules
               have been removed — the consolidated block at the top of the mobile section
               is now the single source of truth for mobile sidebar behaviour. */
        }
        /* --- MOBILE MENU STYLES --- */
        .hamburger {
            display: none;
            flex-direction: column;
            justify-content: space-between;
            width: 24px;
            height: 18px;
            cursor: pointer;
            margin-left: 20px; /* Creates the gap from the login button! */
            z-index: 2000; /* Forces it to stay above the dropdown menu */
        }
        .hamburger span {
            display: block;
            height: 2px;
            width: 100%;
            background-color: white;
            border-radius: 2px;
            transition: all 0.3s ease; /* Smooth animation */
        }
        
        /* Turns the lines into an X when open */
        .hamburger.open span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
        .hamburger.open span:nth-child(2) { opacity: 0; }
        .hamburger.open span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

        @media (max-width: 950px) {
            .hamburger {
                display: flex; 
            }
            .nav-links {
                display: none; 
                position: absolute;
                top: 100%;
                left: 0;
                width: 100%;
                background: var(--bg);
                flex-direction: column;
                padding: 1.5rem 2rem;
                border-bottom: 1px solid var(--border);
                box-shadow: 0 10px 30px rgba(0,0,0,0.9);
                z-index: 99;
            }
            .nav-links.active {
                display: flex; 
            }
            .nav-links a {
                margin: 0 0 15px 0;
                font-size: 1.1rem;
            }
            .nav-links a:last-child {
                margin-bottom: 0;
            }
            .btn-auth {
                padding: 8px 12px;
                font-size: 0.7rem;
            }
        }
        /* --- GLOBAL DROPZONE --- */
        #global-dropzone {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background: rgba(0,0,0,0.85); z-index: 9999;
            display: none; flex-direction: column; align-items: center; justify-content: center;
            color: white; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 2px;
            backdrop-filter: blur(5px); border: 4px dashed #555; pointer-events: none;
        }
        #global-dropzone.active { display: flex; border-color: white; }

        /* ====================================================================
           APPEARANCE EDITOR PANEL — owner-only floating side panel
           --------------------------------------------------------------------
           Docks to the right edge on desktop, becomes a bottom sheet on
           mobile. Uses fixed dark styling that's NOT themed, so the panel
           stays legible even when the page-under-edit is mid-experiment
           with a hostile colour scheme.
           ==================================================================== */
        #appearance-panel {
            position: fixed; top: 0; right: 0; bottom: 0;
            width: 360px; max-width: 90vw;
            background: #0c0c0c; border-left: 1px solid #2a2a2a;
            color: #e0e0e0;
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
            z-index: 9000;
            transform: translateX(100%);
            transition: transform 0.25s ease-out;
            display: flex; flex-direction: column;
            overflow: hidden;
            box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
        }
        #appearance-panel.is-open { transform: translateX(0); }
        @media (max-width: 768px) {
            #appearance-panel {
                width: 100vw; max-width: 100vw;
                top: auto; bottom: 0; height: 75vh;
                border-left: none; border-top: 1px solid #2a2a2a;
                border-radius: 16px 16px 0 0;
                transform: translateY(100%);
            }
            #appearance-panel.is-open { transform: translateY(0); }
        }
        .ap-header {
            display: flex; align-items: center; justify-content: space-between;
            padding: 1rem 1.25rem; border-bottom: 1px solid #2a2a2a;
        }
        .ap-title {
            font-family: 'Courier New', monospace;
            font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1.5px;
        }
        .ap-close {
            background: transparent; border: none;
            color: #888; font-size: 1.4rem; line-height: 1;
            cursor: pointer; padding: 4px 8px;
        }
        .ap-close:hover { color: #fff; }
        .ap-tabs { display: flex; border-bottom: 1px solid #2a2a2a; flex-shrink: 0; }
        .ap-tab {
            flex: 1; background: transparent; border: none; color: #888;
            padding: 0.75rem 0.5rem;
            font-family: 'Courier New', monospace;
            font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1px;
            cursor: pointer; border-bottom: 2px solid transparent;
        }
        .ap-tab.is-active { color: #e0e0e0; border-bottom-color: #5e9eff; }
        /* Unsaved-changes indicator (Jun 11 2026): a small dot on Save
           whenever the working theme has diverged — creatives shouldn't
           have to remember whether they touched something. */
        .ap-btn.is-primary.is-dirty::after {
            content: '';
            display: inline-block;
            width: 6px; height: 6px;
            border-radius: 50%;
            background: #5e9eff;
            margin-left: 8px;
            vertical-align: middle;
        }
        /* Template diagram buttons (Jun 11 2026) — same visual language
           as the Post Size pills: the sketch IS the explanation. */
        .ap-template-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 8px;
            margin-bottom: 1.25rem;
        }
        .ap-tpl {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 7px;
            padding: 12px 8px 10px;
            background: transparent;
            border: 1px solid #2a2a2a;
            color: #888;
            cursor: pointer;
            transition: all 0.15s;
        }
        .ap-tpl:hover { border-color: #666; color: #ccc; }
        .ap-tpl.is-active { border-color: #fff; color: #fff; background: #161616; }
        .ap-tpl-sketch { width: 48px; height: 36px; }
        /* Live previews from the user's own work (Jun 11 2026): when
           images exist, each template button shows the arrangement built
           from their actual pieces; the abstract sketch yields. */
        .ap-tpl .ap-tpl-live {
            position: relative;
            display: block;
            width: 48px; height: 36px;
        }
        .ap-tpl .ap-tpl-live-cell {
            position: absolute;
            background-size: cover;
            background-position: center;
            border-radius: 1px;
            filter: grayscale(35%);
            opacity: 0.85;
            transition: filter 0.15s, opacity 0.15s;
        }
        .ap-tpl.is-active .ap-tpl-live-cell,
        .ap-tpl:hover .ap-tpl-live-cell { filter: grayscale(0%); opacity: 1; }
        .ap-tpl.has-live .ap-tpl-sketch { display: none; }
        .ap-tpl-sketch rect {
            fill: currentColor;
            opacity: 0.28;
            stroke: currentColor;
            stroke-width: 1;
            transition: opacity 0.15s;
        }
        .ap-tpl.is-active .ap-tpl-sketch rect { opacity: 0.85; }
        .ap-tpl-label {
            font-family: var(--font-mono);
            font-size: 0.66rem;
            letter-spacing: 0.06em;
            text-transform: uppercase;
        }
        /* Curated Looks cards (Jun 11 2026) */
        .ap-looks-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
        .ap-look {
            display: flex; flex-direction: column; gap: 7px;
            padding: 10px;
            background: transparent;
            border: 1px solid #2a2a2a;
            cursor: pointer;
            text-align: left;
            transition: all 0.15s;
        }
        .ap-look:hover { border-color: #666; }
        .ap-look.is-active { border-color: #fff; }
        /* Mini page mock: bg swatch with a name bar + two tile blocks
           drawn in the look's own colours, headline in its own face. */
        .ap-look-mock {
            height: 58px; border-radius: 2px; padding: 7px 8px;
            display: flex; flex-direction: column; gap: 5px;
            border: 1px solid rgba(127,127,127,0.25);
        }
        .ap-look-name-line { font-size: 0.72rem; font-weight: 600; line-height: 1; }
        .ap-look-tiles { display: flex; gap: 4px; flex: 1; }
        .ap-look-tile { flex: 1; border-radius: 2px; opacity: 0.85; }
        .ap-look-label {
            font-family: var(--font-mono); font-size: 0.66rem;
            letter-spacing: 0.06em; text-transform: uppercase; color: #ccc;
        }
        .ap-look-tag { font-size: 0.62rem; color: #777; line-height: 1.3; }
        .ap-cover-layouts { display: flex; flex-direction: column; gap: 6px; }
        .ap-cover-layout {
            text-align: left; background: #1a1a1a; border: 1px solid #333; color: #bbb;
            font-family: var(--font-mono); font-size: 0.75rem; padding: 10px 12px; cursor: pointer;
            transition: border-color 0.15s, color 0.15s;
        }
        .ap-cover-layout:hover { border-color: #666; color: #fff; }
        .ap-cover-layout.is-active { border-color: #fff; color: #fff; background: #222; }
        .ap-cover-picker {
            display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
            max-height: 260px; overflow-y: auto;
        }
        .ap-cover-thumb {
            position: relative; aspect-ratio: 1; background-size: cover; background-position: center;
            border: 2px solid transparent; cursor: pointer; background-color: #1a1a1a;
        }
        .ap-cover-thumb.is-selected { border-color: #fff; }
        .ap-cover-thumb .ap-cover-order {
            position: absolute; top: 4px; left: 4px; min-width: 18px; height: 18px;
            background: #fff; color: #000; font-family: var(--font-mono); font-size: 0.65rem;
            display: none; align-items: center; justify-content: center; border-radius: 9px; padding: 0 5px;
        }
        .ap-cover-thumb.is-selected .ap-cover-order { display: inline-flex; }
        .ap-cover-thumb .ap-cover-vbadge {
            position: absolute; bottom: 4px; right: 4px; width: 16px; height: 16px;
            background: rgba(0,0,0,0.6); color: #fff; font-size: 0.5rem; border-radius: 50%;
            display: flex; align-items: center; justify-content: center;
        }
        .ap-cover-thumb .ap-cover-reposition {
            position: absolute; bottom: 4px; left: 4px;
            background: rgba(0,0,0,0.7); color: #fff; border: 1px solid rgba(255,255,255,0.5);
            font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.05em;
            padding: 3px 6px; cursor: pointer; text-transform: uppercase;
        }
        .ap-cover-thumb .ap-cover-reposition:hover { background: #000; border-color: #fff; }
        .ap-cover-picker-empty { grid-column: 1 / -1; font-family: var(--font-mono); font-size: 0.7rem; color: #666; padding: 12px 0; }
        .ap-content { flex: 1; overflow-y: auto; padding: 1.25rem; }
        .ap-section { margin-bottom: 1.5rem; }
        .ap-section-label {
            font-family: 'Courier New', monospace;
            font-size: 0.6rem; text-transform: uppercase; letter-spacing: 1.5px;
            color: #888; margin-bottom: 0.6rem;
        }
        .ap-color-row {
            display: flex; align-items: center; gap: 0.75rem;
            padding: 0.5rem 0; border-bottom: 1px solid #1a1a1a;
        }
        .ap-color-label { flex: 1; font-size: 0.85rem; }
        .ap-color-swatch {
            width: 28px; height: 28px; border-radius: 4px;
            border: 1px solid #2a2a2a; cursor: pointer;
            position: relative; overflow: hidden;
        }
        .ap-color-swatch input[type="color"] {
            position: absolute; top: 0; left: 0;
            width: 100%; height: 100%;
            border: none; padding: 0; cursor: pointer; opacity: 0;
        }
        .ap-color-hex {
            width: 80px;
            background: #1a1a1a; border: 1px solid #2a2a2a; color: #e0e0e0;
            font-family: 'Courier New', monospace; font-size: 0.8rem;
            padding: 6px 8px; border-radius: 4px;
            text-align: center; text-transform: uppercase;
        }

        /* ============================================================
           PALETTE PRESETS (May 8) — one-tap curated colour combinations.
           Each preset is a horizontal swatch strip with its name above.
           Tapping the strip applies the preset's colour values to the
           workingTheme and re-renders the UI. The user can then tweak
           individual colours from the swatches below or hit Save.
           Hover/active states give a small lift so the row feels
           tappable rather than ornamental.
           ============================================================ */
        .ap-presets {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 0.5rem;
            margin-bottom: 0.75rem;
        }
        .ap-preset {
            background: #161616;
            border: 1px solid #2a2a2a;
            border-radius: 6px;
            padding: 0.4rem 0.5rem;
            cursor: pointer;
            display: flex;
            flex-direction: column;
            gap: 0.35rem;
            transition: border-color 0.12s, transform 0.08s;
            -webkit-tap-highlight-color: transparent;
        }
        .ap-preset:hover { border-color: #444; }
        .ap-preset:active { transform: scale(0.985); }
        .ap-preset-name {
            font-family: 'Courier New', monospace;
            font-size: 0.6rem;
            text-transform: uppercase;
            letter-spacing: 1px;
            color: #aaa;
        }
        .ap-preset-strip {
            display: flex;
            height: 22px;
            border-radius: 3px;
            overflow: hidden;
            border: 1px solid #2a2a2a;
        }
        .ap-preset-strip > span {
            flex: 1;
        }
        @media (max-width: 480px) {
            /* On phones, single column reads better than two cramped
               columns — preset names get truncated otherwise. */
            .ap-presets { grid-template-columns: 1fr; }
        }

        .ap-select {
            width: 100%;
            background: #1a1a1a; border: 1px solid #2a2a2a; color: #e0e0e0;
            padding: 8px 10px; border-radius: 4px;
            font-family: 'Inter', sans-serif; font-size: 0.85rem;
            margin-bottom: 0.75rem;
        }
        .ap-range-row { display: flex; align-items: center; gap: 0.75rem; }
        .ap-range {
            flex: 1; -webkit-appearance: none; appearance: none;
            height: 4px; background: #2a2a2a; border-radius: 2px; outline: none;
        }
        .ap-range::-webkit-slider-thumb {
            -webkit-appearance: none; appearance: none;
            width: 16px; height: 16px;
            background: #5e9eff; border-radius: 50%;
            cursor: pointer;
        }
        .ap-range-value {
            font-family: 'Courier New', monospace;
            font-size: 0.75rem; color: #888;
            min-width: 36px; text-align: right;
        }
        .ap-bg-upload {
            border: 1px dashed #2a2a2a; border-radius: 6px;
            padding: 1.5rem; text-align: center;
            cursor: pointer; color: #888; font-size: 0.85rem;
            transition: border-color 0.15s, color 0.15s;
        }
        .ap-bg-upload:hover { border-color: #5e9eff; color: #e0e0e0; }
        .ap-bg-upload.has-image {
            border-style: solid;
            background-size: cover; background-position: center;
            min-height: 120px; color: transparent;
        }
        .ap-bg-actions { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
        .ap-bg-clear-btn {
            flex: 1;
            background: transparent; border: 1px solid #2a2a2a; color: #e0e0e0;
            padding: 6px 12px;
            font-family: 'Courier New', monospace;
            font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1px;
            border-radius: 4px; cursor: pointer;
        }
        .ap-bg-clear-btn:hover { background: #1a1a1a; }
        .ap-contrast-warning {
            background: rgba(255, 152, 0, 0.1);
            border: 1px solid rgba(255, 152, 0, 0.3);
            color: #ffb74d;
            padding: 0.6rem 0.8rem; border-radius: 4px;
            font-size: 0.75rem; margin-top: 0.5rem;
            display: none;
        }
        .ap-contrast-warning.is-visible { display: block; }
        .ap-footer {
            display: flex; gap: 0.5rem;
            padding: 0.75rem 1rem;
            border-top: 1px solid #2a2a2a; flex-shrink: 0;
        }
        .ap-btn {
            flex: 1; padding: 10px 14px; border-radius: 4px;
            font-family: 'Courier New', monospace;
            font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px;
            cursor: pointer; border: 1px solid #2a2a2a;
            background: transparent; color: #e0e0e0;
        }
        .ap-btn:hover { background: #1a1a1a; }
        .ap-btn.is-primary {
            background: #5e9eff; border-color: #5e9eff; color: #000;
        }
        .ap-btn.is-primary:hover { background: #4a8ce8; }
        .ap-btn:disabled { opacity: 0.4; cursor: not-allowed; }
        .ap-reset-link {
            background: transparent; border: none; color: #888;
            font-family: 'Courier New', monospace;
            font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1px;
            cursor: pointer; padding: 0; text-decoration: underline;
        }
        .ap-reset-link:hover { color: #e0e0e0; }
        .ap-pane { display: none; }
        .ap-pane.is-active { display: block; }
/* ============================================================================
   SPARK GRID (.portfolio-grid2d) - 2D arrangeable gallery, natural aspect ratios
   ----------------------------------------------------------------------------
   SparkAspectGrid positions each tile absolutely (left / top / width / height)
   so a tile's height follows its image, nothing is cropped to a square. These
   rules cover the tile chrome, the resize handle, drag feedback, the hover
   caption, the Rearrange toolbar and the mobile single-column fallback.
   ============================================================================ */
.portfolio-grid2d {
    display: block !important;
    width: 100%;
    position: relative;
    /* The grid module reads this computed column-gap as its tile spacing, so the
       Customise "Grid gap" control (which sets --layout-gap) drives the grid. */
    column-gap: var(--layout-gap, 12px);
}
.portfolio-grid2d .work-item {
    overflow: hidden;
    opacity: 1 !important;
    aspect-ratio: auto !important;
    border-radius: var(--media-radius, 6px);
}
.portfolio-grid2d .work-media {
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
}
/* The grid sizes each tile's height on .work-item from the cover aspect, so the
   media-container has to fill that height too. Without this it collapses to the
   media's natural height while the tile stays taller (a video tile is sized to
   the video aspect, but the visible poster is the thumbnail at a different
   aspect), leaving the tile background showing as a grey band beneath the clip.
   The hero and square grids already set this; grid2d was the only one missing it. */
.portfolio-grid2d .media-container {
    height: 100%;
}

/* Hide the masonry-era handles; the grid module supplies its own. */
.portfolio-grid2d .resize-handle,
.portfolio-grid2d .spacing-controls,
.portfolio-grid2d .pan-hint { display: none !important; }

/* Resize handle (bottom-right): owner, Rearrange mode only. */
.portfolio-grid2d .g2d-handle { display: none; }
body.is-adjusting-layout .portfolio-grid2d .g2d-handle {
    display: block; position: absolute; right: 0; bottom: 0;
    width: 30px; height: 30px; cursor: nwse-resize; z-index: 6; touch-action: none;
}
body.is-adjusting-layout .portfolio-grid2d .g2d-handle::after {
    content: ''; position: absolute; right: 6px; bottom: 6px; width: 11px; height: 11px;
    border-right: 2px solid var(--accent, #fff); border-bottom: 2px solid var(--accent, #fff);
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.65));
}
body.is-adjusting-layout .portfolio-grid2d .work-item { cursor: grab; touch-action: none; }
body.is-adjusting-layout .portfolio-grid2d .work-item.g2d-dragging { cursor: grabbing; }

.portfolio-grid2d .work-item.g2d-dragging {
    opacity: 0.6 !important; z-index: 50;
    box-shadow: 0 0 0 2px var(--accent, #fff), 0 12px 30px rgba(0, 0, 0, 0.5);
}
.portfolio-grid2d .work-item.g2d-resizing {
    z-index: 50;
    box-shadow: 0 0 0 2px var(--accent, #fff), 0 12px 30px rgba(0, 0, 0, 0.5);
}

/* Hover caption keeps work identifiable in the grid (viewing mode only). */
.portfolio-grid2d .work-item .work-title {
    position: absolute; left: 0; right: 0; bottom: 0; margin: 0;
    padding: 22px 12px 10px; z-index: 4; pointer-events: none;
    font-family: var(--font-mono); font-size: 0.72rem; color: #fff;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0));
    opacity: 0; transition: opacity 0.18s ease;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.portfolio-grid2d .work-item:hover .work-title { opacity: 1; }
body.is-adjusting-layout .portfolio-grid2d .work-item .work-title { display: none; }

/* Rearrange toolbar: Auto-arrange + Columns. */
#g2d-toolbar {
    position: fixed; left: 50%; transform: translateX(-50%); bottom: 22px;
    z-index: 9000; display: none; align-items: center; gap: 12px;
    padding: 9px 14px; border-radius: 999px;
    background: var(--surface, #181818); border: 1px solid var(--border-bright, #3a3a3a);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5); font-family: var(--font-mono); font-size: 0.7rem;
}
#g2d-toolbar .g2d-tb-label { color: var(--text-muted, #999); text-transform: uppercase; letter-spacing: 0.05em; }
#g2d-toolbar .g2d-seg { display: inline-flex; border: 1px solid var(--border, #333); border-radius: 8px; overflow: hidden; }
#g2d-toolbar .g2d-seg button {
    appearance: none; border: 0; border-right: 1px solid var(--border, #333); background: transparent;
    color: var(--text-muted, #999); font-family: var(--font-mono); font-size: 0.72rem; padding: 5px 11px; cursor: pointer;
}
#g2d-toolbar .g2d-seg button:last-child { border-right: 0; }
#g2d-toolbar .g2d-seg button.on { background: var(--accent, #fff); color: #111; }

/* Mobile (Jul 10 2026): the Spark Grid engine now runs below 900px too,
   packing tiles into 2 fixed columns with absolute positioning, so the
   old single-column static collapse is gone. It had two problems: it
   fought the engine (position: static !important beat the tiles' inline
   styles), and a static .work-item stops being a positioning context,
   which let absolute tile children (the 1/N carousel counter, corner
   badges) escape and anchor to the viewport instead. Owner controls
   stay hidden; the engine supplies geometry via inline styles. */
@media (max-width: 900px) {
    .portfolio-grid2d .g2d-handle { display: none !important; }
    #g2d-toolbar { display: none !important; }
}

/* Auto-arrange button + divider in the Spark Grid toolbar. */
#g2d-toolbar .g2d-auto-btn {
    appearance: none; border: 0; cursor: pointer;
    background: var(--accent, #fff); color: #111;
    font-family: var(--font-mono); font-size: 0.72rem; font-weight: 600;
    padding: 6px 13px; border-radius: 8px; white-space: nowrap;
}
#g2d-toolbar .g2d-auto-btn:hover { filter: brightness(1.05); }
#g2d-toolbar .g2d-auto-btn:disabled { opacity: 0.6; cursor: default; }
#g2d-toolbar .g2d-tb-divider { width: 1px; height: 20px; background: var(--border, #333); }

/* Spark Grid: Auto-arrange button in the Customise panel (accent, prominent). */
.ap-grid-auto-btn {
    appearance: none; border: 0; cursor: pointer;
    background: var(--accent, #fff); color: #111;
    font-family: var(--font-mono, 'Courier New', monospace);
    font-size: 0.78rem; font-weight: 600;
    padding: 11px 14px; border-radius: 9px;
}
.ap-grid-auto-btn:hover { filter: brightness(1.05); }
.ap-grid-auto-btn:disabled { opacity: 0.6; cursor: default; }

/* ================================================================
   PAGE TEMPLATES (Aug 8 2026, v2)
   Six non-grid compositions rendered by the SparkPageTemplates
   post-pass in profile.html. Every rule derives from the theme
   variables (--bg, --surface, --text, --text-muted, --border,
   --accent, --font-heading, --font-sans, --font-mono, --layout-gap,
   --media-radius) so Curated Looks restyle these layouts too.
   v2: full-bleed breakout for the immersive templates, Filmstrip
   and Pinboard explicitly row-direction (the feed's base direction
   is column, which is what stacked them in v1), Ling-style Index,
   ratio-fitted Stage.
   ================================================================ */

/* SCOPING NOTE: renderFeed puts the same tpl-<name> class on <body>
   for page-level hooks. Container rules in this section must target
   .portfolio-feed.tpl-<name>, never bare .tpl-<name>, or they style
   the body itself (which is exactly the bug that once turned the
   whole page into a flex row). Descendant rules are safe either way. */
.portfolio-feed.portfolio-tpl {
    display: block;
    position: relative;
    z-index: 0;
    isolation: isolate;
}
.portfolio-tpl .work-item {
    margin-left: 0;
    margin-right: 0;
}

/* Full-bleed breakout: these compositions own the whole viewport
   width, not the centred content column. html/body already clip
   overflow-x, so the negative margins cannot cause a scrollbar. */
.portfolio-feed.tpl-index,
.portfolio-feed.tpl-stage,
.portfolio-feed.tpl-filmstrip,
.portfolio-feed.tpl-callsheet,
.portfolio-feed.tpl-pinboard {
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
}

/* Shared caption used by Lookbook and Filmstrip */
.portfolio-tpl .tpl-caption {
    display: flex;
    align-items: baseline;
    gap: 0.6em;
    margin-top: 0.6rem;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.05em;
    color: var(--text);
}
.portfolio-tpl .tpl-cap-num { color: var(--text-muted); }
.portfolio-tpl .tpl-cap-title { text-transform: uppercase; }
.portfolio-tpl .tpl-cap-meta { color: var(--text-muted); margin-left: auto; }

/* ---------------- INDEX (v2, lingyu.info structure) ----------------
   Editorial rows: thumbnail left, text block right, information rail
   on the far right. Airy, typographic, no hover tricks. */
.portfolio-feed.tpl-index { padding: 3.5rem 0 6rem !important; }
.tpl-index .tpl-index-wrap {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: clamp(3rem, 6vw, 7rem);
    max-width: 1760px;
    margin: 0 auto;
    padding: 0 4vw;
    align-items: start;
}
.tpl-index .tpl-index-row {
    display: grid;
    grid-template-columns: min(430px, 34vw) minmax(0, 1fr);
    gap: clamp(1.5rem, 3vw, 3.5rem);
    align-items: start;
    margin-bottom: clamp(4rem, 9vh, 7rem);
}
.tpl-index .tpl-index-row:last-child { margin-bottom: 0; }
.tpl-index .tpl-ix-media .work-item {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
    height: auto !important;
    border-radius: var(--media-radius);
    overflow: hidden;
}
.tpl-index .tpl-ix-media .work-item .media-container,
.tpl-index .tpl-ix-media .work-item .work-media,
.tpl-index .tpl-ix-media .work-item .media-container img,
.tpl-index .tpl-ix-media .work-item .media-container video {
    width: 100% !important;
    max-width: 100% !important;
    height: auto !important;
}
/* Image side option (Aug 9 2026): right or alternating rows. */
.tpl-index .tpl-ix-side-right .tpl-index-row,
.tpl-index .tpl-ix-side-alt .tpl-index-row:nth-child(even) {
    grid-template-columns: minmax(0, 1fr) min(430px, 34vw);
}
.tpl-index .tpl-ix-side-right .tpl-index-row .tpl-ix-media,
.tpl-index .tpl-ix-side-alt .tpl-index-row:nth-child(even) .tpl-ix-media { order: 2; }
.tpl-index .tpl-ix-side-right .tpl-index-row .tpl-ix-text,
.tpl-index .tpl-ix-side-alt .tpl-index-row:nth-child(even) .tpl-ix-text { order: 1; }
@media (max-width: 760px) {
    .tpl-index .tpl-ix-side-right .tpl-index-row,
    .tpl-index .tpl-ix-side-alt .tpl-index-row:nth-child(even) { grid-template-columns: 1fr; }
    .tpl-index .tpl-ix-side-right .tpl-index-row .tpl-ix-media,
    .tpl-index .tpl-ix-side-alt .tpl-index-row:nth-child(even) .tpl-ix-media { order: 1; }
    .tpl-index .tpl-ix-side-right .tpl-index-row .tpl-ix-text,
    .tpl-index .tpl-ix-side-alt .tpl-index-row:nth-child(even) .tpl-ix-text { order: 2; }
}
.tpl-index .tpl-ix-head {
    font-family: var(--font-heading);
    font-size: 1rem;
    font-weight: 500;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--text);
    cursor: pointer;
    outline: none;
}
.tpl-index .tpl-ix-head:hover,
.tpl-index .tpl-ix-head:focus-visible { color: var(--accent); }
.tpl-index .tpl-ix-desc {
    margin-top: 1.1rem;
    font-size: 0.92rem;
    line-height: 1.55;
    color: var(--text);
    white-space: pre-line;
    max-width: 48ch;
}
.tpl-index .tpl-ix-link {
    display: inline-block;
    margin-top: 1.4rem;
    font-size: 0.82rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
    outline: none;
}
.tpl-index .tpl-ix-link:hover,
.tpl-index .tpl-ix-link:focus-visible { color: var(--accent); }
.tpl-index .tpl-index-info {
    position: sticky;
    top: 5rem;
    font-size: 0.9rem;
    line-height: 1.55;
    color: var(--text);
}
.tpl-index .tpl-ix-info-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 1.2rem;
}
.tpl-index .tpl-ix-info-title {
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: 0.82rem;
    text-decoration: underline;
    text-underline-offset: 3px;
}
.tpl-index .tpl-ix-info-loc {
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.tpl-index .tpl-ix-bio { white-space: pre-line; }
.tpl-index .tpl-ix-links { margin-top: 1.6rem; display: flex; flex-direction: column; gap: 0.5rem; }
.tpl-index .tpl-ix-links a {
    color: var(--text);
    font-size: 0.85rem;
    letter-spacing: 0.03em;
    text-decoration: underline;
    text-underline-offset: 3px;
    width: fit-content;
}
.tpl-index .tpl-ix-links a:hover { color: var(--accent); }
@media (max-width: 1100px) {
    .tpl-index .tpl-index-wrap { grid-template-columns: 1fr; }
    .tpl-index .tpl-index-info {
        position: static;
        border-top: 1px solid var(--border);
        padding-top: 2rem;
    }
}
@media (max-width: 760px) {
    .tpl-index .tpl-index-row { grid-template-columns: 1fr; gap: 1.2rem; margin-bottom: 4rem; }
    .tpl-index .tpl-ix-media .work-item { max-width: 100%; }
}

/* ---------------- LOOKBOOK ---------------- */
.portfolio-feed.tpl-lookbook { padding: 2rem 0 6rem; }
.tpl-lookbook .tpl-piece {
    max-width: min(820px, 88vw);
    margin: 0 auto clamp(6rem, 16vh, 11rem);
}
.tpl-lookbook .tpl-piece:last-child { margin-bottom: 3rem; }
.tpl-lookbook .work-item {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
    height: auto !important;
    border-radius: var(--media-radius);
    overflow: hidden;
}
.tpl-lookbook .work-item .media-container,
.tpl-lookbook .work-item .work-media,
.tpl-lookbook .work-item .media-container img,
.tpl-lookbook .work-item .media-container video {
    width: 100% !important;
    max-width: 100% !important;
    height: auto !important;
}
.tpl-lookbook .tpl-caption { justify-content: center; }
.tpl-lookbook .tpl-cap-meta { margin-left: 0; }
.tpl-lookbook .tpl-cap-title {
    font-family: var(--font-heading);
    text-transform: none;
    font-size: 0.95rem;
    letter-spacing: 0.01em;
}

/* ---------------- FILMSTRIP (v2, native scrolling) ---------------- */
.portfolio-feed.tpl-filmstrip {
    --fs-h: 56vh;
    display: flex !important;
    flex-direction: row !important;
    align-items: center;
    min-height: 88vh;
    gap: clamp(3rem, 5vw, 7rem);
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    padding: 8vh 7vw 3rem !important;
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
    scrollbar-color: var(--text-muted) transparent;
    -webkit-overflow-scrolling: touch;
}
.tpl-filmstrip .tpl-piece {
    flex: 0 0 auto;
    scroll-snap-align: center;
    margin: 0;
    max-width: none;
}
/* Centre-focus register (Aug 9 2026): pieces sit level and quiet,
   the one nearest the viewport centre carries the caption and a
   gentle lift, like justinesoulie.fr. */
.tpl-filmstrip .tpl-piece {
    transition: transform 0.35s ease, filter 0.35s ease;
    filter: drop-shadow(0 18px 40px rgba(0, 0, 0, 0.35));
}
/* Side pieces crumple like pinned paper (Aug 9 2026): a displacement
   filter wrinkles them and a slight turn angles them toward the
   centre. Safari ignores the displacement and keeps the turn. */
.tpl-filmstrip .tpl-piece.is-left .work-item,
.tpl-filmstrip .tpl-piece.is-right .work-item { filter: url(#spark-crumple); }
.tpl-filmstrip .tpl-piece.is-left { transform: perspective(1400px) rotateY(6deg) scale(0.94); }
.tpl-filmstrip .tpl-piece.is-right { transform: perspective(1400px) rotateY(-6deg) scale(0.94); }
.tpl-filmstrip .tpl-piece.is-centre { transform: scale(1.12); z-index: 5; }
.tpl-filmstrip .tpl-piece.is-centre .work-item { filter: none; }
.tpl-filmstrip .tpl-piece .tpl-caption {
    opacity: 0;
    transition: opacity 0.3s ease;
    justify-content: center;
}
.tpl-filmstrip .tpl-piece.is-centre .tpl-caption { opacity: 1; }
.tpl-filmstrip .work-item {
    width: auto;
    max-width: none;
    min-width: 100px;
    height: var(--fs-h);
    border-radius: var(--media-radius);
    overflow: hidden;
}
.tpl-filmstrip .work-item:not(.text-tile) .media-container {
    height: 100% !important;
    width: 100% !important;
    max-width: none !important;
    aspect-ratio: auto !important;
}
/* The media itself sizes by its own ratio at the strip height
   (Aug 10 2026): width 100% of an auto-width tile was circular,
   which some mobile engines resolve by collapsing — the thin-lines
   bug — and cover-cropping what was left. */
.tpl-filmstrip .work-item:not(.text-tile) .work-media,
.tpl-filmstrip .work-item:not(.text-tile) .media-container img,
.tpl-filmstrip .work-item:not(.text-tile) .media-container video {
    height: 100% !important;
    width: auto !important;
    max-width: none !important;
    aspect-ratio: auto !important;
    object-fit: contain !important;
    margin: 0 auto;
}
.tpl-filmstrip .tpl-caption { max-width: 100%; }
@media (max-width: 900px) {
    .portfolio-feed.tpl-filmstrip { --fs-h: 38vh; padding: 3vh 6vw 2rem !important; }
}
/* Phone filmstrip (Aug 10 2026): the strip becomes a quiet vertical
   stack, one piece per screen at its own ratio with the caption
   under it — the justinesoulie.fr mobile register Felix pointed at.
   Every strip mechanic (fixed height, crumple, tilt, centre focus)
   is switched off; the !importants also outrank fitFilmstrip's
   inline width. */
@media (max-width: 700px) {
    .portfolio-feed.tpl-filmstrip {
        display: flex !important;
        flex-direction: column !important;
        align-items: center;
        overflow-x: visible;
        overflow-y: visible;
        min-height: 0;
        scroll-snap-type: none;
        gap: 3.2rem;
        padding: 1.6rem 7vw 3rem !important;
    }
    .tpl-filmstrip .tpl-piece,
    .tpl-filmstrip .tpl-piece.is-left,
    .tpl-filmstrip .tpl-piece.is-right,
    .tpl-filmstrip .tpl-piece.is-centre {
        transform: none !important;
        filter: none !important;
        width: 100%;
        max-width: 420px;
    }
    .tpl-filmstrip .tpl-piece .work-item,
    .tpl-filmstrip .tpl-piece.is-left .work-item,
    .tpl-filmstrip .tpl-piece.is-right .work-item {
        height: auto !important;
        width: 100% !important;
        min-width: 0;
        filter: none !important;
    }
    .tpl-filmstrip .work-item:not(.text-tile) .media-container,
    .tpl-filmstrip .work-item:not(.text-tile) .work-media,
    .tpl-filmstrip .work-item:not(.text-tile) .media-container img,
    .tpl-filmstrip .work-item:not(.text-tile) .media-container video {
        width: 100% !important;
        height: auto !important;
        object-fit: contain !important;
        margin: 0;
    }
    .tpl-filmstrip .tpl-piece .tpl-caption { opacity: 1 !important; }
}

/* ---------------- STAGE (v2, ratio-fitted slides) ----------------
   applyStage measures each slide's media ratio and sets the tile's
   width and height in px, so the media fills its frame exactly:
   no letterboxing, no black side panels. The rules here just make
   the media obey the tile's box. */
.portfolio-feed.tpl-stage { padding: 0 0 2.5rem !important; }
.tpl-stage .tpl-stage-skip { display: none !important; }
.tpl-stage .tpl-stage-viewport { position: relative; }
.tpl-stage .tpl-stage-slide {
    display: none;
    justify-content: center;
    align-items: center;
    min-height: 74vh;
    padding: 2rem 6vw 1rem;
}
.tpl-stage .tpl-stage-slide.is-active { display: flex; }
.tpl-stage .tpl-stage-slide .work-item {
    position: relative;
    overflow: hidden;
    border-radius: var(--media-radius);
    max-width: 90vw;
}
.tpl-stage .tpl-stage-slide .work-item.text-tile,
.tpl-stage .tpl-stage-slide .work-item.folder-card { max-width: min(640px, 88vw); }
.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .media-container,
.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .work-media,
.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .media-container img,
.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .media-container video {
    width: 100% !important;
    height: 100% !important;
    max-width: none !important;
    max-height: none !important;
    object-fit: cover !important;
}
.tpl-stage .tpl-stage-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: color-mix(in srgb, var(--bg) 72%, transparent);
    color: var(--text);
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
    z-index: 6;
    transition: border-color 0.15s, background 0.15s;
}
.tpl-stage .tpl-stage-arrow:hover { border-color: var(--text); }
.tpl-stage .tpl-stage-prev { left: 2.5vw; }
.tpl-stage .tpl-stage-next { right: 2.5vw; }
.tpl-stage .tpl-stage-bar {
    display: flex;
    align-items: center;
    gap: 1.1rem;
    max-width: 90vw;
    margin: 0.6rem auto 0;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.tpl-stage .tpl-stage-caption {
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.tpl-stage .tpl-stage-progress {
    flex: 1;
    height: 2px;
    min-width: 60px;
    background: var(--border);
    border-radius: 1px;
    overflow: hidden;
}
.tpl-stage .tpl-stage-progress-fill {
    display: block;
    height: 100%;
    width: 0;
    background: var(--accent);
    transition: width 0.25s ease;
}
@media (max-width: 900px) {
    .tpl-stage .tpl-stage-slide { min-height: 56vh; padding: 1rem 4vw 0.6rem; }
    .tpl-stage .tpl-stage-arrow { width: 36px; height: 36px; }
    .tpl-stage .tpl-stage-prev { left: 3vw; }
    .tpl-stage .tpl-stage-next { right: 3vw; }
}

/* ---------------- CALLSHEET ---------------- */
.portfolio-feed.tpl-callsheet > .work-item { display: none !important; }
.portfolio-feed.tpl-callsheet { padding: 0 !important; }
.tpl-callsheet .tpl-cs-hero {
    position: relative;
    min-height: 86vh;
    display: flex;
    align-items: center;
    overflow: hidden;
}
.tpl-callsheet .tpl-cs-backdrop {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center;
    background-color: var(--surface);
    opacity: 0;
    transition: opacity 0.45s ease;
}
.tpl-callsheet .tpl-cs-backdrop.is-showing { opacity: 1; }
.tpl-callsheet .tpl-cs-hero.tpl-cs-clickable { cursor: pointer; }
.tpl-callsheet .tpl-cs-cover-cap {
    position: absolute;
    right: clamp(1rem, 4vw, 3rem);
    bottom: 2rem;
    z-index: 2;
    font-family: var(--font-mono);
    font-size: 0.66rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text);
    background: color-mix(in srgb, var(--bg) 62%, transparent);
    border: 1px solid var(--border);
    padding: 5px 10px;
    pointer-events: none;
    max-width: 40vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.tpl-callsheet .tpl-cs-cover-cap:empty { display: none; }
.tpl-callsheet .tpl-cs-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(100deg,
        color-mix(in srgb, var(--bg) 55%, transparent) 0%,
        color-mix(in srgb, var(--bg) 12%, transparent) 55%,
        transparent 100%);
}
.tpl-callsheet .tpl-cs-panel {
    position: relative;
    z-index: 2;
    margin: 5vh 0 5vh clamp(1rem, 5vw, 5rem);
    width: min(370px, 88vw);
    max-height: 76vh;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--media-radius);
    padding: 1.6rem 1.4rem 1.4rem;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
}
.tpl-callsheet .tpl-cs-name {
    font-family: var(--font-heading);
    font-size: clamp(1.7rem, 3.4vw, 2.4rem);
    line-height: 1.02;
    color: var(--text);
    overflow-wrap: anywhere;
}
.tpl-callsheet .tpl-cs-meta {
    margin-top: 0.45rem;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.tpl-callsheet .tpl-cs-list {
    margin-top: 1.1rem;
    overflow-y: auto;
    min-height: 0;
    flex: 0 1 auto;
    padding-right: 3px;
    scrollbar-width: thin;
    scrollbar-color: var(--text-muted) transparent;
}
.tpl-callsheet .tpl-cs-row {
    display: flex;
    align-items: baseline;
    gap: 0.8rem;
    padding: 0.55rem 0.1rem;
    border-top: 1px solid var(--border);
    cursor: pointer;
    outline: none;
}
.tpl-callsheet .tpl-cs-row:hover,
.tpl-callsheet .tpl-cs-row:focus-visible { background: color-mix(in srgb, var(--text) 6%, transparent); }
.tpl-callsheet .tpl-cs-when {
    font-family: var(--font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
    min-width: 4.4em;
}
.tpl-callsheet .tpl-cs-title {
    font-size: 0.85rem;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.tpl-callsheet .tpl-cs-num {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 0.62rem;
    color: var(--text-muted);
}
.tpl-callsheet .tpl-cs-row.tpl-cs-overflow { display: none; }
.tpl-callsheet .tpl-cs-list.is-open .tpl-cs-row.tpl-cs-overflow { display: flex; }
.tpl-callsheet .tpl-cs-more {
    width: 100%;
    margin-top: 0.4rem;
    padding: 0.45rem 0;
    background: transparent;
    border: 1px dashed var(--border);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.64rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
}
.tpl-callsheet .tpl-cs-more:hover { color: var(--text); border-color: var(--text-muted); }
.tpl-callsheet .tpl-cs-ctas { display: flex; gap: 0.5rem; margin-top: 1.1rem; }
.tpl-callsheet .tpl-cs-cta {
    flex: 1;
    text-align: center;
    padding: 0.55rem 0;
    border: 1px solid var(--accent);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 0.66rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    transition: background 0.15s, color 0.15s;
}
.tpl-callsheet .tpl-cs-cta:hover { background: var(--accent); color: var(--bg); }
@media (max-width: 900px) {
    .tpl-callsheet .tpl-cs-hero { min-height: 78vh; align-items: flex-end; }
    .tpl-callsheet .tpl-cs-panel { margin: 0 auto 4vh; max-height: 68vh; }
    .tpl-callsheet .tpl-cs-cover-cap { display: none; }
}

/* ---------------- PINBOARD (v2, full-bleed rows) ----------------
   flex-direction: row is the fix for v1's single-column stack: the
   feed's base rule is flex-column, and the v1 rule never overrode
   the direction. Widths and scatter margins are set inline by
   applyPinboard; two to three prints per row at desktop widths. */
.portfolio-feed.tpl-pinboard {
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: center;
    gap: 0 !important;
    padding: 7vh 3vw 9vh !important;
}
.tpl-pinboard .work-item.tpl-pin {
    transform: rotate(var(--pb-rot, 0deg));
    border-radius: var(--media-radius);
    overflow: hidden;
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
    transition: transform 0.18s ease;
}
.tpl-pinboard .work-item.tpl-pin:hover {
    transform: rotate(var(--pb-rot, 0deg)) scale(1.035);
    z-index: 60 !important;
}
@media (max-width: 900px) {
    .portfolio-feed.tpl-pinboard { padding: 3vh 3vw 6vh !important; }
    /* Two pins per row on phones (Aug 10 2026): one giant pin per
       row wasted the board. */
    .tpl-pinboard .work-item.tpl-pin {
        width: 46% !important;
        max-width: 48% !important;
    }
}

/* Panel: mini layout mocks on the layout-carrying Curated Looks */
.ap-look-cells { position: relative; flex: 1; }
.ap-look-cell { position: absolute; border-radius: 1px; opacity: 0.9; }

/* Layout tab v2 (Aug 8 2026): the template cards absorb the retired
   Looks tab's job, so each carries a one-line description. */
.ap-tpl { gap: 6px; padding: 12px 10px 11px; }
.ap-tpl-desc {
    font-size: 0.6rem;
    color: #777;
    line-height: 1.35;
    text-align: center;
    letter-spacing: 0.01em;
}
.ap-tpl.is-active .ap-tpl-desc, .ap-tpl:hover .ap-tpl-desc { color: #999; }

/* In-place template rearranging (Aug 8 2026): the Rearrange session
   turns each template's ordered container into a drag list. */
body.is-adjusting-layout .portfolio-tpl .tpl-index-row,
body.is-adjusting-layout .portfolio-tpl .tpl-piece,
body.is-adjusting-layout .portfolio-tpl .tpl-cs-row,
body.is-adjusting-layout .tpl-pinboard .work-item,
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-slide { cursor: grab; }
body.is-adjusting-layout .portfolio-tpl .sortable-ghost {
    opacity: 0.35;
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
}
body.is-adjusting-layout .portfolio-feed.tpl-callsheet .tpl-cs-row.tpl-cs-overflow { display: flex; }
body.is-adjusting-layout .portfolio-feed.tpl-callsheet .tpl-cs-more { display: none; }

/* Stage rearrange view: every slide becomes a draggable thumbnail. */
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-track {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    padding: 1.5rem 4vw;
}
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-slide {
    display: block !important;
    min-height: 0;
    padding: 0;
    width: 150px;
}
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-slide .work-item {
    width: 100% !important;
    height: auto !important;
    max-width: 100% !important;
}
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .media-container,
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .work-media,
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .media-container img,
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-slide .work-item:not(.text-tile):not(.folder-card) .media-container video {
    width: 100% !important;
    height: 150px !important;
    object-fit: cover !important;
}
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-arrow,
body.is-adjusting-layout .portfolio-feed.tpl-stage .tpl-stage-bar { display: none; }

/* Template rearrange chrome (Aug 8 2026): the SPACE handle is a
   masonry spacing control; on page templates the whole tile drags,
   so the handle only misleads. */
body.is-adjusting-layout .portfolio-tpl .spacing-drag-handle { display: none !important; }

/* Square grid row inflation fix (Aug 8 2026). The grid stretches
   every item in a row to the tallest one, and a tile whose media
   carries an inline portrait aspect-ratio (the progressive-load
   reservation, or a carousel's sizing image) inflated its row's
   min-content height, so whole rows rendered as portrait rectangles.
   min-height: 0 stops content from growing the track, overflow
   keeps the crop inside the square, and the media rules neutralise
   inline ratios: inside a square cell the cell defines the size. */
/* Square grid, definitive (Aug 9 2026): the grid stretches a row to
   its tallest item, and any in-flow media child with an intrinsic or
   inline ratio inflates the auto row's max-content height, which is
   why a carousel or reserved-ratio tile kept turning its whole row
   into portrait rectangles. Absolutely positioned children contribute
   nothing to track sizing, so the tile's own 1:1 is the only voice. */
.portfolio-square-grid .work-item {
    position: relative;
    aspect-ratio: 1 / 1 !important;
    height: auto !important;
    min-height: 0;
    overflow: hidden;
}
.portfolio-square-grid .work-item > .media-container,
.portfolio-square-grid .work-item > img.work-media,
.portfolio-square-grid .work-item > video.work-media,
.portfolio-square-grid .work-item > div[style*="position: relative"] {
    position: absolute !important;
    inset: 0 !important;
    width: 100% !important;
    height: 100% !important;
    aspect-ratio: auto !important;
}
.portfolio-square-grid .work-item .media-container img,
.portfolio-square-grid .work-item .media-container video,
.portfolio-square-grid .work-item img.work-media,
.portfolio-square-grid .work-item video.work-media {
    width: 100% !important;
    height: 100% !important;
    aspect-ratio: auto !important;
    object-fit: cover !important;
}


/* Pinboard rearrange (Aug 9 2026): dragging happens on the scattered
   board itself; the seeded geometry stays put and the flow re-slots
   live as prints change order. Overlaps resolve to the top print. */

/* ---------------- RUNWAY (Aug 8 2026, replaces Single Column) ----------------
   Full viewport width, no gaps, no chrome; captions appear on hover.
   Value stays '1' so every saved Single Column upgrades in place. */
.portfolio-feed.tpl-runway {
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding: 0 !important;
    gap: 0 !important;
}
.tpl-runway .tpl-run-piece {
    position: relative;
    margin: 0;
    width: 100%;
}
.tpl-runway .tpl-run-piece .work-item {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
    border-radius: 0;
    overflow: hidden;
    display: block;
    /* Transparent tile (Aug 10 2026): any letterboxing around media
       showed as solid black slabs between pieces. */
    background: transparent !important;
}
.tpl-runway .tpl-run-piece .media-container,
.tpl-runway .tpl-run-piece .work-media,
.tpl-runway .tpl-run-piece .media-container img,
.tpl-runway .tpl-run-piece .media-container video {
    width: 100% !important;
    max-width: 100% !important;
    height: auto !important;
    display: block;
}
.tpl-runway .tpl-run-cap {
    position: absolute;
    left: 1.4rem;
    bottom: 1.2rem;
    z-index: 3;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text);
    background: color-mix(in srgb, var(--bg) 68%, transparent);
    border: 1px solid var(--border);
    padding: 5px 10px;
    opacity: 0;
    transition: opacity 0.18s ease;
    pointer-events: none;
    max-width: 70vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.tpl-runway .tpl-run-piece:hover .tpl-run-cap { opacity: 1; }
@media (max-width: 900px) {
    .tpl-runway .tpl-run-cap { opacity: 1; font-size: 0.62rem; left: 0.8rem; bottom: 0.8rem; }
    /* Natural fit on phones (Aug 10 2026): nothing gets a forced
       height, nothing gets cover-cropped — every piece shows whole
       at its own ratio. */
    .tpl-runway:not(.tpl-runway-h) .tpl-run-piece .work-item { height: auto !important; }
    .tpl-runway:not(.tpl-runway-h) .tpl-run-piece .media-container,
    .tpl-runway:not(.tpl-runway-h) .tpl-run-piece .work-media,
    .tpl-runway:not(.tpl-runway-h) .tpl-run-piece .media-container img,
    .tpl-runway:not(.tpl-runway-h) .tpl-run-piece .media-container video {
        width: 100% !important;
        height: auto !important;
        aspect-ratio: auto !important;
    }
}

/* ---------------- CONTACT SHEET (Aug 9 2026, v2) ----------------
   Authentic 35mm: dark warm emulsion regardless of theme (film is
   film), sprocket holes showing the page colour through, orange
   edge markings, seeded scratches and dust per strip, and strips
   running the full viewport width with no side margins. The select
   circle is a hand-drawn highlighter loop, not a perfect oval. */
.portfolio-feed.tpl-contact {
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding: 3vh 0 8vh !important;
    gap: clamp(10px, 2.4vh, 24px) !important;
}
.tpl-contact .tpl-neg-strip {
    display: flex;
    gap: 10px;
    align-items: center;
    background: #161210;
    padding: 30px 8px 34px;
    position: relative;
    overflow: hidden;
}
.tpl-contact .tpl-neg-strip::before,
.tpl-contact .tpl-neg-strip::after {
    content: '';
    position: absolute;
    left: 6px;
    right: 6px;
    height: 9px;
    background-image: repeating-linear-gradient(90deg,
        var(--bg) 0 10px, transparent 10px 26px);
    border-radius: 1px;
    opacity: 0.92;
    z-index: 1;
}
.tpl-contact .tpl-neg-strip::before { top: 8px; }
.tpl-contact .tpl-neg-strip::after { bottom: 8px; }
.tpl-contact .tpl-neg-grime {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 3;
    mix-blend-mode: screen;
    opacity: 0.85;
}
.tpl-contact .tpl-neg-stock {
    position: absolute;
    top: 7px;
    right: 14px;
    z-index: 2;
    font-family: var(--font-mono);
    font-size: 0.58rem;
    letter-spacing: 0.16em;
    color: #d9a04b;
    opacity: 0.9;
}
.tpl-contact .tpl-frame {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    max-width: 26%;
    margin: 0;
}
@media (max-width: 900px) {
    .tpl-contact .tpl-frame { max-width: none; }
}
.tpl-contact .tpl-frame .work-item {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
    height: clamp(120px, 19vw, 230px);
    border-radius: 1px;
    overflow: hidden;
}
.tpl-contact .tpl-frame .work-item .media-container,
.tpl-contact .tpl-frame .work-item .work-media,
.tpl-contact .tpl-frame .work-item .media-container img,
.tpl-contact .tpl-frame .work-item .media-container video {
    width: 100% !important;
    height: 100% !important;
    max-width: none !important;
    aspect-ratio: auto !important;
    object-fit: cover !important;
}
.tpl-contact .tpl-frame-topnum {
    position: absolute;
    top: -21px;
    left: 4px;
    font-family: var(--font-mono);
    font-size: 0.58rem;
    letter-spacing: 0.14em;
    color: #d9a04b;
    opacity: 0.9;
    z-index: 2;
}
.tpl-contact .tpl-frame-rebate {
    position: absolute;
    left: 4px;
    right: 4px;
    bottom: -24px;
    font-family: var(--font-mono);
    font-size: 0.58rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: #d9a04b;
    opacity: 0.85;
    z-index: 2;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The rebate is the caption here; the tile's own hover overlay only
   collided with the highlighter (Aug 9 2026). */
.tpl-contact .tpl-frame .work-item .work-overlay { display: none !important; }
.tpl-contact .tpl-frame.is-select::after {
    content: '';
    position: absolute;
    inset: -14% -9%;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 150' fill='none'%3E%3Cpath d='M158 22 C196 34 214 66 186 98 C158 130 74 144 36 116 C4 92 12 48 66 26 C108 9 150 12 168 24' stroke='%23C9B117' stroke-width='11' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
    opacity: 0.92;
    pointer-events: none;
    z-index: 4;
}
@media (max-width: 900px) {
    .tpl-contact .tpl-neg-strip { gap: 7px; padding: 24px 6px 26px; }
    .tpl-contact .tpl-frame .work-item { height: 21vw; }
    .tpl-contact .tpl-frame-rebate, .tpl-contact .tpl-frame-topnum { display: none; }
}

/* ---------------- PATTERN TABLE (Aug 8 2026) ---------------- */
.portfolio-feed.tpl-pattern {
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: center;
    gap: 0 !important;
    padding: 2vh 3vw 9vh !important;
}
.tpl-pattern .tpl-pt-rule {
    flex: 1 1 100%;
    height: 14px;
    margin: 0 0 4vh;
    border-bottom: 2px solid var(--text-muted);
    background-image: repeating-linear-gradient(90deg,
        var(--text-muted) 0 1px, transparent 1px 24px);
    background-position: bottom;
    background-size: 100% 8px;
    background-repeat: no-repeat;
    opacity: 0.7;
}
.tpl-pattern .tpl-pat {
    position: relative;
    transform: rotate(var(--pt-rot, 0deg));
    transition: transform 0.18s ease;
}
.tpl-pattern .tpl-pat:hover { transform: rotate(var(--pt-rot, 0deg)) scale(1.02); z-index: 40; }
.tpl-pattern .tpl-pat .work-item {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
    position: relative;
    border-radius: 2px;
    overflow: hidden;
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22);
}
.tpl-pattern .tpl-pat .work-item::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 10%;
    bottom: 10%;
    border-left: 2px dashed var(--bg);
    opacity: 0.55;
    pointer-events: none;
}
.tpl-pattern .tpl-pat-cap {
    margin-top: 0.5rem;
    font-family: var(--font-mono);
    font-size: 0.66rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
@media (max-width: 900px) {
    .tpl-pattern .tpl-pat { width: 60% !important; max-width: 72% !important; }
}

/* Pattern rearranges on its scattered table too. */
body.is-adjusting-layout .portfolio-feed.tpl-pattern .tpl-pat,
body.is-adjusting-layout .portfolio-feed.tpl-runway .tpl-piece,
body.is-adjusting-layout .portfolio-feed.tpl-contact .tpl-frame { cursor: grab; }

/* Lookbook polish (Aug 8 2026): wider stage, plate rhythm. */
.tpl-lookbook .tpl-piece {
    max-width: min(920px, 90vw);
    margin: 0 auto clamp(7rem, 18vh, 12rem);
}
.tpl-lookbook .tpl-piece:first-child { margin-top: 1rem; }


/* Rearrange sessions (Aug 9 2026): media inside template tiles gives
   up the pointer so neither native image drag nor the legacy pan
   handler can pre-empt the reorder drag. Deletes and carousel arrows
   are buttons, not media, and stay live. */
body.is-adjusting-layout .portfolio-tpl img,
body.is-adjusting-layout .portfolio-tpl video {
    pointer-events: none;
    -webkit-user-drag: none;
    user-select: none;
}
body.is-adjusting-layout .portfolio-tpl .tpl-piece,
body.is-adjusting-layout .portfolio-tpl .tpl-index-row,
body.is-adjusting-layout .portfolio-tpl .tpl-cs-row,
body.is-adjusting-layout .portfolio-tpl .tpl-frame,
body.is-adjusting-layout .portfolio-tpl .tpl-pat,
body.is-adjusting-layout .tpl-pinboard .work-item { user-select: none; }

/* Dust and scratches page overlay (Aug 9 2026, v5): the tiles are
   CSS variables so real photographic scans can replace the drawn SVG
   fallbacks without touching the rules. Upload the two reference
   scans to Cloudinary as public IDs spark-dust-a and spark-dust-b,
   then swap the two --dust-* lines below for the commented pair.
   mix-blend-mode: screen drops their black backgrounds, exactly like
   compositing a scan in Photoshop, so no transparency is needed. */
:root {
    --dust-a: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='620' height='520' viewBox='0 0 620 520'%3E%3Cg fill='white'%3E%3Ccircle cx='42' cy='61' r='1.1' opacity='.5'/%3E%3Ccircle cx='188' cy='34' r='.8' opacity='.4'/%3E%3Ccircle cx='341' cy='97' r='1.4' opacity='.55'/%3E%3Ccircle cx='512' cy='55' r='.9' opacity='.42'/%3E%3Ccircle cx='96' cy='214' r='.7' opacity='.38'/%3E%3Ccircle cx='268' cy='176' r='1' opacity='.5'/%3E%3Ccircle cx='447' cy='236' r='1.5' opacity='.5'/%3E%3Ccircle cx='584' cy='188' r='.8' opacity='.4'/%3E%3Ccircle cx='61' cy='362' r='1.2' opacity='.52'/%3E%3Ccircle cx='217' cy='329' r='.7' opacity='.36'/%3E%3Ccircle cx='389' cy='391' r='1' opacity='.48'/%3E%3Ccircle cx='541' cy='344' r='1.3' opacity='.5'/%3E%3Ccircle cx='139' cy='471' r='.9' opacity='.44'/%3E%3Ccircle cx='318' cy='492' r='1.2' opacity='.5'/%3E%3Ccircle cx='489' cy='458' r='.8' opacity='.4'/%3E%3C/g%3E%3Cg stroke='white' fill='none' stroke-linecap='round'%3E%3Cpath d='M84 118 q14 -22 34 -28' stroke-width='.8' opacity='.34'/%3E%3Cpath d='M402 62 q22 8 30 30 t4 44' stroke-width='.7' opacity='.3'/%3E%3Cpath d='M236 258 q-8 26 8 46' stroke-width='.8' opacity='.32'/%3E%3Cpath d='M528 268 q-20 12 -24 38' stroke-width='.7' opacity='.28'/%3E%3Cpath d='M158 396 q26 -6 40 12' stroke-width='.8' opacity='.3'/%3E%3Cpath d='M300 30 l2 34' stroke-width='.9' opacity='.4'/%3E%3Cpath d='M470 402 l-2 46' stroke-width='.8' opacity='.36'/%3E%3C/g%3E%3C/svg%3E");
    --dust-b: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='620' height='520' viewBox='0 0 620 520'%3E%3Cg fill='white'%3E%3Ccircle cx='42' cy='61' r='1.1' opacity='.5'/%3E%3Ccircle cx='188' cy='34' r='.8' opacity='.4'/%3E%3Ccircle cx='341' cy='97' r='1.4' opacity='.55'/%3E%3Ccircle cx='512' cy='55' r='.9' opacity='.42'/%3E%3Ccircle cx='96' cy='214' r='.7' opacity='.38'/%3E%3Ccircle cx='268' cy='176' r='1' opacity='.5'/%3E%3Ccircle cx='447' cy='236' r='1.5' opacity='.5'/%3E%3Ccircle cx='584' cy='188' r='.8' opacity='.4'/%3E%3Ccircle cx='61' cy='362' r='1.2' opacity='.52'/%3E%3Ccircle cx='217' cy='329' r='.7' opacity='.36'/%3E%3Ccircle cx='389' cy='391' r='1' opacity='.48'/%3E%3Ccircle cx='541' cy='344' r='1.3' opacity='.5'/%3E%3Ccircle cx='139' cy='471' r='.9' opacity='.44'/%3E%3Ccircle cx='318' cy='492' r='1.2' opacity='.5'/%3E%3Ccircle cx='489' cy='458' r='.8' opacity='.4'/%3E%3C/g%3E%3Cg stroke='white' fill='none' stroke-linecap='round'%3E%3Cpath d='M84 118 q14 -22 34 -28' stroke-width='.8' opacity='.34'/%3E%3Cpath d='M402 62 q22 8 30 30 t4 44' stroke-width='.7' opacity='.3'/%3E%3Cpath d='M236 258 q-8 26 8 46' stroke-width='.8' opacity='.32'/%3E%3Cpath d='M528 268 q-20 12 -24 38' stroke-width='.7' opacity='.28'/%3E%3Cpath d='M158 396 q26 -6 40 12' stroke-width='.8' opacity='.3'/%3E%3Cpath d='M300 30 l2 34' stroke-width='.9' opacity='.4'/%3E%3Cpath d='M470 402 l-2 46' stroke-width='.8' opacity='.36'/%3E%3C/g%3E%3C/svg%3E");
    /* After uploading the scans, use these instead:
    --dust-a: url("https://res.cloudinary.com/deu1rexgw/image/upload/q_auto/spark-dust-a");
    --dust-b: url("https://res.cloudinary.com/deu1rexgw/image/upload/q_auto/spark-dust-b");
    */
}
/* Continuous dust (Aug 9 2026, v2): all three layers render always;
   --dust-t (0 to 1) shrinks the tiles as it rises, so the specks
   repeat more often and the page reads dustier percent by percent.
   Opacity is eased in JS and hits a true 0 at the slider's foot. */
html[data-dust]::after {
    content: '';
    position: fixed;
    inset: 0;
    z-index: 9990;
    pointer-events: none;
    mix-blend-mode: screen;
    opacity: var(--dust-opacity, 0.55);
    background-image: var(--dust-a), var(--dust-b), var(--dust-a);
    background-size:
        calc(1700px - var(--dust-t, 0.6) * 800px) auto,
        calc(1400px - var(--dust-t, 0.6) * 640px) auto,
        calc(2200px - var(--dust-t, 0.6) * 800px) auto;
    background-position: 0 0, 310px 140px, 520px 360px;
}


/* Pinboard resize grip (Aug 9 2026): visible only in the Rearrange
   session; drag the corner to scale a print. */
.tpl-pb-resize { display: none; }
body.is-adjusting-layout .tpl-pinboard .work-item.tpl-pin { position: relative; }
body.is-adjusting-layout .tpl-pinboard .tpl-pb-resize {
    display: block;
    position: absolute;
    right: 6px;
    bottom: 6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid var(--bg);
    cursor: nwse-resize;
    z-index: 70;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}


/* Horizontal runway (Aug 9 2026): full-viewport-height pieces butted
   side to side, scrolling sideways. */
.portfolio-feed.tpl-runway.tpl-runway-h {
    --rw-h: 88vh;
    display: flex !important;
    flex-direction: row !important;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
}
.tpl-runway.tpl-runway-h .tpl-run-piece {
    flex: 0 0 auto;
    width: auto;
    scroll-snap-align: center;
}
.tpl-runway.tpl-runway-h .tpl-run-piece .work-item {
    height: var(--rw-h) !important;
    width: auto;
}
.tpl-runway.tpl-runway-h .tpl-run-piece .media-container,
.tpl-runway.tpl-runway-h .tpl-run-piece .work-media,
.tpl-runway.tpl-runway-h .tpl-run-piece .media-container img,
.tpl-runway.tpl-runway-h .tpl-run-piece .media-container video {
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
    aspect-ratio: auto !important;
}
@media (max-width: 900px) {
    .portfolio-feed.tpl-runway.tpl-runway-h { --rw-h: 62vh; }
}

/* Pinboard rotate grip (Aug 9 2026): session-only, top-right. */
.tpl-pb-rotate { display: none; }
body.is-adjusting-layout .tpl-pinboard .tpl-pb-rotate {
    display: block;
    position: absolute;
    right: 6px;
    top: 6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--surface);
    border: 2px solid var(--accent);
    cursor: ew-resize;
    z-index: 70;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}


/* Lookbook columns (Aug 9 2026): two or three plates abreast. */
.portfolio-feed.tpl-lookbook.tpl-lb-cols-2,
.portfolio-feed.tpl-lookbook.tpl-lb-cols-3 {
    display: grid !important;
    gap: clamp(2rem, 4vw, 5rem);
    padding: 3rem 6vw 6rem;
    align-items: start;
}
.portfolio-feed.tpl-lookbook.tpl-lb-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.portfolio-feed.tpl-lookbook.tpl-lb-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.tpl-lb-cols-2 .tpl-piece,
.tpl-lb-cols-3 .tpl-piece {
    max-width: none;
    margin: 0 0 clamp(3rem, 8vh, 6rem);
}
@media (max-width: 900px) {
    .portfolio-feed.tpl-lookbook.tpl-lb-cols-2,
    .portfolio-feed.tpl-lookbook.tpl-lb-cols-3 { grid-template-columns: 1fr; }
}


/* Freeform header (Aug 9 2026): Header position > Free. The identity
   block leaves the flow and sits wherever the owner dragged it, at
   the stored angle and size. The rule must out-rank the psec static
   block above, hence the !importants. */
body[data-mast="free"] .profile-topbar { min-height: 340px; }
body[data-psec][data-mast="free"] .topbar-identity,
body[data-mast="free"] .profile-topbar .topbar-identity {
    position: absolute !important;
    inset: auto !important;
    left: var(--mast-x, 50%) !important;
    top: var(--mast-y, 42%) !important;
    transform: translate(-50%, -50%) rotate(var(--mast-rot, 0deg)) scale(var(--mast-scale, 1)) !important;
    width: auto !important;
    max-width: 90% !important;
    margin: 0 !important;
    z-index: 30;
}
#mast-drag-chip {
    display: none;
    position: absolute;
    top: -26px;
    left: 50%;
    transform: translateX(-50%);
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--accent);
    border-radius: 999px;
    padding: 3px 10px;
    cursor: move;
    z-index: 40;
    pointer-events: auto;
    user-select: none;
    white-space: nowrap;
}
body[data-mast="free"] #topbar-identity { pointer-events: auto; }

/* Split name / info blocks (Aug 9 2026): inside Free the owner can
   separate the info line from the name; psecApplyMastSplit reparents
   .topbar-meta to the topbar so the name's rotation and scale leave
   it alone, and it takes its own stored position. */
body[data-mast="free"] .profile-topbar .topbar-meta.meta-free {
    position: absolute !important;
    inset: auto !important;
    left: var(--meta-x, 50%) !important;
    top: var(--meta-y, 60%) !important;
    transform: translate(-50%, -50%) !important;
    margin: 0 !important;
    width: auto !important;
    max-width: 90% !important;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0 10px;
    z-index: 29;
    pointer-events: auto;
}
body[data-mast="free"] .topbar-meta.meta-free:active { cursor: grabbing !important; }
/* The tab row splits too: its own stored position, dragged by its
   grip only, since the row itself is nothing but click targets. */
body[data-mast="free"] .profile-topbar .profile-sections.nav-free {
    position: absolute !important;
    inset: auto !important;
    left: var(--nav-x, 50%) !important;
    top: var(--nav-y, 74%) !important;
    transform: translate(-50%, -50%) !important;
    margin: 0 !important;
    width: auto !important;
    max-width: 90% !important;
    z-index: 29;
}
/* Hidden info line (Aug 9 2026): owner choice, hides for everyone,
   in both the joined and the split placement. */
body.meta-hidden .profile-topbar .topbar-meta { display: none !important; }
/* Placement aids (Aug 11 2026): centre guide lines while a snapped
   drag is live, the panel fading out of the way during any drag, and
   the corner resize grip on the name block in Free. */
.profile-topbar .mast-guide-v,
.profile-topbar .mast-guide-h {
    position: absolute;
    display: none;
    pointer-events: none;
    z-index: 60;
    background: var(--accent);
    opacity: 0.55;
}
.profile-topbar .mast-guide-v { left: 50%; top: 0; bottom: 0; width: 1px; }
.profile-topbar .mast-guide-h { top: 50%; left: 0; right: 0; height: 1px; }
body.is-placing #appearance-panel {
    opacity: 0.12;
    pointer-events: none;
    transition: opacity 0.15s ease;
}
#mast-resize-chip {
    position: absolute;
    right: -16px;
    bottom: -16px;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--accent);
    border-radius: 999px;
    cursor: nwse-resize;
    z-index: 41;
    pointer-events: auto;
    user-select: none;
}
#meta-drag-chip,
#nav-drag-chip {
    position: absolute;
    top: -26px;
    left: 50%;
    transform: translateX(-50%);
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--accent);
    border-radius: 999px;
    padding: 3px 10px;
    cursor: move;
    z-index: 40;
    pointer-events: auto;
    user-select: none;
    white-space: nowrap;
}

/* Header pane: tab-name inputs and the split toggle (Aug 9 2026). */
#ap-psec-labels { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.ap-text {
    width: 100%;
    background: #0e0e0e;
    border: 1px solid #2a2a2a;
    color: #eee;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    padding: 8px 9px;
}
.ap-text:focus { outline: none; border-color: #555; }
.ap-mini-btn {
    background: transparent;
    border: 1px solid #2a2a2a;
    color: #ddd;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 9px 14px;
    cursor: pointer;
}
.ap-mini-btn:hover { border-color: #555; }


/* Crumpled paper across every layout (Aug 9 2026): Layout > below
   Scroll reveal. Same displacement filter the Filmstrip sides use.
   Heavy filter, so it stays off the lightbox and folder cards. */
body.fx-crumple #portfolio-feed .work-item:not(.text-tile):not(.folder-card) {
    filter: url(#spark-crumple);
}
body.fx-crumple .portfolio-grid2d .work-item:not(.text-tile):not(.folder-card) {
    filter: url(#spark-crumple);
}

/* Custom header positions (Aug 9 2026, v2): the action row pins to
   the foot of the cover in Middle, Bottom and Free, so it can never
   overlap the identity block, which is what happened when the
   absolute identity left the row centring itself into the same
   space. Bottom raises the identity to sit clear above the row. */
body[data-mast="middle"] .profile-topbar .topbar-actions,
body[data-mast="bottom"] .profile-topbar .topbar-actions,
body[data-mast="free"] .profile-topbar .topbar-actions {
    position: absolute !important;
    left: 50% !important;
    bottom: 1.6rem !important;
    transform: translateX(-50%) !important;
    margin: 0 !important;
    z-index: 25;
    align-self: auto !important;
}
body[data-mast="free"] .topbar-identity:active { cursor: grabbing !important; }

/* Header positions, deterministic (Aug 9 2026, v2): flex spare-height
   never survived the cover layouts' own topbar rules, so Middle and
   Bottom now place the identity absolutely, the same mechanism Free
   uses and the reason Free rendered while the presets did not. */
body[data-mast] .profile-topbar { min-height: max(380px, 38vh); position: relative; }
body[data-mast="middle"] .profile-topbar .topbar-identity,
body[data-psec][data-mast="middle"] .topbar-identity {
    position: absolute !important;
    inset: auto !important;
    left: 50% !important;
    top: 50% !important;
    transform: translate(-50%, -50%) !important;
    width: auto !important;
    max-width: 92% !important;
    margin: 0 !important;
    z-index: 30;
}
body[data-mast="bottom"] .profile-topbar .topbar-identity,
body[data-psec][data-mast="bottom"] .topbar-identity {
    position: absolute !important;
    inset: auto !important;
    left: 50% !important;
    bottom: 128px !important;
    top: auto !important;
    transform: translateX(-50%) !important;
    width: auto !important;
    max-width: 92% !important;
    margin: 0 !important;
    z-index: 30;
}

/* Lookbook plate width grip (Aug 9 2026): session-only. */
.tpl-lb-resize { display: none; }
body.is-adjusting-layout .tpl-lookbook .tpl-piece { position: relative; }
body.is-adjusting-layout .tpl-lookbook.tpl-lb-cols-2 .tpl-lb-resize,
body.is-adjusting-layout .tpl-lookbook.tpl-lb-cols-3 .tpl-lb-resize {
    display: block;
    position: absolute;
    right: 6px;
    bottom: 6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid var(--bg);
    cursor: ew-resize;
    z-index: 70;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}


/* Header details typography (Aug 9 2026): Type > Header details.
   Governs the role/location/connections line and the section links. */
body[data-hmeta-font="heading"] .topbar-role,
body[data-hmeta-font="heading"] .topbar-meta,
body[data-hmeta-font="heading"] .profile-section-link {
    font-family: var(--font-heading, inherit) !important;
    text-transform: none;
    letter-spacing: 0.01em;
}
body[data-hmeta-font="body"] .topbar-role,
body[data-hmeta-font="body"] .topbar-meta,
body[data-hmeta-font="body"] .profile-section-link {
    font-family: var(--font-body, inherit) !important;
    text-transform: none;
    letter-spacing: 0.01em;
}
body[data-hmeta-size="s"] .topbar-role,
body[data-hmeta-size="s"] .topbar-meta,
body[data-hmeta-size="s"] .profile-section-link { font-size: 0.68rem !important; }
body[data-hmeta-size="l"] .topbar-role,
body[data-hmeta-size="l"] .topbar-meta,
body[data-hmeta-size="l"] .profile-section-link { font-size: 1.02rem !important; }


/* Header text alignment (Aug 9 2026): document-style align control.
   Centre is the default and needs no rule. */
/* The plain selectors below tie with profile.html's inline
   body[data-psec] .topbar-identity centring and lose on source order,
   which is why the control did nothing. The [data-psec] variants add
   one attribute of specificity and win regardless of order. */
body[data-mta="left"] .topbar-identity,
body[data-psec][data-mta="left"] .topbar-identity {
    align-items: flex-start !important;
    text-align: left !important;
}
body[data-mta="left"] .topbar-identity .topbar-meta,
body[data-psec][data-mta="left"] .topbar-identity .topbar-meta { justify-content: flex-start !important; }
body[data-psec][data-mta="left"] .profile-sections { justify-content: flex-start !important; }
body[data-mta="right"] .topbar-identity,
body[data-psec][data-mta="right"] .topbar-identity {
    align-items: flex-end !important;
    text-align: right !important;
}
body[data-mta="right"] .topbar-identity .topbar-meta,
body[data-psec][data-mta="right"] .topbar-identity .topbar-meta { justify-content: flex-end !important; }
body[data-psec][data-mta="right"] .profile-sections { justify-content: flex-end !important; }
#ap-mast-textalign button { font-size: 1rem; line-height: 1; }


/* No-cover styles (Aug 9 2026): active only while nothing renders a
   cover (body lacks data-cover-on); any cover restores the normal
   header untouched. */

/* Masthead bar: the header compresses to a tight ruled band, name
   left, details and section links flowing right, work immediately
   below. Position controls have no canvas here and go quiet. */
body:not([data-cover-on])[data-ncs="masthead"]:not([data-mast="free"]) .profile-topbar {
    min-height: 0 !important;
    /* Clears the floating SPARK button, which sat on the name. */
    padding: 1.1rem 2rem 0.9rem 210px !important;
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: wrap;
    align-items: center !important;
    justify-content: flex-start !important;
    border-bottom: 1px solid var(--border-strong);
}
body:not([data-cover-on])[data-ncs="masthead"]:not([data-mast="free"]) .topbar-identity {
    position: static !important;
    transform: none !important;
    width: 100% !important;
    max-width: none !important;
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: wrap;
    align-items: baseline !important;
    gap: 0.4rem 1.2rem;
    text-align: left !important;
    margin: 0 !important;
}
body:not([data-cover-on])[data-ncs="masthead"]:not([data-mast="free"]) .topbar-identity #p-name {
    font-size: clamp(1.4rem, 2.6vw, 2rem) !important;
    margin: 0 !important;
}
body:not([data-cover-on])[data-ncs="masthead"]:not([data-mast="free"]) .topbar-identity .topbar-meta {
    margin-left: auto !important;
}
body:not([data-cover-on])[data-ncs="masthead"]:not([data-mast="free"]) .profile-topbar .topbar-actions {
    position: static !important;
    transform: none !important;
    flex-basis: 100%;
    justify-content: flex-start !important;
    margin: 0.6rem 0 0 !important;
}
@media (max-width: 760px) {
    body:not([data-cover-on])[data-ncs="masthead"]:not([data-mast="free"]) .profile-topbar { padding-left: 140px !important; }
    body:not([data-cover-on])[data-ncs="masthead"]:not([data-mast="free"]) .topbar-identity .topbar-meta { margin-left: 0 !important; }
}

/* Accent field: a solid block of the theme accent as the cover, the
   identity knocked out in the page colour. Position, alignment and
   Free all work on the block. */
body:not([data-cover-on])[data-ncs="accent"] .profile-topbar {
    background: var(--accent) !important;
    min-height: max(340px, 34vh);
    position: relative;
}
body:not([data-cover-on])[data-ncs="accent"] .topbar-identity #p-name,
body:not([data-cover-on])[data-ncs="accent"] .topbar-identity #p-role,
body:not([data-cover-on])[data-ncs="accent"] .topbar-identity .topbar-role,
body:not([data-cover-on])[data-ncs="accent"] .topbar-identity .topbar-meta,
body:not([data-cover-on])[data-ncs="accent"] .topbar-identity .topbar-meta span,
body:not([data-cover-on])[data-ncs="accent"] .topbar-identity .profile-section-link {
    color: var(--bg) !important;
}
body:not([data-cover-on])[data-ncs="accent"] .topbar-identity .profile-section-link.is-active {
    color: var(--bg) !important;
    border-color: var(--bg) !important;
}


/* Alignment in the absolute positions (Aug 9 2026): with Middle and
   Bottom the identity shrink-wraps, so text alignment alone changed
   nothing. Left and right now anchor the whole block to that edge. */
body[data-mast="middle"][data-mta="left"] .profile-topbar .topbar-identity,
body[data-psec][data-mast="middle"][data-mta="left"] .topbar-identity {
    left: 2.5rem !important;
    transform: translateY(-50%) !important;
    align-items: flex-start !important;
    text-align: left !important;
}
body[data-mast="middle"][data-mta="right"] .profile-topbar .topbar-identity,
body[data-psec][data-mast="middle"][data-mta="right"] .topbar-identity {
    left: auto !important;
    right: 2.5rem !important;
    transform: translateY(-50%) !important;
    align-items: flex-end !important;
    text-align: right !important;
}
body[data-mast="bottom"][data-mta="left"] .profile-topbar .topbar-identity,
body[data-psec][data-mast="bottom"][data-mta="left"] .topbar-identity {
    left: 2.5rem !important;
    transform: none !important;
    align-items: flex-start !important;
    text-align: left !important;
}
body[data-mast="bottom"][data-mta="right"] .profile-topbar .topbar-identity,
body[data-psec][data-mast="bottom"][data-mta="right"] .topbar-identity {
    left: auto !important;
    right: 2.5rem !important;
    transform: none !important;
    align-items: flex-end !important;
    text-align: right !important;
}
/* ============================================================
   Alignment gutters + logo clearance (Aug 9 2026): left and right
   alignment breathe off the page edge like a document margin, and
   Top + Left clears the fixed SPARK float that sat on the name.
   Free placement is excluded — position is by hand there. */
body[data-psec][data-mta="left"]:not([data-mast="free"]) .topbar-identity {
    padding-left: clamp(2rem, 4vw, 4.5rem) !important;
}
body[data-psec][data-mta="right"]:not([data-mast="free"]) .topbar-identity {
    padding-right: clamp(2rem, 4vw, 4.5rem) !important;
}
body[data-psec][data-mast="top"][data-mta="left"] .topbar-identity {
    padding-left: 190px !important;
}

/* Custom cursors (Aug 9 2026): the whole page wears the owner's
   choice; text fields keep the caret so typing never feels broken. */
body[data-cursor="crosshair"], body[data-cursor="crosshair"] * { cursor: crosshair !important; }
body[data-cursor="pen"], body[data-cursor="pen"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28'%3E%3Cpath d='M4 24 L8 14 L20 2 L26 8 L14 20 Z' fill='white' stroke='black' stroke-width='1.5'/%3E%3Ccircle cx='11.5' cy='16.5' r='1.6' fill='black'/%3E%3C/svg%3E") 4 24, auto !important;
}
body[data-cursor="scissors"], body[data-cursor="scissors"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ctext x='1' y='21' font-size='21' fill='white' stroke='black' stroke-width='0.9' paint-order='stroke'%3E%E2%9C%82%3C/text%3E%3C/svg%3E") 4 13, auto !important;
}
body[data-cursor="dot"], body[data-cursor="dot"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14'%3E%3Ccircle cx='7' cy='7' r='4.5' fill='white' stroke='black' stroke-width='1.4'/%3E%3C/svg%3E") 7 7, auto !important;
}
body[data-cursor="star"], body[data-cursor="star"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ctext x='2' y='21' font-size='21' fill='white' stroke='black' stroke-width='0.9' paint-order='stroke'%3E%E2%9C%A6%3C/text%3E%3C/svg%3E") 13 13, auto !important;
}
body[data-cursor="heart"], body[data-cursor="heart"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ctext x='2' y='21' font-size='20' fill='white' stroke='black' stroke-width='0.9' paint-order='stroke'%3E%E2%99%A5%3C/text%3E%3C/svg%3E") 13 13, auto !important;
}
body[data-cursor="smiley"], body[data-cursor="smiley"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ctext x='1' y='21' font-size='21' fill='white' stroke='black' stroke-width='0.9' paint-order='stroke'%3E%E2%98%BA%3C/text%3E%3C/svg%3E") 13 13, auto !important;
}
body[data-cursor="eye"], body[data-cursor="eye"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='20'%3E%3Cpath d='M2 10 Q14 1 26 10 Q14 19 2 10 Z' fill='white' stroke='black' stroke-width='1.4'/%3E%3Ccircle cx='14' cy='10' r='4' fill='black'/%3E%3Ccircle cx='15.5' cy='8.5' r='1.2' fill='white'/%3E%3C/svg%3E") 14 10, auto !important;
}
body[data-cursor="lightning"], body[data-cursor="lightning"] * {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='28'%3E%3Cpath d='M12 1 L3 15 L9 15 L7 27 L17 11 L10 11 Z' fill='white' stroke='black' stroke-width='1.3'/%3E%3C/svg%3E") 10 2, auto !important;
}
html body[data-cursor] input,
html body[data-cursor] textarea,
html body[data-cursor] [contenteditable="true"] { cursor: text !important; }

/* Hover treatments (Aug 9 2026): what a piece does under the
   pointer. Glitch is offset colour shadows, cheap and analogue. */
body[data-thov] .work-item .work-media { transition: filter 0.18s ease; }
body[data-thov="invert"] .work-item:hover .work-media { filter: invert(1); }
body[data-thov="mono"] .work-item:hover .work-media { filter: grayscale(1) contrast(1.15); }
/* Posterise (Aug 10 2026): discrete channel levels via the SVG
   filter in profile.html — the screen-print look. Static, so no
   black flashes when the pointer crosses between tiles. */
body[data-thov="posterize"] .work-item:hover .work-media {
    filter: url(#spark-posterise) saturate(1.15);
}

/* Crop marks (Aug 9 2026): registration ticks just inside every
   piece's corners, the grid as an uncut proof sheet. Difference
   blend keeps them visible on any image. */
body.fx-cropmarks #portfolio-feed .work-item:not(.text-tile),
body.fx-cropmarks .portfolio-grid2d .work-item:not(.text-tile) { position: relative; }
body.fx-cropmarks #portfolio-feed .work-item:not(.text-tile)::after,
body.fx-cropmarks .portfolio-grid2d .work-item:not(.text-tile)::after {
    content: '';
    position: absolute;
    inset: 5px;
    pointer-events: none;
    z-index: 6;
    mix-blend-mode: difference;
    background-image:
        linear-gradient(white, white), linear-gradient(white, white),
        linear-gradient(white, white), linear-gradient(white, white),
        linear-gradient(white, white), linear-gradient(white, white),
        linear-gradient(white, white), linear-gradient(white, white);
    background-repeat: no-repeat;
    background-size: 1px 12px, 12px 1px, 1px 12px, 12px 1px, 1px 12px, 12px 1px, 1px 12px, 12px 1px;
    background-position: left top, left top, right top, right top,
                         left bottom, left bottom, right bottom, right bottom;
}

/* Paper stocks (Aug 10 2026, v2): a dedicated overlay element —
   body's pseudos already belong to other systems on profile pages,
   which is why the first version never showed. Sits just under the
   dust layer, overlay blend so it reads on light and dark. */
#spark-paper {
    position: fixed;
    inset: 0;
    z-index: 9988;
    pointer-events: none;
}
body[data-paper="graph"] #spark-paper,
body[data-paper="newsprint"] #spark-paper { mix-blend-mode: overlay; }
/* Overlay multiplies dark pixels DOWN, which is why the noise stocks
   vanished on near-black themes; screen lifts them instead, and the
   noise is recoloured to white via feColorMatrix (Aug 10 2026). */
body[data-paper="kraft"] #spark-paper,
body[data-paper="vellum"] #spark-paper { mix-blend-mode: screen; }
body[data-paper="graph"] #spark-paper {
    background-image:
        linear-gradient(rgba(255, 255, 255, 0.5) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, 0.5) 1px, transparent 1px);
    background-size: 28px 28px;
    opacity: 0.22;
}
body[data-paper="newsprint"] #spark-paper {
    background-image: radial-gradient(rgba(255, 255, 255, 0.9) 0.7px, transparent 0.9px);
    background-size: 4px 4px;
    opacity: 0.16;
}
body[data-paper="kraft"] #spark-paper {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3CfeColorMatrix values='0%200%200%200%201%200%200%200%200%201%200%200%200%200%201%200.5%200.5%200.5%200%200'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    opacity: 0.28;
}
body[data-paper="vellum"] #spark-paper {
    background-image:
        radial-gradient(ellipse at 28% 18%, rgba(255, 255, 255, 0.35), transparent 58%),
        radial-gradient(ellipse at 76% 82%, rgba(255, 255, 255, 0.28), transparent 55%),
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2'/%3E%3CfeColorMatrix values='0%200%200%200%201%200%200%200%200%201%200%200%200%200%201%200.35%200.35%200.35%200%200'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    opacity: 0.22;
}

/* Ticker line (Aug 9 2026): news-wire crawl under the header. */
#spark-ticker {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
    white-space: nowrap;
    border-top: 1px solid var(--border);
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    z-index: 20;
    pointer-events: none;
}
#spark-ticker .spark-ticker-track {
    display: inline-block;
    padding: 9px 0;
    animation: spark-ticker-scroll 28s linear infinite;
    will-change: transform;
}
@keyframes spark-ticker-scroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
    #spark-ticker .spark-ticker-track { animation: none; }
}
/* Room for the ticker (Aug 10 2026): the topbar grows by the bar's
   height so nothing sits underneath it, and the pinned action rows
   in Middle / Bottom / Free lift clear of it. */
body.has-ticker .profile-topbar { padding-bottom: var(--ticker-h, 0px); }
body.has-ticker[data-mast="middle"] .profile-topbar .topbar-actions,
body.has-ticker[data-mast="bottom"] .profile-topbar .topbar-actions,
body.has-ticker[data-mast="free"] .profile-topbar .topbar-actions {
    bottom: calc(1.6rem + var(--ticker-h, 0px)) !important;
}
/* ============================================================
   BROADSHEET (Aug 11 2026): edition spreads. Each post's lead
   image runs huge; its additional images file into a hairline
   rail. Single-image posts run full width. */
.portfolio-feed.tpl-broadsheet {
    display: block;
    max-width: 1500px;
    margin: 0 auto;
    padding: 3vh 4vw 6vh;
}
.tpl-bs-spread {
    border-top: 1px solid var(--border-strong);
    padding: 16px 0 36px;
}
.tpl-bs-kicker {
    display: flex;
    gap: 16px;
    align-items: baseline;
    font-family: var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.66rem;
    color: #999;
    margin-bottom: 14px;
}
.tpl-bs-kicker .tpl-cap-title { color: var(--text); font-size: 0.8rem; }
.tpl-bs-spread.has-rail .tpl-bs-body {
    display: grid;
    grid-template-columns: minmax(0, 2.1fr) minmax(0, 1fr);
    gap: 20px;
    align-items: start;
}
.tpl-bs-lead .work-item {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
}
.tpl-bs-rail { display: grid; gap: 16px; align-content: start; }
.tpl-bs-cell { border-top: 1px solid var(--border); padding-top: 16px; }
.tpl-bs-cell:first-child { border-top: none; padding-top: 0; }
.tpl-bs-cell .work-item,
.tpl-bs-lead .work-item {
    background: transparent !important;
    min-height: 0 !important;
}
.tpl-bs-cell .work-item {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
}
.tpl-bs-cell .work-item .media-container,
.tpl-bs-cell .work-item .work-media,
.tpl-bs-cell .work-item .media-container img,
.tpl-bs-cell .work-item .media-container video,
.tpl-bs-lead .work-item .media-container,
.tpl-bs-lead .work-item .work-media,
.tpl-bs-lead .work-item .media-container img,
.tpl-bs-lead .work-item .media-container video {
    width: 100% !important;
    height: auto !important;
    object-fit: contain !important;
    aspect-ratio: auto !important;
}
.tpl-bs-cap {
    font-family: var(--font-mono);
    font-size: 0.66rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #999;
    margin-top: 8px;
}
@media (max-width: 760px) {
    .tpl-bs-spread.has-rail .tpl-bs-body { display: block; }
    .tpl-bs-rail {
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: 8px;
        margin-top: 10px;
    }
    .tpl-bs-cell { border-top: none; padding-top: 0; }
}

/* ============================================================
   CONVEYOR (Aug 11 2026, v2): full-bleed marquee bands, alternating
   directions, paused on hover. Tiles carry MEASURED widths from
   their media ratio at the band height, tracks stay small, and
   there is no will-change — v1's viewport-wide tiles ballooned the
   duplicated tracks into compositor layers so large the animation
   starved and other layers (the free-placement header) stopped
   painting. */
.portfolio-feed.tpl-conveyor {
    display: block;
    padding: 2vh 0 4vh;
    overflow: hidden;
    --cv-h: clamp(150px, 26vh, 280px);
}
.portfolio-feed.tpl-conveyor .tpl-cv-row { overflow: hidden; margin-bottom: 16px; }
.portfolio-feed.tpl-conveyor .tpl-cv-track {
    display: inline-flex;
    gap: 16px;
    padding-right: 16px;
    animation: spark-cv-drift 40s linear infinite;
}
.portfolio-feed.tpl-conveyor .tpl-cv-rev .tpl-cv-track { animation-direction: reverse; }
.portfolio-feed.tpl-conveyor .tpl-cv-row:hover .tpl-cv-track { animation-play-state: paused; }
@keyframes spark-cv-drift {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
}
.portfolio-feed.tpl-conveyor .work-item {
    height: var(--cv-h) !important;
    width: calc(var(--cv-h) * 0.8);
    max-width: none !important;
    min-height: 0 !important;
    margin: 0 !important;
    flex: 0 0 auto;
    background: transparent !important;
}
.portfolio-feed.tpl-conveyor .work-item .media-container,
.portfolio-feed.tpl-conveyor .work-item .work-media,
.portfolio-feed.tpl-conveyor .work-item .media-container img,
.portfolio-feed.tpl-conveyor .work-item .media-container video {
    height: 100% !important;
    width: 100% !important;
    max-width: none !important;
    object-fit: cover !important;
    aspect-ratio: auto !important;
}
@media (prefers-reduced-motion: reduce) {
    .portfolio-feed.tpl-conveyor .tpl-cv-track { animation: none; }
    .portfolio-feed.tpl-conveyor .tpl-cv-row { overflow-x: auto; }
}

/* ============================================================
   LANDING STYLES (Aug 12 2026): how the folders-first
   introduction composes. All three run through the plain-grid
   fallback and are composed here; the !importants outrank any
   engine inline sizing. Covers are element-agnostic, so image
   and video folder covers both fill. body.ff-landing also
   compresses the cover band so the folders arrive on screen. */

@media (min-width: 901px) {
    body.ff-landing[data-cover-on] .profile-topbar {
        min-height: clamp(300px, 34vh, 480px) !important;
    }
}

/* ---- Doors: full-height panels, hovered one widens ---- */
#portfolio-feed.ff-doors {
    display: flex !important;
    flex-direction: row !important;
    gap: 4px;
    height: min(86vh, 900px) !important;
    padding: 0 !important;
    overflow: hidden;
}
#portfolio-feed.ff-doors .folder-card {
    position: relative !important;
    display: block !important;
    opacity: 1 !important;
    visibility: visible !important;
    flex: 1 1 0;
    width: auto !important;
    height: 100% !important;
    min-height: 0 !important;
    margin: 0 !important;
    aspect-ratio: auto !important;
    transform: none !important;
    overflow: hidden;
    transition: flex 0.35s ease;
}
#portfolio-feed.ff-doors .folder-card:hover { flex: 2.4 1 0; }
#portfolio-feed.ff-doors .folder-card .folder-cover {
    position: absolute !important;
    inset: 0 !important;
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
}
@media (max-width: 760px) {
    #portfolio-feed.ff-doors { flex-direction: column; height: auto; gap: 6px; }
    #portfolio-feed.ff-doors .folder-card { height: 26vh !important; flex: none; }
}

/* ---- Banners: stacked full-width bands ---- */
#portfolio-feed.ff-banners {
    display: block !important;
    padding: 0 !important;
}
#portfolio-feed.ff-banners .folder-card {
    position: relative !important;
    display: block !important;
    opacity: 1 !important;
    visibility: visible !important;
    width: 100% !important;
    height: clamp(220px, 34vh, 420px) !important;
    min-height: 0 !important;
    margin: 0 0 6px !important;
    aspect-ratio: auto !important;
    transform: none !important;
    overflow: hidden;
}
#portfolio-feed.ff-banners .folder-card .folder-cover {
    position: absolute !important;
    inset: 0 !important;
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
}

/* ---- Contents page: numbered monograph list ---- */
#portfolio-feed.ff-contents {
    display: block !important;
    max-width: 1240px;
    margin: 0 auto;
    padding: 4vh 4vw 6vh !important;
    counter-reset: ffc;
}
#portfolio-feed.ff-contents .folder-card {
    position: relative !important;
    display: flex !important;
    align-items: center;
    gap: 20px;
    width: 100% !important;
    height: 176px !important;
    min-height: 0 !important;
    margin: 0 !important;
    aspect-ratio: auto !important;
    transform: none !important;
    background: transparent !important;
    border: 0 !important;
    border-bottom: 1px solid var(--border) !important;
    counter-increment: ffc;
}
#portfolio-feed.ff-contents .folder-card::before {
    content: counter(ffc, decimal-leading-zero);
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.08em;
    color: #888;
    min-width: 34px;
}
#portfolio-feed.ff-contents .folder-card .folder-cover {
    position: static !important;
    inset: auto !important;
    width: 248px !important;
    height: 148px !important;
    object-fit: cover !important;
    flex: 0 0 auto;
}
@media (max-width: 760px) {
    #portfolio-feed.ff-contents .folder-card { height: 120px !important; }
    #portfolio-feed.ff-contents .folder-card .folder-cover { width: 140px !important; height: 90px !important; }
}
#portfolio-feed.ff-contents .folder-card .folder-label {
    position: static !important;
    transform: none !important;
    padding: 0 !important;
    background: none !important;
}
#portfolio-feed.ff-contents .folder-card .folder-placeholder {
    position: static !important;
    width: 248px !important;
    height: 148px !important;
    aspect-ratio: auto !important;
    flex: 0 0 auto;
}

/* ---- Doors, horizontal: stacked full-width slats, hovered one
   grows tall ---- */
#portfolio-feed.ff-doors-h {
    display: flex !important;
    flex-direction: column !important;
    gap: 4px;
    height: min(86vh, 900px) !important;
    padding: 0 !important;
    overflow: hidden;
}
#portfolio-feed.ff-doors-h .folder-card {
    position: relative !important;
    display: block !important;
    opacity: 1 !important;
    visibility: visible !important;
    flex: 1 1 0;
    width: 100% !important;
    height: auto !important;
    min-height: 0 !important;
    margin: 0 !important;
    aspect-ratio: auto !important;
    transform: none !important;
    overflow: hidden;
    transition: flex 0.35s ease;
}
#portfolio-feed.ff-doors-h .folder-card:hover { flex: 3.2 1 0; }
#portfolio-feed.ff-doors-h .folder-card .folder-cover {
    position: absolute !important;
    inset: 0 !important;
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
}

/* Folder cards need no caption telling you they are folders
   (Aug 12 2026). */
.folder-card .folder-kicker { display: none !important; }

/* ---- No-cover masthead (Aug 12 2026): without a cover the topbar
   collapsed to content height, so Top / Middle / Bottom / Free had
   no stage to act on and the visitor actions stretched edge to
   edge. A real stage height restores every placement control, and
   the actions size to their labels. ---- */
@media (min-width: 901px) {
    body[data-psec]:not([data-cover-on]) .profile-topbar {
        min-height: clamp(260px, 34vh, 460px);
        position: relative;
    }
}
body[data-psec]:not([data-cover-on]) .profile-topbar .topbar-actions {
    width: auto !important;
    max-width: none;
}
body[data-psec]:not([data-cover-on]) .profile-topbar .topbar-actions > * {
    flex: 0 0 auto !important;
    width: auto !important;
    min-width: 0 !important;
    padding-left: 26px;
    padding-right: 26px;
}

/* ---- Chrome earns its place (Aug 12 2026) ---- */
body.spark-no-loose .portfolio-filters { display: none !important; }
body.spark-no-credits .feed-tabs { display: none !important; }

/* Free overrules the masthead pinning (Aug 12 2026): choosing Free
   means the owner wants the canvas, so the coverless header becomes
   a stage and the name block places, drags, rotates and resizes
   exactly as it does over a cover. At EOF so its !importants win
   the order tie against the pin rules above. */
body:not([data-cover-on])[data-ncs="masthead"][data-mast="free"] .profile-topbar {
    min-height: clamp(300px, 38vh, 520px) !important;
    display: block !important;
    padding: 0 !important;
    position: relative;
    border-bottom: 1px solid var(--border-strong);
}