/* Gotool.xyz — stylesheet extracted from index.php */

  :root{
    --bg:#09090b;
    --bg-elev:#151318;
    --bg-elev-2:#1e1c22;
    --border:rgba(255,255,255,.09);
    --border-strong:rgba(255,255,255,.18);
    --text:#f4f1ec;
    --text-dim:#a8a3a8;
    --text-faint:#6c6771;
    --ember:#ff5a1f;
    --ember-hot:#ffb020;
    --ember-dim:rgba(255,90,31,.13);
    /* Cool cyan/blue pair for the nav "3D Print" link -- same shimmer
       mechanism as the ember/gold "AI" link (see .nav-print3d-* below),
       just a distinct color family so the two nav items read as separate
       features rather than variations of the same one. */
    --print3d:#2dd4ee;
    --print3d-hot:#3b82f6;
    --print3d-dim:rgba(45,212,238,.13);
    --ok:#55d68a;
    --err:#ff6b6b;
    --radius:14px;
    --font-display:'Space Grotesk',sans-serif;
    --font-hero:'Archivo Black','Space Grotesk',sans-serif;
    --font-body:'Inter',sans-serif;
    --font-mono:'JetBrains Mono',monospace;
  }
  *{box-sizing:border-box;margin:0;padding:0;}
  html{scroll-behavior:smooth;}
  body{
    background:var(--bg);
    color:var(--text);
    font-family:var(--font-body);
    line-height:1.5;
    -webkit-font-smoothing:antialiased;
  }
  @media (prefers-reduced-motion: reduce){
    html{scroll-behavior:auto;}
    *{animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important;}
  }
  a{color:inherit;text-decoration:none;}
  .mono{font-family:var(--font-mono);}
  img,svg{display:block;}
  :focus-visible{outline:2px solid var(--ember-hot); outline-offset:2px;}

  /* ---------- layout shells ---------- */
  .wrap{max-width:1120px; margin:0 auto; padding:0 24px;}
  /* Photo Editor specifically gets more room to work with -- an editing
     tool benefits from screen width the way a simple one-shot converter
     doesn't, so this is scoped to just this tool (via the data-tool-slug
     already on <body>, see tool-page.php) rather than raising every
     tool's shared .wrap width, which would just add empty space around
     a small dropzone on the other 39 tools instead of helping anything. */
  body[data-tool-slug="photo-editor"] #tool .wrap{max-width:none;}
  /* Video Editor gets the same full-width treatment for the same reason
     -- a multi-lane timeline needs the screen's actual width, not a
     centered 1120px column. */
  body[data-tool-slug="video-editor"] #tool .wrap{max-width:none;}
  /* The remaining photo-editor-specific button classes (no site-wide
     equivalent exists for these -- .btn and .mode-btn were moved to
     their base definitions further down instead, since those ARE used
     site-wide and are meant to be smaller everywhere now, not just here). */
  body[data-tool-slug="photo-editor"] .pe-tool-btn{width:40px; padding:6px 3px;}
  /* The editor's own menubar sticks above even the site nav (z-index 50)
     once scrolled -- covers it rather than sitting below it, closer to
     how a real desktop app's own menu bar takes over the top of the
     screen. Border-radius removed here specifically: rounded corners
     pinned to the literal top edge of the viewport read as a rendering
     glitch, not a floating panel, once nothing is visible above them. */
  body[data-tool-slug="photo-editor"] .pe-menubar{
    position:sticky; top:0; z-index:60; border-radius:0;
    box-shadow:0 6px 20px rgba(0,0,0,.35);
  }
  body[data-tool-slug="photo-editor"] .pe-tool-btn svg{width:15px; height:15px;}
  body[data-tool-slug="photo-editor"] .pe-icon-btn{width:28px; height:28px; font-size:14px;}
  body[data-tool-slug="photo-editor"] .pe-icon-btn svg{width:14px; height:14px;}
  body[data-tool-slug="photo-editor"] .pe-icon-btn-text{padding:0 9px; font-size:11px;}
  /* The guide buttons' labels ("+H guide" etc.) are longer than this
     toolbar's usual short text-buttons ("Fit", "Apply"), which get away
     with the shared .pe-icon-btn 28px fixed width above because they're
     only 3-5 characters. .pe-icon-btn.pe-guide-btn (two classes) is
     needed, not just .pe-guide-btn alone, to out-specificity that same
     body-scoped .pe-icon-btn width rule rather than losing to it. */
  body[data-tool-slug="photo-editor"] .pe-icon-btn.pe-guide-btn{width:auto; padding:0 8px;}
  body[data-tool-slug="photo-editor"] .pe-preset-btn{padding:6px 10px; font-size:11px;}
  /* Gaps between button-related elements, tightened as far as reasonably
     legible -- the vertical tool rail, mode-toggle groups (aspect ratio,
     background choice), the preset row, and the crop/marquee/wand action
     rows. Each value here is roughly half its site-wide default. */
  body[data-tool-slug="photo-editor"] .pe-toolbar{gap:2px;}
  body[data-tool-slug="photo-editor"] .mode-toggle{gap:4px; margin:8px 0;}
  body[data-tool-slug="photo-editor"] .pe-preset-row{gap:4px; margin-bottom:8px;}
  body[data-tool-slug="photo-editor"] .pe-crop-actions{gap:5px;}
  body[data-tool-slug="photo-editor"] .pe-tool-options{gap:10px;}
  body[data-tool-slug="photo-editor"] .pe-bar{gap:10px; margin-top:6px; padding:6px 10px;}
  body[data-tool-slug="photo-editor"] .pe-bar-group{gap:3px;}
  body[data-tool-slug="photo-editor"] .pe-selected-actions{gap:3px;}
  section{padding:88px 0;}
  /* Tighter than the site-wide default specifically for this section --
     it sits right after the FAQ list above it and right before the
     footer below, so it doesn't need the full 88px breathing room a
     section normally gets on both sides. */
  #tool-faqs{padding-top:22px; padding-bottom:22px;}
  @media (max-width:640px){ section{padding:56px 0;} }

  /* ---------- nav ---------- */
  .nav{
    position:sticky; top:0; z-index:50;
    background:rgba(9,9,11,.78);
    backdrop-filter:blur(10px);
    border-bottom:1px solid var(--border);
  }
  .nav-inner{
    max-width:1120px; margin:0 auto; padding:0 24px;
    height:64px; display:flex; align-items:center; justify-content:space-between; gap:24px;
  }
  .logo{display:flex; align-items:center; gap:3px; font-family:var(--font-display); font-weight:700; font-size:18px; letter-spacing:.02em; white-space:nowrap;}
  .logo-text{color:var(--text); text-transform:uppercase;}
  .logo-text .accent{color:var(--ember);}
  .logo-text .logo-suffix{color:var(--text-faint); text-transform:lowercase; font-weight:600;}
  .logo svg, .logo img{width:26px; height:26px; flex-shrink:0; border-radius:50%; display:block;}
  /* Footer's copy of the logo (bottom-left, in the "free tools, forever"
     line) -- deliberately not the .logo class above, which is sized for
     the 18px nav bar; this stays inline at the footer paragraph's own
     13px so a logo-only or text-only choice from admin/branding.php
     doesn't change the surrounding sentence's size or rhythm. */
  .footer-logo{display:inline-flex; align-items:center; gap:6px; vertical-align:middle;}
  .footer-logo img{width:20px; height:20px; flex-shrink:0; border-radius:50%; display:block;}
  .nav-links{display:flex; gap:28px; font-size:14px; color:var(--text-dim);}
  .nav-links a:hover{color:var(--text);}
  .nav-links a.active{color:var(--ember-hot);}

  /* ---- nav "AI" link: always-on structural styling (sizing, color,
     gradient-text clip). Split out from the animation itself (see
     nav-ai-shimmer/nav-ai-twinkle in the prefers-reduced-motion:
     no-preference block further down) specifically so a visitor with that
     preference still gets a correctly-sized sparkle icon and a correctly
     -colored gradient link -- just without the motion -- rather than an
     unsized/invisible icon, which is what happens if sizing only exists
     inside a conditionally-loaded animation block. */
  .nav-ai-link{display:inline-flex; align-items:center; gap:4px;}
  .nav-ai-sparkle{
    width:12px; height:12px; color:var(--ember-hot); flex-shrink:0;
    filter:drop-shadow(0 0 3px rgba(255,176,32,.7));
  }
  .nav-ai-text{
    background:linear-gradient(90deg, var(--ember), var(--ember-hot), #ffd27a, var(--ember-hot), var(--ember));
    background-size:300% auto; background-position:0% center;
    -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
    font-weight:700;
  }

  /* ---- nav "3D Print" link: same shimmer-text + twinkle-sparkle
     mechanism as the "AI" link above (see nav-print3d-shimmer/
     nav-print3d-twinkle in the prefers-reduced-motion:no-preference block
     further down for the animation itself), recolored to a cyan/blue
     family so the two links read as distinct rather than duplicates. Same
     always-on/animation split for the same reduced-motion reason. */
  .nav-print3d-link{display:inline-flex; align-items:center; gap:4px;}
  .nav-print3d-sparkle{
    width:12px; height:12px; color:var(--print3d-hot); flex-shrink:0;
    filter:drop-shadow(0 0 3px rgba(59,130,246,.7));
  }
  .nav-print3d-text{
    background:linear-gradient(90deg, var(--print3d), var(--print3d-hot), #bae6fd, var(--print3d-hot), var(--print3d));
    background-size:300% auto; background-position:0% center;
    -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
    font-weight:700;
  }

  .nav-dropdown{position:relative;}
  .nav-dropdown-trigger{
    display:flex; align-items:center; gap:5px; cursor:pointer; list-style:none;
    color:inherit; user-select:none;
  }
  .nav-dropdown-trigger::-webkit-details-marker{display:none;}
  .nav-dropdown-caret{width:13px; height:13px; transition:transform .15s ease;}
  .nav-dropdown[open] .nav-dropdown-caret{transform:rotate(180deg);}
  .nav-dropdown:hover .nav-dropdown-trigger, .nav-dropdown.active .nav-dropdown-trigger{color:var(--text);}
  .nav-dropdown.active .nav-dropdown-trigger{color:var(--ember-hot);}
  .nav-dropdown-menu{
    position:absolute; top:100%; left:0; margin-top:10px; z-index:50;
    display:flex; flex-direction:column; gap:2px; min-width:170px;
    background:var(--bg-elev); border:1px solid var(--border); border-radius:11px;
    padding:6px; box-shadow:0 16px 40px rgba(0,0,0,.45);
  }
  .nav-dropdown-menu a{
    padding:9px 12px; border-radius:8px; font-size:13.5px; color:var(--text-dim);
    white-space:nowrap;
  }
  .nav-dropdown-menu a:hover{background:var(--bg-elev-2); color:var(--text);}
  .nav-dropdown-menu a.active{color:var(--ember-hot); background:var(--ember-dim);}
  .nav-dropdown-menu form{margin:0;}
  .nav-dropdown-menu-btn{
    width:100%; text-align:left; padding:9px 12px; border-radius:8px; font-size:13.5px;
    color:var(--text-dim); white-space:nowrap; background:none; border:none; cursor:pointer;
    font-family:inherit; transition:background .15s ease, color .15s ease;
  }
  .nav-dropdown-menu-btn:hover{background:var(--bg-elev-2); color:var(--text);}

  /* Nested "Conversion" flyout inside the Tools dropdown menu (Data/PDF/Office). */
  .nav-subdropdown{position:relative;}
  .nav-subdropdown-trigger{
    display:flex; align-items:center; justify-content:space-between; gap:10px;
    padding:9px 12px; border-radius:8px; font-size:13.5px; color:var(--text-dim);
    white-space:nowrap;
  }
  .nav-subdropdown-trigger:hover{background:var(--bg-elev-2); color:var(--text);}
  .nav-subdropdown.active .nav-subdropdown-trigger{color:var(--ember-hot);}
  .nav-submenu{top:0; left:100%; margin-top:0; margin-left:6px;}
  @media (max-width:1080px){ .nav-submenu{left:auto; right:100%; margin-left:0; margin-right:6px;} }

  /* Language selector. Reuses the dropdown machinery; two differences:
     it's the rightmost nav item so its menu is right-aligned (a left-aligned
     one would overflow the viewport edge), and the list of 16 languages is
     capped in height and scrolls rather than running off the screen. */
  .nav-lang-trigger{display:flex; align-items:center; gap:6px; font-size:13.5px; color:var(--text-dim);}
  .nav-lang-trigger svg:first-child{width:17px; height:17px; flex-shrink:0;}
  .nav-lang-label{display:none;}
  .nav-lang:hover .nav-lang-trigger, .nav-lang[open] .nav-lang-trigger{color:var(--ember-hot);}
  .nav-lang-menu{
    left:auto; right:0; max-height:320px; overflow-y:auto; min-width:190px;
  }
  .nav-lang-option.active{color:var(--ember-hot); background:var(--ember-dim);}

  /* Translate hint: a small dismissible bar pinned bottom-right, shown only
     after the visitor picks a non-English language. Deliberately unobtrusive
     -- it's guidance, not a blocking modal. */
  .lang-hint{
    position:fixed; right:18px; bottom:18px; z-index:200; max-width:340px;
    display:flex; align-items:flex-start; gap:12px;
    background:var(--bg-elev); border:1px solid var(--border-strong); border-radius:12px;
    padding:14px 16px; box-shadow:0 16px 44px rgba(0,0,0,.5);
    font-size:13px; line-height:1.5; color:var(--text-dim);
    animation:lang-hint-in .2s ease;
  }
  @keyframes lang-hint-in{from{opacity:0; transform:translateY(8px);} to{opacity:1; transform:none;}}
  .lang-hint-close{
    flex-shrink:0; background:none; border:none; color:var(--text-faint); cursor:pointer;
    font-size:20px; line-height:1; padding:0 2px; transition:color .15s ease;
  }
  .lang-hint-close:hover{color:var(--text);}
  @media (max-width:520px){
    .lang-hint{left:12px; right:12px; max-width:none; bottom:12px;}
  }

  .nav-login-link{
    font-size:13.5px; font-weight:600; color:var(--text); border:1px solid var(--border-strong);
    border-radius:8px; padding:8px 16px; white-space:nowrap; transition:border-color .15s ease, background .15s ease;
  }
  .nav-login-link:hover{border-color:var(--ember); background:var(--bg-elev);}
  .nav-account-link{
    display:flex; align-items:center; gap:7px; font-size:13.5px; font-weight:600; color:var(--text-dim);
    white-space:nowrap; transition:color .15s ease;
  }
  .nav-account-link svg{width:16px; height:16px; flex-shrink:0;}
  .nav-account-link:hover, .nav-account-link.active{color:var(--ember-hot);}

  .nav-badge{
    font-family:var(--font-mono); font-size:12px; color:var(--text-faint);
    display:flex; align-items:center; gap:7px; white-space:nowrap;
  }
  .nav-badge .dot{width:6px; height:6px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 3px rgba(85,214,138,.15);}
  /* Search: icon-only trigger (reuses the .nav-dropdown-trigger look) that
     opens a panel with the real input, rather than the input sitting
     inline in the header at all times. Right-aligned under the icon like
     .nav-lang-menu, since it sits left of the login/account link and a
     left-aligned panel could run past the icon awkwardly. */
  .nav-search-trigger{padding:8px; border-radius:8px; color:var(--text-dim);}
  .nav-search-trigger:hover{background:var(--bg-elev-2); color:var(--text);}
  .nav-search-toggle[open] .nav-search-trigger{background:var(--bg-elev-2); color:var(--text);}
  .nav-search-trigger svg{width:18px; height:18px;}
  .nav-search-menu{left:auto; right:0; min-width:300px; padding:10px;}
  .nav-search-form{position:relative; display:flex; align-items:center;}
  .nav-search-form-icon{position:absolute; left:12px; width:14px; height:14px; color:var(--text-faint); pointer-events:none;}
  .nav-search-form input{
    width:100%; min-width:0; background:var(--bg-elev-2); border:1px solid var(--border); border-radius:999px;
    padding:8px 14px 8px 34px; color:var(--text); font-family:var(--font-body); font-size:13.5px;
    transition:border-color .15s ease, background .15s ease;
  }
  .nav-search-form input::placeholder{color:var(--text-faint);}
  .nav-search-form input:focus{border-color:var(--ember); background:var(--bg-elev);}
  .nav-mobile-toggle{display:none;}
  @media (max-width:760px){
    .nav-links{display:none;}
    .nav-mobile-toggle{display:block;}
    .nav-mobile-trigger{padding:8px; border-radius:8px;}
    .nav-mobile-trigger:hover{background:var(--bg-elev-2);}
    .nav-mobile-trigger svg{width:22px; height:22px;}
    /* Full-width panel anchored to the sticky header's own height (64px,
       see .nav-inner above) rather than the dropdown's usual position:
       absolute/top:100% -- that positioning is relative to the trigger
       itself, which would misalign or risk overflowing off-screen
       depending on where the hamburger lands in the header on a narrow
       viewport. Fixed positioning below the header works the same way
       regardless of screen width or scroll position. */
    .nav-mobile-menu{
      position:fixed; top:64px; left:0; right:0; width:100%;
      max-width:none; margin-top:0; border-radius:0; border-left:none; border-right:none;
      max-height:calc(100vh - 64px); overflow-y:auto; padding:10px;
    }
    .nav-mobile-menu a{font-size:15px; padding:13px 14px;}
    .nav-login-link{display:none;}

    /* Same fixed-below-header treatment as .nav-mobile-menu, so the search
       panel spans the full screen width for easier tapping instead of the
       narrow icon-anchored box used on desktop. */
    .nav-search-menu{
      position:fixed; top:64px; left:0; right:0; width:100%;
      min-width:0; max-width:none; margin-top:0; border-radius:0; border-left:none; border-right:none;
      padding:14px;
    }
  }

  /* ---------- hero ---------- */
  /* The divider is on .hero itself rather than an <hr> in each page's
     markup: 17 files use this layout, so a rule here applies to all of
     them and to any page added later, with no chance of one drifting
     out of step.

     Uses var(--ember) -- the same token as the logo and the accented word
     in each hero heading -- rather than the neutral var(--border) used by
     the footer and store-cta dividers. That's deliberate: this one marks
     the end of the hero and is meant to read as a brand rule, not as a
     quiet structural separator. */
  .hero{position:relative; padding-top:28px; padding-bottom:64px; overflow:hidden;
        border-bottom:1px solid var(--ember);}
  .hero > *{position:relative; z-index:1;}
  .hero-glow{
    position:absolute; top:-60px; left:64%; transform:translateX(-50%);
    width:860px; height:480px; pointer-events:none; z-index:0;
    background:radial-gradient(closest-side, rgba(255,90,31,.30), rgba(255,90,31,.08) 55%, transparent 75%);
    filter:blur(10px);
    animation:pulse 7s ease-in-out infinite;
  }
  @keyframes pulse{ 0%,100%{opacity:.75; transform:translateX(-50%) scale(1);} 50%{opacity:1; transform:translateX(-50%) scale(1.06);} }
  @media (max-width:640px){ .hero{padding-top:20px; padding-bottom:48px;} }
  .eyebrow{
    font-family:var(--font-display); font-size:13px; font-weight:600; color:var(--text-dim);
    text-transform:uppercase; letter-spacing:.14em; margin-bottom:18px;
  }
  .eyebrow a{color:var(--ember-hot); text-decoration:underline; text-underline-offset:3px;}
  .eyebrow a:hover{color:var(--ember);}
  .hero h1{
    font-family:var(--font-hero); font-weight:400;
    font-size:clamp(36px, 6vw, 64px); line-height:1.04; letter-spacing:-.01em;
    max-width:800px;
  }
  .hero h1 .accent{color:var(--ember);}
  .hero-sub{
    margin-top:22px; max-width:560px; font-size:17px; color:var(--text-dim);
  }
  .guide-dateline{
    margin-top:12px; font-size:12.5px; color:var(--text-faint); font-family:var(--font-mono);
  }
  .hero-actions{display:flex; gap:12px; margin-top:34px; flex-wrap:wrap;}
  .btn{
    display:inline-flex; align-items:center; gap:6px;
    padding:7px 14px; border-radius:8px; font-weight:600; font-size:13px;
    border:1px solid transparent; cursor:pointer; transition:transform .15s ease, background .15s ease, border-color .15s ease;
    font-family:var(--font-body);
  }
  .btn:hover{transform:translateY(-1px);}
  .btn-primary{background:linear-gradient(135deg, var(--ember), var(--ember-hot)); color:#160c05;}
  .btn-ghost{background:transparent; border-color:var(--border-strong); color:var(--text);}
  .btn-ghost:hover{border-color:var(--ember);}
  .btn:disabled{opacity:.45; cursor:not-allowed; transform:none;}
  /* Referenced by csv-to-json's Copy/Download buttons and flowchart-maker's
     sample Copy button, but this rule didn't exist until now -- both were
     silently rendering at full .btn size with no visual effect from the
     class. A compact variant for actions that sit beside content (a code
     block, a canvas) rather than standing alone as a primary action. */
  .btn-small{padding:7px 14px; font-size:13px; border-radius:8px; gap:6px;}
  .hero-stat{margin-top:40px; font-size:13px; color:var(--text-faint);}

  /* ---------- hero two-column layout + highlight card ---------- */
  .hero-grid{display:grid; grid-template-columns:1fr 360px; gap:52px; align-items:stretch;}
  @media (max-width:900px){ .hero-grid{grid-template-columns:1fr; gap:40px;} }
  .hero-card{
    border:1px solid rgba(255,90,31,.4); border-radius:14px;
    background:rgba(255,90,31,.05); padding:22px 24px;
  }
  .hero-card-eyebrow{
    font-family:var(--font-mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-faint); padding-bottom:14px; margin-bottom:16px; border-bottom:1px solid var(--border);
  }
  .hero-card-eyebrow a{color:var(--text-dim); text-decoration:underline; text-underline-offset:3px;}
  .hero-card-eyebrow a:hover{color:var(--ember-hot);}
  /* Every item is a literal fixed height (not just a min-height floor),
     sized to the worst case a 2-line clamped headline + 1-line clamped
     subtext can ever reach (~66.5px measured, rounded up to 68px for
     font-rendering headroom) -- otherwise the card's total height depends
     on the random mix of headline lengths/subtext/thumbnail on any given
     rotation, and on pages where the hero-card isn't sitting next to a
     taller neighboring column (index.php's left column happens to be tall
     enough to mask this), the whole hero section visibly grows and
     shrinks every 10 seconds as assets/hero-card.js rotates in a new set
     of 3. A min-height alone doesn't achieve this: content taller than
     the floor still grows the box past it, and the 2nd/3rd item's own
     divider (18px padding-top + 1px border-top, added below) sits inside
     that same border-box, so those two items need 19px more reserved
     height than the 1st item to land on the same fixed number once the
     divider is accounted for -- 87px vs 68px. overflow:hidden is the
     backstop in case any future content combination (e.g. a much longer
     subtext font) ever exceeds the ceiling the clamps below assume. */
  .hero-card-item{display:flex; align-items:center; height:68px; overflow:hidden;}
  .hero-card-item-qr{justify-content:space-between; gap:16px;}
  .hero-card-item + .hero-card-item{margin-top:18px; padding-top:18px; border-top:1px solid var(--border); height:87px;}
  .hero-card-item-text{min-width:0; flex:1;}
  .hero-card-item-thumb{
    flex-shrink:0; width:56px; height:56px; border-radius:8px; overflow:hidden;
    background:#fff; padding:4px; display:block; line-height:0;
    border:1px solid var(--border-strong); transition:border-color .15s ease;
  }
  .hero-card-item-thumb:hover{border-color:var(--ember);}
  .hero-card-item-thumb img{width:100%; height:100%; display:block; object-fit:contain;}
  .hero-card-items{transition:opacity .25s ease;}
  .hero-card-items-fading{opacity:0;}
  /* Clamped to 2 lines (with an ellipsis if a headline runs longer) so an
     unusually long headline can't push a single item past the 64px
     reserved above -- the min-height guarantees the floor, this
     guarantees the ceiling. */
  .hero-card-headline{
    font-size:14.5px; font-weight:600; line-height:1.45; color:var(--text);
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  }
  .hero-card-headline a{color:inherit; text-decoration:none;}
  .hero-card-headline a:hover{color:var(--ember-hot); text-decoration:underline; text-underline-offset:3px;}
  .hero-card-source{
    margin-top:8px; font-family:var(--font-mono); font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--text-faint);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .hero-stat .mono{color:var(--text-dim);}

  /* ---------- Qira (photo editor) spotlight, index.php only ---------- */
  #view-home .spotlight{padding-top:44px; padding-bottom:44px; background:var(--bg);}
  .spotlight-grid{display:grid; grid-template-columns:1fr 460px; gap:52px; align-items:center;}
  @media (max-width:900px){ .spotlight-grid{grid-template-columns:1fr; gap:36px;} }
  .spotlight-preview{
    border-radius:16px; overflow:hidden; border:1px solid var(--border);
    box-shadow:0 24px 60px rgba(0,0,0,.35);
  }
  .spotlight-preview svg{display:block; width:100%; height:auto;}
  .spotlight-eyebrow{
    font-family:var(--font-mono); font-size:12.5px; letter-spacing:.05em; text-transform:uppercase;
    color:var(--text-dim); margin-bottom:14px;
  }
  .spotlight-copy h2{font-family:var(--font-display); font-size:32px; font-weight:700; line-height:1.2; margin-bottom:16px;}
  .spotlight-sub{font-size:15.5px; color:var(--text-dim); line-height:1.65; margin-bottom:24px; max-width:520px;}
  .spotlight-features{list-style:none; display:flex; flex-direction:column; gap:12px; margin-bottom:30px;}
  .spotlight-features li{display:flex; align-items:flex-start; gap:12px; font-size:14.5px; color:var(--text-dim); line-height:1.5;}
  .spotlight-features svg{
    width:18px; height:18px; flex-shrink:0; margin-top:1px; color:var(--ember-hot);
    background:var(--ember-dim); border-radius:50%; padding:3px; box-sizing:content-box;
  }
  .spotlight-actions{display:flex; flex-wrap:wrap; gap:14px;}

  /* ---------- section heads ---------- */
  .section-head{display:flex; align-items:baseline; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:28px;}
  .section-head h2{font-family:var(--font-display); font-size:28px; font-weight:700;}
  .section-head p{color:var(--text-faint); font-size:14px; font-family:var(--font-mono);}

  /* ---------- filter tabs ---------- */
  .filter-tabs{display:flex; gap:8px; margin-bottom:28px; flex-wrap:wrap;}
  .tab{
    font-family:var(--font-mono); font-size:13px; color:var(--text-dim);
    background:var(--bg-elev); border:1px solid var(--border); border-radius:999px;
    padding:7px 16px; cursor:pointer; transition:all .15s ease;
  }
  .tab:hover{border-color:var(--border-strong); color:var(--text);}
  .tab.active{background:var(--ember-dim); border-color:var(--ember); color:var(--ember-hot);}

  /* ---------- tool grid ---------- */
  .tool-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(240px,1fr)); gap:16px;}
  .search-empty{margin-top:24px; text-align:center; font-size:14px; color:var(--text-dim);}
  .search-empty a, .search-empty .link-btn{background:none; border:none; color:var(--ember-hot); text-decoration:underline; text-underline-offset:2px; cursor:pointer; font:inherit; padding:0;}
  .search-empty a:hover, .search-empty .link-btn:hover{color:var(--ember);}
  .related-pages{display:flex; flex-direction:column; gap:10px; margin-bottom:36px;}
  .related-page-link{
    display:flex; flex-direction:column; gap:4px; background:var(--bg-elev); border:1px solid var(--border);
    border-radius:12px; padding:16px 20px; transition:border-color .15s ease, background .15s ease;
  }
  .related-page-link:hover{border-color:var(--ember); background:var(--bg-elev-2);}
  .related-page-link strong{font-family:var(--font-display); font-size:15px; color:var(--text);}
  .related-page-link span{font-size:13.5px; color:var(--text-dim);}
  .tool-card-wrap{position:relative; display:flex; height:100%;}
  .tool-card{
    text-align:left; background:var(--bg-elev); border:1px solid var(--border); border-radius:var(--radius);
    padding:22px; cursor:pointer; transition:border-color .15s ease, transform .15s ease, background .15s ease;
    font-family:var(--font-body); color:var(--text); display:flex; flex-direction:column; gap:10px;
    width:100%;
  }
  .tool-card:hover{border-color:var(--ember); transform:translateY(-2px); background:var(--bg-elev-2);}
  .tool-card-actions{position:absolute; top:14px; right:14px; z-index:2; display:flex; gap:4px;}
  .tool-card-action-form{display:contents;}
  .tool-card-action-btn{
    width:30px; height:30px; border-radius:8px; border:1px solid transparent;
    background:transparent; color:var(--text-faint); display:flex; align-items:center; justify-content:center;
    cursor:pointer; transition:color .15s ease, background .15s ease, border-color .15s ease, transform .1s ease;
  }
  .tool-card-action-btn svg{width:16px; height:16px;}
  .tool-card-action-btn:hover{background:var(--bg-elev-2); border-color:var(--border);}
  .tool-card-action-btn:active{transform:scale(.9);}
  .tool-card-bookmark-btn:hover{color:var(--ember-hot);}
  .tool-card-bookmark-btn.is-bookmarked{color:var(--ember-hot);}
  .tool-card-love-btn:hover{color:#ff5a6b;}
  .tool-card-love-btn.is-loved{color:#ff3049;}
  .hero-actions .tool-card-love-btn svg, .hero-actions .tool-card-bookmark-btn svg, .hero-actions .tool-card-share-btn svg{width:16px; height:16px;}
  .hero-action-form{display:inline-flex; margin:0;}
  .tool-icon{
    width:38px; height:38px; border-radius:9px; background:var(--ember-dim);
    display:flex; align-items:center; justify-content:center; color:var(--ember-hot);
  }
  .tool-icon svg{width:19px; height:19px;}
  .tool-icon-letter{font-family:var(--font-display); font-weight:700; font-size:16px;}
  .tool-card-icon-row{display:flex; align-items:center; gap:10px;}
  .beta-tag{
    font-family:var(--font-mono); font-size:11px; font-weight:700; letter-spacing:.05em;
    text-transform:uppercase; color:var(--err);
  }
  .beta-tag-lg{font-size:14px; vertical-align:middle; margin-left:8px;}
  .verified-badge{
    display:inline-flex; align-items:center; gap:6px; margin:10px 0 0;
    font-family:var(--font-body); font-size:13.5px; font-weight:600; color:var(--ok);
  }
  .verified-badge svg{width:16px; height:16px; flex-shrink:0;}
  .tool-card h3{font-family:var(--font-display); font-size:17px; font-weight:600;}
  .tool-card p{font-size:14px; color:var(--text-dim); flex-grow:1;}
  .tool-meta{font-family:var(--font-mono); font-size:11.5px; color:var(--text-faint); letter-spacing:.02em;}
  .tool-card.is-soon{cursor:default; opacity:.55;}
  .tool-card.is-soon:hover{transform:none; border-color:var(--border); background:var(--bg-elev);}
  .tool-card.is-soon .tool-icon{background:var(--bg-elev-2); color:var(--text-faint);}

  /* ---------- search-box card (new-tools.php, 12th grid slot) ---------- */
  .tool-card-search{justify-content:center; cursor:default;}
  .tool-card-search p{font-size:13.5px; color:var(--text-dim);}
  .tool-card-search-field{position:relative; margin-top:2px;}
  .tool-card-search-icon{position:absolute; left:12px; top:50%; transform:translateY(-50%); width:14px; height:14px; color:var(--text-faint); pointer-events:none;}
  .tool-card-search-field input{
    width:100%; background:var(--bg-elev-2); border:1px solid var(--border-strong); border-radius:999px;
    padding:10px 14px 10px 34px; color:var(--text); font-family:var(--font-body); font-size:13.5px;
    transition:border-color .15s ease;
  }
  .tool-card-search-field input::placeholder{color:var(--text-faint);}
  .tool-card-search-field input:focus{border-color:var(--ember);}
  .tool-card[hidden]{display:none;}
  .tool-card-wrap[hidden]{display:none;}
  .tool-card-search{cursor:default;}
  .tool-card-search:hover{transform:none; border-color:var(--border);}
  .tool-card-search-row{display:flex; gap:8px;}
  .tool-card-search-row input{
    flex:1; min-width:0; background:var(--bg); border:1px solid var(--border); border-radius:8px;
    padding:9px 12px; color:var(--text); font-family:var(--font-body); font-size:13px; cursor:text;
  }
  .tool-card-search-row input:focus{border-color:var(--ember);}
  .tool-card-search-btn{
    flex-shrink:0; width:38px; height:38px; border-radius:8px; border:1px solid var(--border-strong);
    background:var(--bg-elev-2); color:var(--text-dim); display:flex; align-items:center; justify-content:center; cursor:pointer;
  }
  .tool-card-search-btn:hover{border-color:var(--ember); color:var(--ember-hot);}
  .tool-card-search-btn svg{width:16px; height:16px;}

  /* ---------- how it works ---------- */
  .how-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:24px;}
  @media (max-width:760px){ .how-grid{grid-template-columns:1fr;} }
  .how-step{border-top:1px solid var(--border-strong); padding-top:16px;}
  .how-step .num{font-family:var(--font-mono); color:var(--ember); font-size:13px; display:block; margin-bottom:10px;}
  .how-step h3{font-family:var(--font-display); font-size:16px; font-weight:600; margin-bottom:8px;}
  .how-step p{font-size:14px; color:var(--text-dim);}

  /* ---------- faq ---------- */
  .faq-list{display:flex; flex-direction:column; gap:10px; max-width:760px;}
  .faq-item{
    background:var(--bg-elev); border:1px solid var(--border); border-radius:12px;
    padding:4px 20px;
  }
  .faq-item summary{
    cursor:pointer; list-style:none; padding:16px 0; font-family:var(--font-display);
    font-weight:600; font-size:15.5px; display:flex; align-items:center; justify-content:space-between; gap:16px;
  }
  .faq-item summary::-webkit-details-marker{display:none;}
  .faq-item summary::after{
    content:"+"; font-family:var(--font-mono); font-size:20px; color:var(--ember-hot); flex-shrink:0;
    transition:transform .15s ease;
  }
  .faq-item[open] summary::after{transform:rotate(45deg);}
  .faq-item p{font-size:14px; color:var(--text-dim); padding-bottom:18px; max-width:640px;}

  /* aboutus.php, plus every category/listing page (.cat-page -- pdf-tools,
     office-tools, video-tools, image-tools, audio-tools, all-tools,
     new-tools, security-tools, conversion-tools, data-tools) -- two
     columns instead of the single-column list used elsewhere (.faq-list
     is shared, so this is scoped through .about-page/.cat-page rather
     than changed globally). align-items:start keeps each item's own
     height independent, so opening one item doesn't stretch the box next
     to it in the same row. */
  .about-page .faq-list, .cat-page .faq-list{
    display:grid; grid-template-columns:1fr 1fr; grid-auto-flow:row;
    gap:10px 20px; max-width:none; align-items:start;
  }
  @media (max-width:760px){ .about-page .faq-list, .cat-page .faq-list{grid-template-columns:1fr;} }

  /* ---------- per-tool details: how-it-works + faq side by side ---------- */
  .tool-details-grid{display:grid; grid-template-columns:1fr 1fr; gap:48px; align-items:start;}
  @media (max-width:760px){ .tool-details-grid{grid-template-columns:1fr; gap:32px;} }

  .tool-how-steps{display:flex; flex-direction:column; gap:0; list-style:none;}
  .tool-how-step{
    display:flex; align-items:flex-start; gap:16px; padding:16px 0;
    border-top:1px solid var(--border); font-size:15px; color:var(--text-dim);
  }
  .tool-how-step:first-child{border-top:none;}
  .tool-how-num{
    flex-shrink:0; width:28px; height:28px; border-radius:50%; background:var(--bg-elev);
    border:1px solid var(--border-strong); color:var(--ember-hot); font-family:var(--font-mono);
    font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center;
  }
  .tool-how-text{padding-top:3px;}

  .privacy-block{
    margin-top:56px; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg-elev);
    padding:26px 28px; display:flex; flex-direction:row; flex-wrap:nowrap; gap:20px; align-items:center;
  }
  .privacy-block svg{width:22px; height:22px; color:var(--ember-hot); flex-shrink:0;}
  .privacy-block p{font-size:14px; color:var(--text-dim);}
  /* partials/privacy.php wraps this in its own <section>, which already
     gets the standard 88px section padding -- the 56px margin-top above
     only makes sense when this sits directly under a "how it works" grid
     within the SAME section (partials/how-privacy.php's combined layout).
     Without this override, tool pages stacked 88+88+56px of empty space
     between "You might also need" and this box. */
  #how-privacy-wrap{padding-top:21px; padding-bottom:21px;}
  #how-privacy-wrap .privacy-block{margin-top:0;}
  #related{padding-bottom:44px; padding-top:0;}
  /* Tool pages only (see the hero-tool class added in tool-page.php).
     Two separate 44px gaps around the hero/panel boundary: 44px after the
     hero text (this rule) and 44px before the panel content (#tool,
     below) -- kept as two rules on two different selectors rather than
     one combined value so each side can be adjusted independently. */
  .hero-tool{padding-bottom:44px !important;}
  #tool{padding-top:44px; padding-bottom:44px;}
  #tool-details{padding-top:44px; padding-bottom:44px; border-top:1px solid var(--border);}

  /* index.php only (#tools, #how, and .hero are shared ids/classes reused
     across many other pages, so these are scoped through the page's
     unique #view-home wrapper rather than touching them directly --
     otherwise this would also change spacing on every category page,
     which wasn't asked for). Three ad-slot gaps that render empty when
     no ad is configured, each reduced to a plain 44px instead of two
     stacked 88px section paddings. */
  #view-home .hero{padding-bottom:44px;}
  #view-home #tools{padding-top:0; padding-bottom:44px; background:var(--bg);}
  #view-home #how{padding-top:0; padding-bottom:44px; background:var(--bg);}

  /* Same treatment as index.php/tool pages, applied to the category
     listing pages (pdf-tools.php, office-tools.php, video-tools.php,
     image-tools.php, audio-tools.php, all-tools.php, new-tools.php,
     conversion-tools.php, data-tools.php, security-tools.php) via the
     shared .cat-page class added to each page's <main> -- scoped so
     mytools.php and search.php (which also use .hero/#tools but weren't
     asked for this) stay untouched. */
  .cat-page .hero{padding-bottom:44px !important;}
  .cat-page #tools{padding-top:44px; padding-bottom:44px;}
  .cat-page #faq{padding-top:0; padding-bottom:44px;}
  .cat-page #how{padding-top:0; padding-bottom:44px;}
  .privacy-block strong{color:var(--text); font-weight:600;}

  /* login.php only (#auth and #how are shared with forgot-password.php /
     reset-password.php, so this is scoped through the .auth-page class
     added to login.php's <main> rather than touching those other two
     pages, which weren't asked for). */
  .auth-page .hero{padding-bottom:44px !important;}
  .auth-page #auth{padding-top:44px; padding-bottom:44px;}
  .auth-page #how{padding-top:0; padding-bottom:44px;}

  /* aboutus.php only (#faq is shared with the category listing pages, so
     it's scoped through .about-page; the other section ids on this page
     -- #origin, #values, #numbers, #whats-next, #who, #terms, #privacy --
     are unique to aboutus.php and don't need scoping). Same shape as the
     .cat-page treatment: the first section after the hero keeps the
     standard 88px hero gap (44+44), then every later section sits 44px
     from the one before it, all the way down to #privacy just before
     the footer. */
  .about-page .hero{padding-bottom:44px !important;}
  #origin{padding-top:44px; padding-bottom:44px;}
  #values{padding-top:0; padding-bottom:44px;}
  #numbers{padding-top:0; padding-bottom:44px;}
  #whats-next{padding-top:0; padding-bottom:44px;}
  #who{padding-top:0; padding-bottom:44px;}
  #terms{padding-top:0; padding-bottom:44px;}
  .about-page #faq{padding-top:0; padding-bottom:44px;}
  .about-page #privacy{padding-top:0; padding-bottom:44px;}

  /* contactus.php only -- #contact-form is unique to this page, but the
     shared .hero needs the .contact-page scope. Same shape as login.php's
     #auth: standard 88px hero gap, then 44px before the footer. */
  .contact-page .hero{padding-bottom:44px !important;}
  .contact-page #contact-form{padding-top:44px; padding-bottom:44px;}

  /* ---------- tool view ---------- */
  #view-tool{padding-top:44px;}
  .back-link{
    font-family:var(--font-mono); font-size:13px; color:var(--text-dim);
    background:none; border:none; cursor:pointer; display:inline-flex; align-items:center; gap:6px;
    margin-bottom:28px; padding:6px 0;
  }
  .back-link:hover{color:var(--ember-hot);}
  .tool-header{margin-bottom:32px;}
  .tool-header h2{font-family:var(--font-display); font-size:30px; font-weight:700; margin-bottom:8px;}
  .tool-header p{color:var(--text-dim); font-size:15px;}

  .dropzone{
    border:1.5px dashed var(--border-strong); border-radius:var(--radius); background:var(--bg-elev);
    padding:40px 24px; text-align:center; cursor:pointer; transition:border-color .15s ease, background .15s ease;
  }
  .dropzone:hover, .dropzone.dragover{border-color:var(--ember); background:var(--ember-dim);}
  .dropzone svg{width:30px; height:30px; margin:0 auto 14px; color:var(--ember-hot);}
  .dropzone p{font-size:14px; color:var(--text-dim);}
  .dropzone .dz-strong{color:var(--text); font-weight:600;}
  .dropzone .dz-hint{margin-top:6px; font-size:12px; color:var(--text-faint); font-family:var(--font-mono);}
  .dropzone input{display:none;}

  .file-list{margin-top:20px; display:flex; flex-direction:column; gap:8px;}
  .file-row{
    display:flex; align-items:center; gap:12px; background:var(--bg-elev); border:1px solid var(--border);
    border-radius:10px; padding:10px 14px;
  }
  .file-row .f-index{font-family:var(--font-mono); font-size:12px; color:var(--text-faint); width:20px;}
  .file-row .f-name{flex-grow:1; font-size:13.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .file-row .f-size{font-family:var(--font-mono); font-size:12px; color:var(--text-faint); white-space:nowrap;}
  .file-row .f-actions{display:flex; gap:4px;}
  .f-actions button{
    width:26px; height:26px; border-radius:6px; border:1px solid var(--border); background:var(--bg-elev-2);
    color:var(--text-dim); cursor:pointer; display:flex; align-items:center; justify-content:center;
  }
  .f-actions button:hover{border-color:var(--ember); color:var(--ember-hot);}
  .f-actions button:disabled{opacity:.3; cursor:not-allowed;}
  .f-actions button svg{width:13px; height:13px;}

  .tool-actions{margin-top:24px; display:flex; align-items:center; gap:16px; flex-wrap:wrap;}

  .mode-toggle{display:flex; gap:8px; margin:20px 0; flex-wrap:wrap;}

  .effect-grid{
    display:grid; grid-template-columns:repeat(auto-fill, minmax(150px,1fr)); gap:10px; margin:14px 0 4px;
  }
  .effect-card{
    display:flex; flex-direction:column; align-items:flex-start; gap:4px; text-align:left;
    border:1px solid var(--border); border-radius:10px; background:var(--bg-elev);
    padding:12px 13px; cursor:pointer; transition:border-color .15s ease, background .15s ease;
  }
  .effect-card:hover{border-color:var(--border-strong); background:var(--bg-elev-2);}
  .effect-card.active{border-color:var(--ember); background:var(--ember-dim);}
  .effect-emoji{font-size:20px; line-height:1;}
  .effect-name{font-family:var(--font-display); font-size:13.5px; font-weight:600; color:var(--text);}
  .effect-desc{font-size:11.5px; color:var(--text-faint); line-height:1.35;}
  @media (max-width:560px){
    .effect-grid{grid-template-columns:repeat(auto-fill, minmax(130px,1fr));}
  }

  /* Collage layout picker: small cards, each showing an SVG preview of the
     template's cell arrangement above its name. The preview uses
     currentColor, so the card's text colour drives it -- which makes the
     active card's preview turn ember automatically, no separate rule. */
  .tmpl-grid{
    display:grid; grid-template-columns:repeat(auto-fill, minmax(84px,1fr)); gap:10px; margin:12px 0 4px;
  }
  .tmpl-card{
    display:flex; flex-direction:column; align-items:center; gap:8px;
    border:1px solid var(--border); border-radius:10px; background:var(--bg-elev);
    padding:12px 8px; cursor:pointer; color:var(--text-faint);
    transition:border-color .15s ease, background .15s ease, color .15s ease;
  }
  .tmpl-card:hover{border-color:var(--border-strong); background:var(--bg-elev-2); color:var(--text-dim);}
  .tmpl-card.active{border-color:var(--ember); background:var(--ember-dim); color:var(--ember);}
  .tmpl-preview{display:flex; align-items:center; justify-content:center; line-height:0;}
  .tmpl-preview svg{width:44px; height:44px; display:block;}
  .tmpl-label{font-family:var(--font-display); font-size:12px; font-weight:600; color:var(--text);}
  @media (max-width:560px){
    .tmpl-grid{grid-template-columns:repeat(auto-fill, minmax(74px,1fr));}
  }
  .mode-btn{
    font-family:var(--font-body); font-size:12px; font-weight:500; color:var(--text-dim);
    background:var(--bg-elev); border:1px solid var(--border); border-radius:7px; padding:6px 11px; cursor:pointer;
  }
  .mode-btn.active{background:var(--ember-dim); border-color:var(--ember); color:var(--ember-hot);}
  .mode-btn:disabled{opacity:.4; cursor:not-allowed;}

  .range-field{margin-top:14px;}
  .range-field label{display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px;}
  .range-field input{
    width:100%; max-width:420px; background:var(--bg-elev); border:1px solid var(--border); border-radius:9px;
    padding:11px 14px; color:var(--text); font-family:var(--font-mono); font-size:14px;
  }
  .range-field input:focus{border-color:var(--ember);}
  .range-hint{font-size:12px; color:var(--text-faint); margin-top:8px;}

  /* A small "?" badge for putting a plain-language explanation next to a
     label without permanently taking up page space -- uses the browser's
     own native title-attribute tooltip (same mechanism already used for
     the QR tool's disabled "Engraved" button's "Coming soon" hint) rather
     than a custom JS popover, so it needs no extra markup, script, or
     positioning logic to show correctly near a page edge. `cursor:help`
     and `tabindex="0"` (paired with a `title` attribute on the element
     itself) mean it's discoverable by mouse hover and, since native
     titles also surface on keyboard focus in most browsers, reasonably
     usable without a mouse too. */
  .info-hint{
    display:inline-flex; align-items:center; justify-content:center;
    width:15px; height:15px; border-radius:50%;
    background:var(--bg-elev); border:1px solid var(--border);
    color:var(--text-dim); font-size:10px; font-weight:700; line-height:1;
    cursor:help; flex-shrink:0; user-select:none;
  }
  .info-hint:hover, .info-hint:focus-visible{color:var(--text); border-color:var(--text-dim);}

  .page-count{
    font-family:var(--font-mono); font-size:13px; color:var(--text-dim); margin-top:16px;
  }
  .page-count strong{color:var(--ember-hot);}

  .tool-note{
    margin-top:18px; font-size:13px; line-height:1.5; color:var(--text-dim);
    background:var(--bg-elev); border:1px solid var(--border); border-radius:10px;
    padding:13px 15px; display:flex; gap:10px; align-items:flex-start;
  }
  .tool-note svg{width:15px; height:15px; flex-shrink:0; margin-top:2px; color:var(--text-faint);}

  .status{
    margin-top:24px; border-radius:10px; padding:14px 16px; font-size:13.5px; display:none; gap:10px; align-items:flex-start;
  }
  .status.show{display:flex;}
  .status.status-ok{background:rgba(85,214,138,.09); border:1px solid rgba(85,214,138,.3); color:#bdf3d3;}
  .status.status-err{background:rgba(255,107,107,.09); border:1px solid rgba(255,107,107,.3); color:#ffc4c4;}
  .status.status-busy{background:var(--ember-dim); border:1px solid rgba(255,90,31,.3); color:#ffd7bd;}
  .status .job-id{display:block; margin-top:4px; font-family:var(--font-mono); font-size:12px; opacity:.8;}
  .status svg{width:16px; height:16px; flex-shrink:0; margin-top:1px;}

  /* ---------- edit pdf ---------- */
  .pdf-edit-toolbar{display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; margin:18px 0;}
  .pdf-edit-toolbar-left{display:flex; align-items:center; gap:10px;}
  .pdf-edit-toolbar-right{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
  .pdf-edit-nav-btn{padding:7px 14px; font-size:15px; line-height:1;}
  .pdf-edit-page-indicator{font-size:12.5px; color:var(--text-dim);}
  .pdf-edit-size-label{display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--text-dim);}
  .pdf-edit-size-select{
    font-family:var(--font-mono); font-size:12px; background:var(--bg-elev-2);
    border:1px solid var(--border); border-radius:7px; color:var(--text-dim); padding:5px 8px; cursor:pointer;
  }
  .pdf-edit-hint{font-size:13px; color:var(--text-faint); margin-bottom:10px; text-align:center;}
  .pdf-edit-stage{
    position:relative; width:fit-content; max-width:100%; margin:0 auto;
    box-shadow:0 20px 50px rgba(0,0,0,.4); border-radius:4px; overflow:hidden; line-height:0;
  }
  .pdf-edit-stage canvas{display:block; max-width:100%; height:auto;}
  .pdf-edit-overlay{position:absolute; top:0; left:0; right:0; bottom:0; cursor:text;}
  .pdf-edit-input{
    position:absolute; min-width:120px; min-height:1.4em;
    background:rgba(255,255,255,.97); border:1.5px dashed var(--ember); border-radius:3px;
    padding:2px 4px; font-family:var(--font-body); color:#141414; resize:both; overflow:auto; line-height:1.3;
  }
  .pdf-edit-label{
    position:absolute; font-family:var(--font-body); color:#141414; white-space:pre-wrap;
    padding:2px 20px 2px 4px; border-radius:3px; cursor:default; line-height:1.3; max-width:80%;
  }
  .pdf-edit-label:hover{outline:1px dashed rgba(255,90,31,.6);}
  .pdf-edit-label-del{
    position:absolute; top:-2px; right:-2px; width:16px; height:16px; border-radius:50%;
    background:var(--err); color:#fff; border:none; font-size:11px; line-height:1; cursor:pointer;
    display:flex; align-items:center; justify-content:center; padding:0;
  }

  /* ---------- document viewer ---------- */
  .doc-viewer-toolbar{display:flex; align-items:center; justify-content:space-between; gap:16px; max-width:760px; margin:24px auto 0; flex-wrap:wrap;}
  .doc-viewer-toolbar .doc-filename{font-size:12.5px; color:var(--text-dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .doc-viewer-actions{display:flex; gap:10px;}
  .doc-viewer-save, .doc-viewer-clear{padding:8px 16px; font-size:13px;}
  .doc-viewer{
    background:#fdfcfa; color:#201f1c; border-radius:12px; padding:48px 56px;
    max-width:760px; margin:14px auto 0; box-shadow:0 24px 60px rgba(0,0,0,.4);
    line-height:1.65; font-size:15.5px; font-family:var(--font-body);
    outline:none; transition:box-shadow .15s ease;
  }
  .doc-viewer[contenteditable="true"]:focus{box-shadow:0 24px 60px rgba(0,0,0,.4), 0 0 0 3px var(--ember);}
  @media (max-width:640px){ .doc-viewer{padding:30px 22px;} }
  .doc-viewer h1{font-family:var(--font-display); font-size:26px; font-weight:700; margin:0 0 16px; color:#111; line-height:1.2;}
  .doc-viewer h1:not(:first-child){margin-top:30px;}
  .doc-viewer h2{font-family:var(--font-display); font-size:19px; font-weight:700; margin:28px 0 10px; color:#181818;}
  .doc-viewer h3{font-family:var(--font-display); font-size:15.5px; font-weight:700; margin:20px 0 8px; color:#181818;}
  .doc-viewer p{margin:0 0 14px;}
  .doc-viewer ul, .doc-viewer ol{margin:0 0 16px; padding-left:24px;}
  .doc-viewer li{margin-bottom:7px;}
  /* A sub-list rendered inside an <li> (see the stack-based nesting logic
     in renderDocx) sits directly under that item's own text -- reset its
     top margin so it doesn't inherit the standalone list spacing above,
     and drop its own bottom margin since the parent <li> already provides
     spacing before the NEXT top-level item. */
  .doc-viewer li > ul, .doc-viewer li > ol{margin-top:6px; margin-bottom:0;}
  /* A real Word "Columns" section (<w:cols w:num="N">) -- native CSS
     multi-column layout does correct newspaper-style flow (fills the
     first column top to bottom, then continues in the next) with no
     manual line-splitting needed, unlike the PDF export path. */
  .doc-viewer-columns{column-count:2; column-gap:28px; margin:0 0 16px;}
  .doc-viewer-columns p:first-child{margin-top:0;}
  /* The site-wide reset (a{color:inherit;text-decoration:none;}) would
     otherwise make a real <w:hyperlink> render completely invisibly --
     same color as surrounding text, no underline -- which defeats the
     point of having parsed it as a real link at all. */
  .doc-viewer a{color:#1a5fb4; text-decoration:underline;}
  .doc-viewer img{max-width:100%; height:auto; display:block; margin:18px 0; border-radius:4px;}
  /* A floating (anchored, wrapSquare/wrapTight) image gets a real CSS
     float instead of always breaking the text flow -- the specificity
     here (img.class, matching .doc-viewer img's own specificity) is
     needed to actually override that rule's non-floating margin, not
     just add to it. clear:both on headings/tables is the DOM-side twin
     of the PDF path's "always clear the float before a heading" rule --
     content that shouldn't visually squeeze in beside a floated image
     never does, regardless of exactly how many lines the float spans. */
  .doc-viewer img.doc-viewer-float-img{float:left; margin:4px 18px 12px 0; max-width:45%;}
  .doc-viewer h1, .doc-viewer h2, .doc-viewer h3, .doc-viewer table{clear:both;}
  .doc-viewer table{border-collapse:collapse; width:100%; margin:16px 0; font-size:14px;}
  .doc-viewer td{padding:8px 11px; vertical-align:top;}
  .pdf-preview-pagebreak{border:none; border-top:1px dashed var(--border-strong); margin:26px 0; opacity:.6;}
  /* PDF to Word preview pagination. The preview used to be one continuous
     column of text with only a faint dashed rule between source pages --
     on a multi-page document that read as an undifferentiated wall of text
     with no way to tell where one page ended and the next began. Each
     source page now gets an explicit "Page N of M" label above its own
     sheet, and the sheet carries the SOURCE page's real aspect ratio (set
     inline from the PDF's own pgSz) so a portrait original previews as a
     portrait page. Scoped to .pdf-preview-* rather than .doc-viewer, which
     is shared with the View & Edit Word and Word to PDF tools. */
  .pdf-preview-pagelabel{
    font-family:var(--font-mono); font-size:11.5px; letter-spacing:.06em;
    text-transform:uppercase; color:var(--text-dim); margin:26px 0 7px;
    display:flex; align-items:center; gap:11px;
  }
  .pdf-preview-pagelabel::after{content:''; flex:1; border-top:1px dashed var(--border-strong); opacity:.55;}
  .pdf-preview-pagelabel:first-child{margin-top:0;}
  .pdf-preview-page{
    background:#fff; border:1px solid rgba(0,0,0,.14); border-radius:4px;
    box-shadow:0 2px 10px rgba(0,0,0,.16); padding:7% 8%;
    height:auto; box-sizing:border-box;
  }
  /* The source page's shape is applied as a MINIMUM height, via the
     zero-width floated pseudo-element trick, with the ratio passed in as
     --page-ar (a percentage of the sheet's width).
     `aspect-ratio` was tried first and is wrong here: with height:auto it
     FIXES the box height to the ratio, and `min-height:min-content` does
     not override it -- so a page whose extracted content is taller than
     the ratio (very common: a dense statement page) simply overflowed its
     sheet and ran straight over the next page's sheet. Measured on a real
     42-page document: sheets 914px tall holding up to 1110px of content,
     colliding with the following page on 38 of 42 sheets. The float trick
     sets a floor instead of a ceiling, so a sheet always grows to fit. */
  /* Shared page-sheet shape. Used by PDF to Word's per-page preview sheets
     and by the View & Edit Word viewer, so both tools present a document on
     a real page rather than an arbitrary text column. --page-ar is the
     page's height as a percentage of its width, set inline from the
     document's own page setup. */
  .doc-page-sheet::before{
    content:''; display:block; float:left; width:0;
    padding-top:var(--page-ar, 129.4%);
  }
  .doc-page-sheet::after{content:''; display:block; clear:both;}
  .pdf-preview-page::before{
    content:''; display:block; float:left; width:0;
    padding-top:var(--page-ar, 129%);
  }
  .pdf-preview-page::after{content:''; display:block; clear:both;}
  /* The shared .doc-viewer table rules add 8px/11px cell padding and a 16px
     table margin, which is right for the View & Edit Word viewer but wrong
     here: the .docx this preview mirrors zeroes its cell margins and takes
     row height straight from the source's line advance, so the padded
     preview rows stood visibly taller than both the source PDF and the
     downloaded file. Reset inside preview sheets only. */
  .pdf-preview-page table{margin:0 !important; width:100%; table-layout:auto;}
  /* Vertical padding zeroed (row height must come from the source's line
     advance, as in the .docx); a little horizontal padding is kept so
     adjacent columns don't run together -- removing it entirely jammed
     "33,200" and "38,633" into "33,20038,633". */
  .pdf-preview-page td{padding:0 10px 0 0 !important; vertical-align:top;}
  .pdf-preview-page td:last-child{padding-right:0 !important;}
  @media (max-width:640px){ .pdf-preview-page{padding:20px 16px;} }
  /* The previous rule here unconditionally shaded and bolded every
     table's first row, regardless of what the source document actually
     specified -- confirmed against a real document with genuinely bold
     (via real <w:b>, already correctly rendered as <strong>) but
     completely unshaded header cells, which this rule was still coloring
     anyway. Real per-cell shading, when a document actually has it, is
     now applied as an inline background-color at render time instead;
     bold is already handled correctly by the existing run-formatting
     path with no CSS assumption needed at all. */
  @media (max-width:640px){ .doc-viewer{padding:30px 22px;} }

  /* ---------- ad slots ---------- */
  /* Only ever rendered when a slot actually has code (see partials/ad-slot.php),
     so this never leaves an empty gap on the page. */
  .ad-slot{padding:26px 0; text-align:center;}
  /* No border/background "card" around the ad anymore (removed per user
     request — it used to be a bordered box hugging just the ad creative).
     Still an inline-block wrapper, not just for legacy reasons: the
     .ad-slot-badge above the ad is absolutely positioned against this
     element, so it still needs position:relative + some top padding to
     give the badge clearance above the creative, and inline-block still
     shrink-wraps to the ad's own size so the badge sits at the ad's actual
     top-right corner rather than the full 1120px .wrap row's. */
  .ad-slot-frame{
    position:relative; display:inline-block; max-width:100%;
    padding-top:18px;
  }
  .ad-slot-frame > *{max-width:100%;}
  .ad-slot img, .ad-slot iframe{max-width:100%; height:auto; border:0;}
  /* Disclosure badge ("Sponsor" / "Advertisement" / "Partner", picked per
     slot in admin/ads.php — see gotool_ad_label() in data/ads.php). Only
     rendered when the admin chose one, so a slot with "No label" gets no
     extra markup at all. pointer-events:none keeps it from ever stealing a
     click meant for the ad underneath. */
  .ad-slot-badge{
    position:absolute; top:8px; right:12px; z-index:1; pointer-events:none;
    font-family:var(--font-mono); font-size:10px; font-weight:600;
    letter-spacing:.1em; text-transform:uppercase; color:var(--ember-hot);
    text-shadow:0 0 6px rgba(255,176,32,.8), 0 0 16px rgba(255,176,32,.45);
  }

  /* Ad rotation display group (slot 3) -- up to two ads side by side
     (600x600 when there's room), cycled by assets/app.js (see the "AD
     ROTATION DISPLAY" block there). Renders at the top of
     store-cta-main (see partials/footer.php), in line with the top of
     cta-cards-col on the right -- so .ad-rotation-row is just a plain
     flex row filling that column's own width, not a separately
     centred/padded container of its own (store-cta-main and .wrap
     already handle that). Squeezed next to cta-cards-col's 320px
     sidebar, this column tops out well under 600px on most screens, so
     the pair renders proportionally smaller there rather than at literal
     600x600 -- accepted tradeoff for sitting in line with "About Us"
     rather than spanning the full section width. */
  .ad-rotation-row{
    position:relative; display:flex; flex-wrap:wrap; justify-content:center;
    align-items:flex-start; gap:20px;
  }
  /* Half the base .ad-slot rule's 26px top/bottom padding -- scoped to
     just this slot (not the shared .ad-slot rule above, which slots 1/2
     also use) so the banners sit closer to the surrounding content
     without changing spacing anywhere else. */
  .ad-rotation-slot{padding:13px 0;}
  /* flex:1 1 260px (not a fixed 600px) so the pair shrinks together to
     fit the column instead of overflowing it and wrapping to two
     stacked rows -- the 600px cap only ever applies once there's
     actually room for it. 260px keeps each column from squeezing so far
     down that a 600x600 creative pasted in verbatim (no responsive
     sizing of its own) starts clipping against `overflow:hidden` on an
     ordinary phone width; below that this itself wraps to one column
     per row. */
  .ad-rotation-col{
    flex:1 1 260px; max-width:600px; aspect-ratio:1/1; overflow:hidden;
    transition:opacity .26s ease;
  }
  /* Each ad's own .ad-rotation-frame fills its column completely (rather
     than just shrink-wrapping the ad code) so its per-entry .ad-slot-badge
     -- see partials/ad-rotation.php -- has a full-size, position:relative
     box to sit in the corner of, no matter how the pasted ad code itself
     is sized. This is also the element assets/app.js moves between the
     pool and the visible columns, so the badge (rendered inside it)
     travels with its own ad automatically when the rotation swaps it in
     or out -- nothing has to re-match a badge to an ad on each tick. */
  .ad-rotation-frame{
    position:relative; width:100%; height:100%;
    display:flex; align-items:center; justify-content:center;
  }
  .ad-rotation-frame > *{max-width:100%; max-height:100%;}
  .ad-rotation-frame img, .ad-rotation-frame iframe{max-width:100%; height:auto; border:0;}

  /* ---------- store badges (coming soon) + privacy card ---------- */
  .store-cta{border-top:1px solid var(--border); padding:44px 0 40px;}
  /* Two columns on wide screens: text + badges on the left, the two
     grouped link cards stacked on the right (About/Terms/Privacy taller,
     FAQ/Contact shorter below it) -- collapses to one stacked column on
     narrower screens where there's no room for a side-by-side layout. */
  .store-cta-layout{display:grid; grid-template-columns:1fr 320px; gap:32px; align-items:start;}
  .store-cta-main{min-width:0; text-align:center;}
  .cta-cards-col{display:flex; flex-direction:column; gap:16px;}
  @media (max-width:900px){
    .store-cta-layout{grid-template-columns:1fr;}
  }
  .store-cta-eyebrow{
    font-family:var(--font-mono); font-size:11.5px; letter-spacing:.14em; text-transform:uppercase;
    color:var(--ember-hot); margin-bottom:10px;
  }
  .store-cta-heading{font-family:var(--font-display); font-size:23px; font-weight:700; margin-bottom:8px;}
  .store-cta-sub{font-size:14px; color:var(--text-dim); max-width:560px; margin:0 auto 26px;}
  .store-badges{
    display:flex; justify-content:center; align-items:stretch; gap:12px;
    flex-wrap:wrap; list-style:none; padding:0; margin:0;
  }
  .store-badge{
    display:flex; align-items:center; gap:11px; text-align:left;
    border:1px solid var(--border-strong); border-radius:11px;
    padding:9px 16px 9px 13px; background:var(--bg-elev);
    flex:0 1 auto; min-width:0; cursor:default;
  }
  .store-badge-icon{
    flex-shrink:0; width:26px; height:26px; color:var(--text-dim);
    display:flex; align-items:center; justify-content:center;
  }
  .store-badge-icon svg{width:100%; height:100%;}
  .store-badge-text{display:flex; flex-direction:column; line-height:1.25; min-width:0;}
  .store-badge-text small{font-size:10px; color:var(--text-faint); letter-spacing:.02em; white-space:nowrap;}
  .store-badge-text strong{
    font-family:var(--font-display); font-size:14.5px; font-weight:600;
    color:var(--text-dim); white-space:nowrap;
  }

  /* One bordered "card" holding several stacked link rows (About Us/Terms/
     Privacy in one group, FAQ/Contact Us in another) rather than each link
     being its own separate card. */
  .cta-card-group{
    display:flex; flex-direction:column;
    border:1px solid var(--border-strong); border-radius:var(--radius);
    background:var(--bg-elev); overflow:hidden;
  }
  .cta-card-row{
    display:flex; align-items:center; gap:14px; text-align:left;
    padding:16px 18px; transition:background .15s ease;
  }
  .cta-card-row:hover{background:var(--bg-elev-2);}
  .cta-card-row + .cta-card-row{border-top:1px solid var(--border);}
  .cta-card-row svg{width:22px; height:22px; color:var(--ember-hot); flex-shrink:0;}
  .cta-card-row-text{display:flex; flex-direction:column; gap:3px; min-width:0;}
  .privacy-cta-card-title{font-family:var(--font-display); font-size:15px; font-weight:600; color:var(--text);}
  .privacy-cta-card-sub{font-size:12.5px; color:var(--text-faint); line-height:1.4;}
  @media (max-width:560px){
    .store-badges{gap:10px;}
    .store-badge{flex:1 1 calc(50% - 5px); padding:9px 12px;}
    .store-badge-text strong{font-size:13px;}
  }

  /* Link directory (For Business / For Developer & Creator) sitting below
     the store badges -- fills the leftover vertical space in store-cta-main
     caused by the taller cta-cards-col next to it. Explicit 2-column grid
     (not flex-wrap) so the two groups always sit side by side on wide
     screens -- flex-wrap would drop to a stacked single column whenever
     the combined content width nudged past the available space, which
     happened right at the site's default desktop width. */
  .footer-link-dirs{
    display:grid; grid-template-columns:1fr 1fr; gap:32px;
    margin-top:28px; padding-top:28px; border-top:1px solid var(--border);
    text-align:left;
  }
  @media (max-width:640px){
    .footer-link-dirs{grid-template-columns:1fr;}
  }
  .footer-link-col{min-width:0;}
  /* Each group's own link list splits into 2 columns. column-count (not
     grid) deliberately: with 5 links that gives 3-then-2 filled top-to-
     bottom in reading order (col 1: items 1-3, col 2: items 4-5) -- the
     way a person actually reads a short list split into columns. A grid
     with 2 explicit columns would instead fill left-to-right, pairing
     unrelated items row-by-row (1&2, 3&4, 5&nothing), which reads oddly
     for a simple link list. break-inside:avoid on each link stops a
     browser from ever splitting one link's box across the column gap. */
  .footer-link-col-links{
    column-count:2; column-gap:20px; margin-top:4px;
  }
  .footer-link-col-links a{ break-inside:avoid; }
  @media (max-width:420px){
    .footer-link-col-links{column-count:1;}
  }
  .footer-link-col-title{
    font-family:var(--font-display); font-size:14px; font-weight:700;
    color:var(--text); margin-bottom:12px;
  }
  .footer-link-col a{
    display:block; font-size:13.5px; color:var(--text-dim); padding:6px 0;
    transition:color .15s ease;
  }
  .footer-link-col a:hover{color:var(--ember-hot);}

  /* ---------- member accounts ---------- */
  .member-notice{
    margin-top:24px; font-size:13.5px; line-height:1.5; color:var(--text-dim);
    background:var(--bg-elev); border:1px solid var(--border-strong); border-radius:var(--radius);
    padding:16px 18px;
  }
  .member-notice strong{color:var(--text); display:block; margin-bottom:4px;}

  .auth-wrap{padding:8px 0 60px;}
  .auth-pending{max-width:420px; margin:0 auto;}
  .auth-grid{display:grid; grid-template-columns:1fr 340px; gap:40px; align-items:start; max-width:840px; margin:0 auto;}
  @media (max-width:860px){ .auth-grid{grid-template-columns:1fr; gap:32px; max-width:420px;} }
  .auth-form-col{max-width:420px; width:100%;}
  .auth-benefits{
    background:var(--bg-elev); border:1px solid var(--border); border-radius:var(--radius);
    padding:26px 24px; order:2;
  }
  @media (max-width:860px){ .auth-form-col{margin:0 auto;} }
  .auth-benefits-eyebrow{
    font-family:var(--font-mono); font-size:12px; letter-spacing:.05em; text-transform:uppercase;
    color:var(--ember-hot); margin-bottom:18px;
  }
  .auth-benefits-list{list-style:none; display:flex; flex-direction:column; gap:18px;}
  .auth-benefits-list li{display:flex; gap:14px; align-items:flex-start;}
  .auth-benefits-list li + li{padding-top:18px; border-top:1px solid var(--border);}
  .auth-benefits-list .tool-icon{flex-shrink:0;}
  .auth-benefits-list h3{font-size:14px; font-weight:600; color:var(--text); margin-bottom:4px;}
  .auth-benefits-list p{font-size:13px; color:var(--text-dim); line-height:1.5;}
  .auth-tabs{display:flex; gap:8px; margin-bottom:18px;}
  .auth-tab{
    flex:1; text-align:center; padding:11px; border-radius:9px; border:1px solid var(--border);
    font-size:14px; font-weight:600; color:var(--text-dim); background:var(--bg-elev);
    transition:border-color .15s ease, color .15s ease, background .15s ease;
  }
  .auth-tab.active{border-color:var(--ember); color:var(--ember-hot); background:var(--ember-dim);}
  .auth-card{
    border:1px solid var(--border); border-radius:var(--radius); background:var(--bg-elev);
    padding:28px 26px;
  }
  .auth-form{display:flex; flex-direction:column; gap:16px;}
  .auth-field{display:flex; flex-direction:column; gap:7px; font-size:13.5px; color:var(--text-dim);}
  .auth-field-hint{color:var(--text-faint); font-weight:400;}
  .auth-field input{
    background:var(--bg); border:1px solid var(--border); border-radius:9px;
    padding:11px 14px; color:var(--text); font-family:var(--font-body); font-size:14.5px;
    transition:border-color .15s ease;
  }
  .auth-field input:focus{border-color:var(--ember); outline:none;}
  .auth-submit{width:100%; justify-content:center;}
  .auth-error{
    font-size:13px; color:#ffb4b4; background:rgba(255,107,107,.08); border:1px solid rgba(255,107,107,.3);
    border-radius:8px; padding:10px 13px;
  }
  .auth-fineprint{font-size:12px; color:var(--text-faint); line-height:1.5; text-align:center;}

  /* ---------- footer ---------- */
  footer{border-top:1px solid var(--border); padding:28px 0;}
  .footer-inner{display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap;}
  footer p{font-size:13px; color:var(--text-faint); margin:0;}
  .footer-badge{flex-shrink:0;}

  [hidden]{display:none !important;}

  /* ---------- photo editor ---------- */
  .pe-root{margin-top:20px;}

  /* Menu bar: File/Edit/Image/Layer/Filter/View/Window, built with the
     same zero-JS <details>/<summary> dropdown pattern used in
     partials/nav.php. */
  .pe-menubar{
    display:flex; align-items:center; gap:2px; flex-wrap:wrap;
    background:var(--bg-elev); border:1px solid var(--border); border-radius:12px 12px 0 0;
    padding:5px 8px;
  }
  .pe-menu{position:relative;}
  .pe-menu summary{
    list-style:none; padding:8px 12px; border-radius:7px; font-size:13px; font-weight:600;
    color:var(--text-dim); cursor:pointer; font-family:var(--font-body); user-select:none;
  }
  .pe-menu summary::-webkit-details-marker{display:none;}
  .pe-menu summary:hover{background:var(--bg-elev-2); color:var(--text);}
  /* The menu now opens on mouseover, not just a click -- handled in
     app.js by setting the underlying <details>'s real `open` property on
     mouseenter/mouseleave, rather than trying to fake it with a plain
     CSS :hover override on `display` (a closed <details>'s content is
     hidden by the browser in a way that override can't reach). So
     [open] alone is still the right signal for this "active" summary
     look -- it's true both when hovered open and when opened by a
     click/tap on touch. */
  .pe-menu[open] > summary{background:var(--ember-dim); color:var(--ember-hot);}
  .pe-menu-dropdown{
    /* margin-top is deliberately 0, not a gap like nav.php's equivalent
       dropdown uses. app.js closes the menu on mouseleave, which (like
       :hover) stops holding as soon as the pointer isn't over the
       .pe-menu element or one of its rendered descendant boxes -- and
       this dropdown is position:absolute, so it doesn't contribute to
       its own parent's box size the way normal-flow content would.
       Moving the mouse straight down from the summary into any gap
       below it would momentarily leave every one of those boxes and
       close the menu before the pointer ever reaches it. Flush
       placement removes that dead zone entirely. */
    position:absolute; top:100%; left:0; margin-top:0; min-width:200px; z-index:60;
    display:flex; flex-direction:column; gap:2px;
    background:var(--bg-elev-2); border:1px solid var(--border-strong); border-radius:10px;
    padding:6px; box-shadow:0 16px 40px rgba(0,0,0,.5);
  }
  .pe-menu-dropdown button{
    display:flex; align-items:center; justify-content:space-between; gap:14px;
    width:100%; text-align:left; background:none; border:none; border-radius:7px;
    color:var(--text-dim); font-family:var(--font-body); font-size:13px; padding:8px 10px; cursor:pointer;
  }
  .pe-menu-dropdown button:hover{background:var(--bg-elev); color:var(--text);}
  /* Shortcut hint shown beside a menu item's label, e.g. "Undo  Ctrl+Z" --
     only present on items that actually have a working keyboard shortcut,
     so its presence/absence is itself informative. Pushed to the far
     right of its button via the button's own flex+space-between above. */
  .pe-menu-shortcut{
    font-family:var(--font-mono, monospace); font-size:11px; color:var(--text-faint);
    flex-shrink:0; letter-spacing:.02em;
  }
  .pe-menu-dropdown button:hover .pe-menu-shortcut{color:var(--text-dim);}

  /* "Open in app" pill -- pops the editor out into its own chromeless
     window (see the onclick in the panel markup). Pushed to the far
     right of the menu bar via margin-left:auto since .pe-menubar is a
     flex row; sized/shaped as a pill rather than reusing .btn as-is so
     it reads as distinct from the File/Edit/... menu buttons next to it. */
  .pe-open-in-app-btn{
    display:inline-flex; align-items:center; gap:6px; margin-left:auto;
    padding:7px 14px 7px 12px; border-radius:999px; border:none; cursor:pointer;
    background:linear-gradient(135deg, var(--ember), var(--ember-hot)); color:#160c05;
    font-family:var(--font-body); font-size:12.5px; font-weight:700;
    transition:transform .15s ease;
  }
  .pe-open-in-app-btn:hover{transform:translateY(-1px);}
  .pe-open-in-app-btn svg{width:15px; height:15px; flex-shrink:0;}

  /* A submenu (currently just "Save As") looks like a regular dropdown
     item, not a bold top-level menu trigger -- these rules share the
     same specificity as the .pe-menu ones above and rely on coming AFTER
     them in the file to correctly override, rather than a stronger
     selector, so this needs to stay below that block if it's ever moved. */
  .pe-submenu summary{
    padding:8px 10px; font-weight:400; display:flex; align-items:center; justify-content:space-between; gap:10px;
  }
  .pe-submenu summary::after{content:'\203a'; color:var(--text-faint); font-size:15px; line-height:1;}
  .pe-submenu[open] summary{background:var(--bg-elev); color:var(--text);}
  /* Same flush-placement reasoning as the top-level .pe-menu-dropdown
     above, just horizontal here: margin-left:0 keeps no dead zone
     between "Save As..." and its own nested dropdown to its right. */
  .pe-submenu-dropdown{top:0; left:100%; margin-top:0; margin-left:0;}

  /* Workspace: left icon rail + center canvas column + right side panel,
     a classic three-column editor layout. */
  .pe-workspace{
    display:flex; align-items:flex-start; gap:14px;
    border:1px solid var(--border); border-top:none; border-radius:0 0 12px 12px;
    background:var(--bg-elev); padding:14px;
  }
  .pe-toolbar{
    display:flex; flex-direction:column; gap:4px; flex-shrink:0;
    background:var(--bg-elev-2); border:1px solid var(--border); border-radius:10px; padding:6px;
  }
  .pe-fgbg{position:relative; width:34px; height:34px; margin:10px auto 6px;}
  .pe-fgbg-swatches{position:relative; width:100%; height:100%;}
  .pe-fgbg-swatches input[type="color"]{
    position:absolute; width:22px; height:22px; padding:0; border:2px solid var(--border-strong); border-radius:5px;
    cursor:pointer; background:none; appearance:none; -webkit-appearance:none;
  }
  .pe-fgbg-swatches input[type="color"]::-webkit-color-swatch-wrapper{padding:0;}
  .pe-fgbg-swatches input[type="color"]::-webkit-color-swatch{border:none; border-radius:3px;}
  .pe-fgbg-swatches input[type="color"]::-moz-color-swatch{border:none; border-radius:3px;}
  .pe-fgbg-fg{top:0; left:0; z-index:2;}
  .pe-fgbg-bg{bottom:0; right:0; z-index:1;}
  .pe-fgbg-swap, .pe-fgbg-reset{
    position:absolute; width:15px; height:15px; padding:2px; border-radius:4px;
    background:var(--bg-elev); border:1px solid var(--border); color:var(--text-faint); cursor:pointer;
    display:flex; align-items:center; justify-content:center;
  }
  .pe-fgbg-swap{top:-7px; right:-2px; z-index:3;}
  .pe-fgbg-swap svg{width:11px; height:11px;}
  .pe-fgbg-reset{bottom:-2px; left:-7px; z-index:0;}
  .pe-fgbg-reset svg{width:12px; height:12px;}
  .pe-fgbg-swap:hover, .pe-fgbg-reset:hover{border-color:var(--ember); color:var(--ember-hot);}
  .pe-tool-btn{
    display:flex; flex-direction:column; align-items:center; gap:4px; width:52px;
    background:transparent; border:1px solid transparent; border-radius:9px; padding:9px 4px;
    color:var(--text-dim); cursor:pointer; font-family:var(--font-body); font-size:11px;
    transition:background .15s ease, border-color .15s ease, color .15s ease;
  }
  .pe-tool-btn svg{width:18px; height:18px;}
  .pe-tool-btn span{display:none;} /* icon-only rail; title attr covers the tooltip */
  .pe-tool-btn:hover{background:var(--bg-elev); color:var(--text);}
  .pe-tool-btn.active{background:var(--ember-dim); border-color:var(--ember); color:var(--ember-hot);}

  .pe-center{flex:1; min-width:0; display:flex; flex-direction:column; position:relative;}
  .pe-side{
    width:300px; flex-shrink:0; max-height:78vh; overflow-y:auto;
    background:var(--bg-elev-2); border:1px solid var(--border); border-radius:10px; padding:14px;
  }

  @media (max-width:900px){
    .pe-workspace{flex-direction:column;}
    .pe-toolbar{flex-direction:row; flex-wrap:wrap; width:100%;}
    .pe-side{width:100%; max-height:none;}
  }

  /* App-mode fill chain: in the "Open in app" popup (includes/tool-page.php's
     ?app=1 branch, body.app-mode) the page's own <main> is already stretched
     to the full popup height (see that file's inline style block). Here we
     continue that flex chain down through the editor's own markup so the
     canvas/side panel actually use the extra room instead of stopping at
     their normal in-page vh-relative caps (70vh/78vh), which were sized for
     sitting inside a scrolling page, not filling a purpose-sized window. Only
     active in app mode -- the normal in-page layout is unaffected. */
  body.app-mode[data-tool-slug="photo-editor"] #tool,
  body.app-mode[data-tool-slug="photo-editor"] #tool .wrap{
    display:flex; flex-direction:column; flex:1; min-height:0;
  }
  body.app-mode[data-tool-slug="photo-editor"] .pe-root{
    margin-top:0; flex:1; min-height:0; display:flex; flex-direction:column;
  }
  body.app-mode[data-tool-slug="photo-editor"] .pe-workspace{
    flex:1; min-height:0; align-items:stretch;
  }
  body.app-mode[data-tool-slug="photo-editor"] .pe-center{
    min-height:0;
  }
  body.app-mode[data-tool-slug="photo-editor"] .pe-stage{
    flex:1; min-height:0; max-height:none;
  }
  body.app-mode[data-tool-slug="photo-editor"] .pe-side{
    max-height:none;
  }

  .pe-tool-options{
    display:flex; flex-wrap:wrap; align-items:center; gap:18px; margin-top:12px;
    background:var(--bg-elev); border:1px solid var(--border); border-radius:12px; padding:12px 14px;
  }
  .pe-inline-field{display:flex; align-items:center; gap:8px; font-size:13px; color:var(--text-dim); white-space:nowrap;}
  .pe-inline-field select{
    background:var(--bg-elev-2); border:1px solid var(--border); border-radius:7px; color:var(--text);
    padding:6px 8px; font-family:var(--font-body); font-size:13px;
  }
  .pe-slider-field select{
    background:var(--bg-elev-2); border:1px solid var(--border); border-radius:7px; color:var(--text);
    padding:6px 8px; font-family:var(--font-body); font-size:13px;
  }
  .pe-inline-field input[type="color"]{
    width:32px; height:28px; border:1px solid var(--border); border-radius:7px; background:none; cursor:pointer; padding:0;
  }
  .pe-inline-field input[type="range"]{width:110px;}
  .pe-checkbox-field{gap:6px;}
  .pe-checkbox-field input{width:16px; height:16px; accent-color:var(--ember);}
  .pe-inline-label{font-size:12.5px; color:var(--text-faint); font-family:var(--font-mono);}
  .pe-crop-actions{display:flex; gap:10px; margin-left:auto;}

  /* Recent/saved color palette (Brush, Shape, Eyedropper panels) --
     see photo-editor-color-palette.php and refreshColorPalettes() in
     assets/app.js. A full-width flex row so it wraps onto its own line
     within the already-wrapping .pe-tool-options bar rather than
     squeezing into whatever horizontal space the sliders left. */
  .pe-color-palette{display:flex; align-items:center; flex-wrap:wrap; gap:8px; width:100%;}
  .pe-palette-swatches{display:flex; flex-wrap:wrap; gap:5px;}
  .pe-palette-empty{font-size:11.5px; color:var(--text-faint); font-family:var(--font-mono);}
  .pe-palette-swatch-wrap{position:relative; display:inline-flex;}
  .pe-palette-swatch{
    width:18px; height:18px; border-radius:5px; border:1px solid var(--border-strong);
    cursor:pointer; padding:0;
  }
  .pe-palette-swatch:hover{border-color:var(--ember-hot); transform:scale(1.12);}
  .pe-palette-swatch-remove{
    position:absolute; top:-6px; right:-6px; width:13px; height:13px; border-radius:50%;
    background:var(--bg-elev-2); border:1px solid var(--border-strong); color:var(--text-faint);
    font-size:10px; line-height:1; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center;
  }
  .pe-palette-swatch-remove:hover{border-color:var(--err); color:var(--err);}
  .pe-palette-save-btn{
    width:16px; height:16px; border-radius:4px; border:1px solid var(--border-strong); background:var(--bg-elev-2);
    color:var(--text-dim); font-size:11px; line-height:1; cursor:pointer; padding:0; margin-left:4px;
  }
  .pe-palette-save-btn:hover{border-color:var(--ember-hot); color:var(--ember-hot);}

  .pe-stage{
    margin-top:14px; border:1px solid var(--border); border-radius:14px; background:
      repeating-conic-gradient(#2a2830 0% 25%, #201e24 0% 50%) 0 0/22px 22px;
    overflow:auto; max-height:70vh; display:flex; align-items:flex-start; justify-content:center; padding:18px;
    transition:border-color .15s ease, background .15s ease;
  }
  .pe-stage.dragover{border-color:var(--ember); background:var(--ember-dim);}
  .pe-empty-state{
    width:100%; min-height:320px; margin:auto; display:flex; flex-direction:column;
    align-items:center; justify-content:center; text-align:center; cursor:pointer;
  }
  .pe-empty-state svg{width:30px; height:30px; margin-bottom:14px; color:var(--ember-hot);}
  .pe-empty-state p{font-size:14px; color:var(--text-dim);}
  .pe-empty-state .dz-strong{color:var(--text); font-weight:600;}
  .pe-empty-state .dz-hint{margin-top:6px; font-size:12px; color:var(--text-faint); font-family:var(--font-mono);}
  /* Live frame/border preview wrapper -- see the markup comment in
     includes/tool-panels/photo-editor.php and syncFramePreview() in
     app.js. All the actual sizing (padding/background/box-shadow, the
     two strip elements' height/background) is set inline by JS since it
     depends on the current frame style, its fields, and zoom; this just
     establishes the layout (a centered column so the two strips can sit
     directly above/below the canvas) and sensible transitions so
     changing a frame field animates instead of snapping. */
  /* Deliberately no transition here: this updates live while a color/
     width/radius/padding slider is being dragged, so it needs to track
     the pointer 1:1 every step -- animating each intermediate value
     would make it visibly lag behind the input instead of tracking it. */
  .pe-frame-preview{
    display:flex; flex-direction:column; align-items:center; flex-shrink:0;
  }
  .pe-frame-preview-strip{
    width:100%; box-sizing:border-box; flex-shrink:0;
    display:flex; align-items:center; justify-content:center;
    font-family:"Inter", sans-serif; text-align:center;
  }
  .pe-canvas-wrap{position:relative; flex-shrink:0; touch-action:none;}
  .pe-canvas-wrap canvas{display:block; border-radius:4px; box-shadow:0 10px 40px rgba(0,0,0,.5);}
  /* Space-bar-drag panning: pe-pan-ready while Space is held (cursor hints
     that dragging will pan), pe-panning while an actual pan drag is in
     progress -- see the spacePanning/panActive state in app.js. */
  .pe-canvas-wrap.pe-pan-ready, .pe-canvas-wrap.pe-pan-ready canvas{cursor:grab;}
  .pe-canvas-wrap.pe-panning, .pe-canvas-wrap.pe-panning canvas{cursor:grabbing;}

  /* Mini navigator/minimap overlay -- see updateNavigator() in app.js.
     Positioned absolutely within .pe-center (not inside the scrollable
     #pe-stage) so it stays fixed in the viewport corner instead of
     scrolling away with the canvas underneath it. */
  .pe-navigator{
    position:absolute; right:24px; bottom:24px; width:130px; height:130px;
    border:1px solid var(--border-strong); border-radius:8px; overflow:hidden;
    background:rgba(20,19,24,.88); box-shadow:0 6px 20px rgba(0,0,0,.45);
    cursor:pointer; touch-action:none; z-index:3;
  }
  .pe-navigator canvas{display:block; width:100%; height:100%;}
  .pe-navigator-rect{
    position:absolute; border:1.5px solid var(--ember-hot); background:rgba(255,90,31,.15);
    pointer-events:none; box-sizing:border-box;
  }
  @media (max-width: 900px){ .pe-navigator{display:none;} }
  .pe-crop-box{
    position:absolute; border:2px dashed var(--ember-hot); background:rgba(255,90,31,.08); cursor:move; box-sizing:border-box;
  }
  .pe-crop-handle{
    position:absolute; width:16px; height:16px; background:var(--ember-hot); border:2px solid var(--bg);
    border-radius:50%; transform:translate(-50%,-50%);
  }
  .pe-crop-handle[data-handle="nw"]{top:0; left:0; cursor:nwse-resize;}
  .pe-crop-handle[data-handle="ne"]{top:0; left:100%; cursor:nesw-resize;}
  .pe-crop-handle[data-handle="sw"]{top:100%; left:0; cursor:nesw-resize;}
  .pe-crop-handle[data-handle="se"]{top:100%; left:100%; cursor:nwse-resize;}

  /* Guide lines: screen-only alignment aids, same "never baked into the
     export" spirit as the grid overlay -- rendered as plain DOM (not
     drawn on the canvas), so they can never end up in a downloaded
     photo. Each is a full-width/height strip with a thin cyan line
     centered in it -- the strip is the actual drag/click target,
     wider than the visible line itself so it's easy to grab. */
  .pe-guides-layer{position:absolute; inset:0; pointer-events:none;}
  .pe-guide{position:absolute; pointer-events:auto;}
  .pe-guide-h{left:0; right:0; height:9px; margin-top:-4px; cursor:ns-resize;}
  .pe-guide-v{top:0; bottom:0; width:9px; margin-left:-4px; cursor:ew-resize;}
  .pe-guide-h::after{content:""; position:absolute; left:0; right:0; top:4px; height:1px; background:#4fd8e8; box-shadow:0 0 0 1px rgba(0,0,0,.35);}
  .pe-guide-v::after{content:""; position:absolute; top:0; bottom:0; left:4px; width:1px; background:#4fd8e8; box-shadow:0 0 0 1px rgba(0,0,0,.35);}

  .pe-size-bar{
    display:flex; flex-wrap:wrap; align-items:center; gap:14px; margin-top:14px;
    background:var(--bg-elev); border:1px solid var(--border); border-radius:12px; padding:8px 12px;
  }
  .pe-size-info{font-size:12.5px; color:var(--text-faint);}
  .pe-size-info strong{color:var(--text); font-family:var(--font-mono); font-weight:600;}
  .pe-size-divider{color:var(--text-faint);}
  .pe-size-field input[type="number"]{
    width:72px; background:var(--bg-elev-2); border:1px solid var(--border); border-radius:7px;
    color:var(--text); padding:6px 8px; font-family:var(--font-mono); font-size:13px;
  }
  .pe-size-field input[type="number"]:focus{border-color:var(--ember);}

  .pe-bar{
    display:flex; flex-wrap:wrap; align-items:center; gap:18px; margin-top:10px;
    background:var(--bg-elev); border:1px solid var(--border); border-radius:12px; padding:8px 12px;
  }
  /* flex-shrink:0 so a group that doesn't fit on the current row wraps
     whole onto the next one (the .pe-bar flex-wrap above already
     supports this) instead of every button inside it individually
     compressing below its own text width -- which is what was
     happening to the guide buttons' labels before this was added. */
  .pe-bar-group{display:flex; align-items:center; gap:6px; flex-shrink:0;}
  .pe-icon-btn{
    width:34px; height:34px; display:flex; align-items:center; justify-content:center; border-radius:8px;
    border:1px solid var(--border); background:var(--bg-elev-2); color:var(--text-dim); cursor:pointer;
    font-size:16px; font-weight:600;
  }
  .pe-icon-btn svg{width:16px; height:16px;}
  .pe-icon-btn:hover{border-color:var(--ember); color:var(--ember-hot);}
  .pe-icon-btn.active{background:var(--ember-dim); border-color:var(--ember); color:var(--ember-hot);}
  .pe-icon-btn:disabled{opacity:.35; cursor:not-allowed;}
  .pe-icon-btn-text{width:auto; padding:0 12px; font-size:12.5px; font-family:var(--font-mono); white-space:nowrap;}
  .pe-icon-btn-danger:hover{border-color:var(--err); color:var(--err);}
  .pe-zoom-label{font-size:12.5px; color:var(--text-faint); min-width:42px; text-align:center;}

  .pe-panel-tabs{display:flex; gap:8px; margin-top:0; border-bottom:1px solid var(--border);}
  .pe-panel-tab{
    background:none; border:none; color:var(--text-faint); font-family:var(--font-body); font-weight:600;
    font-size:13.5px; padding:10px 4px; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px;
  }
  .pe-panel-tab.active{color:var(--ember-hot); border-color:var(--ember);}
  .pe-panel{padding-top:16px;}

  /* Document tabs -- browser-tab-style, one per open photo. Hidden by
     default (see the panel markup) so the editor looks unchanged for the
     common single-photo case, and only appears once a second photo is
     opened. */
  .pe-doctabs{display:flex; align-items:center; gap:2px; padding:10px 20px 0; overflow-x:auto; border-bottom:1px solid var(--border);}
  .pe-doctab{
    display:flex; align-items:center; gap:8px; padding:8px 8px 8px 14px; border-radius:8px 8px 0 0;
    background:var(--bg-elev); border:1px solid var(--border); border-bottom:none; color:var(--text-dim);
    font-family:var(--font-body); font-size:13px; cursor:pointer; white-space:nowrap; flex-shrink:0; max-width:180px;
  }
  .pe-doctab.active{background:var(--bg); color:var(--text); border-color:var(--border-strong);}
  .pe-doctab-name{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .pe-doctab-close{
    flex-shrink:0; width:18px; height:18px; border-radius:5px; display:flex; align-items:center; justify-content:center;
    color:var(--text-faint); background:none; border:none; cursor:pointer; font-size:14px; line-height:1;
  }
  .pe-doctab-close:hover{background:var(--bg-elev-2); color:var(--text);}
  .pe-doctab-add{
    flex-shrink:0; width:30px; height:30px; border-radius:8px; display:flex; align-items:center; justify-content:center;
    color:var(--text-faint); background:none; border:1px dashed var(--border-strong); cursor:pointer; margin-left:6px; font-size:16px;
  }
  .pe-doctab-add:hover{color:var(--ember-hot); border-color:var(--ember);}

  .pe-preset-row{display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px;}
  .pe-preset-btn{
    background:var(--bg-elev); border:1px solid var(--border); border-radius:8px; color:var(--text-dim);
    font-family:var(--font-body); font-size:12.5px; padding:8px 13px; cursor:pointer;
  }
  .pe-preset-btn:hover{border-color:var(--border-strong); color:var(--text);}
  .pe-preset-btn.active{border-color:var(--ember); color:var(--ember-hot); background:var(--ember-dim);}

  .pe-sliders{display:flex; flex-wrap:wrap; gap:16px 22px;}
  .pe-slider-field{display:flex; align-items:center; gap:10px; font-size:13px; color:var(--text-dim);}
  .pe-slider-field input[type="range"]{width:130px; accent-color:var(--ember);}
  .pe-slider-field .mono{min-width:40px; color:var(--text-faint); font-size:12px;}

  .pe-histogram-canvas{
    display:block; width:100%; height:70px; background:var(--bg-elev);
    border:1px solid var(--border); border-radius:8px; margin-bottom:16px;
  }
  .pe-curve-canvas{
    display:block; width:100%; max-width:256px; height:180px; background:var(--bg-elev);
    border:1px solid var(--border); border-radius:8px; cursor:crosshair; touch-action:none;
  }

  /* History panel: a plain clickable list, most-recent state at the
     bottom (append order), matching the order edits actually happened
     in -- same top-to-bottom-is-oldest-to-newest convention as the
     Layers panel's bottom-to-top-is-back-to-front doesn't try to match,
     since these are unrelated orderings with no reason to agree. */
  .pe-history-list{list-style:none; display:flex; flex-direction:column; gap:4px; max-height:360px; overflow-y:auto;}
  .pe-history-item{
    padding:8px 11px; border-radius:8px; border:1px solid transparent; cursor:pointer;
    font-size:13px; color:var(--text-dim); background:var(--bg-elev);
  }
  .pe-history-item:hover{border-color:var(--border-strong);}
  .pe-history-item.active{border-color:var(--ember); background:var(--ember-dim); color:var(--text); font-weight:600;}

  .pe-layers-hint{font-size:12px; color:var(--text-faint); margin-bottom:10px;}
  /* Only shown once 2+ layers are checked -- see refreshLayersList's
     checkedIds handling -- rather than a permanently-visible button
     that's disabled/no-op most of the time. */
  .pe-layers-toolbar{margin-bottom:10px;}
  .pe-layers-list{list-style:none; display:flex; flex-direction:column; gap:6px;}
  .pe-layer-row{
    display:flex; align-items:center; gap:8px; background:var(--bg-elev); border:1px solid var(--border);
    border-radius:9px; padding:9px 11px; cursor:pointer; font-size:13px; color:var(--text-dim);
  }
  .pe-layer-row:hover{border-color:var(--border-strong);}
  .pe-layer-row.selected{border-color:var(--ember); background:var(--ember-dim); color:var(--text);}
  .pe-layer-row svg{width:15px; height:15px; flex-shrink:0; color:var(--text-faint);}
  .pe-layer-name{flex-grow:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .pe-layer-row .f-actions{display:flex; gap:4px;}
  .pe-layer-check{flex-shrink:0; accent-color:var(--ember); cursor:pointer;}
  /* Shared look for the small eye/lock/caret toggle buttons prepended to
     every row -- same footprint as the up/down/delete f-actions buttons
     so the row stays visually consistent whichever ones are present. */
  .pe-layer-icon-btn{
    flex-shrink:0; display:flex; align-items:center; justify-content:center;
    width:22px; height:22px; padding:0; border-radius:6px; border:1px solid transparent;
    background:none; color:var(--text-faint); cursor:pointer;
  }
  .pe-layer-icon-btn:hover{border-color:var(--border-strong); color:var(--text);}
  .pe-layer-icon-btn svg{width:14px; height:14px; margin:0;}
  .pe-layer-icon-btn.active{color:var(--ember-hot);}
  /* Hidden/locked layers stay in the list (never removed) but read as
     visually de-emphasized so their state is obvious at a glance without
     needing to check each icon individually. */
  .pe-layer-row.pe-layer-hidden{opacity:.5;}
  .pe-layer-row.pe-layer-locked .pe-layer-name{color:var(--text-faint);}
  .pe-layer-name-input{
    flex-grow:1; min-width:0; background:var(--bg-elev-2); border:1px solid var(--ember);
    border-radius:5px; padding:2px 6px; font-size:13px; font-family:inherit; color:var(--text);
  }
  /* Group folder row reads slightly bolder/brighter than a plain layer
     row so it's immediately clear it's a container, not a leaf layer;
     its children render as normal .pe-layer-row items directly below it
     with extra left padding for the nested indent. */
  .pe-layer-group-row{font-weight:600; color:var(--text);}
  .pe-layer-group-row svg{color:var(--ember-hot);}
  .pe-layer-row-nested{padding-left:30px;}
  .pe-layer-caret{transition:transform .15s ease;}
  .pe-layer-caret.collapsed{transform:rotate(-90deg);}

  .pe-selected-panel{
    margin-top:16px; background:var(--bg-elev); border:1px solid var(--ember); border-radius:12px; padding:14px 16px;
  }
  .pe-selected-head{display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px;}
  .pe-selected-head span{font-size:13px; font-weight:600; color:var(--text);}
  .pe-selected-actions{display:flex; gap:6px;}
  .pe-sel-fields{margin-top:12px;}
  .pe-textarea{
    width:100%; background:var(--bg); border:1px solid var(--border); border-radius:9px; color:var(--text);
    padding:10px 12px; font-family:var(--font-body); font-size:14px; resize:vertical; margin-bottom:12px;
  }
  .pe-textarea:focus{border-color:var(--ember); outline:none;}

  .pe-export-row{margin-top:20px;}

  @media (max-width:640px){
    .pe-stage{padding:10px; max-height:60vh;}
    .pe-sliders{gap:12px 16px;}
    .pe-slider-field input[type="range"]{width:100px;}
  }

  /* ================= FEATURED TOOLS SPOTLIGHT (featured-tools.php) ================= */
  /* One reusable two-column layout, reused five times: a fake app-window
     illustration on the left, marketing copy on the right. .eyebrow, .btn,
     .btn-primary, .btn-ghost are all reused as-is from elsewhere in this
     file rather than redefined here.
     NOTE: named .ft-spotlight / .ft-spotlight-grid (not .spotlight /
     .spotlight-grid) on purpose -- those names are already taken by the
     Qira/Collage Maker sections on index.php, which use a different
     padding rhythm (44px, no border) and different column proportions.
     They used to collide here (same class, second definition silently
     winning by source order for .spotlight-grid, and leaking an unwanted
     border-bottom into index.php's sections for .spotlight) until this
     was namespaced. This page still reuses the *inner* shared classes on
     purpose -- .spotlight-preview, .spotlight-copy, .spotlight-actions,
     .spotlight-features -- only the two outer wrapper classes needed
     their own name. */
  .ft-spotlight{padding:72px 0; border-bottom:1px solid var(--border);}
  .ft-spotlight:last-of-type{border-bottom:none;}
  .ft-spotlight-grid{
    display:grid; grid-template-columns:minmax(280px, 460px) 1fr; gap:56px; align-items:center;
  }
  @media (max-width:900px){
    .ft-spotlight-grid{grid-template-columns:1fr; gap:36px;}
  }

  /* ---- the fake app window ---- */
  .mock-window{
    background:var(--bg-elev); border:1px solid var(--border); border-radius:var(--radius);
    overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.45);
  }
  .mock-titlebar{
    display:flex; align-items:center; gap:7px; padding:12px 14px; border-bottom:1px solid var(--border);
  }
  .mock-dot{width:9px; height:9px; border-radius:50%; background:var(--border-strong);}
  .mock-dot-active{background:var(--ember);}
  .mock-body{display:flex; min-height:340px;}
  .mock-sidebar{
    display:flex; flex-direction:column; gap:8px; padding:12px 9px; border-right:1px solid var(--border);
    flex-shrink:0;
  }
  .mock-tool-btn{
    width:34px; height:34px; border-radius:9px; display:flex; align-items:center; justify-content:center;
    border:1px solid var(--border); background:var(--bg); color:var(--text-faint);
  }
  .mock-tool-btn svg{width:16px; height:16px;}
  .mock-tool-btn.active{border-color:var(--ember); background:var(--ember-dim); color:var(--ember-hot);}
  .mock-canvas{flex:1; padding:18px; position:relative; display:flex; flex-direction:column; gap:14px;}

  /* ---- photo-editor mock: image area with crop handles + floating adjust panel ---- */
  .mock-photo{
    position:relative; border-radius:10px; overflow:hidden; flex:1;
    background:linear-gradient(135deg, #ff8a3d 0%, #d3452f 55%, #7d1f3a 100%);
  }
  .mock-photo-sun{position:absolute; top:22px; right:26px; width:34px; height:34px; border-radius:50%; background:var(--ember-hot); opacity:.92;}
  .mock-photo-mtn{position:absolute; left:0; right:0; bottom:0; height:55%;
    background:linear-gradient(180deg, transparent 0%, rgba(0,0,0,.38) 100%);
    clip-path:polygon(0% 100%, 18% 30%, 34% 62%, 52% 18%, 70% 58%, 100% 40%, 100% 100%);
  }
  .mock-crop-box{
    position:absolute; top:16%; left:12%; width:56%; height:52%;
    border:2px dashed var(--ember-hot); border-radius:2px;
  }
  .mock-crop-handle{position:absolute; width:9px; height:9px; border-radius:50%; background:var(--ember-hot); border:2px solid #1c0f06;}
  .mock-crop-handle.tl{top:-5px; left:-5px;} .mock-crop-handle.tr{top:-5px; right:-5px;}
  .mock-crop-handle.bl{bottom:-5px; left:-5px;} .mock-crop-handle.br{bottom:-5px; right:-5px;}
  .mock-adjust-panel{
    position:absolute; right:10px; bottom:10px; width:44%; background:rgba(21,19,24,.94);
    border:1px solid var(--border-strong); border-radius:9px; padding:10px 12px;
    backdrop-filter:blur(2px);
  }
  .mock-adjust-title{font-family:var(--font-mono); font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--text-faint); margin-bottom:9px;}
  .mock-slider-track{position:relative; height:2px; background:var(--border-strong); border-radius:2px; margin-bottom:11px;}
  .mock-slider-track:last-child{margin-bottom:0;}
  .mock-slider-dot{position:absolute; top:50%; width:9px; height:9px; border-radius:50%; background:var(--ember-hot); transform:translate(-50%,-50%);}
  .mock-text-row{display:flex; align-items:center; gap:10px; padding:10px 12px; background:var(--bg); border:1px solid var(--border); border-radius:9px;}
  .mock-text-aa{font-family:Georgia,serif; font-style:italic; font-size:17px; color:var(--text-dim); flex-shrink:0;}
  .mock-text-bars{display:flex; flex-direction:column; gap:6px; flex:1;}
  .mock-text-bar{height:5px; border-radius:3px; background:var(--border-strong);}

  /* ---- Qira home-spotlight demo: the photo-editor mock above, but wired
     up live (assets/app.js, the "QIRA HOME SPOTLIGHT" block) -- the crop
     handles and adjust sliders actually drag, the sidebar buttons actually
     switch mode, and the adjust sliders actually change the mock photo's
     CSS filter in real time. Everything here is index.php-only (gated in
     JS on #qira-mock existing) and layered on top of the shared .mock-*
     rules above, which every other page's inert copy of this markup keeps
     using unmodified.
     Structural (mode show/hide, cursor affordances, button reset) lives
     here, unconditionally, so a reduced-motion visitor still sees the
     right end state -- only the hover/drag *transitions* and the ambient
     idle animation are gated behind prefers-reduced-motion further down. */
  .qira-mock .mock-tool-btn{font:inherit; cursor:pointer; appearance:none;}
  .qira-mock .mock-slider-dot{cursor:grab;}
  .qira-mock .mock-slider-dot.is-dragging{cursor:grabbing;}
  .qira-mock .mock-crop-handle{cursor:nwse-resize;}
  .qira-mock .mock-crop-handle.tr, .qira-mock .mock-crop-handle.bl{cursor:nesw-resize;}
  /* The crop box's bottom-right corner sits right where the adjust panel
     overlaps it (both are position:absolute inside .mock-photo) -- without
     an explicit stacking order the panel, being later in the DOM, wins and
     the "br" handle becomes ungrabbable underneath it. Handles need to
     win over both the box's own dashed outline and the panel. */
  .qira-mock .mock-crop-box{z-index:2;}
  .qira-mock .mock-crop-handle{z-index:3;}
  .qira-mock .mock-adjust-panel{z-index:1;}
  .qira-mock #qira-photo{filter:var(--qira-photo-filter, none);}
  /* tool-mode variants, toggled by app.js setting data-qira-mode on .qira-mock */
  .qira-mock[data-qira-mode="shape"] .mock-crop-box{border-style:solid; border-radius:8px;}
  .qira-mock[data-qira-mode="shape"] .mock-crop-box .mock-crop-handle{display:none;}
  .qira-mock[data-qira-mode="shape"] .mock-adjust-panel{opacity:0; pointer-events:none;}
  .qira-mock[data-qira-mode="move"] .mock-crop-box{opacity:0; pointer-events:none;}
  .qira-mock[data-qira-mode="move"] .mock-adjust-panel{opacity:0; pointer-events:none;}
  .qira-mock[data-qira-mode="move"] #qira-photo{cursor:grab;}
  .qira-mock[data-qira-mode="text"] #qira-text-row{border-color:var(--ember); box-shadow:0 0 0 1px var(--ember), 0 0 20px rgba(255,90,31,.25);}

  /* ---- collage-maker mock: 2x2 grid + preset pills ---- */
  .mock-collage-grid{
    display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; gap:6px; flex:1;
  }
  .mock-collage-cell{border-radius:8px; position:relative; overflow:hidden;}
  .mock-collage-cell.c1{background:linear-gradient(135deg, #ffb347, #ff8a3d);}
  .mock-collage-cell.c2{background:linear-gradient(135deg, #e14b4b, #7d1f3a);}
  .mock-collage-cell.c3{background:linear-gradient(135deg, #6b2545, #3a1730);}
  .mock-collage-cell.c4{background:linear-gradient(135deg, #ffcf6b, #ff8a3d);}
  .mock-collage-cell.c1::after{content:''; position:absolute; top:10px; right:12px; width:16px; height:16px; border-radius:50%; background:var(--ember-hot); opacity:.9;}
  .mock-collage-cell.c3::after{
    content:''; position:absolute; left:0; right:0; bottom:0; height:60%;
    background:rgba(0,0,0,.34); clip-path:polygon(0% 100%, 20% 35%, 40% 68%, 58% 20%, 100% 55%, 100% 100%);
  }
  .mock-pill-row{display:flex; gap:8px; flex-wrap:wrap;}
  .mock-pill{
    padding:6px 13px; border-radius:999px; border:1px solid var(--border); font-family:var(--font-mono);
    font-size:10.5px; letter-spacing:.06em; text-transform:uppercase; color:var(--text-faint);
  }
  .mock-pill.active{border-color:var(--ember); color:var(--ember-hot); background:var(--ember-dim);}

  /* ---- Collage Maker home-spotlight demo: the mock above, wired up live
     (assets/app.js, the "COLLAGE HOME SPOTLIGHT" block) -- the three
     sidebar layout buttons actually reflow the grid (and auto-cycle on
     their own), and clicking any cell shuffles which photo sits where (a
     stand-in for the real tool's reorder). The size/format pills below
     the grid stay plain, static <span>s -- same markup as every other
     page's inert copy of this mock, not a click target here. index.php-
     only, same unconditional-structure / motion-gated-flourish split as
     the Qira demo above. .mock-tool-btn is a <button> here (a <div> on
     every other page's copy), so it needs the browser-default reset. */
  .collage-mock .mock-tool-btn{font:inherit; cursor:pointer; appearance:none;}
  .collage-mock .mock-collage-cell{cursor:pointer;}
  /* layout variants, toggled by app.js setting data-layout on #collage-grid
     -- "grid" (the base 2x2 rule above) is the default/reset state. */
  .mock-collage-grid[data-layout="big-left"]{grid-template-columns:1.5fr 1fr; grid-template-rows:1fr 1fr;}
  .mock-collage-grid[data-layout="big-left"] .mock-collage-cell.c1{grid-column:1; grid-row:1 / 3;}
  .mock-collage-grid[data-layout="big-left"] .mock-collage-cell.c2{grid-column:2; grid-row:1;}
  .mock-collage-grid[data-layout="big-left"] .mock-collage-cell.c3{grid-column:2; grid-row:2;}
  .mock-collage-grid[data-layout="big-left"] .mock-collage-cell.c4{display:none;}
  .mock-collage-grid[data-layout="big-right"]{grid-template-columns:1fr 1.5fr; grid-template-rows:1fr 1fr;}
  .mock-collage-grid[data-layout="big-right"] .mock-collage-cell.c1{grid-column:1; grid-row:1 / 3;}
  .mock-collage-grid[data-layout="big-right"] .mock-collage-cell.c2{grid-column:2; grid-row:1;}
  .mock-collage-grid[data-layout="big-right"] .mock-collage-cell.c3{grid-column:2; grid-row:2;}
  .mock-collage-grid[data-layout="big-right"] .mock-collage-cell.c4{display:none;}

  /* ---- flowchart-maker mock: a small branded flow diagram ---- */
  .mock-flow{flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0;}
  .mock-flow-node{
    font-family:var(--font-body); font-size:11.5px; font-weight:600; text-align:center;
    padding:8px 14px; z-index:1;
  }
  .mock-flow-node.terminal{background:var(--ember); color:#1c0f06; border-radius:999px;}
  .mock-flow-node.process{background:var(--bg-elev-2); color:var(--text); border:1px solid var(--border-strong); border-radius:8px;}
  .mock-flow-node.decision{
    background:var(--ember-hot); color:#1c0f06; width:112px; height:56px; padding:0;
    display:flex; align-items:center; justify-content:center; transform:rotate(45deg); border-radius:6px;
  }
  .mock-flow-node.decision span{transform:rotate(-45deg); font-size:10.5px; line-height:1.15;}
  .mock-flow-arrow{width:1.5px; height:16px; background:var(--border-strong); position:relative;}
  .mock-flow-branches{display:flex; align-items:flex-start; gap:26px; margin:6px 0;}
  .mock-flow-branch{display:flex; flex-direction:column; align-items:center;}
  .mock-flow-branch-label{
    font-family:var(--font-mono); font-size:9px; letter-spacing:.06em; text-transform:uppercase;
    color:var(--text-faint); margin-bottom:4px;
  }
  .mock-flow-merge{width:120px; height:14px; border-left:1.5px solid var(--border-strong); border-right:1.5px solid var(--border-strong); border-top:none; margin-top:-2px;}

  /* ---- copy column ---- */
  .spotlight-h2{
    font-family:var(--font-hero); font-weight:400; letter-spacing:-.01em;
    font-size:clamp(26px, 3.4vw, 38px); line-height:1.1; margin-bottom:16px;
  }
  .spotlight-desc{color:var(--text-dim); font-size:15.5px; line-height:1.65; max-width:540px; margin-bottom:26px;}
  .spotlight-checklist{display:flex; flex-direction:column; gap:14px; margin-bottom:32px;}
  .spotlight-checklist li{display:flex; align-items:flex-start; gap:12px; font-size:14.5px; color:var(--text-dim); line-height:1.5;}
  .spotlight-check{
    flex-shrink:0; width:22px; height:22px; border-radius:50%; background:var(--ember-dim); color:var(--ember-hot);
    display:flex; align-items:center; justify-content:center; margin-top:1px;
  }
  .spotlight-check svg{width:12px; height:12px;}

  /* ---- chart-maker mock: simple bar chart ---- */
  .mock-chart-title{
    font-family:var(--font-mono); font-size:10px; letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-faint); text-align:center; margin-bottom:4px;
  }
  .mock-bars{flex:1; display:flex; align-items:flex-end; gap:14px; padding:0 6px 4px;}
  .mock-bar{
    flex:1; border-radius:6px 6px 2px 2px;
    background:linear-gradient(180deg, var(--ember-hot), var(--ember));
  }

  /* ---- qr-code-generator mock: a decorative (non-scannable) QR pattern ---- */
  .mock-qr-wrap{flex:1; display:flex; align-items:center; justify-content:center;}
  .mock-qr{
    position:relative; width:180px; height:180px; background:#fff; border-radius:8px;
    display:grid; grid-template-columns:repeat(11, 1fr); grid-template-rows:repeat(11, 1fr);
    padding:12px; gap:2px;
  }
  .mock-qr-cell{background:transparent; border-radius:1px;}
  .mock-qr-cell.on{background:#1c0f06;}
  .mock-qr-finder{
    position:absolute; width:38px; height:38px; border:6px solid #1c0f06; border-radius:4px;
    display:flex; align-items:center; justify-content:center;
  }
  .mock-qr-finder::after{content:''; width:14px; height:14px; background:#1c0f06; border-radius:1px;}
  .mock-qr-finder.tl{top:12px; left:12px;}
  .mock-qr-finder.tr{top:12px; right:12px;}

  /* ---- video-editor mock: a preview frame + a mini multi-lane timeline ---- */
  .mock-video-preview{
    position:relative; flex:1; border-radius:10px; overflow:hidden;
    background:linear-gradient(135deg, #2a2440 0%, #1c1730 55%, #120e20 100%);
    display:flex; align-items:center; justify-content:center;
  }
  .mock-video-play{
    width:40px; height:40px; border-radius:50%; background:rgba(255,255,255,.14);
    border:1.5px solid rgba(255,255,255,.55); display:flex; align-items:center; justify-content:center;
    backdrop-filter:blur(2px);
  }
  .mock-video-play svg{width:15px; height:15px; margin-left:2px;}
  .mock-video-timeline{
    position:relative; background:var(--bg); border:1px solid var(--border); border-radius:9px;
    padding:10px 12px; display:flex; flex-direction:column; gap:6px;
  }
  .mock-video-track{display:flex; gap:3px; height:16px;}
  .mock-video-track-caption{height:10px;}
  .mock-video-clip{height:100%; border-radius:4px; flex-shrink:0;}
  .mock-video-clip.c1{background:linear-gradient(135deg, #ff8a3d, #d3452f);}
  .mock-video-clip.c2{background:linear-gradient(135deg, #4fb8e0, #2d6f8f);}
  .mock-video-clip.c3{background:linear-gradient(135deg, #ffcf6b, #ff8a3d);}
  .mock-video-clip.cap{background:var(--border-strong); border-radius:3px;}
  .mock-video-playhead{
    position:absolute; top:4px; bottom:4px; width:2px; background:var(--ember-hot);
    border-radius:1px; box-shadow:0 0 6px rgba(255,176,32,.7);
  }
  .mock-video-playhead::before{
    content:''; position:absolute; top:-4px; left:50%; transform:translateX(-50%);
    width:7px; height:7px; border-radius:50%; background:var(--ember-hot);
  }

  /* ---------- /guides/ section ---------- */
  /* Guide cards on guides.php reuse .tool-card's exact look (border, hover,
     h3/p sizing already defined above) rather than a parallel set of rules
     -- same visual language as a tool card, just linking to an article
     instead of a tool, so this only adds what's actually different: a
     small category badge in place of an icon. */
  .guide-card-badge{
    font-family:var(--font-mono); font-size:10.5px; letter-spacing:.06em; text-transform:uppercase;
    color:var(--ember-hot); background:var(--ember-dim); border-radius:6px; padding:4px 9px;
    align-self:flex-start;
  }
  /* Long-form article body (guides/*.php via includes/guide-page.php).
     Plain semantic h2/p/ul in each guide's data/guides.php entry, styled
     once here -- the site's four original static pages (about/privacy/
     terms/contact) each restate this same color/line-height/font-size as
     inline styles per paragraph; worth a real class this time given how
     much more prose this section adds. */
  .guide-prose-wrap{max-width:760px;}
  .guide-prose{color:var(--text-dim); line-height:1.75; font-size:15.5px;}
  .guide-prose > p{margin-top:18px;}
  .guide-prose > p:first-child{margin-top:0;}
  .guide-prose h2{
    font-family:var(--font-display); font-size:20px; font-weight:700; color:var(--text);
    margin-top:36px; margin-bottom:4px;
  }
  .guide-prose ul{margin:14px 0 0 22px;}
  .guide-prose li{margin-bottom:8px;}
  .guide-prose strong{color:var(--text);}
  .guide-prose a{color:var(--ember-hot); text-decoration:underline; text-underline-offset:3px;}
  .guide-prose a:hover{color:var(--ember);}
  /* Quick-reference decision tables in comparison-style guides (e.g.
     "PDF to Word vs. PowerPoint vs. Excel") -- added so a scannable
     decision table has somewhere to actually look right, instead of the
     browser's unstyled default table rendering. Kept simple (no zebra
     striping, no hover states) to match the guide prose's own restrained
     style rather than looking like a dashboard widget dropped into an
     article. */
  .guide-prose table{
    width:100%; margin-top:18px; border-collapse:collapse;
    font-size:14.5px; background:var(--bg-elev); border:1px solid var(--border); border-radius:10px;
    overflow:hidden;
  }
  .guide-prose th, .guide-prose td{
    text-align:left; padding:12px 14px; border-bottom:1px solid var(--border);
    vertical-align:top;
  }
  .guide-prose thead th{
    font-family:var(--font-display); font-size:12.5px; letter-spacing:.03em; text-transform:uppercase;
    color:var(--text-faint); background:var(--bg-elev-2);
  }
  .guide-prose tbody td:first-child{color:var(--text); font-weight:600;}
  .guide-prose tbody tr:last-child td{border-bottom:none;}
  .mock-qr-finder.bl{bottom:12px; left:12px;}

  /* ================= Stream Tester + Playlist Checker ================= */
  /* Shared bits between the two: a section heading row with an optional
     right-aligned badge (LIVE, in Stream Tester's case), and small-print
     helper text -- neither existed elsewhere in this file before these
     two tools, since no earlier tool needed either. */
  .panel-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;}
  .panel-head h2{font-family:var(--font-display); font-size:16px; font-weight:700; color:var(--text);}
  .pc-small-note, .st-mute-note{font-size:12.5px; color:var(--text-faint); margin-top:8px;}

  /* ---------- Stream Tester ---------- */
  .st-input-panel{margin-bottom:20px;}
  .st-url-row{display:flex; gap:10px; margin-top:14px;}
  .st-url-row input{
    flex-grow:1; background:var(--bg-elev); border:1px solid var(--border-strong); border-radius:10px;
    padding:11px 14px; color:var(--text); font-family:var(--font-mono); font-size:13.5px;
  }
  .st-url-row input:focus{outline:none; border-color:var(--ember);}
  .st-layout{display:grid; grid-template-columns:1fr 320px; gap:28px; align-items:start;}
  @media (max-width:860px){ .st-layout{grid-template-columns:1fr;} }
  .st-video-shell{
    position:relative; background:#000; border-radius:var(--radius); border:1px solid var(--border);
    overflow:hidden; aspect-ratio:16/9; display:flex; align-items:center; justify-content:center;
  }
  .st-video-shell video{width:100%; height:100%; object-fit:contain; background:#000;}
  .st-video-placeholder{
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center; text-align:center;
    padding:24px; color:var(--text-faint); font-size:13.5px; pointer-events:none;
  }
  .st-info-col{background:var(--bg-elev); border:1px solid var(--border); border-radius:var(--radius); padding:20px;}
  .st-live-badge{
    font-family:var(--font-mono); font-size:11px; font-weight:700; letter-spacing:.06em; color:#160c05;
    background:var(--err); border-radius:999px; padding:3px 10px;
  }
  .st-info-row{padding:12px 0; border-top:1px solid var(--border);}
  .st-info-row:first-of-type{border-top:none; padding-top:0;}
  .st-info-label{font-size:13px; font-weight:600; color:var(--text);}
  .st-info-sub{font-size:11.5px; color:var(--text-faint); margin-top:1px;}
  .st-info-value{
    display:inline-block; margin-top:7px; font-family:var(--font-mono); font-size:12.5px; color:var(--ember-hot);
    background:var(--bg-elev-2); border:1px solid var(--border); border-radius:7px; padding:3px 9px;
  }

  /* ---------- Playlist Checker ---------- */
  .pc-field-label{display:block; font-size:13px; color:var(--text-dim); margin-bottom:8px;}
  .pc-tab-panel{margin-top:16px;}
  .pc-textarea, .pc-text-input{
    width:100%; background:var(--bg-elev); border:1px solid var(--border-strong); border-radius:10px;
    padding:11px 14px; color:var(--text); font-family:var(--font-mono); font-size:13px;
  }
  .pc-textarea:focus, .pc-text-input:focus{outline:none; border-color:var(--ember);}
  .pc-textarea{resize:vertical; min-height:120px;}
  .pc-dropzone-filename{margin-top:6px; font-size:12.5px; color:var(--ember-hot); font-family:var(--font-mono); min-height:1em;}
  .pc-params{display:flex; align-items:flex-end; gap:16px; margin-top:20px; flex-wrap:wrap;}
  .pc-params > div{width:120px;}
  .pc-submit{margin-left:auto;}
  @media (max-width:640px){ .pc-submit{margin-left:0; width:100%;} .pc-submit .btn{width:100%; justify-content:center;} }

  .pc-cards{display:grid; grid-template-columns:repeat(auto-fit, minmax(110px,1fr)); gap:12px; margin-top:28px;}
  .pc-card{background:var(--bg-elev); border:1px solid var(--border); border-radius:12px; padding:14px 16px;}
  .pc-card-num{font-family:var(--font-display); font-size:24px; font-weight:700; color:var(--text);}
  .pc-card-lbl{font-size:11.5px; color:var(--text-faint); margin-top:3px; text-transform:uppercase; letter-spacing:.06em;}
  .pc-card-online .pc-card-num{color:var(--ok);}
  .pc-card-reachable .pc-card-num{color:#4fb8e0;}
  .pc-card-timeout .pc-card-num{color:var(--ember-hot);}
  .pc-card-offline .pc-card-num{color:var(--err);}

  .pc-toolbar{display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; margin:20px 0 14px;}
  .pc-toolbar-actions{display:flex; gap:10px;}

  .pc-list{display:flex; flex-direction:column; gap:8px; max-height:640px; overflow-y:auto;}
  .pc-empty{text-align:center; color:var(--text-faint); font-size:13.5px; padding:30px 0;}
  .pc-item{
    display:flex; align-items:center; gap:12px; background:var(--bg-elev); border:1px solid var(--border);
    border-radius:10px; padding:10px 14px;
  }
  .pc-idx{font-family:var(--font-mono); font-size:11.5px; color:var(--text-faint); width:22px; flex-shrink:0;}
  .pc-status-pill{
    flex-shrink:0; font-family:var(--font-mono); font-size:11px; font-weight:700; letter-spacing:.03em;
    border-radius:999px; padding:4px 10px; white-space:nowrap;
  }
  .pc-status-online{background:rgba(85,214,138,.13); color:var(--ok);}
  .pc-status-reachable{background:rgba(79,184,224,.13); color:#4fb8e0;}
  .pc-status-timeout{background:var(--ember-dim); color:var(--ember-hot);}
  .pc-status-offline{background:rgba(255,107,107,.13); color:var(--err);}
  .pc-row-main{flex-grow:1; min-width:0;}
  .pc-row-name{font-size:13.5px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .pc-row-url{font-size:11.5px; font-family:var(--font-mono); color:var(--text-faint); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:2px;}
  .pc-time{flex-shrink:0; font-family:var(--font-mono); font-size:11.5px; color:var(--text-faint); width:56px; text-align:right;}
  .pc-icons{display:flex; gap:4px; flex-shrink:0;}
  .pc-icon-btn{
    width:28px; height:28px; border-radius:7px; border:1px solid var(--border); background:var(--bg-elev-2);
    color:var(--text-dim); cursor:pointer; font-size:13px; display:flex; align-items:center; justify-content:center;
  }
  .pc-icon-btn:hover{border-color:var(--ember); color:var(--ember-hot);}
  .pc-icon-btn:disabled{opacity:.4; cursor:not-allowed;}

  .pc-modal-backdrop{
    display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:80;
    align-items:center; justify-content:center; padding:24px;
  }
  .pc-modal-backdrop.open{display:flex;}
  .pc-modal{
    background:var(--bg-elev); border:1px solid var(--border-strong); border-radius:var(--radius);
    padding:20px 22px; max-width:640px; width:100%; max-height:80vh; overflow-y:auto;
  }
  .pc-builder-textarea{min-height:260px; font-size:12.5px;}
  .pc-modal-actions{display:flex; justify-content:flex-end; gap:10px; margin-top:14px;}

  /* ================= Blur Image Background + Pixelate Image ================= */
  /* Shared two-column layout (canvas preview + controls) for both tools --
     "bib" prefix is reused by Pixelate Image's panel too rather than
     duplicating an identical layout under a second name. */
  .bib-layout{display:flex; gap:26px; flex-wrap:wrap; align-items:flex-start; margin-top:16px;}
  .bib-canvas-wrap{position:relative; width:100%; max-width:520px; touch-action:none;}
  .bib-canvas-wrap canvas{
    display:block; width:100%; height:auto; max-height:70vh; object-fit:contain;
    border-radius:8px; border:1px solid var(--border-strong); background:var(--bg-elev-2);
    box-shadow:0 10px 40px rgba(0,0,0,.5); cursor:crosshair;
  }
  .bib-controls{flex:1; min-width:260px;}
  .bib-size-row{display:flex; align-items:center; gap:14px; flex-wrap:wrap;}
  .bib-sub-row{margin-top:10px;}
  .pix-canvas-hint{font-size:12px; color:var(--text-faint); margin-top:8px; max-width:520px;}
  .pix-region-count{font-size:12.5px; color:var(--text-dim); margin-top:10px;}

  /* Pixelate Image: same canvas-as-upload-target treatment as Blur Image
     Background -- the canvas itself is the click/drag dropzone, styled
     with a checkerboard "transparent" placeholder before an image loads,
     rather than a separate dropzone box that then reveals a canvas. Keeps
     the shared .bib-canvas-wrap's own aspect-following sizing (height:auto)
     instead of a fixed-height frame, since Pixelate's drag-to-mark math
     (toImageCoords()) maps screen position straight off the canvas's own
     box with no letterboxing to account for -- but stretched to the full
     available width (not the shared class's 520px cap), stacked above and
     below its own controls rather than the shared two-column .bib-layout. */
  #pix-config-top{width:100%; margin-bottom:22px;}
  #pix-config-bottom{width:100%; margin-top:22px;}
  #pix-canvas-wrap{
    width:100%; max-width:100%; cursor:pointer; border-radius:8px; transition:box-shadow .15s ease;
  }
  #pix-canvas-wrap canvas{
    cursor:pointer;
    background-color:#2b2b30;
    background-image:
      linear-gradient(45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(-45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(45deg, transparent 75%, rgba(255,255,255,.08) 75%),
      linear-gradient(-45deg, transparent 75%, rgba(255,255,255,.08) 75%);
    background-size:24px 24px;
    background-position:0 0, 0 12px, 12px -12px, -12px 0;
    min-height:240px;
  }
  #pix-canvas-wrap.dragover canvas{border-color:var(--ember); box-shadow:0 0 0 2px var(--ember-dim), 0 10px 40px rgba(0,0,0,.5);}
  #pix-canvas-wrap #pix-info{margin-top:8px;}

  /* Blur Image Background: controls sit above a full-width canvas frame
     (per the tool's own layout, not the shared two-column .bib-layout used
     by Pixelate Image) -- the canvas itself is the upload target, styled
     with a checkerboard "transparent" placeholder before an image loads.
     The frame stays a constant max-width box regardless of the loaded
     photo's own dimensions: canvas uses object-fit:contain inside a fixed
     height range instead of the box reshaping to match every photo's
     aspect ratio. */
  #bib-config-top{width:100%; margin-bottom:22px;}
  #bib-config-bottom{width:100%; margin-top:22px;}
  .bib-row-pair{display:flex; gap:24px; flex-wrap:wrap; margin-top:14px;}
  .bib-row-col{flex:1; min-width:240px;}
  .bib-row-col .range-hint{margin-top:0;}
  /* Fill/blur-strength sub-sections stack inside each column independently
     -- this is what makes the right column's "Blur strength" start right
     after "Or set an exact size" rather than waiting for the left column's
     (often taller, wrapping) ratio-button row to finish first. */
  .bib-fill-block{margin-top:14px;}
  #bib-canvas-wrap{
    position:relative; width:100%; max-width:100%; touch-action:none;
    height:min(560px, 62vh); min-height:320px;
    border-radius:8px; cursor:pointer; overflow:hidden; transition:box-shadow .15s ease;
  }
  #bib-canvas-wrap canvas{
    display:block; width:100%; height:100%; object-fit:contain;
    border-radius:8px; border:1px solid var(--border-strong);
    box-shadow:0 10px 40px rgba(0,0,0,.5); cursor:pointer;
    background-color:#2b2b30;
    background-image:
      linear-gradient(45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(-45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(45deg, transparent 75%, rgba(255,255,255,.08) 75%),
      linear-gradient(-45deg, transparent 75%, rgba(255,255,255,.08) 75%);
    background-size:24px 24px;
    background-position:0 0, 0 12px, 12px -12px, -12px 0;
  }
  #bib-canvas-wrap.dragover canvas{border-color:var(--ember); box-shadow:0 0 0 2px var(--ember-dim), 0 10px 40px rgba(0,0,0,.5);}
  .bib-canvas-empty{
    position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
    text-align:center; padding:24px; pointer-events:none;
  }
  .bib-canvas-empty svg{width:30px; height:30px; margin:0 auto 14px; color:var(--ember-hot);}
  .bib-canvas-empty p{font-size:14px; color:var(--text-dim);}
  #bib-canvas-wrap #bib-info{position:absolute; left:10px; bottom:8px; margin:0; font-size:11.5px; background:rgba(9,9,11,.65); padding:3px 8px; border-radius:6px; pointer-events:none;}

  /* Crop-mode selection box: an HTML overlay on top of the canvas (not
     drawn into the canvas bitmap), so it can be dragged/resized with plain
     DOM pointer events independent of the image render pipeline. The huge
     box-shadow dims everything outside the selection, standard crop-tool
     styling. */
  .bib-marquee{
    position:absolute; border:2px solid var(--ember-hot); cursor:move;
    box-shadow:0 0 0 9999px rgba(0,0,0,.5); touch-action:none;
  }
  .bib-marquee-handle{
    position:absolute; width:14px; height:14px; background:var(--ember-hot);
    border:2px solid #111; border-radius:3px; touch-action:none;
  }
  .bib-marquee-handle[data-handle="nw"]{left:-8px; top:-8px; cursor:nwse-resize;}
  .bib-marquee-handle[data-handle="ne"]{right:-8px; top:-8px; cursor:nesw-resize;}
  .bib-marquee-handle[data-handle="sw"]{left:-8px; bottom:-8px; cursor:nesw-resize;}
  .bib-marquee-handle[data-handle="se"]{right:-8px; bottom:-8px; cursor:nwse-resize;}
  .bib-canvas-empty .dz-strong{color:var(--text); font-weight:600;}
  .bib-canvas-empty .dz-hint{margin-top:6px; font-size:12px; color:var(--text-faint); font-family:var(--font-mono);}
  #bib-canvas-wrap #bib-info{margin-top:10px;}

  /* Background Remover: same full-width canvas-as-upload-target treatment
     as Pixelate Image (checkerboard placeholder, no fixed-height frame,
     since its brush math maps screen position straight off the canvas's
     own box the same way Pixelate's toImageCoords() does) -- reuses
     #pix-canvas-wrap's exact rules under a new id rather than duplicating
     the checkerboard gradient a third time under a shared class, since
     "background remover on a checkerboard canvas" is also the one case on
     this site where that pattern is literally showing the real transparent
     result, not just an empty-state placeholder. */
  #bgr-config-top{width:100%; margin-bottom:22px;}
  #bgr-config-bottom{width:100%; margin-top:22px;}
  #bgr-canvas-wrap{
    width:100%; max-width:100%; cursor:pointer; border-radius:8px; transition:box-shadow .15s ease;
  }
  #bgr-canvas-wrap canvas{
    cursor:pointer;
    background-color:#2b2b30;
    background-image:
      linear-gradient(45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(-45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(45deg, transparent 75%, rgba(255,255,255,.08) 75%),
      linear-gradient(-45deg, transparent 75%, rgba(255,255,255,.08) 75%);
    background-size:24px 24px;
    background-position:0 0, 0 12px, 12px -12px, -12px 0;
    min-height:240px;
  }
  #bgr-canvas-wrap.dragover canvas{border-color:var(--ember); box-shadow:0 0 0 2px var(--ember-dim), 0 10px 40px rgba(0,0,0,.5);}
  #bgr-canvas-wrap #bgr-info{margin-top:8px;}

  /* Cartoonize Photo: same full-width canvas-as-upload-target treatment as
     Pixelate Image and Background Remover -- this tool's #ctn-canvas-wrap
     was left on the shared .bib-canvas-wrap default (a 520px cap meant for
     the two-column Blur Image Background layout) when the tool first
     shipped, which is why its canvas rendered narrow instead of stretching
     to the panel's full width like its two closest siblings. */
  #ctn-config-top{width:100%; margin-bottom:22px;}
  #ctn-config-bottom{width:100%; margin-top:22px;}
  #ctn-canvas-wrap{
    width:100%; max-width:100%; cursor:pointer; border-radius:8px; transition:box-shadow .15s ease;
  }
  #ctn-canvas-wrap canvas{
    cursor:pointer;
    background-color:#2b2b30;
    background-image:
      linear-gradient(45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(-45deg, rgba(255,255,255,.08) 25%, transparent 25%),
      linear-gradient(45deg, transparent 75%, rgba(255,255,255,.08) 75%),
      linear-gradient(-45deg, transparent 75%, rgba(255,255,255,.08) 75%);
    background-size:24px 24px;
    background-position:0 0, 0 12px, 12px -12px, -12px 0;
    min-height:240px;
  }
  #ctn-canvas-wrap.dragover canvas{border-color:var(--ember); box-shadow:0 0 0 2px var(--ember-dim), 0 10px 40px rgba(0,0,0,.5);}
  #ctn-canvas-wrap #ctn-info{margin-top:8px;}

  /* Passport Photo Maker: same dragover highlight treatment as the other
     canvas-drop targets above, scoped to its own wrap ID. */
  #pp-canvas-wrap.dragover canvas{border-color:var(--ember); box-shadow:0 0 0 2px var(--ember-dim), 0 10px 40px rgba(0,0,0,.5);}

  /* ================= GEN-Z POLISH: MICRO-INTERACTIONS ================= */
  /* Purely additive layer on top of the rules above -- hover glows, lift,
     a corner "shine sweep" on cards, shimmering accent text, and press
     feedback for the homepage and other core landing pages. .hero,
     .tool-card, .btn, .tab, .spotlight-preview/.mock-window, and
     .faq-item are all shared classes (see the "hero" comment near the
     top of this file: 17 pages use that one layout), so this one block
     reaches every landing page's hero, tool grid, and buttons at once
     rather than needing a repeat per page.
     Built entirely from the existing --ember/--ember-hot palette -- no
     new brand colors introduced -- and gated behind
     prefers-reduced-motion so anyone whose OS asks for less motion still
     gets the full site, minus the animation. Everything here is additive
     (new properties, or a later, more specific override of an existing
     one) rather than a rewrite of any rule above, so nothing above this
     comment needed to change. */
  @media (prefers-reduced-motion: no-preference) {

    /* ---- buttons: shimmer sweep + glow + squashy press feedback ---- */
    .btn-primary{
      background:linear-gradient(120deg, var(--ember), var(--ember-hot), var(--ember));
      background-size:220% 100%; background-position:0% 0%;
      box-shadow:0 0 0 rgba(255,90,31,0);
      transition:transform .25s cubic-bezier(.34,1.56,.64,1), background-position .6s ease, box-shadow .3s ease;
    }
    .btn-primary:hover{
      background-position:100% 0%; transform:translateY(-3px) scale(1.03);
      box-shadow:0 10px 28px rgba(255,90,31,.4), 0 0 20px rgba(255,176,32,.25);
    }
    .btn-primary:active{transform:translateY(-1px) scale(.97); box-shadow:0 4px 14px rgba(255,90,31,.3);}
    .btn-ghost{transition:transform .25s cubic-bezier(.34,1.56,.64,1), border-color .2s ease, background .2s ease, box-shadow .3s ease;}
    .btn-ghost:hover{transform:translateY(-3px); box-shadow:0 8px 20px rgba(255,90,31,.16);}
    .btn-ghost:active{transform:translateY(-1px) scale(.97);}

    /* ---- tool cards: glowing border, diagonal shine sweep, icon pop ----
       The shine is a ::before with a diagonal light-colored band, parked
       off-screen (background-position 140% 140% on an oversized
       280%-of-box background) and swept across to -40% -40% on hover.
       .tool-card gets explicit position:relative (harmless for a flex
       item -- doesn't change its flex sizing) so the ::before's inset:0
       lines up with the card itself rather than its wrapper; its direct
       children get z-index:1 against the ::before's z-index:0 so the
       shine always renders BEHIND the icon/title/description text,
       regardless of the ambient stacking context. */
    .tool-card{
      position:relative; overflow:hidden; box-shadow:0 0 0 rgba(255,90,31,0);
      transition:border-color .3s ease, transform .3s cubic-bezier(.34,1.56,.64,1), background .3s ease, box-shadow .35s ease;
    }
    .tool-card::before{
      content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
      background:linear-gradient(115deg, transparent 35%, rgba(255,176,32,.16) 47%, rgba(255,90,31,.22) 50%, rgba(255,176,32,.16) 53%, transparent 65%);
      background-size:280% 280%; background-position:140% 140%;
      opacity:0; transition:opacity .3s ease, background-position .85s ease;
    }
    .tool-card:hover::before{opacity:1; background-position:-40% -40%;}
    /* "Coming soon" cards are deliberately inert (opacity:.55, no lift,
       no border-color change -- see the pre-existing .is-soon rules
       above) -- the shine sweep would otherwise still fire on hover and
       read as "this is clickable", so it's turned back off here. */
    .tool-card.is-soon:hover::before{opacity:0;}
    .tool-card > *{position:relative; z-index:1;}
    .tool-card:hover{
      transform:translateY(-6px) scale(1.02);
      box-shadow:0 18px 38px rgba(0,0,0,.4), 0 0 0 1px rgba(255,90,31,.3), 0 0 26px rgba(255,90,31,.2);
    }
    .tool-card.is-soon:hover{box-shadow:none;}
    .tool-icon{transition:transform .35s cubic-bezier(.34,1.56,.64,1), box-shadow .3s ease;}
    .tool-card:hover:not(.is-soon) .tool-icon{transform:scale(1.14) rotate(-6deg); box-shadow:0 0 18px rgba(255,176,32,.55);}
    .tool-card-action-btn{transition:color .15s ease, background .15s ease, border-color .15s ease, transform .25s cubic-bezier(.34,1.56,.64,1);}
    .tool-card-action-btn:hover{transform:scale(1.15) rotate(-4deg);}

    /* ---- hero: a second ambient drifting orb + shimmering accent text ----
       .hero-glow already existed with a 7s pulse; layering a slow drift
       on top (instead of replacing it) makes it feel alive rather than
       just breathing in place. The new ::after is a second, smaller,
       independently-drifting glow low in the hero -- .hero already has
       overflow:hidden and position:relative from the base rules above,
       and .hero > * {position:relative; z-index:1} already keeps real
       content above z-index:0 layers, so this needed no markup change. */
    .hero-glow{animation:glow-pulse 6s ease-in-out infinite, glow-drift 15s ease-in-out infinite;}
    @keyframes glow-pulse{ 0%,100%{opacity:.75; transform:translateX(-50%) scale(1);} 50%{opacity:1; transform:translateX(-50%) scale(1.08);} }
    @keyframes glow-drift{ 0%,100%{margin-left:0; margin-top:0;} 50%{margin-left:-36px; margin-top:22px;} }
    .hero::after{
      content:''; position:absolute; z-index:0; pointer-events:none;
      bottom:-140px; left:-100px; width:440px; height:440px; border-radius:50%;
      background:radial-gradient(closest-side, rgba(255,176,32,.16), transparent 70%);
      filter:blur(24px);
      animation:orb-drift 12s ease-in-out infinite;
    }
    @keyframes orb-drift{ 0%,100%{transform:translate(0,0) scale(1);} 50%{transform:translate(44px,-28px) scale(1.1);} }
    .hero h1 .accent, .spotlight-eyebrow .accent, .logo-text .accent{
      background:linear-gradient(90deg, var(--ember), var(--ember-hot), var(--ember));
      background-size:220% auto; background-position:0% center;
      -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
      animation:accent-shimmer 5s ease-in-out infinite;
    }
    @keyframes accent-shimmer{ 0%,100%{background-position:0% center;} 50%{background-position:100% center;} }

    /* ---- nav "AI" link: shimmering gradient text + a twinkling sparkle
       (see the always-on structural rules for this pair near .nav-links
       below -- sizing/color/gradient-clip live there unconditionally so a
       reduced-motion visitor still gets a correctly-sized, correctly-
       colored icon and gradient-text link, just without the motion; only
       the animation itself is added here). Same shimmer mechanism as
       .accent above (animated background-position on a clipped gradient),
       tuned for the nav's smaller font size, plus a small 4-point sparkle
       icon (the same shape used in the AI tools' own card icons) that
       gently scales/rotates in place. */
    .nav-ai-sparkle{animation:nav-ai-twinkle 2.4s ease-in-out infinite;}
    .nav-ai-text{animation:nav-ai-shimmer 4s ease-in-out infinite;}
    .nav-ai-link:hover .nav-ai-sparkle{animation-duration:1.1s;}
    .nav-ai-link:hover .nav-ai-text{animation-duration:1.6s;}
    @keyframes nav-ai-shimmer{ 0%,100%{background-position:0% center;} 50%{background-position:100% center;} }
    @keyframes nav-ai-twinkle{
      0%,100%{opacity:.55; transform:scale(.75) rotate(0deg);}
      50%{opacity:1; transform:scale(1.15) rotate(10deg);}
    }

    /* ---- nav "3D Print" link: identical mechanism to the "AI" link
       above, just its own keyframe names/timing so the two stay
       independently tunable. */
    .nav-print3d-sparkle{animation:nav-print3d-twinkle 2.4s ease-in-out infinite;}
    .nav-print3d-text{animation:nav-print3d-shimmer 4s ease-in-out infinite;}
    .nav-print3d-link:hover .nav-print3d-sparkle{animation-duration:1.1s;}
    .nav-print3d-link:hover .nav-print3d-text{animation-duration:1.6s;}
    @keyframes nav-print3d-shimmer{ 0%,100%{background-position:0% center;} 50%{background-position:100% center;} }
    @keyframes nav-print3d-twinkle{
      0%,100%{opacity:.55; transform:scale(.75) rotate(0deg);}
      50%{opacity:1; transform:scale(1.15) rotate(10deg);}
    }

    /* ---- spotlight / mock-window preview panels: subtle 3D tilt + glow ---- */
    .spotlight-preview, .mock-window{transition:transform .4s cubic-bezier(.34,1.2,.64,1), box-shadow .4s ease;}
    .spotlight-preview:hover, .mock-window:hover{
      transform:perspective(1200px) rotateX(1deg) rotateY(-1.5deg) translateY(-4px) scale(1.008);
      box-shadow:0 32px 70px rgba(0,0,0,.45), 0 0 32px rgba(255,90,31,.16);
    }

    /* ---- Qira home-spotlight demo: hover/drag feedback + idle life ----
       Structural rules (mode show/hide, cursors) are unconditional, above
       -- only the motion itself lives here. */
    .qira-mock .mock-tool-btn{transition:transform .15s ease, border-color .15s ease, background .15s ease, color .15s ease;}
    .qira-mock .mock-tool-btn:hover{transform:translateY(-1px); border-color:var(--border-strong);}
    .qira-mock .mock-tool-btn.active:hover{border-color:var(--ember);}
    .qira-mock .mock-slider-dot{transition:transform .15s ease, box-shadow .15s ease;}
    .qira-mock .mock-slider-dot:hover{transform:translate(-50%,-50%) scale(1.25); box-shadow:0 0 0 5px var(--ember-dim);}
    .qira-mock .mock-slider-dot.is-dragging{transform:translate(-50%,-50%) scale(1.35); box-shadow:0 0 0 6px var(--ember-dim);}
    .qira-mock .mock-crop-handle{transition:transform .15s ease;}
    .qira-mock .mock-crop-handle:hover, .qira-mock .mock-crop-handle.is-dragging{transform:scale(1.4);}
    .qira-mock .mock-crop-box{transition:opacity .25s ease, border-style .25s ease;}
    .qira-mock .mock-adjust-panel{transition:opacity .25s ease;}
    .qira-mock #qira-photo{transition:filter .12s ease;}
    .qira-mock #qira-text-row{transition:border-color .25s ease, box-shadow .25s ease;}
    @keyframes qira-sun-pulse{ 0%,100%{opacity:.92; transform:scale(1);} 50%{opacity:1; transform:scale(1.08);} }
    .qira-mock .mock-photo-sun{animation:qira-sun-pulse 3.2s ease-in-out infinite;}
    @keyframes qira-crop-glow{ 0%,100%{box-shadow:0 0 0 rgba(255,176,32,0);} 50%{box-shadow:0 0 16px rgba(255,176,32,.35);} }
    .qira-mock .mock-crop-box{animation:qira-crop-glow 3.2s ease-in-out infinite;}

    /* ---- Collage Maker home-spotlight demo: hover/click feedback + idle life ----
       Structural rules (layout variants, cursors) are unconditional, above. */
    .collage-mock .mock-tool-btn{transition:transform .15s ease, border-color .15s ease, background .15s ease, color .15s ease;}
    .collage-mock .mock-tool-btn:hover{transform:translateY(-1px); border-color:var(--border-strong);}
    .collage-mock .mock-tool-btn.active:hover{border-color:var(--ember);}
    .mock-collage-grid{transition:grid-template-columns .35s cubic-bezier(.34,1.2,.64,1);}
    .mock-collage-cell{transition:transform .2s ease, box-shadow .2s ease, grid-column .35s ease, grid-row .35s ease;}
    .collage-mock .mock-collage-cell:hover{transform:scale(1.03); box-shadow:0 0 0 2px var(--ember-hot) inset;}
    .collage-mock .mock-collage-cell.is-shuffling{transform:scale(.96); box-shadow:0 0 0 2px var(--ember-hot) inset;}
    /* a slow spotlight sweep idling across the four cells, one at a time,
       so the grid feels alive even before anyone clicks it */
    @keyframes collage-cell-glow{ 0%,100%{box-shadow:0 0 0 rgba(255,176,32,0);} 50%{box-shadow:0 0 18px rgba(255,176,32,.4) inset;} }
    .collage-mock .mock-collage-cell{animation:collage-cell-glow 6.4s ease-in-out infinite;}
    .collage-mock .mock-collage-cell.c1{animation-delay:0s;}
    .collage-mock .mock-collage-cell.c2{animation-delay:1.6s;}
    .collage-mock .mock-collage-cell.c3{animation-delay:3.2s;}
    .collage-mock .mock-collage-cell.c4{animation-delay:4.8s;}

    /* ---- filter tabs: bouncy hover + glowing active pill ---- */
    .tab{transition:transform .2s cubic-bezier(.34,1.56,.64,1), border-color .2s ease, color .2s ease, background .2s ease, box-shadow .3s ease;}
    .tab:hover{transform:translateY(-2px) scale(1.04);}
    .tab.active{box-shadow:0 0 0 1px var(--ember), 0 0 14px rgba(255,90,31,.35);}

    /* ---- faq accordion: a little more life on hover/open ---- */
    .faq-item{transition:border-color .25s ease, box-shadow .25s ease;}
    .faq-item:hover{border-color:var(--border-strong);}
    .faq-item[open]{border-color:var(--ember); box-shadow:0 0 0 1px rgba(255,90,31,.2), 0 0 20px rgba(255,90,31,.1);}
    .faq-item summary::after{transition:transform .3s cubic-bezier(.34,1.56,.64,1), color .2s ease;}
    .faq-item:hover summary::after{color:var(--ember-hot);}
  }


/* ================= GEN-Z POLISH: STARFIELD BACKGROUND (hero + footer only) ================= */
@media (prefers-reduced-motion: no-preference) {

  /* hero sections already establish a stacking context (position:relative;
     overflow:hidden) and already promote their real content to z-index:1
     via ".hero > *" above, so a z-index:0 starfield pseudo-element here
     paints behind the heading/copy/buttons without touching anything else. */
  .hero::before{
    content:'';
    position:absolute;
    inset:0;
    z-index:0;
    pointer-events:none;
    background-repeat:repeat;
    background-image:
    radial-gradient(1.3px 1.3px at 40px 209px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 83px 97px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 116px 104px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 35px 48px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 107px 220px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 26px 167px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 255px 37px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 129px 53px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 153px 193px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 42px 255px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 148px 207px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 194px 182px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 2px 98px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 132px 152px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 218px 205px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 176px 4px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 23px 218px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 238px 6px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 84px 57px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 230px 164px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 75px 234px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 235px 58px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 66px 242px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 113px 95px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 20px 128px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 139px 86px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.3px 1.3px at 92px 88px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.3px 1.3px at 52px 36px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 238px 78px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 211px 156px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 155px 231px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 140px 49px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 221px 184px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 40px 170px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.3px 1.3px at 4px 233px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 47px 15px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 101px 79px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 129px 145px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.3px 1.3px at 70px 9px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.3px 1.3px at 110px 12px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 48px 38px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.3px 1.3px at 128px 2px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.3px 1.3px at 88px 252px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.3px 1.3px at 81px 9px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 84px 200px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.3px 1.3px at 113px 54px, rgba(255,90,31,.6), transparent 100%);
    background-size:260px 260px;
    animation:star-drift-far 220s linear infinite;
    opacity:.65;
  }

  /* .store-cta (which holds ad slot 3) and footer share one continuous
     starfield so it reads as a single field drifting from around the ad
     slot down through the bottom bar, rather than two disjoint patches. */
  .store-cta, footer{position:relative; overflow:hidden;}
  .store-cta > *, footer > *{position:relative; z-index:1;}
  .store-cta::before, footer::before{
    content:'';
    position:absolute;
    inset:0;
    z-index:0;
    pointer-events:none;
    background-repeat:repeat;
    background-image:
    radial-gradient(1.7px 1.7px at 164px 318px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.7px 1.7px at 140px 113px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 287px 147px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.7px 1.7px at 165px 101px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.7px 1.7px at 239px 281px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.7px 1.7px at 153px 144px, rgba(255,176,32,.75), transparent 100%),
    radial-gradient(1.7px 1.7px at 124px 316px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 197px 82px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 118px 142px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.7px 1.7px at 322px 187px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.7px 1.7px at 36px 231px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 58px 58px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 150px 247px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.7px 1.7px at 148px 21px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.7px 1.7px at 248px 284px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.7px 1.7px at 334px 337px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.7px 1.7px at 87px 22px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 267px 260px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.7px 1.7px at 320px 169px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.7px 1.7px at 113px 111px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 38px 301px, rgba(255,255,255,.9), transparent 100%),
    radial-gradient(1.7px 1.7px at 260px 170px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.7px 1.7px at 52px 142px, rgba(255,90,31,.6), transparent 100%),
    radial-gradient(1.7px 1.7px at 113px 245px, rgba(244,241,236,.8), transparent 100%),
    radial-gradient(1.7px 1.7px at 332px 120px, rgba(255,255,255,.7), transparent 100%),
    radial-gradient(1.7px 1.7px at 267px 180px, rgba(255,176,32,.75), transparent 100%);
    background-size:340px 340px;
    animation:star-drift-near 130s linear infinite;
    opacity:.55;
  }

  @keyframes star-drift-far{
    0%{background-position:0 0;}
    100%{background-position:-260px 260px;}
  }
  @keyframes star-drift-near{
    0%{background-position:0 0;}
    100%{background-position:340px -340px;}
  }

  /* ---- occasional shooting stars in the hero ---- */
  /* .hero-glow already sits at z-index:0 behind the real hero content (see
     ".hero > *" above), so hanging two streaks off its own ::before/::after
     keeps them safely behind the heading/copy/buttons on every hero page
     without adding new markup. Each streak is invisible almost all the time
     and only flashes across for a fraction of its (long, staggered) cycle,
     so the two never sync up and it reads as "every once in a while". */
  .hero-glow::before, .hero-glow::after{
    content:'';
    position:absolute;
    width:130px; height:2px;
    background:linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.95) 60%, rgba(255,176,32,.9));
    border-radius:2px;
    filter:drop-shadow(0 0 6px rgba(255,255,255,.75));
    opacity:0;
    pointer-events:none;
  }
  .hero-glow::before{
    top:130px; left:40px;
    transform:rotate(-22deg);
    animation:shooting-star-a 11s ease-in infinite;
    animation-delay:2.5s;
  }
  .hero-glow::after{
    top:300px; left:520px;
    transform:rotate(-16deg);
    animation:shooting-star-b 16s ease-in infinite;
    animation-delay:8s;
  }
  @keyframes shooting-star-a{
    0%, 90%{opacity:0; transform:translate(0,0) rotate(-22deg);}
    91%{opacity:1;}
    96%{opacity:1; transform:translate(-230px,130px) rotate(-22deg);}
    97%, 100%{opacity:0; transform:translate(-230px,130px) rotate(-22deg);}
  }
  @keyframes shooting-star-b{
    0%, 93%{opacity:0; transform:translate(0,0) rotate(-16deg);}
    94%{opacity:1;}
    98%{opacity:1; transform:translate(-190px,80px) rotate(-16deg);}
    99%, 100%{opacity:0; transform:translate(-190px,80px) rotate(-16deg);}
  }
}

/* ---------- Home hero: Star Wars-style "flying toward camera" starfield ---------- */
/* A real <canvas> layer (assets/hero-starfield.js), separate from the CSS
   drift-starfield above -- that one is a slow, static-feeling tiled dot
   pattern meant for the hero AND the footer; this one is a small flock of
   ~50 points with genuine 3D perspective motion (each has its own x/y/z,
   z shrinks every frame so it appears to fly toward the viewer, then
   resets to the far distance once it passes the camera) plus a short
   trail line behind each star, index.php's hero only. Sits in the same
   z-index:0 layer as .hero-glow/the drift starfield, still safely behind
   the real heading/copy/buttons (".hero > *" above promotes those to
   z-index:1). The canvas itself is always in the DOM; the script no-ops
   (leaves it empty/transparent) under prefers-reduced-motion, same as the
   drift starfield's own media-query gate just above. */
.hero-starfield{
  position:absolute; inset:0; z-index:0; pointer-events:none;
  width:100%; height:100%; display:block;
}

/* ---------- Video Editor tool: Clipchamp-style layout ---------- */
/* Top bar (project name + export), left icon rail + slide-out drawer
   (Media/Text/Music), a center preview, and a full-width timeline strip
   along the bottom -- the same overall shape as Clipchamp/CapCut-style
   editors, built from this site's existing dark theme tokens rather than
   a new palette. */
.ve-app{margin-top:20px; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg-elev); overflow:hidden;}

.ve-topbar{
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:12px 16px; border-bottom:1px solid var(--border); background:var(--bg-elev-2);
}
.ve-topbar-title{font-family:var(--font-display); font-size:15px; font-weight:700;}
.ve-topbar-actions{display:flex; align-items:center; gap:14px;}
.ve-history-btns{display:flex; gap:4px;}
.ve-history-btn{
  width:30px; height:30px; border-radius:7px; border:1px solid var(--border-strong);
  background:transparent; color:var(--text-dim); cursor:pointer; display:flex; align-items:center; justify-content:center;
}
.ve-history-btn svg{width:16px; height:16px;}
.ve-history-btn:hover:not(:disabled){border-color:var(--ember); color:var(--text);}
.ve-history-btn:disabled{opacity:0.35; cursor:default;}

.ve-main{display:flex; align-items:stretch; min-height:420px;}

.ve-rail{
  display:flex; flex-direction:column; align-items:stretch; gap:2px; flex-shrink:0;
  width:76px; padding:10px 6px; border-right:1px solid var(--border); background:var(--bg-elev-2);
}
.ve-rail-btn{
  display:flex; flex-direction:column; align-items:center; gap:5px;
  background:transparent; border:1px solid transparent; border-radius:9px; padding:10px 4px;
  color:var(--text-dim); cursor:pointer; font-family:var(--font-body); font-size:11px;
}
.ve-rail-btn svg{width:19px; height:19px;}
.ve-rail-btn:hover{background:var(--bg-elev); color:var(--text);}
.ve-rail-btn.active{background:var(--ember-dim); border-color:var(--ember); color:var(--ember-hot);}

.ve-drawer{
  width:300px; flex-shrink:0; border-right:1px solid var(--border);
  padding:14px; overflow-y:auto; max-height:78vh;
}
.ve-drawer-cta{width:100%; margin-bottom:12px;}
.ve-drawer .range-hint{margin-top:12px;}

.ve-center{flex:1; min-width:0; display:flex; flex-direction:column; padding:18px;}
.ve-stage{
  position:relative; flex:1; min-height:320px; border-radius:12px;
  background:repeating-conic-gradient(#2a2830 0% 25%, #201e24 0% 50%) 0 0/22px 22px;
  display:flex; align-items:center; justify-content:center; overflow:hidden;
  border:1px solid var(--border); transition:border-color .15s ease, background .15s ease;
}
.ve-stage.dragover{border-color:var(--ember); background:var(--ember-dim);}
.ve-empty-state{
  width:100%; min-height:280px; margin:auto; display:flex; flex-direction:column;
  align-items:center; justify-content:center; text-align:center; cursor:pointer; padding:0 24px;
}
.ve-empty-state svg{width:30px; height:30px; margin-bottom:14px; color:var(--ember-hot);}
.ve-empty-state p{font-size:14px; color:var(--text-dim);}
.ve-empty-state .dz-strong{color:var(--text); font-weight:600;}
.ve-empty-state .dz-hint{margin-top:6px; font-size:12px; color:var(--text-faint); font-family:var(--font-mono);}
.ve-canvas-wrap{width:100%; height:100%; display:flex; align-items:center; justify-content:center;}
/* Held at 50% of the browser viewport's width regardless of how much room
   the left rail+drawer take up -- min() with 100% is just a safety net so
   it still shrinks to fit on a narrow viewport instead of overflowing its
   own stage, not a change to the 50% target on any normal screen size. */
#ve-canvas{width:min(50vw, 100%); max-width:min(50vw, 100%); max-height:100%; height:auto; aspect-ratio:16/9; background:#000; border-radius:6px; box-shadow:0 10px 40px rgba(0,0,0,.5);}
/* The real <video>/<img> elements the canvas draws from every frame --
   never shown directly (the canvas is the only visible surface), but kept
   in normal document flow rather than display:none so decoding keeps
   running reliably across browsers while off-screen. */
.ve-source-el{position:absolute; width:1px; height:1px; overflow:hidden; opacity:0; pointer-events:none;}
.ve-preview-controls{display:flex; align-items:center; justify-content:center; gap:14px; margin-top:14px; flex-shrink:0;}
.ve-preview-time{font-size:12.5px; color:var(--text-dim);}

/* ---- Media / Text bins in the left drawer ---- */
/* Just name/size/remove -- trimming a clip's start/stop happens on the
   timeline itself (drag its block's edges, or use the inspector fields
   that appear above the timeline once it's selected), not in this list. */
.ve-media-list{display:flex; flex-direction:column; gap:8px;}
.ve-media-item{
  display:flex; align-items:center; gap:10px; padding:8px 10px;
  border:1px solid var(--border); border-radius:10px; background:var(--bg-elev-2); cursor:pointer;
}
.ve-media-item.selected{border-color:var(--ember); background:var(--ember-dim);}
.ve-media-item-icon{
  width:32px; height:32px; flex-shrink:0; border-radius:7px; display:flex; align-items:center; justify-content:center;
  background:var(--bg); color:var(--ember-hot);
}
.ve-media-item-icon svg{width:16px; height:16px;}
.ve-media-item-body{flex:1; min-width:0;}
.ve-media-item-name{font-size:12.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.ve-media-item-meta{font-family:var(--font-mono); font-size:10.5px; color:var(--text-faint); margin-top:2px;}
.ve-media-item-lane{
  font-family:var(--font-mono); font-size:9.5px; color:var(--text-faint); background:var(--bg);
  border:1px solid var(--border); border-radius:4px; padding:1px 5px; flex-shrink:0;
}
.ve-media-item-remove{
  width:24px; height:24px; border-radius:6px; border:1px solid var(--border-strong); background:transparent;
  color:var(--text-dim); cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
.ve-media-item-remove:hover{border-color:var(--ember); color:var(--ember-hot);}
.ve-media-item-remove svg{width:12px; height:12px;}

/* Music track list -- reuses .ve-media-item's row layout (icon/body/remove). */
.ve-music-list{display:flex; flex-direction:column; gap:8px; margin-top:10px;}

.ve-text-list{display:flex; flex-direction:column; gap:10px;}
.ve-text-row{
  border:1px solid var(--border); border-radius:var(--radius); background:var(--bg-elev-2);
  padding:12px; display:flex; flex-wrap:wrap; align-items:center; gap:10px;
}
.ve-text-row input[type="text"].ve-text-input{
  flex:1 1 140px; min-width:100px; padding:8px 10px; border-radius:7px; border:1px solid var(--border-strong);
  background:var(--bg); color:var(--text); font-size:13px;
}
.ve-text-row input[type="text"].ve-text-time{
  width:56px; padding:6px 8px; border-radius:7px; border:1px solid var(--border-strong);
  background:var(--bg); color:var(--text); font-family:var(--font-mono); font-size:12px; text-align:center;
}
.ve-text-row select{
  padding:6px 8px; border-radius:7px; border:1px solid var(--border-strong);
  background:var(--bg); color:var(--text); font-size:12px;
}
.ve-text-row input[type="color"]{width:28px; height:28px; padding:0; border:1px solid var(--border-strong); border-radius:6px; background:none; cursor:pointer;}
.ve-text-row .ve-text-sep{color:var(--text-faint); font-size:12px;}
.ve-text-remove{
  margin-left:auto; width:24px; height:24px; border-radius:6px; border:1px solid var(--border-strong);
  background:transparent; color:var(--text-dim); cursor:pointer; display:flex; align-items:center; justify-content:center;
}
.ve-text-remove:hover{border-color:var(--ember); color:var(--ember-hot);}
.ve-text-remove svg{width:13px; height:13px;}

.ve-music-dropzone{padding:24px 18px;}
.ve-music-dropzone svg{width:24px; height:24px;}
#ve-music-info .file-row{margin-bottom:4px;}
#ve-music-info .range-field{margin-top:16px;}
#ve-music-info .range-hint{margin-top:12px;}

/* ---- Bottom timeline ---- */
.ve-timeline-panel{border-top:1px solid var(--border); background:var(--bg-elev-2); padding:12px 16px 16px;}
.ve-timeline-toolbar{min-height:20px;}
.ve-timeline-hint{font-size:12px; color:var(--text-faint);}
.ve-clip-inspector{display:flex; flex-wrap:wrap; align-items:center; gap:14px 20px;}
.ve-clip-field{display:flex; align-items:center; gap:6px;}
.ve-clip-field label{font-size:12px; color:var(--text-dim); white-space:nowrap;}
.ve-clip-field input[type="text"]{
  width:64px; padding:6px 8px; border-radius:7px; border:1px solid var(--border-strong);
  background:var(--bg); color:var(--text); font-family:var(--font-mono); font-size:12.5px; text-align:center;
}
.ve-clip-field input[type="text"]:focus{border-color:var(--ember);}
.ve-clip-field .ve-clip-of{color:var(--text-faint); font-size:11.5px;}
.ve-clip-split-btn, .ve-clip-inspector-btn{
  font-family:var(--font-mono); font-size:11.5px; padding:6px 10px; border-radius:7px;
  border:1px solid var(--border-strong); background:transparent; color:var(--text); cursor:pointer;
}
.ve-clip-split-btn:hover, .ve-clip-inspector-btn:hover{border-color:var(--ember);}
.ve-clip-inspector-btn.danger:hover{border-color:#ff5a5a; color:#ff8080;}
.ve-clip-field select.ve-clip-speed{
  padding:6px 8px; border-radius:7px; border:1px solid var(--border-strong);
  background:var(--bg); color:var(--text); font-family:var(--font-mono); font-size:12px;
}
.ve-clip-audio-field{display:flex; align-items:center; gap:8px;}
.ve-clip-audio-field label{font-size:12px; color:var(--text-dim); white-space:nowrap;}
.ve-clip-audio-field input[type="range"]{width:80px;}
.ve-clip-mute-btn{
  width:26px; height:26px; border-radius:6px; border:1px solid var(--border-strong);
  background:transparent; color:var(--text-dim); cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
.ve-clip-mute-btn svg{width:13px; height:13px;}
.ve-clip-mute-btn.active{border-color:var(--ember); color:var(--ember-hot); background:var(--ember-dim);}

.ve-clip-field select.ve-clip-pos,
.ve-clip-field select.ve-clip-filter{
  padding:6px 8px; border-radius:7px; border:1px solid var(--border-strong);
  background:var(--bg); color:var(--text); font-family:var(--font-mono); font-size:12px;
}
.ve-clip-fx-field{display:flex; align-items:center; gap:8px;}
.ve-clip-fx-field label{font-size:12px; color:var(--text-dim); white-space:nowrap;}
.ve-clip-fx-toggles{display:flex; gap:4px; flex-wrap:wrap;}
.ve-clip-fx-btn{
  font-family:var(--font-mono); font-size:11px; padding:5px 9px; border-radius:7px;
  border:1px solid var(--border-strong); background:transparent; color:var(--text-dim); cursor:pointer;
}
.ve-clip-fx-btn:hover{border-color:var(--ember); color:var(--text);}
.ve-clip-fx-btn.active{border-color:var(--ember); color:var(--ember-hot); background:var(--ember-dim);}

.ve-timeline-zoom{display:flex; justify-content:flex-end; gap:6px; margin:8px 0 4px;}
.ve-tl-zoom-btn{
  width:26px; height:26px; border-radius:6px; border:1px solid var(--border-strong); background:var(--bg-elev);
  color:var(--text-dim); cursor:pointer; font-size:15px; line-height:1; display:flex; align-items:center; justify-content:center;
}
.ve-tl-zoom-btn:hover{border-color:var(--ember); color:var(--ember-hot);}

.ve-timeline-scroll{position:relative; overflow-x:auto; overflow-y:hidden; border-radius:8px; background:var(--bg);}
.ve-tl-ruler{position:relative; height:22px; border-bottom:1px solid var(--border); cursor:ew-resize;}
.ve-tl-tick{
  position:absolute; top:0; bottom:0; border-left:1px solid var(--border-strong);
  padding-left:4px; font-family:var(--font-mono); font-size:10px; color:var(--text-faint); white-space:nowrap;
}
/* ---- Media lanes: Main (lane 0, sequential) + any number of Overlay
   lanes (picture-in-picture, freely positioned in time) and/or Cutaway
   lanes (full-screen swap, freely positioned in time). Each lane is its
   own row so multiple clips can play at once, one per lane. ---- */
.ve-tl-media-lanes{position:relative;}
.ve-tl-lane-row{position:relative; height:64px; border-top:1px solid var(--border);}
.ve-tl-lane-row:first-child{border-top:none;}
.ve-tl-lane-row.ve-tl-lane-row-main{background:rgba(255,255,255,.015);}
.ve-tl-block{
  position:absolute; top:6px; bottom:6px; border-radius:7px; overflow:hidden;
  border:1.5px solid var(--border-strong); cursor:grab; display:flex; align-items:center;
  padding:0 8px; box-sizing:border-box;
}
.ve-tl-block:active{cursor:grabbing;}
.ve-tl-block.ve-tl-block-video{background:linear-gradient(160deg, #2a3a52, #1c2636);}
.ve-tl-block.ve-tl-block-image{background:linear-gradient(160deg, #3a2a52, #261c36);}
.ve-tl-block.ve-tl-block-overlay.ve-tl-block-video{background:linear-gradient(160deg, #2a4a4a, #1c3636);}
.ve-tl-block.ve-tl-block-overlay.ve-tl-block-image{background:linear-gradient(160deg, #4a3a2a, #362a1c);}
.ve-tl-block.ve-tl-block-cutaway.ve-tl-block-video{background:linear-gradient(160deg, #4a2a52, #331c3a);}
.ve-tl-block.ve-tl-block-cutaway.ve-tl-block-image{background:linear-gradient(160deg, #522a3a, #3a1c29);}
.ve-tl-block.selected{border-color:var(--ember); box-shadow:0 0 0 1px var(--ember);}
.ve-tl-block.dragging{opacity:.45;}
.ve-tl-block-name{
  font-size:11px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  pointer-events:none;
}
/* Left/right edge-drag handles for trimming a clip's start/stop directly
   on its timeline block -- the numeric Start/End fields in the inspector
   above stay in sync and remain the precise fallback. */
.ve-tl-handle{
  position:absolute; top:0; bottom:0; width:9px; z-index:2; cursor:ew-resize;
}
.ve-tl-handle-left{left:0;}
.ve-tl-handle-right{right:0;}
.ve-tl-handle::after{
  content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:3px; height:18px; border-radius:2px; background:rgba(255,255,255,.55);
}
.ve-tl-block:hover .ve-tl-handle::after, .ve-tl-block.selected .ve-tl-handle::after{background:var(--ember-hot);}
/* Positioned near the TOP of the clip lane rather than dead-center on the
   block boundary -- dead-center would sit exactly on top of the trim
   handles below (both represent the same x position), stealing their
   clicks. Offset vertically instead so both stay independently grabbable. */
.ve-tl-transition-select{
  position:absolute; top:3px; transform:translateX(-50%); z-index:5;
  width:30px; height:18px; padding:0 2px; border-radius:9px; border:1.5px solid var(--border-strong);
  background:var(--bg-elev); color:var(--text-dim); font-size:9px; text-align:center; cursor:pointer; appearance:none;
}
.ve-tl-transition-select:hover, .ve-tl-transition-select:focus{border-color:var(--ember); color:var(--ember-hot);}
/* "+ Add overlay lane" / "+ Add cutaway lane" -- a slim row appended after
   the last media lane, one button per lane type so it's clear up front
   which behavior a new lane will have. */
.ve-tl-lane-add{border-top:1px solid var(--border); padding:5px 8px; display:flex; gap:8px;}
.ve-tl-lane-add-btn{
  font-family:var(--font-mono); font-size:10.5px; padding:4px 9px; border-radius:6px;
  border:1px dashed var(--border-strong); background:transparent; color:var(--text-faint); cursor:pointer;
}
.ve-tl-lane-add-btn:hover{border-color:var(--ember); color:var(--ember-hot); border-style:solid;}
.ve-tl-lane-add-btn-cutaway:hover{border-color:#b06bd6; color:#d19bf0;}

/* ---- Caption lanes: one row per "layer" of non-overlapping captions --
   extra rows appear automatically only when captions' times actually
   overlap, via a greedy interval-scheduling pass recomputed on every
   render, not a manually-assigned lane. ---- */
.ve-tl-text-lanes{position:relative;}
.ve-tl-text-track{position:relative; height:34px; border-top:1px solid var(--border);}
.ve-tl-text-lanes .ve-tl-text-track:first-child{margin-top:2px;}
.ve-tl-text-block{
  position:absolute; top:5px; bottom:5px; border-radius:6px; overflow:hidden;
  border:1.5px solid var(--border-strong); background:linear-gradient(160deg, #3a4a2a, #26361c);
  cursor:grab; display:flex; align-items:center; padding:0 8px; box-sizing:border-box;
}
.ve-tl-text-block:active{cursor:grabbing;}
.ve-tl-text-block.selected{border-color:var(--ember); box-shadow:0 0 0 1px var(--ember);}
.ve-tl-text-block .ve-tl-block-name{font-size:10.5px;}

/* ---- Music lanes: any number of simultaneous tracks, each its own
   full-width bar (music always plays across the whole video, looped or
   cut short -- see the Fit setting in its inspector). ---- */
.ve-tl-music-lanes{position:relative;}
.ve-tl-music-track{position:relative; height:34px; border-top:1px solid var(--border);}
.ve-tl-music-lanes .ve-tl-music-track:first-child{margin-top:2px;}
.ve-tl-music-block{
  position:absolute; top:5px; bottom:5px; left:0; border-radius:6px;
  border:1.5px solid var(--border-strong); background:linear-gradient(160deg, #4a2a52, #361c3a);
  cursor:pointer; display:flex; align-items:center; padding:0 8px; box-sizing:border-box;
}
.ve-tl-music-block.selected{border-color:var(--ember); box-shadow:0 0 0 1px var(--ember);}
.ve-tl-music-empty{
  position:absolute; top:0; left:8px; line-height:34px; font-size:11px; color:var(--text-faint);
}
.ve-tl-lane-label{
  position:sticky; left:4px; top:0; z-index:3; display:inline-block; margin:4px 0 0 0;
  font-family:var(--font-mono); font-size:9.5px; letter-spacing:.02em; text-transform:uppercase;
  color:var(--text-faint); background:var(--bg-elev-2); border:1px solid var(--border); border-radius:4px; padding:2px 6px;
  pointer-events:none;
}
.ve-tl-playhead{
  position:absolute; top:0; bottom:0; width:2px; background:var(--ember-hot);
  box-shadow:0 0 6px rgba(255,176,32,.7); pointer-events:none; z-index:6;
}
.ve-tl-playhead::before{
  content:''; position:absolute; top:-1px; left:50%; transform:translateX(-50%);
  width:0; height:0; border-left:5px solid transparent; border-right:5px solid transparent; border-top:7px solid var(--ember-hot);
}

@media (max-width:900px){
  .ve-main{flex-direction:column;}
  .ve-rail{width:100%; flex-direction:row; justify-content:center; border-right:none; border-bottom:1px solid var(--border);}
  .ve-drawer{width:100%; max-height:none; border-right:none; border-bottom:1px solid var(--border);}
}

/* ================= 3D Print Name Tag Maker ================= */
/* The live preview (assets/app.js) renders on a transparent WebGL canvas,
   so what shows through wherever there's no model is this element's own
   CSS background -- the same checkerboard "transparency" convention
   already used by the photo editor's .pe-stage and video editor's
   .ve-stage canvases (same colors, same 22px tile), so it reads as
   consistent with the rest of the site rather than a one-off pattern.
   Overrides the shared .bib-canvas-wrap canvas{background:...} by ID
   specificity -- no !important needed. */
#ntag-canvas{
  /* Solid dark fallback, not the repeating-conic-gradient checkerboard
     this used to be -- a real, in-scene 10mm reference grid (assets/app.js)
     now covers the frame, matching the storage organizer tool. max-height
     override: this preview is square (height follows the wrap's own
     width), and the shared .bib-canvas-wrap canvas{max-height:70vh} rule
     always wins over an inline height regardless of specificity, so
     without this override the square would silently squash back into a
     short rectangle on any viewport under ~740px tall. */
  background:#0d0f14;
  max-height:none;
}

/* This tool's controls column sits next to a preview column inside the
   shared .bib-layout two-column shell, so it's noticeably narrower than
   the full-width forms the shared .bib-row-col's 240px min-width was
   sized for -- at 240px, grouping three fields (Text style/Font size/
   Text depth, Width/Height/Thickness) or even two (Corners/Corner
   radius, Hole position/Hole type) onto one line would immediately wrap
   back to one-per-line in this column's actual available width. Scoped
   override rather than editing the shared class, since Blur/Pixelate's
   wider single-column forms still want the roomier 240px default. */
.ntag-controls .bib-row-pair{gap:14px;}
.ntag-controls .bib-row-col{min-width:130px;}

/* The shared .mode-toggle's own margin:20px 0 (sized for when a toggle
   sits alone on the page, with plenty of room to breathe) left a much
   bigger gap under a label like "Text style" or "Corners" than the
   effectively-zero gap above a slider's .pe-inline-field in the same row
   (the site's *{margin:0} reset leaves nothing there). Zeroed out here so
   every label-to-control gap in this tool's controls column matches,
   whether the control below it is a toggle or a slider. */
.ntag-controls .mode-toggle{margin:0;}

/* The shared .pe-inline-field keeps its slider + value label on one
   nowrap line, sized for the roomy single-column forms it was built
   for. In this tool's narrow two-column controls area, "Font size"'s
   value label can grow to "48 mm (capped to fit)" (see the
   fontSizeCapNotice logic in assets/app.js) -- too wide for its
   ~240px column, so with nowrap in effect it overflowed straight
   into the next column and visually collided with "Text depth"'s
   label and slider. Letting the field wrap keeps the value label
   (and its "(capped to fit)" suffix, when present) confined to its
   own column, dropping to a second line instead of spilling into the
   neighboring one -- the short, common case ("25 mm") still renders
   on a single line since there's nothing forcing an early break. */
.ntag-controls .pe-inline-field{flex-wrap:wrap;}
.ntag-controls .pe-inline-field .mono{white-space:normal;}

/* ================= 3D Print QR Code Maker ================= */
/* Same transparent-canvas + checkerboard convention as the name-tag tool
   above -- kept as its own ID rule (rather than combined into one
   selector) so each tool's block stays independently editable. */
#nqr-canvas{
  /* See #ntag-canvas's comment above -- same real in-scene 10mm grid,
     square-preview max-height override. */
  background:#0d0f14;
  max-height:none;
}

/* Same narrow-controls-column override as the name-tag tool's
   .ntag-controls above, applied here for the same reason: this tool's
   panel now also sits in the shared two-column .bib-layout, so its
   controls column is too narrow for the shared .bib-row-col's 240px
   min-width to let the QR-style/Module-depth/Error-correction and
   Width/Height/Thickness rows sit on one line. */
.nqr-controls .bib-row-pair{gap:14px;}
.nqr-controls .bib-row-col{min-width:130px;}

/* Same label-to-control gap standardization as the name-tag tool's
   .ntag-controls above -- zeroes the shared .mode-toggle's own
   margin:20px 0 so "QR code style"/"Corners"/"Hole position"/"Hole type"
   sit as close to their label as "Module depth" or "Width" sit to theirs. */
.nqr-controls .mode-toggle{margin:0;}

/* ================= 3D Print Photo Converter ================= */
/* Same transparent-canvas + checkerboard convention as the other two
   print3d tools above. */
#nphoto-canvas{
  /* See #ntag-canvas's comment above -- same real in-scene 10mm grid,
     square-preview max-height override. */
  background:#0d0f14;
  max-height:none;
}

/* This tool's panel now uses the same shared two-column .bib-layout shell
   as the other two print3d tools, and its controls column is likewise too
   narrow for the shared .bib-row-col's 240px min-width to let the
   Width/Height/Frame-thickness and Corners/Corner-radius and Hole-position/
   Hole-type rows sit on one line -- same override, same reasoning, as
   .ntag-controls and .nqr-controls above. */
.nphoto-controls .bib-row-pair{gap:14px;}
.nphoto-controls .bib-row-col{min-width:130px;}

/* Same label-to-control gap standardization as the other two print3d
   tools' controls columns above -- zeroes the shared .mode-toggle's own
   margin:20px 0 so "Style"/"Detail level"/"Corners"/"Hole position"/
   "Hole type" sit as close to their label as a slider does to its own. */
.nphoto-controls .mode-toggle{margin:0;}

/* Same defensive wrap as .ntag-controls above: keeps a slider's value
   label (and any future long suffix, e.g. a capped-value notice) confined
   to its own narrow column instead of spilling into the next one. Not
   currently exercised by this tool (no capped-value labels exist here
   yet), but applied for the same structural reason and to stay consistent
   with the other two print3d tools' controls columns. */
.nphoto-controls .pe-inline-field{flex-wrap:wrap;}
.nphoto-controls .pe-inline-field .mono{white-space:normal;}

/* ================= 3D print orientation nav cube ================= */
/* Shared by all four print3d tools (name-tag, qr-code, photo, storage
   organizer) -- an AutoCAD/Fusion-360-style "ViewCube" corner gizmo: a
   small CSS 3D cube (no extra WebGL scene/viewport needed) that always
   shows the model's current orientation and snaps the camera to a face's
   view on click. Each tool's own app.js IIFE wires up its own camTheta/
   camPhi/updateCamera() to this same markup+CSS, matching this project's
   established per-tool-duplication convention for behavior while sharing
   the purely-visual CSS here (the same way .bib-canvas-wrap etc. are
   already shared). Lives inside each tool's .bib-canvas-wrap, which is
   already position:relative, so this can sit absolutely in a corner. */
.nav-cube{
  position:absolute; top:10px; right:10px; width:58px; height:58px;
  perspective:220px; z-index:5; -webkit-user-select:none; user-select:none;
}
.nav-cube-inner{
  position:relative; width:100%; height:100%; transform-style:preserve-3d;
  transition:transform .35s ease;
  transform:rotateX(-20deg) rotateY(-35deg);
}
.nav-cube-face{
  position:absolute; width:58px; height:58px;
  background:rgba(28,32,42,0.85); border:1px solid rgba(160,180,255,0.45);
  display:flex; align-items:center; justify-content:center;
  font:600 8.5px/1 "Space Grotesk", sans-serif; letter-spacing:.04em;
  color:#cfe0ff; cursor:pointer;
}
.nav-cube-face:hover{background:rgba(70,110,220,0.6); color:#fff;}
.nav-cube-front{transform:translateZ(29px);}
.nav-cube-back{transform:rotateY(180deg) translateZ(29px);}
.nav-cube-right{transform:rotateY(90deg) translateZ(29px);}
.nav-cube-left{transform:rotateY(-90deg) translateZ(29px);}
.nav-cube-top{transform:rotateX(90deg) translateZ(29px);}
.nav-cube-bottom{transform:rotateX(-90deg) translateZ(29px);}
@media (max-width:640px){
  .nav-cube{width:44px; height:44px; top:6px; right:6px;}
  .nav-cube-face{width:44px; height:44px; font-size:7px; transform:translateZ(22px);}
  .nav-cube-back{transform:rotateY(180deg) translateZ(22px);}
  .nav-cube-right{transform:rotateY(90deg) translateZ(22px);}
  .nav-cube-left{transform:rotateY(-90deg) translateZ(22px);}
  .nav-cube-top{transform:rotateX(90deg) translateZ(22px);}
  .nav-cube-bottom{transform:rotateX(-90deg) translateZ(22px);}
}

/* ================= 3D Print Storage Organizer ================= */
/* Unlike the other three print3d tools, this canvas's checkerboard isn't a
   flat CSS background -- it's a real, flat plane drawn IN the THREE.js
   scene (see assets/app.js), tiled so each square is exactly 1mm across,
   so it stays true-to-scale and correctly perspective/rotated as the
   camera orbits, matching a CAD viewport's own reference grid. The CSS
   background here is just a plain dark fallback color behind that
   transparent canvas (visible for a frame before the scene first renders,
   or beyond the grid plane's edge at extreme zoom-out) -- deliberately
   NOT the repeating-conic-gradient checkerboard the other tools use,
   since stacking a second, differently-scaled checkerboard behind the
   real 1mm one would just look like a moire/scale mismatch bug. */
#nso-canvas{
  background:#0d0f14;
  /* Overrides the shared .bib-canvas-wrap canvas{max-height:70vh} rule.
     This tool's preview is square (assets/app.js's resizeRenderer() sets
     height = width, unlike the other three print3d tools' fixed 0.62
     aspect ratio) -- but max-height always wins over an inline height
     regardless of specificity, so on any viewport under ~740px tall the
     70vh cap would silently squash the square back into a short
     rectangle. max-height:none here lets the square actually stay square,
     driven purely by the wrap's own width as intended. */
  max-height:none;
}

/* #nso-dim-svg (the "actual sizing" overlay, see assets/app.js) is an
   <svg>, not a second <canvas>, layered on top of #nso-canvas via its own
   inline position:absolute styles. This was a real, confirmed bug fix,
   not just a style choice: an earlier version used a second <canvas> with
   a 2D context here, and that stacked-canvas-on-canvas setup broke the
   WebGL canvas's own on-screen rendering -- root-caused by comparing the
   WebGL canvas's actual buffer contents (via gl.readPixels, toDataURL,
   and drawImage+getImageData, which all agreed the model was rendering
   correctly) against what the page visually composited (nothing but the
   overlay canvas's lines), then confirming removing the second canvas
   made the model appear immediately. An SVG overlay draws the exact same
   lines/labels in the exact same layered position without introducing a
   second canvas element, so it doesn't share that failure mode. */

/* Same narrow-controls-column overrides as the other three print3d
   tools' controls columns -- this tool's panel uses the same shared
   two-column .bib-layout shell, and rows like the Base-type/Base toggles
   need the same tightened min-width/gap to fit comfortably. */
.nso-controls .bib-row-pair{gap:14px;}
.nso-controls .bib-row-col{min-width:130px;}
.nso-controls .mode-toggle{margin:0;}

/* The Volume/Est. weight/Triangles readout, the build status line, and
   the Exterior/Interior summary used to live here, right under the tabs
   bar (#nso-stats-block in tool-panel.php). All of it has since moved
   into the topbar's own File menu (see #nso-file-menu-summary in
   generator.php, which carries its own scoped styling), per the user's
   own explicit request. */

/* Base bottom height / Grip size are a .bib-row-pair of two sliders, so
   they ALSO match the generic "every slider in this tool" grid rule
   above (.nso-controls div:has(> p.range-hint + div.pe-inline-field)),
   which sets its own flex-basis of calc(50% - 4px) -- correct for the
   8px gap that rule's other slider rows use (widthdepth-row, sizeRow,
   etc.), but this row's parent .bib-row-pair uses a wider 14px gap (the
   narrow-controls-column override just above), so two columns each
   still claiming 50%-4px overflowed the row by the 6px difference and
   silently wrapped to two separate lines instead of sitting side by
   side like Base type/Base bottom do right above them. Re-declared here
   with the correct half-of-14px subtraction and a smaller min-width so
   they fit -- higher specificity (.nso-controls .bib-row-pair > div:has(...))
   than the generic rule, so this wins without touching that shared rule
   or any other slider row that already fit fine. */
.nso-controls .bib-row-pair > div:has(> p.range-hint + div.pe-inline-field){
  flex: 1 1 calc(50% - 7px);
  min-width: 130px;
}

/* Every slider field in this tool -- Width/Depth/Height/Diameter/Wall
   (or Base) thickness up top, plus every per-item Thickness/Height/Size/
   Position field for outer walls, dividers, holes, texts, wall designs,
   shelves, and the outer-corner Extent field further down -- is built
   from the exact same shape: a wrapping div containing a
   <p class="range-hint"> label immediately followed by a
   <div class="pe-inline-field"> holding the <input type="range"> and its
   <span class="mono"> value (see makeSizeField()/makePlacementField()/
   makeHoleSizeField()/makeTextSizeField()/makeWDSizeField() and the
   outer-corner Extent field in storage-organizer.js -- all ten of them).
   Restyled here ONCE, matched structurally with :has() rather than by
   giving every one of those a shared class, so every slider in the tool
   picks this up automatically with no per-field markup to keep in sync.
   Scoped to .nso-controls only -- .pe-inline-field/.range-hint/.mono are
   shared classes other tools on this site also use, and this restyle
   must not leak into those.

   Look matches the "Live Parametric Playground" demo sliders on the
   homepage (index.php's .pg-slider-row/.pg-slider-label/.pg-val) at that
   demo's own literal size/color -- label + value on one line, a slim 4px
   track, a small glowing 16px yellow thumb -- per the user's own request
   to use "the same as index.php", including its yellow rather than this
   tool's usual ember/orange. The label itself is normal weight, not
   bold, per the user's own later explicit request that slider text not
   be bold -- bold is reserved for section headers/row names elsewhere in
   this tool (.nso-section-label/.nso-field-label-bold). Two fields land
   on one line wherever a row has room (each field's own wrap below is
   sized to ~50% of its row), matching every other flex-wrap:wrap field
   row this tool already builds (sizeRow/placementRow/shelfRow/etc.), and
   drops to one per line on a narrow panel the same way those already do. */
.nso-controls .pe-inline-field{display:contents;}
.nso-controls div:has(> p.range-hint + div.pe-inline-field){
  display:grid; grid-template-columns:1fr auto; grid-template-areas:"label value" "slider slider";
  row-gap:6px; column-gap:8px; align-items:center;
  flex:1 1 calc(50% - 4px); min-width:150px;
}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) > p.range-hint{
  grid-area:label; margin:0 !important; font-size:0.82rem; font-weight:400; color:var(--text);
}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) input[type="range"]{
  grid-area:slider;
  -webkit-appearance:none; appearance:none; width:100%; height:4px;
  border-radius:4px; background:var(--bg-elev-2); outline:none; cursor:pointer;
}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) span.mono{
  grid-area:value; justify-self:end; color:#f5ff00; font-weight:700; font-size:0.85rem; white-space:nowrap;
}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none; width:16px; height:16px; border-radius:50%;
  background:#f5ff00; box-shadow:0 0 10px rgba(245,255,0,0.6);
  border:2px solid var(--bg); cursor:pointer; margin-top:-6px;
}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) input[type="range"]::-moz-range-thumb{
  width:16px; height:16px; border-radius:50%; background:#f5ff00;
  box-shadow:0 0 10px rgba(245,255,0,0.6); border:2px solid var(--bg); cursor:pointer;
}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) input[type="range"]:disabled{cursor:not-allowed; opacity:0.4;}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) input[type="range"]:disabled::-webkit-slider-thumb{box-shadow:none;}
.nso-controls div:has(> p.range-hint + div.pe-inline-field) input[type="range"]:disabled::-moz-range-thumb{box-shadow:none;}

/* "Base type" label -- same bold/white treatment as the Width/Depth/etc.
   labels just above (font-size/weight/color kept in sync with that
   :has() rule's own p.range-hint styling), plus a yellow right-pointing
   arrow in front of the text, per the user's explicit request. Scoped to
   its own .nso-section-label class (added only to this one <p>) rather
   than to .range-hint generally, since .range-hint is a shared class used
   all over this site for plain small hint text that must stay as-is. */
.nso-controls .nso-section-label{
  font-size:0.82rem; font-weight:700; color:var(--text); display:flex; align-items:center; gap:6px;
}
.nso-controls .nso-section-label .nso-arrow{color:#f5ff00; font-weight:700;}

/* Same "Base type"/section-header font treatment (size/color, no arrow),
   reused for individual field labels and row names scattered throughout
   this tool's many per-row builders (Outer walls/corners/Holes/Text/Wall
   design), per the user's own explicit request to bring them in line.
   Two variants: -bold matches Base type exactly (bold); -plain keeps the
   same size/color but skips the bold weight, for the specific labels the
   user called out as "no need to bold". Row NAME badges (e.g. "A1 --
   Front", "Hole 1") also get -bold here instead of their old monospace
   ".mono" styling, so they read as normal section-style text rather than
   a distinct typewriter font. */
.nso-controls .nso-field-label-bold{
  font-size:0.82rem; font-weight:700; color:var(--text); font-family:var(--font-body);
}
.nso-controls .nso-field-label-plain{
  font-size:0.82rem; font-weight:400; color:var(--text); font-family:var(--font-body);
}

/* Base-type / Base-bottom chips -- the collapsed "just show what's
   selected" view of each picker (see #nso-shape-chip/#nso-shape-toggle
   and #nso-base-chip/#nso-base-toggle's own show/hide logic in
   storage-organizer.js's change handlers and their X button handlers).
   Yellow outer ring + yellow selected text (not bold -- the ring alone
   is enough to mark it as selected), kept small/compact, per the user's
   own explicit spec; the X reopens the full choice <select> so a
   different value can be picked, then collapses back down to the chip
   once one is. Both chips share this one rule -- Base type and Base
   bottom render side by side in the same row and are meant to look
   identical apart from their own label text. */
.nso-shape-chip, .nso-base-chip{
  display:inline-flex; align-items:center; gap:6px;
  border:1px solid #f5ff00; border-radius:100px;
  padding:3px 5px 3px 10px; background:rgba(245,255,0,0.06);
  margin-top:6px;
}
.nso-shape-chip span, .nso-base-chip span{color:#f5ff00; font-weight:400; font-size:0.75rem;}
.nso-shape-chip button, .nso-base-chip button{
  background:none; border:none; color:#f5ff00; font-size:13px; line-height:1;
  cursor:pointer; width:16px; height:16px; border-radius:50%; padding:0;
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
.nso-shape-chip button:hover, .nso-base-chip button:hover{background:rgba(245,255,0,0.18);}

/* Reselection control (shown once a chip's X is clicked) -- a native
   <select> (Base type's Square/Rectangular/Circle/Oval, Base bottom's
   Raised feet/Recessed grip/Flat) rather than a button list, per the
   user's own explicit spec: yellow outer ring, yellow text, black
   background, sized to match the collapsed chip's own compact pill.
   Collapses back down to the chip on "change" (a real pick) or "blur"
   with the value unchanged (opened just to look, then clicked/tabbed
   away) -- see storage-organizer.js. Both Base type and Base bottom
   share this one class. */
.nso-shape-select{
  display:inline-block; width:auto; max-width:160px; margin-top:6px;
  background:#000; color:#f5ff00; border:1px solid #f5ff00; border-radius:100px;
  padding:3px 8px; font-family:var(--font-body); font-size:0.75rem; font-weight:400;
  cursor:pointer; outline:none;
}
.nso-shape-select:focus{box-shadow:0 0 0 2px rgba(245,255,0,0.25);}
.nso-shape-select option{background:#000; color:#f5ff00;}
.nso-shape-select:disabled{opacity:0.5; cursor:not-allowed;}

/* Every OTHER dropdown in this tool -- a hole's Wall select, Text/Wall
   design's Wall/Style/Wall face selects, etc. -- shares the plain
   ".admin-text-input" class (borrowed from the admin panel's own text
   inputs, which has no CSS of its own anywhere in this site, so these
   were rendering as bare, un-themed browser-default white dropdowns).
   Restyled here to match Base type/Base bottom's own black-background,
   yellow-ring, yellow-text, full-pill-rounded format -- scoped to this
   tool's own panel so .admin-text-input stays exactly as it was
   everywhere else it's used (e.g. the daemon admin panel). Sized down
   to the same compact padding/font-size as .nso-shape-select (Base
   type/Edge style/Corner type's own pill) so every dropdown in this
   panel now reads at one consistent size, per the user's own explicit
   request -- but kept at width:100% (max, not fixed) rather than
   .nso-shape-select's own inline-block/160px cap, since these sit
   inline with their own row's other fields (often paired two-up) and
   some carry longer option text ("Bottom -- Base", "Rounded rect")
   that a fixed narrow pill would clip. */
.nso-controls select.admin-text-input{
  display:block; width:100%; max-width:100%;
  background:#000; color:#f5ff00; border:1px solid #f5ff00; border-radius:100px;
  padding:3px 8px; font-family:var(--font-body); font-size:0.75rem; font-weight:400;
  cursor:pointer; outline:none;
}
.nso-controls select.admin-text-input:focus{box-shadow:0 0 0 2px rgba(245,255,0,0.25);}
.nso-controls select.admin-text-input option{background:#000; color:#f5ff00;}

/* Text's own "Text" content field (a real typed <input>, not a <select>)
   -- same black background / yellow ring / yellow text / full-pill format
   as the selects just above, sized down to match them too. */
.nso-controls input.admin-text-input{
  display:block; width:100%; max-width:100%;
  background:#000; color:#f5ff00; border:1px solid #f5ff00; border-radius:100px;
  padding:3px 8px; font-family:var(--font-body); font-size:0.75rem; font-weight:400;
  outline:none;
}
.nso-controls input.admin-text-input::placeholder{color:rgba(245,255,0,0.5);}
.nso-controls input.admin-text-input:focus{box-shadow:0 0 0 2px rgba(245,255,0,0.25);}

/* Volume / Est. weight / Triangles readout -- same 3-up stat-card grid as
   index.php's .pg-readout, but fed REAL numbers computed from the
   currently-built mesh (see so3dComputeReadout() in storage-organizer.js)
   rather than that homepage demo's illustrative box-volume approximation.
   "Export time" from the demo doesn't have an honest equivalent here --
   STL export from an already-built Three.js mesh is effectively instant
   client-side -- so Triangles (already shown in the status line) takes
   that slot instead, since it's a real, useful number for anyone about
   to slice the file. */
.nso-readout{
  display:grid; grid-template-columns:repeat(3, 1fr); gap:0.75rem;
  margin-top:10px; padding:14px 16px;
  border:1px solid var(--border); border-radius:12px; background:var(--bg-elev);
}
.nso-readout .r-n{font-size:1.05rem; font-weight:700; color:var(--text);}
.nso-readout .r-l{font-size:0.68rem; color:var(--text-faint); text-transform:uppercase; letter-spacing:0.08em; margin-top:3px;}
@media (max-width:640px){
  .nso-readout{grid-template-columns:repeat(3, 1fr); gap:0.5rem; padding:12px;}
}

/* ---- History tab: the edit log + its A-to-Z playback bar ----
   Entries are written by recordTimelineEntry() in storage-organizer.js
   (see the History section's own markup comment in app/tool-panel.php).
   Scoped to .nso-controls like every other tool-specific rule in this
   file so none of it leaks into the three other print3d tools that
   share this stylesheet. */
.nso-controls .nso-history-playbar{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin-top:10px; margin-bottom:10px;
}
.nso-controls .nso-history-play-btn{
  display:inline-flex; align-items:center; gap:6px;
  padding:6px 14px; border-radius:100px; cursor:pointer;
  font-family:inherit; font-size:0.78rem; font-weight:700;
  border:1px solid rgba(245,255,0,0.45); background:rgba(245,255,0,0.10); color:#f5ff00;
  transition:background 0.15s ease;
}
.nso-controls .nso-history-play-btn:hover{background:rgba(245,255,0,0.2);}
.nso-controls .nso-history-play-btn:disabled{opacity:0.4; cursor:not-allowed; background:rgba(245,255,0,0.1);}
.nso-controls .nso-history-mini-btn{
  display:inline-flex; align-items:center; justify-content:center;
  width:30px; height:30px; border-radius:50%; cursor:pointer;
  font-family:inherit; font-size:0.95rem; line-height:1;
  border:1px solid var(--border-strong); background:transparent; color:var(--text);
  transition:background 0.15s ease;
}
.nso-controls .nso-history-mini-btn:hover{background:rgba(255,255,255,0.06);}
.nso-controls .nso-history-mini-btn:disabled{opacity:0.4; cursor:not-allowed; background:transparent;}
.nso-controls select.nso-history-speed{width:auto; min-width:62px; padding:5px 8px; font-size:0.75rem;}
.nso-controls .nso-history-counter{margin-left:auto; font-size:0.72rem; color:var(--text-faint); white-space:nowrap;}

/* The list itself -- capped in height with its own scroll so a long
   session's history never pushes the rest of the panel off-screen. */
.nso-controls .nso-history-list{
  display:flex; flex-direction:column; gap:4px;
  max-height:420px; overflow-y:auto; overscroll-behavior:contain;
}
.nso-controls .nso-history-row{
  display:flex; align-items:flex-start; gap:9px; width:100%; text-align:left;
  padding:7px 9px; border-radius:8px; cursor:pointer;
  font-family:inherit; border:1px solid transparent; background:var(--bg-elev);
  color:var(--text); transition:background 0.12s ease, border-color 0.12s ease;
}
.nso-controls .nso-history-row:hover{background:var(--bg-elev-2); border-color:var(--border);}
/* The step the model is actually showing right now. */
.nso-controls .nso-history-row.current{
  border-color:rgba(245,255,0,0.5); background:rgba(245,255,0,0.07);
}
/* Steps AFTER the current one -- still replayable (click to go forward,
   same as redo), just dimmed so "where am I in the timeline" reads at a
   glance rather than needing the step counter. */
.nso-controls .nso-history-row.future{opacity:0.45;}
/* A step belonging to a version a non-author isn't allowed to jump
   straight into (see isGroupLocked() in storage-organizer.js) -- still
   fully readable (label/detail unchanged), just visibly un-clickable. */
.nso-controls .nso-history-row.locked{cursor:not-allowed; opacity:0.55;}
.nso-controls .nso-history-row.locked:hover{background:var(--bg-elev); border-color:transparent;}
.nso-controls .nso-history-num{
  flex:0 0 auto; min-width:20px; padding-top:1px;
  font-size:0.66rem; font-weight:700; color:var(--text-faint);
  font-variant-numeric:tabular-nums;
}
.nso-controls .nso-history-row.current .nso-history-num{color:#f5ff00;}
.nso-controls .nso-history-body{flex:1 1 auto; min-width:0;}
.nso-controls .nso-history-label{
  font-size:0.78rem; font-weight:600; line-height:1.3;
  overflow-wrap:anywhere;
}
.nso-controls .nso-history-detail{
  font-size:0.7rem; color:var(--text-dim); margin-top:2px; line-height:1.35;
  overflow-wrap:anywhere;
}
.nso-controls .nso-history-detail .h-to{color:#f5ff00; font-weight:600;}

/* Saved-version groups (see timelineGroups() in storage-organizer.js):
   every History step between one Save and the next is collapsed under a
   clickable header naming who saved it ("You" or the original author's
   username -- see timelineAuthorLabel()) and when, with its own small
   Play control to replay just that version rather than the whole
   timeline. Steps made since the last save (if any) get their own
   trailing, unlabeled-as-a-version "Unsaved changes" group at the bottom. */
.nso-controls .nso-history-group{
  border:1px solid var(--border); border-radius:9px; overflow:hidden;
  background:var(--bg-elev);
  /* .nso-history-list is a column flexbox with its own overflow-y:auto
     (below) meant to scroll when its total content is taller than the
     list's max-height. But this group's own overflow:hidden (kept for the
     rounded corners) makes its flexbox auto-minimum size 0 per spec, so a
     flex-shrink:1 default let a tall expanded group get silently squeezed
     down to fit the list's box -- clipping its own bottom rows via that
     same overflow:hidden instead of ever reaching the list's scrollbar.
     flex-shrink:0 keeps every group at its natural content height, so an
     expanded group with many steps correctly pushes the list past its
     max-height and the list's own scrollbar (not clipping) is what kicks
     in. */
  flex-shrink:0;
}
.nso-controls .nso-history-group + .nso-history-group{margin-top:6px;}
.nso-controls .nso-history-group-header{
  display:flex; align-items:center; gap:8px; width:100%; text-align:left;
  padding:8px 10px; cursor:pointer; font-family:inherit;
  border:none; background:transparent; color:var(--text);
  transition:background 0.12s ease;
}
.nso-controls .nso-history-group-header:hover{background:var(--bg-elev-2);}
/* The group currently holding the model's own step gets the same accent
   treatment a lone .nso-history-row.current uses, so "which version am I
   looking at" reads at a glance even while collapsed. */
.nso-controls .nso-history-group.current-group > .nso-history-group-header{
  background:rgba(245,255,0,0.07);
}
.nso-controls .nso-history-group-caret{
  flex:0 0 auto; font-size:0.6rem; color:var(--text-faint);
  transition:transform 0.15s ease; transform:rotate(0deg);
}
.nso-controls .nso-history-group.expanded .nso-history-group-caret{transform:rotate(90deg);}
.nso-controls .nso-history-group-title{flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:1px;}
.nso-controls .nso-history-group-name{font-size:0.8rem; font-weight:700;}
.nso-controls .nso-history-group-meta{
  font-size:0.68rem; color:var(--text-faint); overflow-wrap:anywhere;
}
/* "unpublished ones visibly listed but locked" -- explains why a
   non-author can't jump into this version's own steps, right on its
   header rather than only being discoverable by trying to click one. */
.nso-controls .nso-history-group-lock{
  font-size:0.66rem; color:var(--text-faint); font-weight:600;
}
.nso-controls .nso-history-group.locked{opacity:0.82;}
.nso-controls .nso-history-group-play{
  flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center;
  width:24px; height:24px; border-radius:50%; cursor:pointer;
  font-size:0.62rem; border:1px solid var(--border-strong); background:transparent;
  color:var(--text); transition:background 0.12s ease, color 0.12s ease;
}
.nso-controls .nso-history-group-play:hover{background:rgba(245,255,0,0.15); color:#f5ff00; border-color:rgba(245,255,0,0.45);}
.nso-controls .nso-history-group-body{
  display:flex; flex-direction:column; gap:4px;
  padding:0 7px 7px;
}
.nso-controls .nso-history-group-body[hidden]{display:none;}
.nso-controls .nso-history-group-body .nso-history-row{background:var(--bg-elev-2);}

/* The small "select this version for multi-playback" checkbox in each
   group header (see renderHistoryList()) -- sits before the caret so it
   reads left-to-right as "pick me, then open me". */
.nso-controls .nso-history-group-select{
  flex:0 0 auto; width:15px; height:15px; margin:0; cursor:pointer; accent-color:#f5ff00;
}
/* "starts from Version N, step S" continuation line (see
   timelineGroups()'s own startsFrom info) -- a step down in prominence
   from the author/date/step-count meta line just above it. */
.nso-controls .nso-history-group-continues{
  font-size:0.66rem; color:var(--text-faint); font-style:italic; margin-top:1px;
}

/* Multi-select playback bar -- hidden (see the `hidden` attribute in
   app/tool-panel.php) until at least one saved version is checked. */
.nso-controls .nso-history-selection-bar{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin-bottom:8px; padding:6px 9px; border-radius:8px;
  background:rgba(245,255,0,0.07); border:1px solid rgba(245,255,0,0.25);
}
.nso-controls .nso-history-selection-bar[hidden]{display:none;}
.nso-controls #nso-history-selected-count{font-size:0.72rem; color:var(--text-dim);}

/* Author-only History controls (see isProjectAuthor in
   storage-organizer.js) -- "author can delete version or step which
   author find it irrelevant and also select which version to publish to
   public", per the user's own explicit request. Never shown to a viewer
   who is just looking at someone else's shared model. */

/* Per-step delete -- sits at the far right of its own row, same
   role="button" treatment as the per-group Play control (a nested real
   <button> would be invalid HTML inside the row's own <button>). */
.nso-controls .nso-history-row-delete{
  flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center;
  width:20px; height:20px; margin-top:1px; border-radius:50%; cursor:pointer;
  font-size:0.62rem; color:var(--text-faint); transition:background 0.12s ease, color 0.12s ease;
}
.nso-controls .nso-history-row-delete:hover{background:rgba(255,90,90,0.15); color:#ff6b6b;}

/* Per-version "Publish"/"Public" toggle -- sits in the group header next
   to the existing Play control. Outlined star = "click to make this the
   public version"; filled/highlighted = "already is". */
.nso-controls .nso-history-group-publish{
  flex:0 0 auto; display:inline-flex; align-items:center; gap:3px;
  padding:3px 8px; border-radius:999px; cursor:pointer; white-space:nowrap;
  font-size:0.64rem; font-weight:600; border:1px solid var(--border-strong);
  background:transparent; color:var(--text-dim);
  transition:background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.nso-controls .nso-history-group-publish:hover{border-color:rgba(245,255,0,0.45); color:#f5ff00;}
.nso-controls .nso-history-group-publish.is-published{
  background:rgba(245,255,0,0.12); border-color:rgba(245,255,0,0.5); color:#f5ff00;
}

/* Per-version/per-group delete -- same circular treatment as the row
   delete, just sized to match the header's other icon controls. */
.nso-controls .nso-history-group-delete{
  flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center;
  width:24px; height:24px; border-radius:50%; cursor:pointer;
  font-size:0.62rem; color:var(--text-faint); transition:background 0.12s ease, color 0.12s ease;
}
.nso-controls .nso-history-group-delete:hover{background:rgba(255,90,90,0.15); color:#ff6b6b;}
