/* GOODWOOD 360 — editorial base styles (ported from v2) */
  :root{
    --ink:#1a1a18; --mute:#9a9a95; --faint:#cfcfca; --line:#eceae6; --bg:#fff;
    /* A true dark gray, distinct from --ink (which is a near-black warm
       ink used for body copy) — used for "selected"/"active" emphasis in
       the Products/Material tab switcher and the Material swatch picker,
       where --ink read as flat pure black rather than an intentional
       accent shade. */
    --charcoal:#55534c;
    --serif:Georgia,"Iowan Old Style","Times New Roman",serif;
    --sans:"Manrope",system-ui,-apple-system,"Segoe UI",sans-serif;
    /* Used only by the new admin-panel additions below (selection bar,
       "Rebuild manifest" emphasis, the incomplete-filter active state) —
       nothing on the visitor-facing page references these, so defining them
       cannot shift any existing color anywhere. */
    --accent:#9a6a3c; --accent-soft:rgba(154,106,60,.14); --accent-ink:#6e4b28;
    /* Shared width basis for the header/footer column AND the stage column
       (see .block and .stagewrap below) — defined once here so both can
       reference the identical formula without one deriving from the
       other's rendered size (deriving .stagewrap's width from "100% of
       .block" is what made it impossible to make .block wider than
       .stagewrap without .stagewrap silently growing to match and erasing
       the difference). */
    /* 4th term = the width actually free for .block: viewport minus the
       left edge content starts at (--content-left: 22px normally, or just
       past the open Products panel — set by adjustContentForPanel() in
       js/library.js) minus the 22px right padding, minus .block's own
       2 x overhang, since .block = --stage-col + 2 x overhang. Leaving the
       overhang out is what let the header run ~72px past the right edge
       and clip "Products"/"Fullscreen" on narrower windows. --oh-fit is 0
       on phones, where that part of the layout works differently. */
    --oh-fit:72px;
    --stage-col:min(96vw,var(--stage-vh,80vh),1200px,calc(100vw - var(--content-left,22px) - 22px - 2*var(--oh-fit)));
    /* Same, as if the panel were closed — read (via .lib-width-probe) to
       decide whether the panel would overlap the centered layout at all. */
    --stage-col-natural:min(96vw,var(--stage-vh,80vh),1200px,calc(100vw - 44px - 2*var(--oh-fit)));
    --logo:url("data:image/webp;base64,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");
  }
  *{box-sizing:border-box;}
  html,body{height:100%;}
  /* overflow-x:hidden — body.lib-open below can push .app's left edge past
     the right side of the viewport on narrow screens (the panel goes full
     width there), which would otherwise open a horizontal scrollbar. */
  body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);overflow-x:hidden;}

  /* flex-start (not center) so the header never shifts when content below it
     changes height (upload's back-home button, etc.) — centering the whole
     block against a variable-height child re-centers on every change,
     dragging the header along with it. */
  /* transform (not margin-left) drives the collision-aware shift — see
     adjustContentForPanel() in js/library.js. margin-left was the original
     approach, but on this element it turned out to silently not apply at
     all in real Chrome (confirmed live: margin-top/margin-right/transform
     all worked on this exact node, margin-left alone never took effect —
     unexplained after extensive isolation, not a specificity/cascade/JS
     conflict; nothing else in this codebase touches it). The visible
     symptom was a content jump to the wrong place (flush against .app's
     own edge, ignoring the panel's actual width) instead of the intended
     shifted position, since align-items still switched instantly while the
     margin silently no-opped. transform:translateX() is verified to work
     reliably here, and is also compositor-only — lighter than a
     margin-left transition, which forces layout on every frame. */
  /* --content-tx moves the centered layout just far enough to clear the
     open Products panel (never re-centers it in the leftover space — a
     5px overlap must mean a ~65px nudge, not a 300px jump). Both it and
     --content-left (which narrows .block via --stage-col) are computed in
     js/library.js from the probe's NATURAL width, never from .block while
     it's mid-transition. */
  .app{min-height:100vh;display:flex;flex-direction:column;justify-content:flex-start;align-items:center;padding:32px 22px 16px;
       transform:translateX(var(--content-tx,0px));transition:transform .3s ease;}
  .lib-width-probe{position:absolute;left:0;top:0;height:0;visibility:hidden;pointer-events:none;
       width:calc(var(--stage-col-natural) + 2*var(--stage-overhang,72px));}
  /* While the panel is being actively drag-resized, the shift transform
     recomputes on every mousemove — a .3s easing transition can't keep up
     with that rate of change, so it visibly lags/rubber-bands behind the
     cursor instead of tracking it. Cutting the transition during the drag
     makes .block follow the handle 1:1; re-enabling it on release is what
     makes the SETTLE (or any later open/close) still animate smoothly. */
  /* .lib-resize-handle included here too — it now has its own transition:left
     (so it slides smoothly on open/close, since it moved outside .library
     in the DOM, see .lib-resize-handle below), and that same "timed
     transition chasing a value that changes every mousemove" problem hit
     it independently of .app/.block: while actively dragging, the handle's
     grip icon visibly lagged behind the panel's real edge instead of
     sticking to it. */
  /* body.resizing — the very same lag, triggered by an actual window
     resize instead of a divider drag. Dragging the OS window edge fires a
     'resize' event on nearly every frame, and adjustContentForPanel() /
     the .block width formula recompute on each one; with the transition
     still active, the shift transform/width/left spent the whole resize
     gesture chasing a constantly-moving target instead of tracking it,
     then snapped hard once the drag stopped. See the debounced
     resizing-class toggle in js/library.js — it's held for as long as
     resize events keep arriving and cleared (transition back on) once
     they stop, so the FINAL settle after you let go of the window edge is
     still animated. */
  body.lib-dragging .app,body.lib-dragging .block,body.lib-dragging .lib-resize-handle,
  body.resizing .app,body.resizing .block,body.resizing .lib-resize-handle{transition:none;}
  /* Products/Material panel is position:fixed (an overlay, not docked —
     see .library below), so by default it floats OVER the page without the
     rest of the layout knowing it's there.
     Collision-aware shift (JS-driven, see adjustContentForPanel() in
     js/library.js): the landing/centered layout (align-items:center, no
     transform) is the default and STAYS the default even while the panel
     is open, as long as there's enough room either side of the centered
     product view for the panel not to touch it. Only when the panel's live
     width would actually overlap the centered content does JS apply an
     inline transform:translateX() — just far enough to clear the overlap,
     never more, and never unconditionally. align-items stays center the
     whole time now (no separate flex-start state needed): translating the
     whole .app box by (target position − its current centered position)
     lands .block exactly on the intended target without having to also
     flip how it's aligned inside .app. This replaced an earlier version
     that shifted content right by the FULL panel width any time the panel
     was open, even on wide monitors with plenty of room — which the user
     correctly flagged as unnecessary and visually "weirder", not better. */
  /* .block sets the width of the HEADER and FOOTER.
     UPDATE 2026-08-28: this used to deliberately ignore --stage-vh (the
     live-measured height budget) so the header/footer always rendered at
     their full 96vw/1200px width, even when the square stage itself was
     forced smaller by limited vertical room — reasoning: tying to it
     "visibly shortened the header on shorter windows". That tradeoff
     reversed on typical desktop monitors once the width cap was raised to
     1200px (see below): screens are wider than tall, so height is now
     USUALLY the binding constraint on the stage, and header/footer would
     stretch noticeably wider than the product square beneath them — which
     read as broken, not intentional. Capping .block to var(--stage-vh) as
     well now matches header/footer to whatever the stage actually rendered
     at. The old downside (a short-but-wide window narrows the header) can
     recur — if that's reported again, the two concerns need a real
     trade-off decision, not another blind toggle. */
  /* The 4th --stage-col term (see :root) keeps .block inside the free
     width, including when the Products panel is open. */
  /* min-width:0 — .block is a flex item of .app, and flex items default to
     min-width:auto, which floors their shrink at the CONTENT's min-content
     size rather than honoring `width` below that. Before the panel could
     push the target width narrower than the content's own minimum (mainly
     the product stage, sized independently via --stage-vh), that floor was
     never actually reached so this went unnoticed. Once the collision-aware
     shift (adjustContentForPanel in js/library.js) could ask for a width
     smaller than --stage-vh — which happens on an ordinary resize while the
     panel is open — .block silently ignored it and stayed at its old,
     wider size, so the header/viewer rendered past the visible edge or sat
     underneath the panel instead of next to it. */
  /* + 2*var(--stage-overhang) — the product image (.stage) deliberately
     bleeds past its own column by --stage-overhang on each side (see
     .stage below), so it renders WIDER than --stage-col alone. Header and
     footer were only ever matching the narrower unbled column, not what a
     visitor actually sees as "the product view" — this widens them to
     match .stage's real, bled width instead. */
  .block{width:calc(var(--stage-col) + 2*var(--stage-overhang,72px));min-width:0;padding-bottom:28px;position:relative;
         transition:width .3s ease;}

  /* flex-wrap:wrap — .block now regularly gets squeezed narrower than its
     old fixed range (the Products/Material panel shrinks it to make room
     for itself, see .block's width formula above), and neither .brand nor
     .actions ever wrapped internally, so at a narrow-enough width the two
     sides collided/overlapped instead of making room ("SANDBOX" running
     straight into "PRODUCTS" with no gap). Wrapping .actions onto its own
     row when it doesn't fit is a graceful, always-safe fallback — at any
     width with room to spare (the common case) nothing changes; wrap only
     ever engages once .top would otherwise overflow/collide. */
  .top{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;row-gap:8px;
       padding-bottom:14px;border-bottom:1px solid var(--line);animation:rise .6s ease both .05s;}
  /* margin:0 — .brand is an <h1> (the page had no heading at all before,
     leaving screen readers without a document title in the content). The UA
     stylesheet gives h1 a ~0.67em block margin, which would have pushed the
     header down; every other value here was already explicit, so only the
     margin needed neutralising to keep the rendering byte-identical. */
  .brand{display:flex;align-items:center;margin:0;font-family:var(--sans);font-size:22px;font-weight:600;letter-spacing:-.005em;line-height:1;
         text-decoration:none;color:inherit;cursor:pointer;}
  .brand i{font-style:normal;font-family:var(--sans);font-size:10.5px;letter-spacing:.16em;
           text-transform:uppercase;color:var(--mute);margin-left:10px;vertical-align:middle;}
  /* Anchored to .block (flush with the header's own edges, same as the nav
     buttons in .top which sit flush too) rather than to .stage, whose own
     width shrinks on short windows — see the HTML comment above these
     elements. --stage-top-offset is published live by fitStageHeight() in
     viewer.js: the measured distance from .block's top to .stagewrap's top,
     so this still lines up with the top of the product image itself even
     though it's no longer a descendant of .stage.
     Deliberately NOT uppercase, unlike the rest of the chrome: this is a
     product path, and SKUs like Terra1Seater-NT-SUN_Bluebay_080 get much
     harder to read shouted in caps. max-width stops a long path from running
     underneath the badge. */
  .crumb{position:absolute;top:calc(var(--stage-top-offset,86px) + 14px);left:0;z-index:3;
         max-width:calc(100% - 120px);   /* everything up to the 360° badge (stage-topright); ellipsis only when that runs out */
         display:flex;align-items:center;gap:10px;min-width:0;
         /* 16px + --mute = exactly the header nav (Products / Fullscreen),
            so the title and the nav read as one line of type */
         font-family:var(--sans);font-size:16px;letter-spacing:-.01em;
         text-transform:none;color:var(--mute);line-height:32px;
         /* Matches .top/.stagewrap's entrance fade — without this it popped
            in instantly while everything around it eased in, which read as
            a glitch on refresh. Safe to animate transform here: this only
            moves .crumb itself, not .stagewrap, so it can't reintroduce the
            --stage-top-offset measurement bug (that was about measuring
            .stagewrap mid-animation, not .crumb). */
         animation:rise .6s ease both .2s;}
  .crumb-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
         /* Selectable on purpose (unlike the rest of this decorative overlay
            chrome) — the path/SKU here is exactly the kind of text a visitor
            wants to copy. pointer-events:auto only affects this element; the
            stage underneath is untouched since .crumb sits in the gap above
            it, not over the product image. */
         pointer-events:auto;cursor:text;user-select:text;-webkit-user-select:text;}
  /* Reuses the same visual language as .stage-icon-btn (26px circle) but
     smaller, since it sits inline with 11.5px breadcrumb text rather than
     alone in a stage corner. flex:none so a long path never squeezes it. */
  .crumb-share{flex:none;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;
         width:26px;height:26px;border:1px solid var(--line);border-radius:999px;
         background:rgba(255,255,255,.7);color:var(--faint);cursor:pointer;padding:0;
         pointer-events:auto;transition:color .15s,border-color .15s,background .15s;}
  .crumb-share:hover{color:var(--ink);border-color:var(--ink);background:#fff;}
  .crumb-share:active{background:#f1f1ef;}
  .crumb-share svg{width:13px;height:13px;}
  .actions{display:flex;gap:28px;}
  /* Same type as the sidebar's Products/Materials tabs (16px regular,
     sentence case) so the page header and the panel read as one system. */
  .ctl{font-family:var(--sans);font-size:16px;font-weight:400;line-height:25px;letter-spacing:-.01em;
       color:var(--mute);background:none;border:0;padding:0;cursor:pointer;transition:color .2s;}
  .ctl:hover{color:var(--ink);}
  /* (removed) .res/.res-lbl/.res-opt/.res-sep — a resolution-switcher UI
     that no longer exists in any page's markup. */

  /* .stage has no independent height by design (aspect-ratio:1/1 + height:auto
     further down), so its WIDTH governs the rendered height. Capping the wrap
     here shrinks the product to fit the room that actually exists — measured
     live by fitStageHeight() in js/viewer.js — without touching the header. */
  /* var(--stage-col), not min(100%,...) — this used to size itself as a
     percentage of .block, which was safe back when .block's own width WAS
     --stage-col exactly. Now that .block is intentionally wider (it adds
     the stage's bleed on top, see .block above), sizing off "100% of
     .block" would have pulled that extra width in here too and compounded
     it into .stage's own bleed math below, overshooting further on every
     reflow. Referencing the same shared --stage-col directly keeps this
     independent of whatever .block ends up rendering at. */
  .stagewrap{display:flex;justify-content:center;animation:rise .7s ease both .16s;
             width:var(--stage-col);margin-left:auto;margin-right:auto;}
  .stage-row{display:flex;gap:16px;align-items:flex-start;width:100%;justify-content:center;}
  .stage-row .stage{flex:1;min-width:0;}
  .stage{position:relative;
         width:calc(100% + 2 * var(--stage-overhang, 72px));
         max-width:none;
         margin-left:calc(-1 * var(--stage-overhang, 72px));
         margin-right:calc(-1 * var(--stage-overhang, 72px));
         /* No `height` here on purpose: the flat `.stage{aspect-ratio:1/1;
            height:auto}` rule further down wins, so the square is derived
            from the width above. A `height:min(96vw,80vh,1200px)` used to
            sit here claiming to "match .block's cap" — it was already dead
            (overridden), and the claim went stale once .block moved to
            var(--stage-col) + bleed. Removed rather than corrected: a rule
            that never applies is worse than no rule. */
         background:var(--bg);
         overflow:hidden;cursor:grab;touch-action:none;user-select:none;}
  .stage.dragging{cursor:grabbing;}
  /* fullscreen: full viewport, centered, no horizontal overhang */
  .stage:fullscreen{width:100vw;height:100vh;margin:0;background:var(--bg);display:flex;align-items:center;justify-content:center;}
  .stage:fullscreen #frame{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
                           transform:scale(var(--fs-scale,0.92))!important;}
  #frame{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
         pointer-events:none;transform-origin:center center;will-change:transform;}
  .loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--bg);}
  /* Centered card (product-switch loader) — a spinner plus a title/subtitle
     tells the visitor in plain language what's happening and that it's
     expected to take a moment, instead of a bare progress bar that reads as
     frozen the instant it stops ticking between frames. */
  .loader-box{display:flex;flex-direction:column;align-items:center;width:min(370px,84%);
              padding:52px 44px;}
  .loader-spinner{width:22px;height:22px;border-radius:50%;flex:none;margin-bottom:16px;
                  border:2px solid var(--line);border-top-color:var(--ink);
                  animation:gw-spin .7s linear infinite;opacity:.6;}
  .loader-title{font-size:14px;font-weight:600;color:var(--ink);text-align:center;margin:0 0 5px;}
  /* min-height reserves room for the longest of the two subtitle strings (the
     slow-connection one wraps to 2 lines) so swapping between them never
     resizes the card — its outline stays put whichever text is showing. */
  .loader-sub{font-size:12px;color:var(--mute);text-align:center;line-height:1.45;margin:0;
              min-height:calc(1.45em * 2);}
  @keyframes gw-spin{to{transform:rotate(360deg)}}
  @media (prefers-reduced-motion:reduce){.loader-spinner{animation:none;border-top-color:var(--line);}}

  /* Shared "still alive" treatment — a wavefront pixel grid plus a shimmering
     label — for indicators that sit still for a while with no obvious motion
     of their own (currently: the "processing …" chip, shown while full-res
     frames keep filling in behind an already-interactive product). */
  .px-grid{display:grid;grid-template-columns:repeat(3,3px);gap:1.5px;flex:none;}
  .px-grid i{display:block;width:3px;height:3px;border-radius:1px;background:currentColor;opacity:.12;}
  .px-grid i.on{animation:gw-pixel-on 650ms ease-in-out infinite;}
  .shimmer-text{background-image:linear-gradient(90deg,var(--mute) 35%,var(--ink) 50%,var(--mute) 65%);
                background-size:200% 100%;-webkit-background-clip:text;background-clip:text;
                color:transparent;animation:gw-shimmer 1.4s linear infinite;}
  @keyframes gw-pixel-on{0%,100%{opacity:.15}50%{opacity:1}}
  @keyframes gw-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
  @media (prefers-reduced-motion:reduce){
    .px-grid i.on{animation:none;opacity:.55}
    .shimmer-text{animation:none;background:none;color:inherit;-webkit-text-fill-color:initial;}
  }

  /* fabric / resolution row — always reserves the same vertical space so the layout never shifts */
  /* fabric / resolution row — disabled in favor of the Library (apply flow) */
  .config{display:none !important;}
  .swatches{display:flex;flex-wrap:wrap;justify-content:center;gap:14px;max-width:100%;}
  .swatch{width:30px;height:30px;border-radius:50%;padding:0;border:1px solid rgba(0,0,0,.12);
          cursor:pointer;flex:none;background:#e9e7e2;transition:transform .18s,box-shadow .18s;}
  .swatch:hover{transform:translateY(-2px);}
  .swatch.active{box-shadow:0 0 0 1.5px var(--bg),0 0 0 3px var(--ink);}
  .swatch.loading{animation:pulse 1s ease-in-out infinite;}
  .fabric-name{font-size:12px;letter-spacing:.02em;color:var(--ink);min-height:14px;}

  /* Tried shrinking this gap to give the stage more room — reverted: .block's
     width (and so .stage's rendered size, since .stage follows it) is capped
     by a fixed ceiling (see .block above — 1200px as of 2026-08-28, was
     940px). On any viewport wide enough to hit that cap, the stage was
     ALREADY at its max size and could not grow — shrinking this gap didn't
     enlarge anything, it just shortened the page and pushed the footer up,
     leaving dead space below it instead. Back to the original 16px/14px;
     the real fix for "product looks small" is raising that cap, not this. */
  .bottom{display:flex;align-items:center;padding-top:14px;margin-top:16px;border-top:1px solid var(--line);
          animation:rise .6s ease both .26s;}
  .label{font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);white-space:nowrap;}
  .num{color:var(--ink);font-variant-numeric:tabular-nums;}
  .prog{flex:1;height:1px;background:var(--line);position:relative;margin:0 24px;}
  .prog i{position:absolute;left:0;top:0;height:1px;background:var(--ink);width:0;}
  /* var(--stage-col) — this error panel sits directly under the product
     column, so it tracks the same shared width basis .block/.stagewrap use
     instead of repeating an older hardcoded formula that no longer matched
     either of them (it predates --stage-col). */
  .panel{width:var(--stage-col);margin-top:16px;font-size:12.5px;color:#c92a2a;text-align:center;line-height:1.6;display:none;}
  /* Used to be its own row between the stage and footer (display:block,
     margin:8px 0) — reserved that space in the vertical flow even when it
     mattered least, leaving a gap the visitor couldn't explain. Moved into
     the header .actions row instead (styled by .ctl, same as Products/
     Fullscreen) — only occupies space when actually shown (upload preview
     mode), and the stage reclaims the rest. */
  .back-home[hidden]{display:none;}
  /* .aftermath used to wrap the version on its own row below the footer; the
     version moved INTO .bottom, and dropping that row is part of what gives
     the product its height back. Rule kept harmless in case of stale markup. */
  .bottom .version{margin-left:18px;flex:none;}
  .version{font-family:var(--sans);font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:var(--faint);}

  /* uploader overlay */
  .uploader{position:fixed;inset:0;background:var(--bg);z-index:50;display:none;
            flex-direction:column;align-items:center;justify-content:center;padding:24px;}
  .uploader.show{display:flex;}
  .up-card{width:min(92vw,540px);text-align:center;}
  .up-title{font-family:var(--sans);font-size:18px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;margin:0 0 8px;}
  .up-title small{font-size:10px;font-weight:400;letter-spacing:.06em;color:var(--mute);text-transform:none;margin-left:6px;}
  .up-sub{color:var(--mute);font-size:13px;margin:0 0 24px;line-height:1.5;}
  .dropzone{border:1.5px dashed #d9d8d2;border-radius:18px;padding:42px 26px;transition:.2s;}
  .dropzone.over{border-color:var(--ink);background:#fafaf8;}
  .dz-text{font-size:13px;color:var(--mute);margin:0 0 20px;}
  .up-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
  .btn{font:inherit;font-size:12px;letter-spacing:.06em;padding:11px 18px;border:1px solid var(--ink);
       border-radius:10px;background:var(--ink);color:#fff;cursor:pointer;}
  .btn.secondary{background:#fff;color:var(--ink);}
  .up-hint{font-size:11px;color:var(--faint);margin:20px 0 0;line-height:1.6;}
  .up-close{margin-top:22px;font-family:var(--sans);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);
            background:none;border:0;cursor:pointer;}

  /* library overlay */
  /* library as a left side panel */
  /* --lib-w is set inline by js/library.js while dragging the resize handle
     (persisted to localStorage so it survives a reload); 560px is the
     original fixed width, still the default until the visitor drags it. */
  /* A near-invisible 40px/.05-alpha blur read as barely a shadow at all —
     the panel sat flush against the page rather than reading as a raised
     surface floating over it. A tighter contact shadow plus a wider,
     slightly stronger ambient one gives it actual presence. */
  /* --mute is darkened inside the sidebar only: the site-wide #9a9a95 is
     2.8:1 on white (2.5:1 on the selected-row tint), below WCAG AA's 4.5:1
     for small text — and the sidebar is mostly small muted text (product /
     variant names, inactive tab, placeholders). #6f6d66 = 5.2:1 on white,
     4.6:1 on the selection tint. The rest of the site keeps its own gray. */
  .library{--mute:#6f6d66;}
  .library{position:fixed;top:0;bottom:0;left:0;width:min(92vw,var(--lib-w,560px));background:var(--bg);z-index:60;
           display:flex;flex-direction:column;padding:32px 24px 20px;
           border-right:1px solid var(--line);
           box-shadow:0 1px 3px rgba(0,0,0,.04),0 0 60px rgba(0,0,0,.09);
           transform:translateX(-102%);transition:transform .3s ease;}
  .library.show{transform:translateX(0);}
  /* An icon-collapse mode (Dribbble-inspired) was tried here and removed —
     a lone floating icon + toggle in an otherwise blank white column read
     as broken/unpolished rather than sleek. Back to a plain drawer. */
  /* panel overlays the page — the product stays exactly where it is */
  /* align-items:center, not baseline — baseline made sense when both children
     were text (.lib-title + a "Close" text button). .lib-close is now an
     icon-only button with no text baseline of its own, which baseline
     alignment resolves against the button's bottom margin edge instead,
     dropping it out of line with the tab pill beside it. */
  /* padding-bottom:0 — the active-tab underline (.lib-tab-highlight) sits
     directly on this border, so the tabs' own bottom padding provides the
     spacing instead. flex-start + the close button's negative margin keeps
     the X optically centered on the tab text, not on text+padding. */
  .lib-head{display:flex;align-items:flex-start;justify-content:space-between;
            border-bottom:1px solid var(--line);padding-bottom:0;}
  /* (removed) .lib-title — the panel head shows the Products/Material tab
     switcher now, not a text title. */
  /* Icon-only (an X), in the borderless rounded-square footprint the tree's
     own .set-pin button uses — a circular bordered variant was
     tried and rejected for looking out of place here. Refinement therefore
     lives in the glyph itself (thinner stroke, slightly inset, round caps)
     rather than in a ring around it.
     The panel's FOOTER close button (.lib-cancel) deliberately stays a text
     button — this one sits beside the tabs where a second text label
     competed with them for attention; the footer one has its row to itself.
     flex:none so the tabs can't squeeze it when the panel is dragged narrow. */
  .lib-close{flex:none;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;
             width:26px;height:26px;border:0;border-radius:7px;margin-top:0;
             background:none;color:var(--mute);cursor:pointer;padding:0;
             transition:color .15s,background .15s;}
  .lib-close:hover{color:var(--ink);background:rgba(0,0,0,.05);}
  .lib-close:active{background:rgba(0,0,0,.09);}

  /* Products/Materials tab switcher. Editorial underline tabs, not a pill segmented control. The pill (and
     the icons tried inside it) read as a generic UI-kit widget; premium
     furniture/material libraries use plain sentence-case words with a thin
     rule under the active one, sitting right on the header's own divider.
     .lib-tab-highlight is now that rule — still positioned by the same
     translateX+width logic in js/library.js. Same font-weight in both
     states so the measured width never changes between them. */
  .lib-tabs{position:relative;display:flex;gap:28px;}
  .lib-tab-highlight{position:absolute;bottom:-1px;left:0;width:0;height:2px;
    background:var(--charcoal);pointer-events:none;
    transition:transform .3s cubic-bezier(.4,0,.2,1),width .3s cubic-bezier(.4,0,.2,1);}
  /* 16px regular = the size production's plain "Products" panel title
     uses. line-height 25px + 14px bottom padding, with the panel's 32px
     top padding (= .app's), puts this header's divider on exactly the
     same line as the page header's divider (.top: 32px + its 25px
     baseline-aligned logo/nav row + 14px padding) — the panel and the
     page read as one horizon. */
  .lib-tab{position:relative;font-family:var(--sans);font-size:16px;font-weight:400;line-height:25px;
    letter-spacing:-.01em;color:var(--mute);background:none;border:0;cursor:pointer;
    padding:0 0 14px;transition:color .2s;white-space:nowrap;}
  .lib-tab:hover{color:var(--charcoal);}
  .lib-tab.is-active{color:var(--ink);}
  .lib-tab:focus-visible{outline:none;color:var(--ink);}
  .lib-tab:focus-visible::after{content:"";position:absolute;inset:-4px -6px 10px;
    border:1px solid var(--mute);}
  /* Used to sit flush against .lib-head's own bottom border because the
     hint paragraph that used to open each panel provided the breathing
     room above the search box as a side effect — removing that hint left
     the search box crowding the divider line right above it. */
  .lib-panel{display:flex;flex-direction:column;flex:1;min-height:0;padding-top:16px;}
  .lib-panel[hidden]{display:none;}
  .lib-material-placeholder{flex:1;display:flex;align-items:center;justify-content:center;
    text-align:center;font-family:var(--sans);font-size:12px;color:var(--faint);
    letter-spacing:.02em;padding:40px 20px;}
  /* Material tab — flat list of category sections (Wood/Fabric/Aluminium/
     Rope/Stone), all shown at once and scrolled together rather than one
     at a time, per the reference the user shared. Every category is
     treated as an equal-weight swatch grid in the UI even though the
     rendering pipeline treats Wood differently under the hood (Wood swaps
     the base Master render, the rest are compositable deltas) — that
     distinction is a backend/data concern, not something the UI surfaces. */
  .lib-material-tree{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;
    padding:6px 18px 16px 8px;}
  /* Sections are separated with generous whitespace only — no divider
     lines. A border-top per section looked unprofessional and, while the
     panel is actively being resized, visibly snapped/broke as the grid's
     row count changed underneath it, since its width and vertical position
     both depend on the reflow happening at the same time. Spacing alone has
     nothing to misalign. */
  .lib-mat-section{margin-top:26px;padding-top:24px;border-top:1px solid var(--line);}
  .lib-mat-section:first-child{margin-top:0;padding-top:0;border-top:0;}
  /* Category header as a real typographic heading with the option count on
     the right — the tiny letter-spaced uppercase label it replaces is the
     generic "UI kit section label" look; a sentence-case heading at a
     confident size gives the list an editorial, catalog-like hierarchy. */
  /* The whole header row is the fold/unfold button (h3 kept around it for
     document structure). */
  .lib-mat-heading{margin:0 0 14px;font:inherit;}
  .lib-mat-head{display:flex;align-items:center;gap:12px;width:100%;padding:2px 0;margin:0;
    background:none;border:0;cursor:pointer;text-align:left;font-family:var(--sans);color:inherit;}
  .lib-mat-head:focus-visible{outline:1px solid var(--mute);outline-offset:4px;}
  .lib-mat-label{font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--ink);}
  .lib-mat-count{margin-left:auto;font-size:11px;color:var(--mute);white-space:nowrap;
    font-variant-numeric:tabular-nums;}
  .lib-mat-chev{flex:none;display:inline-flex;color:var(--mute);transition:transform .25s ease,color .15s;}
  .lib-mat-head:hover .lib-mat-chev{color:var(--ink);}
  .lib-mat-section.is-collapsed .lib-mat-chev{transform:rotate(-90deg);}
  .lib-mat-section.is-collapsed .lib-mat-heading{margin-bottom:0;}
  /* Height animates via grid-template-rows 1fr <-> 0fr (no measured
     heights). Clipping only applies while folded or mid-animation
     (.is-animating, set briefly by js/library.js) — the rest of the time
     overflow stays visible so hover lift shadows and the selected frame
     around edge swatches are never cut off. */
  .lib-mat-body{display:grid;grid-template-rows:1fr;transition:grid-template-rows .3s ease;}
  .lib-mat-section.is-collapsed .lib-mat-body{grid-template-rows:0fr;}
  .lib-mat-body-in{min-height:0;transition:opacity .25s ease;}
  .lib-mat-section.is-collapsed .lib-mat-body-in{opacity:0;}
  .lib-mat-section.is-collapsed .lib-mat-body-in,.lib-mat-section.is-animating .lib-mat-body-in{overflow:hidden;}
  /* Fixed column count (not auto-fill/minmax) — minmax-based reflow changed
     the column count at arbitrary width thresholds while dragging the
     panel's resize handle, which read as the grid "breaking"/re-wrapping
     mid-drag. A steady 3 columns keeps row membership constant; only the
     swatch size itself scales with the panel width. */
  /* Fixed-size swatches: the tiles never scale with the panel — widening
     it just fits more of them per row (auto-fill of 150px columns). min()
     only lets a tile shrink when the panel is dragged narrower than one
     tile (320px minimum), so it never overflows. */
  .lib-mat-grid{display:grid;grid-template-columns:repeat(auto-fill,min(150px,100%));gap:22px 16px;}
  /* Grid/flex items default to min-width:auto, which floors their shrink
     at their content's own min-content width — an unbroken token like
     "SUN_Bluebay_080" (no spaces to wrap on) has no smaller min-content
     size, so it refused to shrink with the column and forced the whole
     grid wider than the panel (clipped swatch + horizontal scrollbar on a
     narrow drag). min-width:0 lifts that floor so the swatch actually
     shrinks with its column; the code text itself is truncated below. */
  .lib-mat-swatch{display:flex;flex-direction:column;align-items:flex-start;
    min-width:0;width:100%;background:none;border:none;padding:0;margin:0;
    border-radius:0;cursor:pointer;font-family:var(--sans);text-align:left;}
  .lib-mat-swatch:focus-visible{outline:none;}
  /* Square 1:1 sample cards, square corners. The resting edge
     is an inset hairline (so pale swatches like stone still have a
     boundary) instead of a border that shifted size between states. */
  .lib-mat-chip{display:block;position:relative;width:100%;aspect-ratio:1;
    background-blend-mode:soft-light;
    box-shadow:inset 0 0 0 1px rgba(0,0,0,.07);transition:box-shadow .15s ease;}
  .lib-mat-swatch:hover .lib-mat-chip{box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);}
  /* Selected = a framed sample: charcoal rule hugging the edge with a thin
     white mat inside it. One shape, square, no floating ring and no second
     highlight element under the text. */
  .lib-mat-swatch.is-selected .lib-mat-chip{
    box-shadow:inset 0 0 0 3px #fff,0 0 0 1.5px var(--charcoal);}
  .lib-mat-swatch:focus-visible .lib-mat-chip{box-shadow:inset 0 0 0 1px rgba(0,0,0,.07),0 0 0 2px var(--mute);}
  /* Dummy-data textures — procedural SVG noise (feTurbulence) blended
     soft-light over each swatch's own color, one recipe per category, so
     placeholders read as actual grain/weave/brushed/stone surfaces instead
     of flat fills or obviously-CSS stripes. Swap for real swatch photos
     once they exist (see D:\Material_Untuk_Website.md). */
  .lib-mat-chip--wood{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.5 .016' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='2' intercept='-.5'/%3E%3CfeFuncG type='linear' slope='2' intercept='-.5'/%3E%3CfeFuncB type='linear' slope='2' intercept='-.5'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");}
  .lib-mat-chip--fabric{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1.7' intercept='-.35'/%3E%3CfeFuncG type='linear' slope='1.7' intercept='-.35'/%3E%3CfeFuncB type='linear' slope='1.7' intercept='-.35'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
    repeating-linear-gradient(0deg,rgba(255,255,255,.22) 0 1px,rgba(0,0,0,.12) 1px 2px,transparent 2px 3px),
    repeating-linear-gradient(90deg,rgba(255,255,255,.16) 0 1px,rgba(0,0,0,.1) 1px 2px,transparent 2px 3px);}
  /* Aluminium = matte powder-coat: one fine, low-contrast grain layer,
     soft-light on true 50% grey (sRGB filter space — linearRGB made the
     noise render light and washed the finish out). No large-scale
     mottle: it read as a gradient. */
  /* + one soft, directional light (top-left +8% → bottom-right −5%, centre
     untouched) — the same on every chip, like a single light source; not
     random mottle. Normal-blended white/black washes, so the finish colour
     in the middle of the chip is exact. */
  .lib-mat-chip--aluminium{background-image:linear-gradient(135deg,rgba(255,255,255,.08) 0%,rgba(255,255,255,0) 45%,rgba(0,0,0,0) 55%,rgba(0,0,0,.05) 100%),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.8' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.336' intercept='0.332'/%3E%3CfeFuncG type='linear' slope='0.336' intercept='0.332'/%3E%3CfeFuncB type='linear' slope='0.336' intercept='0.332'/%3E%3CfeFuncA type='linear' slope='0' intercept='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    background-size:100% 100%,160px 160px;background-blend-mode:normal,soft-light;}
  .lib-mat-chip--rope{background-image:repeating-linear-gradient(50deg,rgba(255,255,255,.55) 0 2px,rgba(0,0,0,.35) 2px 5px,transparent 5px 7px),
    repeating-linear-gradient(-50deg,rgba(255,255,255,.3) 0 2px,rgba(0,0,0,.3) 2px 5px,transparent 5px 7px),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");}
  .lib-mat-chip--stone{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.045' numOctaves='5' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1.5' intercept='-.3'/%3E%3CfeFuncG type='linear' slope='1.5' intercept='-.3'/%3E%3CfeFuncB type='linear' slope='1.5' intercept='-.3'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.6' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");}
  /* Single-line + ellipsis, not wrapping — a 2-line name (e.g. "Weathered
     Oak") made that row of the grid taller than rows whose names fit on
     one line, so swatches read as uneven sizes even though the chips
     themselves were always the same size. Truncating keeps every card the
     same height regardless of name length. */
  /* Color-only emphasis, not font-weight — switching to bold on selection
     changes the text's own metrics (the ellipsis truncation point shifts
     with it), which read as jittery/unpolished. Starting from a muted
     default and lighting up to full ink on selection gives a clear, stable
     highlight with no layout movement. */
  .lib-mat-name{margin-top:10px;font-size:12.5px;font-weight:500;letter-spacing:-.005em;
    color:var(--mute);line-height:1.3;max-width:100%;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .15s ease;}
  .lib-mat-swatch:hover .lib-mat-name{color:var(--charcoal);}
  .lib-mat-swatch.is-selected .lib-mat-name{color:var(--ink);}
  .lib-mat-code{margin-top:2px;font-size:10.5px;color:var(--faint);letter-spacing:.02em;
    font-variant-numeric:tabular-nums;max-width:100%;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .15s ease;}
  .lib-mat-swatch.is-selected .lib-mat-code{color:var(--mute);}
  .lib-mat-empty{font-family:var(--sans);font-size:12px;color:var(--mute);padding:32px 0;text-align:center;}
  /* Materials header block: product on screen + the picked material set */
  .lib-mat-current{flex:none;margin:0 0 22px;padding:14px 16px;background:#faf9f7;border:1px solid var(--line);
    font-family:var(--sans);}
  .lmc-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;}
  .lmc-product{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    font-size:14px;font-weight:600;color:var(--ink);letter-spacing:-.005em;}
  .lmc-clear{flex:none;background:none;border:0;padding:0;cursor:pointer;font:inherit;font-size:11.5px;color:var(--mute);
    text-decoration:underline;text-decoration-color:var(--faint);text-underline-offset:3px;}
  .lmc-clear:hover{color:var(--ink);text-decoration-color:currentColor;}
  .lmc-list{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:6px;}
  .lmc-list li{display:flex;align-items:center;gap:8px;min-width:0;font-size:12.5px;}
  .lmc-cat{flex:none;width:76px;color:var(--mute);}
  .lmc-chip{flex:none;width:14px;height:14px;background-blend-mode:soft-light;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08);}
  .lmc-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);}
  .lmc-empty{margin:6px 0 0;font-size:12px;color:var(--mute);}
  /* .lib-mat-swatch{display:flex} above outranks the UA [hidden]{display:none}
     rule (same 0,1,0 specificity, author sheet wins) — the same class of bug
     hit elsewhere in this project. Search filtering sets .hidden on swatches,
     so it needs an explicit win here. */
  .lib-mat-swatch[hidden]{display:none;}
  /* Drag handle on the panel's own right edge — widens/narrows .library via
     --lib-w, and doubles as an open/close toggle (a plain click, not a
     drag, calls GW.library.toggle() — see js/library.js). A generous 10px
     hit target with only a thin 1px visual line so it doesn't look like
     part of the content.
     position:fixed with JS-driven `left` (not position:absolute inside
     .library) — this element now lives OUTSIDE .library in the DOM
     specifically so it can stay on screen when the panel is closed: an
     ancestor with `transform` becomes the containing block for even a
     position:fixed descendant, so as long as this sat inside .library
     (which has transform:translateX for its slide animation) it would've
     been dragged off-screen right along with the panel, with no way to
     peek out and invite a click to reopen. adjustHandlePosition() in
     js/library.js sets `left` to the panel's live right edge minus 5px
     while open, or a small negative value while closed so roughly half of
     it still peeks onto the screen. */
  .lib-resize-handle{position:fixed;top:0;bottom:0;left:3px;width:10px;cursor:pointer;
    z-index:61;background:transparent;transition:left .3s ease;}
  .library.show ~ .lib-resize-handle{cursor:col-resize;}
  /* z-index:1 — with no explicit stacking order, ::after paints AFTER (on
     top of) ::before in the same box, so the divider line was rendering
     over the grip pill instead of stopping behind it — most visible on
     hover, when the line darkens. The pill (::before, z-index:2 below)
     now explicitly stacks above it, so the icon area reads as solid. */
  .lib-resize-handle::after{content:'';position:absolute;top:0;bottom:0;left:4px;width:1px;z-index:1;
    background:var(--line);transition:background .15s;}
  .lib-resize-handle:hover::after,.lib-resize-handle.is-dragging::after{background:var(--mute);}
  /* Grip icon (small pill with two columns of dots, echoing the shadcn
     ResizableHandle-withHandle reference) — always visible so a visitor can
     tell the divider is draggable, without hunting for a 1px hairline
     first. Centered on the handle, drawn as a background SVG so no extra
     DOM element is needed on top of the existing ::after line.
     opacity is deliberately NOT used here (even with z-index putting this
     pill above the line, opacity blends the whole pill — background
     included — with whatever's underneath, so the line still ghosted
     through, worse on hover once it darkened). background-color is fully
     opaque instead, so the icon area is genuinely solid; hover/drag only
     adjust border-color/shadow, which don't affect what shows through. */
  /* Sized up twice now — 15x26 originally, then 22x40, still reported as
     too easy to miss while the panel is closed, so it settled at 28x50 and
     the closed resting spot moved to 3px (adjustHandlePosition() in
     js/library.js) to put more of the pill on screen.
     The BORDER deliberately matches ::after's line exactly (1px/--line,
     hover --mute) rather than being heavier: an earlier attempt at
     visibility bumped it to 1.5px/--mute, which made the pill's outline
     visibly darker than the very divider line it sits on — the two read as
     mismatched. Visibility now comes from the pill's size, its opaque fill
     against the line behind it, the dot glyphs, and the shadow — none of
     which require out-shouting the line's own weight. */
  .lib-resize-handle::before{content:'';position:absolute;top:50%;left:4px;z-index:2;
    width:28px;height:50px;transform:translate(-50%,-50%);border-radius:12px;
    background-color:var(--bg);border:1px solid var(--line);
    box-shadow:0 2px 8px rgba(0,0,0,.12);
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='22' viewBox='0 0 12 22'%3E%3Ccircle cx='3' cy='3' r='2' fill='%239a9a95'/%3E%3Ccircle cx='9' cy='3' r='2' fill='%239a9a95'/%3E%3Ccircle cx='3' cy='11' r='2' fill='%239a9a95'/%3E%3Ccircle cx='9' cy='11' r='2' fill='%239a9a95'/%3E%3Ccircle cx='3' cy='19' r='2' fill='%239a9a95'/%3E%3Ccircle cx='9' cy='19' r='2' fill='%239a9a95'/%3E%3C/svg%3E");
    background-repeat:no-repeat;background-position:center;
    transition:border-color .15s,box-shadow .15s;}
  .lib-resize-handle:hover::before,.lib-resize-handle.is-dragging::before{
    border-color:var(--mute);box-shadow:0 3px 10px rgba(0,0,0,.18);}
  .lib-hint{margin:12px 0 8px;font-family:var(--sans);font-size:10.5px;letter-spacing:.01em;color:var(--mute);line-height:1.55;}
  /* Underlined field with a leading search glyph (the Kettal reference's
     treatment) instead of a rounded pill box — matches the underline tabs
     above it, so the panel head reads as one editorial system. */
  .lib-search-wrap{margin:0 0 20px;}
  .lib-search{width:100%;box-sizing:border-box;-webkit-appearance:none;appearance:none;
              font-family:var(--sans);font-size:13px;color:var(--ink);
              border:0;border-bottom:1px solid var(--line);border-radius:0;outline:none;
              background:transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239a9a95' stroke-width='1.7' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M15.5 15.5 20 20'/%3E%3C/svg%3E") no-repeat 0 50%/15px 15px;
              padding:10px 0 10px 25px;transition:border-color .2s;}
  .lib-search::placeholder{color:var(--mute);}
  .lib-search:focus{border-bottom-color:var(--charcoal);}
  /* Products search: filter icon at the field's right end + type popover */
  .lib-search-wrap--filter{position:relative;}
  .lib-search-wrap--filter .lib-search{padding-right:40px;}
  .lib-filter-btn{position:absolute;right:0;top:50%;transform:translateY(-50%);min-width:32px;height:32px;
    display:inline-flex;align-items:center;justify-content:center;gap:5px;border:0;padding:0 8px;background:none;
    color:var(--mute);cursor:pointer;transition:color .15s,background .15s;}
  .lib-filter-btn:hover,.lib-filter-btn[aria-expanded="true"]{color:var(--ink);background:rgba(0,0,0,.04);}
  .lib-filter-btn.is-active{color:var(--charcoal);}
  .lib-filter-pop{position:absolute;right:0;top:calc(100% + 6px);z-index:5;width:240px;max-height:min(60vh,420px);overflow:auto;
    background:#fff;border:1px solid var(--line);box-shadow:0 1px 3px rgba(0,0,0,.05),0 14px 32px -12px rgba(0,0,0,.22);
    padding:8px 0 6px;font-family:var(--sans);}
  .lib-filter-pop[hidden]{display:none;}
  .lib-filter-head{display:flex;align-items:baseline;justify-content:space-between;padding:4px 14px 8px;
    border-bottom:1px solid var(--line);margin-bottom:4px;font-size:12px;font-weight:600;color:var(--ink);}
  .lib-filter-clear{background:none;border:0;padding:0;cursor:pointer;font:inherit;font-size:11.5px;font-weight:400;
    color:var(--mute);text-decoration:underline;text-decoration-color:var(--faint);text-underline-offset:3px;}
  .lib-filter-clear[hidden]{display:none;}
  .lib-filter-clear:hover{color:var(--ink);}
  .lib-filter-opt{display:flex;align-items:center;gap:10px;padding:7px 14px;cursor:pointer;font-size:13px;color:var(--charcoal);}
  .lib-filter-opt:hover{background:#faf9f7;color:var(--ink);}
  .lib-filter-opt input{margin:0;width:14px;height:14px;accent-color:var(--charcoal);cursor:pointer;}
  /* padding-right — the scrollbar is laid out at this scroller's right
     padding edge, so with the previous horizontal padding of 0 the rows
     butted directly against the scrollbar track while having the panel's
     full 24px of breathing room on their left: visibly lopsided. This
     opens a gutter on the scrollbar's inner side so the space either side
     of it reads as balanced. Kept a touch under the panel's own 24px
     because the scrollbar's outer side already borrows that padding. */
  .lib-tree{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;padding:0 14px 16px 0;
            font-family:var(--sans);font-size:13px;line-height:1.3;}

  /* Products tree — the same visual language as the Materials tab: quiet
     typographic rows for collections/products, and each product's
     variants as a grid of square photo tiles. Nesting is shown by
     indentation and whitespace only — no guide lines or side bars (several
     kinds of vertical rule at different offsets read as clutter). */
  .lib-row{position:relative;display:flex;align-items:center;gap:10px;min-height:38px;
           margin-left:calc(var(--d,0) * 24px);padding:0 8px 0 2px;cursor:pointer;white-space:nowrap;
           color:var(--mute);transition:color .15s,background .15s;}
  .lib-row:hover{color:var(--ink);}
  .lib-row:focus-visible{outline:1px solid var(--mute);outline-offset:-1px;}
  .lib-row .ic{flex:none;display:inline-flex;color:var(--faint);transition:transform .2s ease,color .2s;}
  .lib-row:hover .ic{color:var(--mute);}
  .lib-row.is-open .ic{transform:rotate(90deg);color:var(--mute);}
  .lib-row .nm-wrap{min-width:0;display:flex;flex-direction:column;gap:2px;padding:9px 0;}
  .lib-row .nm{min-width:0;overflow:hidden;text-overflow:ellipsis;}
  .lib-row.is-open,.lib-fabric-row.has-live{color:var(--ink);}
  /* Folded row that holds the loaded variant: the same slightly darker
     tint as the loaded variant row itself, and nothing else — the tint alone
     reads as "your selection is in here" (the full path is already in the
     viewer's breadcrumb, so repeating it here only added noise). */
  .lib-fabric-row.has-live:not(.is-open){background:#f2f1ed;}
  /* Product rows carry a small thumbnail (js/library.js). multiply melts
     the frame's white studio background into whatever the row is — white
     on a normal row, the selection tint on the selected one — so the
     product sits directly on its row instead of in a white box. */
  .row-img{flex:none;display:block;width:44px;height:44px;background:transparent;overflow:hidden;}
  .row-img img{display:block;width:100%;height:100%;object-fit:contain;mix-blend-mode:multiply;}
  /* collection = section header; a hairline between collections gives the
     long list its rhythm (the only horizontal rule in the tree) */
  .lib-fabric-row.depth-0{min-height:48px;font-size:14px;font-weight:600;letter-spacing:-.005em;color:var(--ink);}
  .lib-fabric-row.depth-0:not(:first-child){border-top:1px solid var(--line);}
  /* breathing room between an open collection's last row (often the tinted
     selected one) and the next collection's divider */
  .lib-row:not(.depth-0) + .lib-fabric-row.depth-0,.lib-vrow + .lib-fabric-row.depth-0,
  .lib-frames + .lib-fabric-row.depth-0{margin-top:10px;}
  .lib-fabric-row:not(.depth-0){font-size:14px;}
  .lib-fabric-row.is-product{min-height:54px;}
  /* product rows have no chevron — the thumbnail takes its place (open the
     variant list with a double-click); collections keep theirs */
  .lib-fabric-row.is-product .ic{display:none;}
  /* product rows are indented with padding, not margin, so their
     selection tint spans the full row width — the same length as a
     collection row's — instead of starting at the thumbnail (content
     position unchanged) */
  .lib-fabric-row.is-product{margin-left:0;padding-left:calc(var(--d,0) * 24px + 2px);}
  .row-go{flex:none;margin-left:auto;width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;
    border:0;padding:0;background:none;color:var(--charcoal);cursor:pointer;transition:background .15s,color .15s,transform .15s;}
  .row-go:hover{background:rgba(0,0,0,.06);color:var(--ink);transform:translateX(2px);}
  .row-go:focus-visible{outline:1px solid var(--mute);outline-offset:-1px;}
  .lib-fabric-row.is-product .nm{font-weight:500;}
  .lib-fabric-row .nm-variant{font-weight:400;color:var(--mute);}

  /* variant rows — indented to line up with the product name above them */
  /* margin-left lines the variant text up with the product NAME above it
     (row indent 2 + thumbnail 44 + gap 10, minus this row's own 10px
     padding) — hierarchy by alignment, not by imagery. */
  .lib-vrow{position:relative;display:flex;align-items:center;gap:12px;min-width:0;cursor:pointer;
            margin:0 0 0 calc(var(--d,0) * 24px + 46px);padding:0 8px 0 10px;min-height:34px;
            transition:background .15s;outline:none;}
  .lib-vrow:hover{background:#faf9f7;}
  /* Tree connector, file-explorer style: a hairline trunk dropping from
     the centre of the product thumbnail (22px left of this row's box: row
     indent 2px + half the 44px thumbnail, vs this row's 46px offset), a
     short branch into each variant, and a square elbow (└) on the last one.
     Kept lighter than the collection dividers so it supports the list
     rather than competing with it; branches stay short so they read as
     connectors, not as table rules. */
  .lib-vrow::before,.lib-vrow::after,.lib-frames.has-next::before{content:"";position:absolute;
    left:-22px;pointer-events:none;border:0 solid #e4e2dd;}
  .lib-vrow::before{top:0;bottom:0;border-left-width:1px;}
  .lib-vrow::after{top:50%;width:14px;border-top-width:1px;}
  .lib-vrow.is-last::before{bottom:50%;width:14px;border-bottom-width:1px;}
  .lib-vrow.is-last::after{display:none;}
  .lib-frames.has-next::before{top:-4px;bottom:-10px;border-left-width:1px;}
  .lib-vrow:focus-visible{box-shadow:inset 0 0 0 1px var(--mute);}
  /* one line per variant — just the readable name (full folder code in the
     tooltip); a touch smaller and lighter than the product row above */
  .vt-text{flex:1;min-width:0;display:flex;}
  .vt-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
           font-size:13px;color:var(--mute);transition:color .15s;}
  .lib-vrow:hover .vt-name{color:var(--charcoal);}
  /* selected: a slightly darker row and a full-ink name */
  .lib-vrow.live{background:#f2f1ed;cursor:default;}
  .lib-vrow.live .vt-name{color:var(--ink);font-weight:500;}
  .lib-vrow .set-pin{flex:none;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;
    border:0;padding:0;background:none;cursor:pointer;color:var(--faint);opacity:0;transition:opacity .15s,color .15s,background .15s;}
  .lib-vrow:hover .set-pin,.lib-vrow:focus-within .set-pin,.lib-vrow .set-pin.pinned{opacity:1;}
  .lib-vrow .set-pin:hover{background:rgba(0,0,0,.05);color:var(--ink);}
  .lib-vrow .set-pin.pinned{color:var(--charcoal);}
  @media (hover:none){.lib-vrow .set-pin{opacity:1;}}

  /* frame list for a double-clicked row, directly under it */
  .lib-frames{position:relative;display:none;margin:4px 6px 10px calc(var(--d,0) * 24px + 46px);padding:10px 12px 8px;
              background:#faf9f7;border:1px solid var(--line);}
  .lib-frames.open{display:block;}
  .lib-frames-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
    padding-bottom:8px;margin-bottom:4px;border-bottom:1px solid var(--line);
    font-size:12px;font-weight:600;color:var(--ink);}
  .lib-frames-head span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .lib-frames-head button{flex:none;background:none;border:0;padding:0;cursor:pointer;font:inherit;
    font-size:11.5px;font-weight:400;color:var(--mute);text-decoration:underline;
    text-decoration-color:var(--faint);text-underline-offset:3px;}
  .lib-frames-head button:hover{color:var(--ink);}
  .lib-frame{display:flex;align-items:center;gap:8px;padding:5px 4px;cursor:pointer;
             font-size:11px;color:var(--faint);transition:color .15s;}
  .lib-frame:hover{color:var(--ink);}
  .lib-frame .ic{flex:none;display:inline-flex;opacity:.6;}
  .lib-frame span:last-child{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  .lib-empty{color:var(--mute);font-size:12.5px;text-align:center;padding:32px 16px;font-family:var(--sans);}


  /* pinned · quick access (session only — cleared on refresh) */
  .lib-pinned{flex:none;margin:0 0 16px;border:1px solid var(--line);background:#fbfaf8;
              padding:10px 12px 6px;max-height:30vh;overflow:auto;min-height:0;}
  .lib-pinned-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-family:var(--sans);
                    font-size:12.5px;font-weight:600;color:var(--ink);padding-bottom:8px;
                    border-bottom:1px solid var(--line);margin-bottom:4px;}
  .lib-pinned-title em{font-style:normal;font-size:11px;font-weight:400;color:var(--mute);margin-left:6px;}
  .lib-pinned-clear{flex:none;background:none;border:0;padding:0;cursor:pointer;font:inherit;font-size:11.5px;
    font-weight:400;color:var(--mute);text-decoration:underline;text-decoration-color:var(--faint);
    text-underline-offset:3px;transition:color .15s;}
  .lib-pinned-clear:hover{color:var(--ink);text-decoration-color:currentColor;}
  .qa-row{display:flex;align-items:center;gap:10px;padding:6px 2px;cursor:pointer;
          font-family:var(--sans);font-size:12.5px;color:var(--mute);transition:color .15s;}
  .qa-row:hover{color:var(--ink);}
  .qa-row.live{color:var(--ink);cursor:default;}
  .qa-row.live .nm{font-weight:500;}
  .qa-row .nm{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .qa-row .ctx{margin-left:auto;flex:none;font-family:var(--sans);font-size:10.5px;color:var(--faint);
               max-width:38%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .qa-unpin{flex:none;width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;
            border:0;background:none;color:var(--faint);cursor:pointer;font-size:12px;line-height:1;
            transition:background .15s,color .15s;}
  .qa-unpin:hover{background:rgba(0,0,0,.06);color:var(--ink);}

  /* library footer — now just a status line + close */
  .lib-foot{padding-top:14px;border-top:1px solid var(--line);
            display:flex;align-items:center;justify-content:flex-end;gap:12px;}
  /* (removed) .lib-status — a footer status line that no page renders. */
  .lib-actions{display:flex;gap:8px;justify-content:flex-end;flex:none;}
  /* Sentence case + square corners, matching the tabs and swatches. A quiet
     outlined button, not a solid one — the Materials selection summary now
     shares this footer and is the more important information there. */
  .lib-btn{font:inherit;font-size:12.5px;font-weight:500;letter-spacing:0;text-transform:none;
           padding:9px 20px;border-radius:0;cursor:pointer;
           transition:background .2s,color .2s,border-color .2s;}
  .lib-cancel{background:none;border:1px solid var(--line);color:var(--charcoal);}
  .lib-cancel:hover{border-color:var(--charcoal);color:var(--ink);}

  @keyframes rise{from{opacity:0;transform:translateY(9px);}to{opacity:1;transform:none;}}
  @keyframes pulse{0%,100%{opacity:1;}50%{opacity:.4;}}

  /* (removed) .maint-banner + body.has-maint — leftovers from when
     maintenance mode rendered as a top banner. js/maintenance.js now draws a
     full-screen overlay with its own inline styles (deliberately
     self-contained so it still renders if css/app.css fails to load), and
     nothing ever adds the has-maint class. Maintenance mode itself is
     unaffected: flip GW_CONFIG.maintenance.enabled as before. */

  /* preview.html only — same fixed-banner-with-compensating-padding pattern
     as .maint-banner above, just always on for that page (no toggle needed,
     it's a separate static file, not something js/maintenance.js controls). */
  .preview-banner{position:fixed;top:0;left:0;right:0;z-index:150;background:var(--ink);color:#fff;
    font-family:var(--sans);font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
    text-align:center;padding:9px 16px;line-height:16px;}   /* fixed 34px total height — the two overrides below both key off this exact number */
  body.has-preview-banner .app{padding-top:66px;}   /* .app's own baseline padding-top is 32px + this banner's 34px */
  /* .library is position:fixed with its own top:0 (see .library rule above),
     so .app's padding-top doesn't push it down — without this override the
     banner sat on top of and clipped the panel's own header/close button.
     34px here, NOT 66px: .library has no 32px baseline of its own to add
     on top of like .app does, so re-using .app's number left a blank gap
     between the banner and the panel's header. */
  body.has-preview-banner .library{top:34px;}

  /* toast — slides in at the top-right when a set is loaded */
  .toast{position:fixed;right:28px;top:24px;bottom:auto;left:auto;transform:translateX(24px);
         background:var(--ink);color:#fff;font-family:var(--sans);font-size:11px;letter-spacing:.04em;
         padding:11px 16px;border-radius:10px;max-width:calc(100vw - 56px);text-align:left;
         display:flex;align-items:center;gap:9px;white-space:nowrap;
         pointer-events:none;opacity:0;transition:opacity .25s ease,transform .25s ease;z-index:200;
         box-shadow:0 8px 28px rgba(0,0,0,.22);}
  .toast.show{opacity:1;transform:translateX(0);}
  .toast .toast-lead{flex:none;font-weight:600;text-transform:uppercase;letter-spacing:.12em;font-size:9px;color:#cfcfca;}
  .toast .toast-path{font-family:var(--sans);font-size:11px;color:#fff;}

  /* brand logo (single embedded asset, reused in both screens) */
  .brand-logo{height:22px;aspect-ratio:1500/174;background:var(--logo) left center/contain no-repeat;}
  .gate-logo{height:30px;aspect-ratio:1500/174;background:var(--logo) center/contain no-repeat;margin:0 auto 8px;}

  /* password gate */
  /* z-index above .preview-banner (150): preview.html is gated now, and at
     100 the banner painted on top of the password screen. */
  .gate{position:fixed;inset:0;background:var(--bg);z-index:200;display:flex;align-items:center;justify-content:center;padding:24px;}
  .gate-card{width:min(90vw,360px);text-align:center;}
  .gate-sub{font-size:10.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute);margin-bottom:24px;}
  .gate input{width:100%;font:inherit;font-size:14px;padding:12px 14px;border:1px solid var(--line);
              border-radius:10px;text-align:center;letter-spacing:.1em;outline:none;margin-bottom:12px;}
  .gate input:focus{border-color:var(--ink);}
  .gate .btn{width:100%;}
  .gate-err{margin-top:14px;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#c92a2a;opacity:0;transition:opacity .2s;}

  /* =====================  MOBILE  =====================
     Everything below only applies on narrow screens; the desktop layout
     above is left exactly as-is.                                          */
  @media (max-width:720px){
    :root{--oh-fit:0px;}
    .app{padding:28px 14px 14px;}
    .block{width:100%;padding-bottom:24px;}

    /* header: brand on top, nav row beneath, both centered */
    .top{flex-direction:column;align-items:center;gap:10px;padding-bottom:12px;text-align:center;}
    .brand{flex-direction:column;align-items:center;font-size:18px;}
    .brand i{display:block;margin-left:0;margin-top:6px;font-size:9px;}
    .actions{gap:8px;row-gap:10px;flex-wrap:wrap;justify-content:center;}
    /* real tap targets: the desktop version is text-only with zero padding
       (13px tall) — comfortable on a mouse, error-prone on a finger. Pad
       each button out to a proper touch-friendly size instead. */
    .ctl{font-size:14px;padding:10px 12px;min-height:40px;display:inline-flex;align-items:center;}

    /* stage: drop the big horizontal overhang so nothing bleeds off-screen,
       and use a square-ish area that fits the phone width */
    .stage{--stage-overhang:0px;width:100%;margin-left:0;margin-right:0;
           height:min(86vw,62vh);}
    /* .block has no horizontal padding, so left:0/right:0 would sit flush
       against the screen edge on a phone — keep a small inset here. */
    .stage-topright{right:14px;}
    .crumb{left:14px;max-width:calc(100% - 28px - 72px);font-size:14px;}
    .fill-chip{left:14px;top:calc(var(--stage-top-offset,58px) + 14px + 40px);}

    /* bottom bar: smaller, tighter */
    .bottom{padding-top:12px;margin-top:14px;flex-wrap:wrap;row-gap:10px;}
    .label{font-size:9px;letter-spacing:.12em;}
    .prog{margin:0 12px;}
    .spin-btn{width:36px;height:36px;}         /* 28px was under the ~40px comfortable tap size */
    .spin-btn svg{width:13px;height:13px;}

    /* library becomes a full-width sheet */
    .library{width:100vw;max-width:100vw;padding:20px 16px 16px;}
    .lib-resize-handle{display:none;}   /* nothing to resize against at full-width */
    /* No separate mobile margin-left rule needed — adjustContentForPanel()
       in js/library.js measures the panel's real (100vw here) width live
       and always finds an overlap on a narrow screen, so it applies the
       same shift through the shared .lib-shifted mechanism. */
    .lib-row{min-height:44px;}                 /* taller rows = easier to tap the right one */
    .lib-search{padding:12px 0 12px 25px;}     /* match the 40px-ish touch target used elsewhere */

    /* uploader + gate fit the screen */
    .up-card{width:92vw;}
    .up-title{font-size:15px;}
    .dropzone{padding:30px 16px;}
    .up-actions{flex-direction:column;}
    .btn{width:100%;}

    /* toast spans the top neatly instead of overflowing */
    .toast{right:12px;left:12px;top:12px;max-width:none;white-space:normal;font-size:10.5px;}
    .toast .toast-path{white-space:normal;word-break:break-word;}
  }

  /* very small phones — shave a little more */
  @media (max-width:380px){
    .actions{gap:16px;}
    .ctl{font-size:13px;letter-spacing:0;}
    .stage{height:min(92vw,58vh);}
  }

/* ============================================================================
   v3 ADDITIONS — hardened gate polish, viewer controls, fullscreen library,
   admin panel. Same editorial language: white, hairlines, serif accents.
   ========================================================================== */

/* ---- motion safety ---- */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;}
}

/* ---- gate refinements ---- */
.gate.gate-out{opacity:0;transition:opacity .4s ease;}
.gate.shake .gate-card{animation:gwshake .4s ease;}
@keyframes gwshake{20%{transform:translateX(-7px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(2px)}}

/* ---- stage extras ---- */
.stage{aspect-ratio:1/1;height:auto;touch-action:none;}
/* Reverted per feedback: the shimmer skeleton was more distracting than
   helpful during product switches — plain white while loading (with the
   existing spinner/percentage overlay) reads as calmer and closer to how
   this looked before. The .stage-empty class is still toggled by
   viewer.js, just no longer styled, so no JS change was needed. */
.stage img,.stage canvas{will-change:transform;}
.drag-hint{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);
  display:flex;align-items:center;gap:9px;pointer-events:none;color:var(--mute);
  font-family:var(--sans);font-size:10px;letter-spacing:.16em;text-transform:uppercase;
  opacity:0;animation:hintIn .8s ease 1.2s forwards;}
.drag-hint svg{width:30px;height:16px;stroke:var(--mute);}
.drag-hint.gone{animation:none;opacity:0;transition:opacity .5s ease;}
@keyframes hintIn{to{opacity:1}}
/* Anchored to .block, same as .crumb above — flush with the header's own
   right edge (where the nav buttons in .top already sit), independent of
   .stage's own width. z-index:3 (matching .crumb) is required here: .stagewrap
   forms its own stacking context (its `rise` animation touches `transform`),
   and without an explicit z-index this element — despite coming earlier in
   the DOM — painted BEHIND that stacking context, i.e. behind the product
   image, making the badge invisible. */
.stage-topright{position:absolute;top:calc(var(--stage-top-offset,86px) + 14px);right:0;z-index:3;
  display:flex;align-items:center;gap:8px;
  /* Matches .crumb's entrance fade (see its comment) so the two corners
     appear together instead of the badge popping in with no transition. */
  animation:rise .6s ease both .2s;}
.badge-360{box-sizing:border-box;height:26px;display:inline-flex;align-items:center;
  font-family:var(--sans);font-size:10.5px;letter-spacing:.12em;color:var(--mute);
  border:1px solid var(--line);border-radius:999px;padding:0 11px;pointer-events:none;
  background:rgba(255,255,255,.7);}
/* (removed) .stage-icon-btn — an on-stage icon button no page renders. The
   surviving equivalent is .crumb-share, which carries the same circular
   bordered treatment. */
/* Sits directly UNDER the breadcrumb, sharing its left edge (now .block's
   edge, same anchor as .crumb above). The crumb's line box is 32px tall, so
   +42px clears it with a little breathing room. Same stacking-context issue
   as .stage-topright above — needs an explicit z-index or .stagewrap paints
   over it. */
.fill-chip{position:absolute;top:calc(var(--stage-top-offset,86px) + 14px + 42px);left:0;z-index:3;font-family:var(--sans);font-size:9px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--mute);border:1px solid var(--line);
  border-radius:999px;padding:4px 9px;background:rgba(255,255,255,.75);
  display:inline-flex;align-items:center;gap:5px;
  opacity:0;transition:opacity .3s;pointer-events:none;}
.fill-chip .px-grid{color:var(--mute);}
.fill-chip-time{font-variant-numeric:tabular-nums;color:var(--faint);}
  /* Deliberately NOT given the .crumb/.stage-topright entrance animation:
     unlike those two (always on screen, animate once at load), this chip's
     own visibility is show/hide-toggled by JS — and it most often FIRST
     appears while a product's frames are still loading, i.e. within that
     same ~0.8s entrance window right after page load. Adding a .6s slide-in
     on top of its own .3s opacity fade made "processing" visibly take
     longer to look settled than before, right when it's most likely seen. */
.fill-chip.show{opacity:1;}
.ctl.on{color:var(--ink);border-bottom:1px solid var(--ink);}

/* pseudo-fullscreen (iPhone Safari has no element fullscreen) */
body.pseudo-full{overflow:hidden;}
/* .stagewrap animates `transform` (@keyframes rise) with fill-mode `both`,
   which makes it a CONTAINING BLOCK for position:fixed descendants. That broke
   pseudo-fullscreen twice over: the stage's width:calc(100% + overhang)
   resolved against the wrap (so "fullscreen" was only ~1075px wide), and even
   top/left:0 anchored to the wrap's corner instead of the screen's. Killing
   the animation here removes the containing block so the stage can anchor to
   the viewport properly; the viewport units then guarantee full coverage. */
/* .app included too — it's an ancestor further up than .stagewrap/.stage-row,
   and the containing-block search for a position:fixed descendant walks up
   to the NEAREST ancestor with a transform, not just the immediate parent.
   If the Products panel happened to be open+shifted (giving .app its own
   transform) at the same time as pseudo-fullscreen, .stage would still end
   up contained by .app instead of the real viewport even with the two
   resets below in place. */
/* !important on .app specifically — adjustContentForPanel() (js/library.js)
   sets transform via inline style, which normally beats any stylesheet
   rule regardless of selector; without !important this reset would be
   silently ignored whenever the panel happened to be shifted. */
body.pseudo-full .app{animation:none;transform:none !important;}
body.pseudo-full .stagewrap,
body.pseudo-full .stage-row{animation:none;transform:none;}
body.pseudo-full .stage{position:fixed;top:0;left:0;z-index:60;background:#fff;
  width:100vw;width:100dvw;height:100vh;height:100dvh;
  margin:0;aspect-ratio:auto;border:0;}
.stage:fullscreen{background:#fff;}

/* Exit-fullscreen button — lives inside #stage (see index.html/preview.html
   comment: the native Fullscreen API fullscreens #stage itself, so anything
   outside it, including the header's own Fullscreen toggle, isn't rendered
   at all while active — this has to be a descendant to be reachable).
   Hidden by default; shown only while actually fullscreen, via BOTH paths:
   `:fullscreen` for the native API and `body.pseudo-full` for the iOS
   fallback. Deliberately not gated by a `[hidden]` attribute toggled from
   JS — a `fullscreenchange` listener would still miss the pseudo-full path
   entirely, and CSS alone covers both without an event listener. */
/* Same subtle, borderless-square treatment as .lib-close (the Products
   panel's own X) instead of a filled dark circle — the first version read
   as a loud, obvious overlay control; this one recedes until you look for
   it, same as the rest of this site's icon buttons. */
.fs-close{display:none;position:absolute;top:14px;right:14px;z-index:70;
  box-sizing:border-box;width:26px;height:26px;border-radius:7px;
  align-items:center;justify-content:center;
  background:none;color:var(--mute);border:0;cursor:pointer;padding:0;
  transition:color .15s,background .15s;}
.fs-close:hover{color:var(--ink);background:rgba(0,0,0,.05);}
.fs-close:active{background:rgba(0,0,0,.09);}
:fullscreen .fs-close, :-webkit-full-screen .fs-close, body.pseudo-full .fs-close{display:flex;}

/* Fullscreen-only, full breadcrumb (#fsCrumb) — same hidden-until-fullscreen
   pattern as .fs-close above, and the same reason: it's a descendant of
   #stage so it actually renders during NATIVE fullscreen, where #crumb
   (living outside #stage — see its own CSS comment) can't. max-width here
   is just a safety net against the fs-close button in the opposite corner
   and an absurdly long path — nowhere near the header #crumb's aggressive
   50%-of-container cap, so in practice the whole path shows, which was
   the actual point of this element. */
.fs-crumb{display:none;position:absolute;top:14px;left:14px;z-index:70;
  max-width:calc(100% - 60px);
  font-family:var(--sans);font-size:16px;letter-spacing:-.01em;color:var(--charcoal);
  background:rgba(255,255,255,.9);border-radius:7px;padding:6px 12px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
:fullscreen .fs-crumb, :-webkit-full-screen .fs-crumb, body.pseudo-full .fs-crumb{display:block;}

/* .crumb/.stage-topright/.fill-chip are now children of .block (see index.html
   comment), so pseudo-fullscreen — which hides everything except .stage —
   would otherwise hide them too. Switch them to viewport-fixed corners,
   above the stage's z-index:60, so they stay visible over the fullscreen
   image exactly like before. */
body.pseudo-full .crumb, body.pseudo-full .stage-topright, body.pseudo-full .fill-chip{
  position:fixed;z-index:61;}
body.pseudo-full .crumb{top:14px;left:14px;max-width:calc(100vw - 150px);}
body.pseudo-full .stage-topright{top:14px;right:14px;}
body.pseudo-full .fill-chip{top:48px;left:14px;}

/* ---- spin controls (in the bottom bar) ---- */
.spin-controls{display:flex;align-items:center;gap:8px;flex:none;}
.spin-btn{width:28px;height:28px;border-radius:50%;border:1px solid var(--line);
  background:#fff;color:var(--mute);display:inline-flex;align-items:center;justify-content:center;
  cursor:pointer;padding:0;transition:color .2s,border-color .2s,background .2s;}
.spin-btn svg{width:10px;height:10px;fill:currentColor;}
.spin-btn:hover{color:var(--ink);border-color:#c9c8c1;}
.spin-btn.on{background:var(--ink);border-color:var(--ink);color:#fff;}
.spin-btn:focus-visible{outline:1.5px solid var(--ink);outline-offset:2px;}

/* ============================================================================
   YSA — admin panel  (design matches the viewer: same tokens, same feel)
   ========================================================================== */

body.ysa-body{background:var(--bg);color:var(--ink);}
.ysa-page{min-height:100vh;display:flex;flex-direction:column;
  padding:22px 22px 0;max-width:1080px;margin:0 auto;}
.ysa-top{padding-top:2px;}
.ysa-main{flex:1;padding:32px 0 60px;display:flex;flex-direction:column;gap:22px;}
.ysa-bottom{max-width:1080px;margin:0 auto;padding:14px 22px 22px;width:calc(100% - 44px);
  display:flex;justify-content:space-between;align-items:baseline;
  font-family:var(--sans);font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);
  border-top:1px solid var(--line);}
.ysa-bottom .num{color:var(--faint);}
/* The rule above's `margin:0 auto` silently zeroed the intended 22px top
   margin, and its width calc double-counted .ysa-page's own padding —
   leaving the footer 44px narrower than every card above it. */
.ysa-bottom{width:100%;max-width:none;margin:22px auto 0;padding:14px 0 22px;}

/* The "← Viewer" link in the admin header rendered underlined next to
   identical-looking .ctl buttons. */
.ctl{text-decoration:none;}

.ysa-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px 24px;
  animation:rise .5s ease both;}
.ysa-card + .ysa-card{margin-top:0;}
.ysa-h{font-family:var(--sans);font-size:18px;font-weight:400;margin:0 0 6px;letter-spacing:.005em;}
.ysa-card-head{display:flex;justify-content:space-between;align-items:baseline;gap:16px;flex-wrap:wrap;margin-bottom:2px;}
.ysa-card-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.ysa-p{font-family:var(--sans);font-size:12.5px;color:var(--mute);line-height:1.8;margin:0 0 14px;}
.ysa-p-sub{margin:6px 0 18px;}
.ysa-p b{color:var(--ink);font-weight:600;}
.ysa-p code{font-size:11.5px;background:#f4f4f2;border:1px solid var(--line);border-radius:4px;padding:1px 6px;}
.ysa-hint{font-family:var(--sans);font-size:11px;color:var(--faint);line-height:1.75;margin:10px 0 0;}
.ysa-hint b{color:var(--mute);}

/* form field */
.ysa-fld{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;}
.ysa-fld label{font-family:var(--sans);font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute);}
.ysa-fld input,.ysa-fld select{font-family:var(--sans);font-size:13px;color:var(--ink);
  border:1px solid var(--line);border-radius:8px;padding:10px 12px;background:#fff;outline:none;
  transition:border-color .2s;}
.ysa-fld input:focus,.ysa-fld select:focus{border-color:var(--ink);}
.ysa-fld-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px 14px;}
@media (max-width:720px){.ysa-fld-row{grid-template-columns:1fr;}}

/* buttons */
.ysa-btn{font-family:var(--sans);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  background:none;border:1px solid var(--line);color:var(--ink);border-radius:999px;
  padding:10px 20px;cursor:pointer;transition:background .18s,color .18s,border-color .18s,opacity .18s;}
.ysa-btn:hover{border-color:var(--ink);}
.ysa-btn:disabled{opacity:.35;cursor:default;border-color:var(--line);}
.ysa-btn-primary{background:var(--ink);color:#fff;border-color:var(--ink);}
.ysa-btn-primary:hover{opacity:.86;border-color:var(--ink);}
.ysa-btn-primary:disabled{background:#c9c8c1;border-color:#c9c8c1;color:#fff;}
.ysa-btn-ghost{color:var(--mute);border-color:var(--line);}
.ysa-btn-ghost:hover{color:var(--ink);}
.ysa-btn-danger{color:#8d3b3b;border-color:#dcc6c6;}
.ysa-btn-danger:hover{background:#faf3f3;border-color:#c99d9d;color:#8d3b3b;}
.ysa-btn-primary.ysa-btn-danger{background:#8d3b3b;color:#fff;border-color:#8d3b3b;}
.ysa-btn-mini{padding:6px 12px;font-size:9px;}

/* Icon-only row-action buttons (star/monitor/eye/pencil/trash) — replaced the
   text pills (Set default / Publish / Rename / Delete) once a 5th action
   (preview-default) made the row too wide/overwhelming. title+aria-label on
   the button itself carries the label now instead of visible text. */
.ysa-btn-icon{width:26px;height:26px;padding:0;flex:none;display:inline-flex;
  align-items:center;justify-content:center;border-radius:50%;color:var(--mute);border-color:var(--line);}
.ysa-btn-icon:hover{color:var(--ink);border-color:var(--ink);}
.ysa-btn-icon.ysa-btn-danger{color:#8d3b3b;border-color:#dcc6c6;}
.ysa-btn-icon.ysa-btn-danger:hover{background:#faf3f3;border-color:#c99d9d;}
/* "This one's currently applied" marker — the already-default and
   already-preview-default star/monitor, both plain non-interactive spans
   once achieved (there's no "un-default" action here; you set a DIFFERENT
   item as default instead). Was a muted colored pill (green/blue) at the
   same 26px size as the button row around it — but it also carried a stray
   `ysa-pill` class that collided with the unrelated generic `.ysa-pill`
   text-pill rule declared later in this file (same specificity, later
   source order wins), which clobbered its size/shape into a small squashed
   dash. Given a clean class of its own now, and inverted (solid fill, white
   icon) so it reads unambiguously as "active" next to full-strength icon
   buttons, where a pale color pill read as barely-there. Plain black/white —
   this site's whole palette is monochrome (ink/mute/line, no accent hue), so
   introducing a color here (an earlier pass tried the warm accent brown used
   for "Rebuild manifest") broke that consistency instead of fixing anything.
   Publish/Unpublish does NOT use this class, even when published — it's a
   real toggle button in both states (unlike these two), and giving it the
   same solid-fill treatment made a row read as 2-3 identical filled circles
   with no way to tell which meant what. */
.ysa-icon-active{width:26px;height:26px;padding:0;border-radius:50%;flex:none;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--ink);color:#fff;border:1px solid var(--ink);}

/* login card — `margin:auto` (not a fixed `8vh` top offset) on a flex child
   of `.ysa-main` (which is `flex:1`, so it already fills the remaining
   viewport height below the header) centers it BOTH ways for free: a small
   card pinned near the top of an otherwise fully blank page read as
   unfinished/broken, especially on a tall window. Doesn't touch the
   post-login dashboard cards — those have no `.ysa-login` class, so they
   keep stretching full-width in the normal top-down flow as before.
   `#loginBox` (its id) rather than just `.ysa-login`: `#loginBox` sits right
   after the (hidden, but still DOM-adjacent) `#noRemote` card, so
   `.ysa-card + .ysa-card{margin-top:0}` — declared earlier but with HIGHER
   specificity (0,2,0) than a single class (0,1,0) — was winning the
   margin-top property specifically and pinning the card to the top no
   matter what this rule said. An id selector (1,0,0) safely outranks it. */
#loginBox.ysa-login{max-width:400px;width:100%;margin:auto;padding:28px 26px;box-shadow:0 12px 32px rgba(0,0,0,.06);}
/* Uppercase was tried right before this and reverted — read as harder to
   read than the plain-case heading, so back to normal case, just bold with
   an icon (matching the reference design), scoped to just this heading —
   the dashboard's other section headings (Upload, Products online, …) stay
   exactly as they were, no .ysa-h change outside this card. */
.ysa-login .ysa-h{display:flex;align-items:center;gap:9px;font-size:19px;font-weight:700;}
/* A solid badge box behind the icon was tried and reverted — heavier and
   worse than the plain reference. Plain line icon, just colored to match
   the heading text instead of the usual muted icon gray. */
.ysa-login-icon{flex:none;color:var(--ink);}
.ysa-login-sub{font-family:var(--sans);font-size:12.5px;color:var(--mute);margin:4px 0 20px;}
/* Bigger, softer field — the site's default 9px letter-spaced uppercase
   label and compact input read as a dense admin-table style; this card is
   the very first thing anyone sees, so it gets its own calmer, larger
   treatment instead. Scoped to `.ysa-login .ysa-fld`, not `.ysa-fld`
   generally — every other form in the dashboard (Upload overrides, etc.)
   keeps the compact style. */
.ysa-login .ysa-fld{gap:8px;margin-bottom:18px;}
.ysa-login .ysa-fld label{font-size:13px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--ink);}
.ysa-login .ysa-fld input{font-size:14px;padding:12px 14px;border-radius:10px;}
.ysa-login .ysa-btn{margin-top:6px;width:100%;font-size:14px;font-weight:600;
  letter-spacing:0;text-transform:none;border-radius:10px;padding:13px 20px;}

/* messages */
.ysa-msg{font-family:var(--sans);font-size:11px;color:var(--mute);margin:0;min-height:16px;line-height:1.7;}
.ysa-msg-err{color:#8d3b3b;}
.ysa-msg-ok{color:#2f7a4f;}

/* storage indicator */
.ysa-usage{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 18px;
  animation:rise .5s ease both;}
.ysa-usage-row{display:flex;justify-content:space-between;align-items:baseline;
  font-family:var(--sans);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);}
.ysa-usage-label{color:var(--ink);font-weight:600;}
.ysa-usage-num{color:var(--ink);letter-spacing:.06em;text-transform:none;font-size:12px;}
.ysa-usage-track{height:4px;background:var(--line);border-radius:99px;overflow:hidden;margin:9px 0 8px;}
.ysa-usage-track i{display:block;height:100%;width:0;background:var(--ink);border-radius:99px;transition:width .3s ease;}
.ysa-usage-track i.warn{background:#c69028;}
.ysa-usage-track i.crit{background:#8d3b3b;}
.ysa-usage-meta{font-size:9.5px;letter-spacing:.14em;color:var(--faint);}
/* Loading skeleton — before the first refreshCloud() resolves this card
   used to show bare "—" placeholders next to a flat empty track, which read
   as broken/unfinished rather than "still loading". A moving shimmer on the
   track plus soft pulsing text placeholders reads as active work instead. */
.ysa-usage.is-loading .ysa-usage-num,
.ysa-usage.is-loading #usageStats{display:inline-block;width:90px;height:10px;border-radius:4px;
  background:var(--line);animation:ysaPulse 1.3s ease-in-out infinite;}
.ysa-usage.is-loading #usageQuota{visibility:hidden;}
.ysa-usage.is-loading .ysa-usage-track{background:var(--line);position:relative;}
.ysa-usage.is-loading .ysa-usage-track i{width:40%!important;background:var(--mute);
  animation:ysaShimmer 1.3s ease-in-out infinite;}
@keyframes ysaPulse{0%,100%{opacity:.5}50%{opacity:1}}
@keyframes ysaShimmer{0%{transform:translateX(-100%)}100%{transform:translateX(250%)}}

/* dropzone */
.ysa-drop{border:1.5px dashed var(--faint);border-radius:14px;padding:28px 24px;text-align:center;
  cursor:pointer;transition:border-color .2s,background .2s;background:#fdfdfc;}
.ysa-drop:hover,.ysa-drop.over{border-color:var(--ink);background:#faf9f6;}
.ysa-drop-icon{color:var(--faint);margin-bottom:8px;transition:color .2s;}
.ysa-drop:hover .ysa-drop-icon,.ysa-drop.over .ysa-drop-icon{color:var(--ink);}
.ysa-drop-lead{font-family:var(--sans);font-size:15.5px;color:var(--ink);margin-bottom:6px;}
.ysa-drop-sub{font-family:var(--sans);font-size:11.5px;color:var(--mute);line-height:1.7;}
.ysa-drop-sub b{color:var(--ink);font-weight:600;}
.ysa-drop-actions{text-align:center;margin-top:10px;}

/* detected plan */
.ysa-plan{margin-top:18px;border:1px solid var(--line);border-radius:10px;padding:14px 16px;background:#fbfbfa;}
.ysa-plan-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;gap:12px;}
.ysa-plan-title{font-family:var(--sans);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);}
.ysa-plan-body{font-family:var(--sans);font-size:12px;color:var(--ink);max-height:280px;overflow:auto;padding-right:4px;}
.ysa-plan-col{margin-top:8px;color:var(--ink);}
.ysa-plan-col:first-child{margin-top:0;}
.ysa-plan-caret{color:var(--faint);}
.ysa-plan-prod{margin-left:16px;color:var(--mute);padding:4px 0;font-size:11.5px;}
.ysa-plan-prod-check{display:inline-flex;align-items:center;gap:6px;cursor:pointer;}
.ysa-plan-prod.excluded{opacity:.45;}
.ysa-plan-var.excluded{opacity:.45;}
.ysa-plan-var{margin-left:32px;padding:5px 8px;display:flex;flex-wrap:wrap;align-items:baseline;gap:10px;
  border-radius:6px;font-size:11.5px;}
.ysa-plan-var:hover{background:rgba(0,0,0,.02);}
.ysa-plan-var-name{color:var(--ink);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ysa-plan-var-meta{font-size:10px;letter-spacing:.06em;color:var(--faint);flex:none;}
.ysa-plan-var-meta em{color:#2f7a4f;font-style:normal;}
.ysa-plan-var-warn{flex:none;font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:#8d3b3b;}
.ysa-plan-var.incomplete{background:rgba(141,59,59,.04);}
.ysa-plan-strip{flex-basis:100%;display:flex;gap:4px;margin-top:4px;}
.ysa-plan-strip img{width:38px;height:38px;object-fit:cover;border-radius:4px;border:1px solid var(--line);background:#f4f4f2;}
.ysa-error-list{margin-top:10px;max-height:280px;overflow:auto;border-top:1px solid var(--line);}
.ysa-error-row{display:flex;flex-wrap:wrap;gap:4px 10px;padding:8px 4px;border-bottom:1px solid var(--line);
  font-family:var(--sans);font-size:11.5px;}
.ysa-error-time{flex:none;color:var(--mute);font-size:10px;}
.ysa-error-msg{flex:1;min-width:200px;color:#8d3b3b;}
.ysa-error-url{flex-basis:100%;color:var(--faint);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

/* Selection status bar — ticking a checkbox used to give no feedback until
   you clicked a row's Rename/Delete button; this surfaces the count and the
   bulk actions as soon as anything is selected. Warm accent tokens (additive
   — nothing else on this page used --accent before, so this cannot shift any
   existing color anywhere). */
.ysa-selection-bar{display:none;align-items:center;gap:10px;margin-top:10px;padding:9px 14px;
  background:var(--accent-soft,#f5ede2);border:1px solid var(--accent,#9a6a3c);border-radius:10px;
  font-family:var(--sans);font-size:12px;color:var(--accent-ink,#6b4726);}
.ysa-selection-bar.show{display:flex;}
.ysa-selection-bar span{font-weight:600;margin-right:auto;}
#filterIncomplete.active{color:var(--accent-ink,#6b4726);border-color:var(--accent,#9a6a3c);background:var(--accent-soft,#f5ede2);}
#filterHidden.active{color:#8d3b3b;border-color:#c98f8f;background:#faf1f1;}

/* Catalogue history (manifest rollback) — the Worker already kept these
   snapshots; this surfaces them so a bad bulk edit is one click from undone. */
.ysa-backup-list{margin-top:10px;max-height:300px;overflow:auto;border-top:1px solid var(--line);}
.ysa-backup-row{display:flex;align-items:center;gap:12px;padding:9px 4px;border-bottom:1px solid var(--line);
  font-family:var(--sans);font-size:12.5px;}
.ysa-backup-row:last-child{border-bottom:0;}
.ysa-backup-when{flex:1;min-width:0;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ysa-backup-meta{flex:none;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--mute);}

.ysa-analytics-list{margin-top:10px;max-height:340px;overflow:auto;border-top:1px solid var(--line);}
.ysa-analytics-row{display:flex;align-items:center;gap:10px;padding:8px 4px;border-bottom:1px solid var(--line);
  font-family:var(--sans);font-size:12px;}
.ysa-analytics-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);}
.ysa-analytics-meta{flex:none;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--mute);}

/* Access history — anonymous session/view log, grouped into one collapsible
   card per visit (sid) so every product a visitor opened reads together
   instead of as unrelated rows. Rows stay in one flat chronological list;
   a left gutter (padding-left) reserves room for the connector lines drawn
   by drawIdentityConnectors() linking rows that share a profile, even when
   they're not adjacent. */
.ysa-access-list{position:relative;margin-top:10px;
  max-height:440px;overflow:auto;border-top:1px solid var(--line);}
.ysa-access-session{border-bottom:1px solid var(--line);}
.ysa-access-session:last-child{border-bottom:0;}
/* Colored per-identity (set inline from JS, matching that profile's own
   avatar color) so the line and the avatars it links read as obviously the
   same thing, rather than a generic gray connector. Positioned relative to
   .ysa-access-list (its nearest positioned ancestor), NOT indented via
   padding on the list itself — that would've shifted the filter bar/
   empty-state text too, which don't have rows to connect. The gutter
   instead lives in .ysa-access-head's own left padding below, so only
   actual session rows reserve the space. */
.ysa-access-connector-line{position:absolute;left:8px;width:2px;border-radius:1px;opacity:.55;pointer-events:none;}
.ysa-access-connector-tick{position:absolute;left:8px;width:8px;height:2px;border-radius:1px;opacity:.55;pointer-events:none;}

.ysa-access-head{width:100%;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  padding:9px 4px 9px 20px;background:none;border:0;cursor:pointer;text-align:left;
  font-family:var(--sans);font-size:12px;color:inherit;transition:background .15s;}
.ysa-access-head:hover{background:var(--bg-alt,rgba(0,0,0,.02));}
.ysa-access-session.open .ysa-access-chevron{transform:rotate(180deg);}
.ysa-access-chevron{flex:none;color:var(--mute);font-size:10px;transition:transform .15s;transform-origin:center;}
.ysa-access-when{flex:none;color:var(--mute);font-size:11px;white-space:nowrap;}
/* No real identity exists behind a session (one shared password, no
   accounts) — color is a rank-based gradient keyed to ipHash (falling back
   to vid), see visitorBadge/buildAccessColorMap above; it's still never an
   attempt at tracking who's who, just telling rows apart from each other. */
/* box-sizing:border-box so the border below doesn't push the circle to
   22px — width/height stay the intended 20px including that border. The
   border itself: the icon's white shoulders reach the avatar's own bottom
   edge, which otherwise sat directly against the row's white background —
   no border meant the white icon visually bled straight into the page,
   losing the circle's own boundary right where it mattered most. */
.ysa-access-avatar{flex:none;width:20px;height:20px;border-radius:50%;box-sizing:border-box;
  border:1.5px solid rgba(0,0,0,.16);
  display:flex;align-items:center;justify-content:center;overflow:hidden;}
/* Only avatars with a `title` got a click handler (an identity key exists to
   isolate on) — the plain [title] selector doubles as "is this clickable"
   without a separate class, since every clickable avatar sets one anyway
   for the tooltip. */
.ysa-access-avatar[title]{cursor:pointer;box-shadow:0 0 0 0 transparent;transition:box-shadow .15s;}
.ysa-access-avatar[title]:hover{box-shadow:0 0 0 2px var(--bg,#fff),0 0 0 3.5px var(--line);}
.ysa-access-meta{flex:1;min-width:160px;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--mute);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ysa-access-count{flex:none;font-size:11px;color:var(--ink);white-space:nowrap;}
.ysa-access-duration{flex:none;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--mute);white-space:nowrap;}
.ysa-access-tag{flex:none;font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--accent-ink,#6b4726);
  background:var(--accent-soft,#f5ede2);border:1px solid var(--accent,#9a6a3c);border-radius:5px;padding:1px 6px;}
.ysa-access-tag-new{color:var(--mute);background:none;border-color:var(--line);}

.ysa-access-filter-bar{display:flex;align-items:center;gap:8px;padding:8px 10px;
  font-family:var(--sans);font-size:11.5px;color:var(--mute);background:#f7f6f3;
  border-bottom:1px solid var(--line);}
.ysa-access-filter-clear{font:inherit;color:var(--ink);text-decoration:underline;
  background:none;border:0;cursor:pointer;padding:0;}

.ysa-access-details{padding:2px 4px 12px 26px;}
.ysa-access-view-row{display:flex;align-items:baseline;gap:10px;padding:5px 0;
  font-family:var(--sans);font-size:11.5px;border-bottom:1px dashed var(--line);}
.ysa-access-view-row:last-child{border-bottom:0;}
.ysa-access-view-rel{flex:1;min-width:0;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ysa-access-view-dwell{flex:none;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--mute);}
.ysa-access-empty-views, .ysa-access-referrer{font-size:11px;color:var(--mute);padding:4px 0;}

/* Admin toolbar: 7 undifferentiated mini-buttons wrapped into 3-4 ragged rows
   at tablet width, with the destructive-ish "Rebuild manifest" looking exactly
   like "Refresh". Group them and let the search field take the slack. */
.ysa-card-tools{row-gap:8px;}
.ysa-card-tools .ysa-search{flex:1 1 180px;min-width:150px;}
.ysa-card-tools #auditManifest{margin-left:auto;}
.ysa-card-tools #rebuildManifest{border-color:#e0d5c4;color:var(--accent-ink,#6b4726);}
@media (max-width:820px){
  .ysa-card-tools{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));align-items:stretch;}
  .ysa-card-tools .ysa-search{grid-column:1/-1;width:100%;}
  .ysa-card-tools #auditManifest{margin-left:0;}
}

/* overrides */
.ysa-overrides{margin-top:14px;padding:14px 16px;background:#fbfbfa;border:1px solid var(--line);border-radius:10px;}
.ysa-overrides .ysa-hint{margin:0 0 10px;color:var(--mute);}

/* options (radio groups) */
.ysa-opts{margin-top:18px;display:grid;grid-template-columns:1fr 1fr;gap:14px;}
@media (max-width:720px){.ysa-opts{grid-template-columns:1fr;}}
.ysa-opt{border:1px solid var(--line);border-radius:10px;padding:12px 14px;background:#fff;}
.ysa-opt-label{font-family:var(--sans);font-size:9px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--mute);margin-bottom:8px;}
.ysa-opt-choices{display:flex;flex-direction:column;gap:4px;}
.ysa-radio{display:flex;align-items:baseline;gap:9px;font-family:var(--sans);font-size:12px;color:var(--ink);
  cursor:pointer;padding:4px 0;}
.ysa-radio input{margin:0;accent-color:var(--ink);}
.ysa-radio em{font-style:normal;color:var(--faint);font-size:10.5px;letter-spacing:.02em;}
.ysa-radio:hover em{color:var(--mute);}

/* progress dashboard */
.ysa-progress{margin-top:18px;border:1px solid var(--line);border-radius:10px;padding:14px 16px;background:#fbfbfa;}
.ysa-progress-head{display:flex;justify-content:space-between;align-items:baseline;
  font-family:var(--sans);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--mute);}
.ysa-progress-title{color:var(--ink);}
.ysa-progress-track{height:4px;background:var(--line);border-radius:99px;overflow:hidden;margin:8px 0 12px;}
.ysa-progress-track i{display:block;height:100%;width:0;background:var(--ink);border-radius:99px;transition:width .2s;}
.ysa-progress-list{display:flex;flex-direction:column;gap:5px;max-height:280px;overflow:auto;padding-right:4px;}
.ysa-prog-row{display:flex;align-items:center;gap:10px;font-family:var(--sans);font-size:11.5px;color:var(--ink);}
.ysa-prog-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ysa-prog-bar{flex:none;width:120px;height:3px;background:var(--line);border-radius:99px;overflow:hidden;}
.ysa-prog-bar i{display:block;height:100%;width:0;background:var(--ink);transition:width .2s;}
.ysa-prog-row.done .ysa-prog-bar i{background:#2f7a4f;}
.ysa-prog-row.err{color:#8d3b3b;}
.ysa-prog-row.err .ysa-prog-bar i{background:#8d3b3b;}
.ysa-prog-count{flex:none;font-size:10px;letter-spacing:.06em;color:var(--mute);min-width:70px;text-align:right;}

.ysa-actions-row{margin-top:16px;display:flex;align-items:center;gap:14px;flex-wrap:wrap;}

/* cloud tree */
.ysa-search{font-family:var(--sans);font-size:12px;color:var(--ink);
  border:1px solid var(--line);border-radius:999px;background:#fff;padding:7px 12px;width:200px;outline:none;
  transition:border-color .2s;}
.ysa-search:focus{border-color:var(--ink);}
/* min-height matters specifically for the very first load: #cloudTree is
   completely empty until the first refreshCloud() resolves, so without this
   the loading overlay had nothing to sit inside and just floated as a tiny
   stray spinner in a lot of blank page instead of reading as a proper
   loading card. Once real rows exist their own height takes over anyway.
   The min-height needs to live on BOTH the wrap and #cloudTree itself —
   #cloudTree's own border only wraps its (empty, zero-height) content, so
   with the min-height only on the wrap the pill sat in blank margin below
   an invisible, zero-height bordered box instead of inside a visible card. */
.ysa-tree-wrap{position:relative;min-height:140px;}
.ysa-tree{font-family:var(--sans);font-size:12.5px;margin-top:10px;border:1px solid var(--line);border-radius:10px;overflow:hidden;min-height:140px;}
/* Was a plain text line under the list ("Loading…") — easy to miss entirely
   while scrolled past it. Sitting directly over the list itself instead is
   unmissable regardless of scroll position. Tried pinning this to the
   viewport instead (fixed, top-center) so it wouldn't land below the fold on
   very long lists — user preferred it back inside the list, so it's absolute
   within .ysa-tree-wrap again. Kept as a small auto-width pill (not a
   full-width/full-height translucent rectangle) — the earlier full-bleed
   version showed through the rows underneath as a dirty grey smear across
   the top of the whole card. */
/* [hidden] and .ysa-tree-loading{display:flex} are equal specificity (0,1,0)
   — without this override the class rule wins the cascade tie (it's
   declared later than the browser's UA default) and the element stays
   visible even with the hidden attribute set. */
.ysa-tree-loading[hidden]{display:none;}
/* Default = dead center of the (empty, first-load) card. admin.js overrides
   `top`/`transform` inline via style attributes for the row-anchored case
   (Set default/Publish…), which wins over these since inline beats a class
   selector regardless of source order. */
.ysa-tree-loading{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:5;
  display:flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--line);
  border-radius:999px;padding:7px 14px;box-shadow:0 4px 14px rgba(0,0,0,.08);
  font-family:var(--sans);font-size:12px;color:var(--ink);white-space:nowrap;}
.ysa-tree-loading-spin{width:14px;height:14px;border-radius:50%;flex:none;
  border:2px solid var(--line);border-top-color:var(--ink);animation:gw-spin .7s linear infinite;}
/* The pill above only reads as "loading" wherever it's actually sitting —
   scrolled away from it on a long list, an admin sees no motion at all and
   can mistake it for a freeze. Greying + pulsing the whole list underneath
   makes "still loading" visible no matter where they're scrolled to.
   Toggled via a plain .is-scanning class from admin.js (not :has()) so it
   doesn't depend on newer selector support. Grayscale + a wider opacity
   swing (not just the mild .5-1 from the Storage card's skeleton) — a
   first pass reusing that exact animation read as barely-there against
   this list's already-light rows. */
.ysa-tree-wrap.is-scanning #cloudTree{filter:grayscale(85%);animation:ysaPulseWide 1.1s ease-in-out infinite;pointer-events:none;}
@keyframes ysaPulseWide{0%,100%{opacity:1}50%{opacity:.32}}
.ysa-tree-row{display:flex;align-items:center;gap:10px;padding:8px 4px;border-bottom:1px solid var(--line);}
.ysa-tree-row:last-child{border-bottom:0;}
.ysa-tree-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);}
.ysa-tree-meta{flex:none;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--mute);}

/* Three visually distinct tiers so the hierarchy reads at a glance instead
   of every row looking the same weight: collection = bold header block with
   its own background, product = a labeled sub-header, variant = a plain
   indented leaf row with a tree-guide line connecting it to its product. */
.ysa-tree-col{cursor:pointer;padding:12px 14px;background:#f7f6f3;border-bottom:1px solid var(--line);}
.ysa-tree-col .ysa-tree-name{font-family:var(--sans);font-size:14.5px;font-weight:600;letter-spacing:.005em;}
.ysa-tree-col .ysa-plan-caret{flex:none;color:var(--mute);font-size:11px;width:14px;}
.ysa-tree-col:hover{background:#f1f0ec;}
.ysa-tree-col.is-open{background:#fff;}

.ysa-tree-prod{cursor:pointer;padding:8px 14px 8px 30px;background:#fbfbfa;position:relative;}
.ysa-tree-prod::before{content:'';position:absolute;left:18px;top:0;bottom:0;width:1px;background:var(--line);}
.ysa-tree-prod .ysa-tree-name{font-weight:600;font-size:12.5px;letter-spacing:.005em;color:var(--mute);}
.ysa-tree-prod .ysa-plan-caret{flex:none;color:var(--mute);font-size:10px;width:12px;}
.ysa-tree-prod:hover{background:#f1f0ec;}
.ysa-tree-prod.is-open{background:#fff;}

.ysa-tree-set{padding:7px 14px 7px 44px;position:relative;}
.ysa-tree-set::before{content:'';position:absolute;left:18px;top:0;bottom:50%;width:1px;background:var(--line);}
.ysa-tree-set:last-child::before{bottom:0;}
.ysa-tree-set::after{content:'';position:absolute;left:18px;top:50%;width:12px;height:1px;background:var(--line);}
.ysa-tree-set .ysa-tree-name{color:var(--ink);font-size:12px;}
.ysa-tree-set:hover{background:#fafaf8;}
.ysa-tree-set.is-default{background:#f4faf6;}

.ysa-pill{font-family:var(--sans);font-size:8.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--mute);border:1px solid var(--line);border-radius:999px;padding:3px 9px;background:#fff;}
.ysa-pill-new{color:#1a6ea8;border-color:#cfe0ec;background:#f2f8fc;}
.ysa-pill-hidden{color:#8d3b3b;border-color:#e3cfcf;background:#faf4f4;}
/* Dimmed, not hidden outright — an admin still needs to see and un-hide
   these rows; only visitors' Products tab actually suppresses them.
   Dimming the WHOLE row (previously plain `opacity:.55` on the row itself)
   washed out its own action buttons at the same time as its label — exactly
   the controls the admin needs to find to act on it. Only the descriptive
   text dims now; icon buttons stay full-strength. */
.ysa-tree-row.is-hidden .ysa-tree-name,
.ysa-tree-row.is-hidden .ysa-tree-meta,
.ysa-tree-row.is-hidden .ysa-tree-check{opacity:.55;}
.ysa-tree-row.is-hidden .ysa-tree-name{font-style:italic;}
.ysa-tree-meta.warn{color:#8d3b3b;}
.ysa-tree-check{flex:none;width:14px;height:14px;cursor:pointer;accent-color:var(--ink);}

.ysa-empty{padding:24px 6px;color:var(--mute);font-family:var(--sans);font-size:12px;text-align:center;}

/* batch rename popup — one editable row per selected item */
.ysa-batch-rename-list{max-height:50vh;overflow:auto;display:flex;flex-direction:column;gap:8px;}
.ysa-batch-rename-row{display:flex;align-items:center;gap:10px;}
.ysa-batch-rename-old{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-family:var(--sans);font-size:11.5px;color:var(--mute);}
.ysa-batch-rename-row .ysa-modal-input{flex:1;min-width:0;}

/* modal */
.ysa-modal{position:fixed;inset:0;background:rgba(26,26,24,.32);backdrop-filter:blur(2px);
  display:flex;align-items:center;justify-content:center;z-index:100;animation:fadein .18s ease;padding:22px;}
.ysa-modal[hidden]{display:none;}
body.ysa-modal-open{overflow:hidden;}
@keyframes fadein{from{opacity:0;}to{opacity:1;}}
.ysa-modal-card{background:#fff;border-radius:14px;padding:26px 28px 22px;max-width:480px;width:100%;
  box-shadow:0 20px 60px -20px rgba(26,26,24,.4);animation:rise .3s cubic-bezier(.22,.8,.3,1);}
.ysa-modal-title{font-family:var(--sans);font-size:19px;font-weight:400;margin:0 0 12px;}
.ysa-modal-body{font-family:var(--sans);font-size:13px;color:var(--ink);line-height:1.75;margin-bottom:20px;}
.ysa-modal-body p{margin:0 0 8px;}
.ysa-modal-body p:last-child{margin-bottom:0;}
.ysa-modal-body code{font-size:11.5px;background:#f4f4f2;border:1px solid var(--line);border-radius:4px;padding:2px 7px;
  display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;}
.ysa-modal-warn{color:#8d3b3b;font-size:11.5px;}
.ysa-modal-hint{color:var(--mute);font-size:12px;}
.ysa-modal-actions{display:flex;justify-content:flex-end;gap:10px;}
.ysa-modal-input{width:100%;box-sizing:border-box;font-family:var(--sans);font-size:14px;color:var(--ink);
  border:1px solid var(--line);border-radius:8px;padding:9px 11px;margin-top:2px;}
.ysa-modal-input:focus{outline:none;border-color:var(--ink);}

/* toast notifications */
.ysa-toast-wrap{position:fixed;top:20px;right:20px;z-index:200;display:flex;flex-direction:column;gap:8px;pointer-events:none;}
.ysa-toast{font-family:var(--sans);font-size:12.5px;padding:10px 18px;border-radius:10px;
  background:#fff;color:var(--ink);border:1px solid var(--line);
  box-shadow:0 8px 30px -10px rgba(26,26,24,.25);pointer-events:auto;
  animation:toastIn .3s cubic-bezier(.22,.8,.3,1);}
.ysa-toast-ok{border-color:#cfe3d4;background:#f4faf6;color:#2f7a4f;}
.ysa-toast-err{border-color:#e8cece;background:#fdf5f5;color:#8d3b3b;}
.ysa-toast.fade-out{animation:toastOut .25s ease forwards;}
@keyframes toastIn{from{opacity:0;transform:translateX(40px);}to{opacity:1;transform:translateX(0);}}
@keyframes toastOut{from{opacity:1;transform:translateX(0);}to{opacity:0;transform:translateX(40px);}}

/* Persistent "still uploading" reminder — unlike .ysa-toast it never
   auto-dismisses on its own; it's torn down explicitly once the batch ends
   (see uploadStatusPill() in admin.js), so it stays visible the whole run
   even if the admin scrolls away from the inline progress bar. */
.ysa-upload-pill{position:fixed;left:20px;bottom:20px;z-index:200;display:flex;align-items:center;gap:9px;
  font-family:var(--sans);font-size:12.5px;color:var(--ink);background:#fff;
  border:1px solid var(--line);border-radius:999px;padding:10px 18px 10px 14px;
  box-shadow:0 8px 28px rgba(0,0,0,.14);}
.ysa-upload-pill-spin{width:14px;height:14px;border-radius:50%;flex:none;
  border:2px solid var(--line);border-top-color:var(--ink);animation:gw-spin .7s linear infinite;}

/* set-default highlight */
.ysa-tree-set.is-default{background:#f4faf6;}
.ysa-tree-set.flash{animation:flashDefault .6s ease;}
@keyframes flashDefault{0%{background:#d9f0e0;}100%{background:#f4faf6;}}

/* ---- materials backend (admin) ---- */
.ysa-mat-wrap{overflow-x:auto;}
.ysa-mat-table{width:100%;border-collapse:collapse;font-size:13px;}
.ysa-mat-table th{text-align:left;font-weight:600;color:#6f6d66;padding:6px 8px;border-bottom:1px solid var(--line);}
.ysa-mat-table td{padding:5px 8px;border-bottom:1px solid var(--line);vertical-align:middle;}
.ysa-mat-dict .ysa-search{width:100%;min-width:90px;}
.ysa-mat-dict .ysa-mat-color{width:84px;min-width:0;}
.ysa-mat-chip{display:inline-block;width:14px;height:14px;border:1px solid var(--line);vertical-align:middle;margin-right:6px;}
.ysa-mat-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin:12px 0;}
.ysa-mat-field{display:flex;flex-direction:column;gap:4px;font-size:12px;color:#6f6d66;}
.ysa-mat-field .ysa-search{width:100%;}
.ysa-mat-preview{max-height:40vh;overflow:auto;}
.ysa-mat-tag{font-size:11px;color:#55534c;}
.is-nearest .ysa-mat-tag{color:#9a5b00;font-weight:600;}
.is-first-variant .ysa-mat-tag{color:#9a9a95;}
.ysa-mat-follow{font-size:11px;color:#6f6d66;margin-left:6px;white-space:nowrap;}
.ysa-mat-code{font-family:inherit;font-weight:600;color:var(--ink);}
/* uploaded swatch photo replaces the procedural texture */
.lib-mat-chip.has-photo,.lmc-chip.has-photo{background-blend-mode:normal;}
.lib-mat-chip.has-photo::before,.lib-mat-chip.has-photo::after,.lmc-chip.has-photo::before,.lmc-chip.has-photo::after{display:none;}
.ysa-mat-sw{width:40px;height:40px;border:1px solid var(--line);background:#f4f3f0 center/cover no-repeat;cursor:pointer;display:block;padding:0;}
.ysa-mat-sw:hover{border-color:#55534c;}
.ysa-mat-sw-cell{display:flex;align-items:center;gap:6px;}
.ysa-mat-sw-x{font-size:11px;color:#6f6d66;background:none;border:0;cursor:pointer;text-decoration:underline;padding:0;}

/* ---- Apply bar: staged flag edits, one write ---- */
.ysa-pending-bar{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:60;
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;width:min(720px,calc(100vw - 32px));box-sizing:border-box;
  padding:10px 12px 10px 16px;background:#2b2a27;color:#fff;font-family:var(--sans);font-size:14px;
  box-shadow:0 8px 28px rgba(0,0,0,.18);}
.ysa-pending-bar[hidden]{display:none;}
.ysa-pending-count{background:none;border:0;color:#fff;font:inherit;font-weight:600;cursor:pointer;padding:4px 0;text-decoration:underline;text-underline-offset:3px;text-decoration-color:rgba(255,255,255,.35);}
.ysa-pending-spacer{flex:1;}
.ysa-pending-list{order:5;flex-basis:100%;margin:4px 0 2px;padding:8px 0 0 18px;border-top:1px solid rgba(255,255,255,.15);max-height:30vh;overflow:auto;font-size:13px;color:#dedcd6;line-height:1.7;}
.ysa-pending-discard,.ysa-pending-apply{font:inherit;font-size:13px;font-weight:600;padding:8px 16px;cursor:pointer;border:1px solid rgba(255,255,255,.35);background:none;color:#fff;}
.ysa-pending-apply{background:#fff;color:#2b2a27;border-color:#fff;}
.ysa-pending-discard:hover{border-color:#fff;}
.ysa-pending-apply:disabled,.ysa-pending-discard:disabled{opacity:.5;cursor:default;}
body.has-pending{padding-bottom:90px;}
.ysa-tree-row.is-pending{box-shadow:inset 3px 0 0 #b0673a;}
#materialsBox.is-pending{box-shadow:inset 3px 0 0 #b0673a;}
.ysa-whoami{font-size:13px;color:var(--mute);margin-right:14px;}
.ysa-adminlog td:first-child{white-space:nowrap;color:#6f6d66;}
.ysa-adminlog-n{color:#9a9a95;font-size:12px;}
.ysa-plan-issues{margin:4px 0 2px;padding-left:18px;font-size:12px;color:#8a5a1e;line-height:1.55;}
.ysa-plan-issues li.is-err{color:#a3382d;font-weight:600;}

/* =================== ADMIN LAYOUT v2: tabs, More, detail panel =================== */
.ysa-top{display:flex;align-items:center;gap:28px;}
.ysa-tabs{display:flex;gap:24px;align-self:stretch;margin-right:auto;}
.ysa-tabs button{background:none;border:0;border-bottom:2px solid transparent;padding:10px 0;font:inherit;font-family:var(--sans);
  font-size:15px;color:var(--mute);cursor:pointer;white-space:nowrap;}
.ysa-tabs button.on{color:var(--ink);border-bottom-color:var(--charcoal);}
.ysa-tabs button:hover{color:var(--ink);}
.ysa-tabs .n{font-size:12px;color:#9a9a95;margin-left:4px;}
body[data-tab] .ysa-main [data-tab]{display:none !important;}
body[data-tab="products"] .ysa-main [data-tab="products"],
body[data-tab="materials"] .ysa-main [data-tab="materials"],
body[data-tab="activity"] .ysa-main [data-tab="activity"]:not([hidden]){display:block !important;}
body[data-tab="products"] .ysa-page,body[data-tab="materials"] .ysa-page{max-width:1440px;}

.ysa-more{position:relative;}
.ysa-more>summary{list-style:none;cursor:pointer;}
.ysa-more>summary::-webkit-details-marker{display:none;}
.ysa-more-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:30;background:#fff;border:1px solid var(--line);
  box-shadow:0 10px 30px rgba(0,0,0,.08);min-width:200px;padding:6px;display:flex;flex-direction:column;}
.ysa-more-menu .ysa-btn{justify-content:flex-start;text-align:left;border:0;border-radius:0;width:100%;padding:9px 10px;}
.ysa-more-menu .ysa-btn:hover{background:#f6f5f2;}

.ysa-prod-layout{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:24px;align-items:start;}
.ysa-prod-layout .ysa-tree-row .ysa-btn-icon,.ysa-prod-layout .ysa-tree-row .ysa-icon-active{display:none;}
.ysa-tree-row{cursor:pointer;}
.ysa-tree-row.is-selected{background:#ecebe6 !important;box-shadow:inset 3px 0 0 var(--charcoal);}
.ysa-tree-row.is-selected.is-pending{box-shadow:inset 3px 0 0 #b0673a;}
.ysa-tree-code{margin-left:8px;font-size:12px;color:#a3a19a;}
.ysa-pill-role{border-color:var(--charcoal) !important;color:var(--ink) !important;}

.ysa-detail{position:sticky;top:16px;border:1px solid var(--line);background:#fff;padding:18px;min-height:220px;font-family:var(--sans);}
.ysa-detail-empty{display:flex;flex-direction:column;gap:6px;color:var(--mute);font-size:14px;padding:40px 8px;text-align:center;}
.ysa-detail-empty b{color:var(--ink);font-weight:600;}
.ysa-detail-close{display:none;position:absolute;right:10px;top:8px;background:none;border:0;font-size:24px;line-height:1;color:var(--mute);cursor:pointer;}
.ysa-detail-thumb{height:170px;background:#f4f3f0;display:flex;align-items:center;justify-content:center;margin:-18px -18px 14px;}
.ysa-detail-thumb img{max-width:100%;max-height:100%;object-fit:contain;mix-blend-mode:multiply;}
.ysa-detail-kind{font-size:12px;color:var(--mute);}
.ysa-detail-title{font-size:18px;font-weight:600;margin:2px 0 2px;line-height:1.3;}
.ysa-detail-code{font-size:12px;color:#a3a19a;word-break:break-all;}
.ysa-detail-path{font-size:12px;color:var(--mute);margin-top:6px;}
.ysa-detail-facts{font-size:12px;color:var(--mute);margin-top:2px;}
.ysa-detail-sec{margin-top:14px;border-top:1px solid var(--line);}
.ysa-detail-k{font-size:12px;color:var(--mute);margin:16px 0 6px;}
.ysa-sw-row{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;background:none;border:0;border-bottom:1px solid var(--line);
  padding:11px 0;font:inherit;text-align:left;cursor:pointer;color:var(--ink);}
.ysa-sw-row:disabled{cursor:default;opacity:.75;}
.ysa-sw-l{display:flex;flex-direction:column;font-size:14px;}
.ysa-sw-l small{font-size:12px;color:var(--mute);}
.ysa-sw{flex:none;width:36px;height:20px;border:1px solid var(--charcoal);position:relative;background:#fff;}
.ysa-sw::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;background:var(--charcoal);transition:left .15s;}
.ysa-sw.on{background:var(--charcoal);}
.ysa-sw.on::after{left:18px;background:#fff;}
.ysa-act{display:flex;flex-direction:column;width:100%;text-align:left;border:1px solid var(--line);background:#fff;padding:9px 12px;margin-top:6px;
  font:inherit;font-size:14px;color:var(--ink);cursor:pointer;}
.ysa-act:hover{border-color:var(--charcoal);}
.ysa-act small{font-size:12px;color:var(--mute);margin-top:1px;}
.ysa-act.is-done{cursor:default;background:#f6f5f2;border-color:#f6f5f2;}
.ysa-detail-danger{margin-top:20px;padding-top:12px;border-top:1px solid var(--line);}
.ysa-danger-link{background:none;border:0;padding:0;font:inherit;font-size:14px;color:#a3382d;cursor:pointer;}
.ysa-danger-link:hover{text-decoration:underline;}

/* ---- responsive ---- */
@media (max-width:1099px){
  .ysa-prod-layout{grid-template-columns:1fr;}
  .ysa-detail{display:none;}
  body.detail-open .ysa-detail{display:block;position:fixed;left:0;right:0;bottom:0;top:auto;z-index:70;max-height:78vh;overflow:auto;
    border-left:0;border-right:0;box-shadow:0 -12px 40px rgba(0,0,0,.18);}
  body.detail-open .ysa-detail-close{display:block;}
  body.detail-open.has-pending .ysa-pending-bar{bottom:calc(78vh + 10px);}
}
@media (max-width:760px){
  .ysa-page{padding:14px 14px 0;}
  .ysa-top{flex-wrap:wrap;gap:10px 18px;}
  .ysa-tabs{order:3;width:100%;overflow-x:auto;gap:20px;margin-right:0;}
  .ysa-card{padding:16px;border-radius:0;}
  .ysa-card-head{flex-direction:column;align-items:stretch;}
  .ysa-card-tools{flex-wrap:wrap;}
  .ysa-search{flex:1 1 100%;}
  .ysa-tree-meta{display:none;}
  .ysa-mat-dict th:nth-child(2),.ysa-mat-dict td:nth-child(2){display:none;}
}

/* =================== ADMIN v2 — visual pass (editorial, calm) =================== */
body.ysa-v2 .ysa-top{border-bottom:1px solid var(--line);padding-bottom:0;min-height:60px;}
body.ysa-v2 .ysa-tabs button{padding:18px 0 16px;}
body.ysa-v2 .ysa-whoami{font-size:13px;}
/* list pages sit on the page, not in a card inside a card */
body.ysa-v2 .ysa-main>#adminMain>section[data-tab="products"]:not(.ysa-tab-upload),
body.ysa-v2 .ysa-main>#adminMain>section[data-tab="materials"]{border:0;background:none;padding:6px 0 0;border-radius:0;animation:none;}
/* Upload keeps its own boxed card look, just relocated above the product list */
body.ysa-v2 .ysa-main>#adminMain>section.ysa-tab-upload{margin-bottom:20px;}
body.ysa-v2 .ysa-card .ysa-h{font-size:22px;font-weight:600;letter-spacing:-.01em;}
body.ysa-v2 .ysa-card .ysa-h .ysa-pill{border:0;background:none;padding:0;font-size:14px;font-weight:400;color:var(--mute);letter-spacing:0;text-transform:none;vertical-align:baseline;}
body.ysa-v2 .ysa-p-sub{font-size:13px;color:var(--mute);margin:2px 0 14px;}
/* buttons: sentence case, readable size, square */
body.ysa-v2 .ysa-btn{font-size:13px;letter-spacing:0;text-transform:none;border-radius:0;}
body.ysa-v2 .ysa-btn-mini{padding:7px 12px;font-size:13px;}
body.ysa-v2 .ysa-btn-ghost{color:var(--charcoal);border-color:var(--line);background:#fff;}
body.ysa-v2 .ysa-btn-ghost:hover{border-color:var(--charcoal);color:var(--ink);}
body.ysa-v2 .ysa-btn-ghost.active{border-color:var(--charcoal);background:var(--charcoal);color:#fff;}
/* toolbar on one line */
body.ysa-v2 [data-tab="products"] .ysa-card-head{align-items:center;margin-bottom:6px;}
body.ysa-v2 [data-tab="products"] .ysa-card-tools{flex-wrap:nowrap;gap:8px;}
body.ysa-v2 [data-tab="products"] .ysa-card-tools .ysa-search{flex:0 1 300px;min-width:160px;}
body.ysa-v2 [data-tab="products"] .ysa-card-tools #auditManifest{margin-left:0;}
body.ysa-v2 .ysa-search{font-size:14px;border:0;border-bottom:1px solid var(--line);border-radius:0;padding:8px 2px;background:transparent;}
body.ysa-v2 .ysa-search:focus{outline:none;border-bottom-color:var(--charcoal);}
body.ysa-v2 select.ysa-search{border:1px solid var(--line);padding:7px 10px;font-size:13px;background:#fff;}
/* the list */
body.ysa-v2 .ysa-tree{border:0;border-top:1px solid var(--line);border-radius:0;font-size:14px;margin-top:4px;}
body.ysa-v2 .ysa-tree-row{padding:10px 12px;gap:12px;}
body.ysa-v2 .ysa-tree-col{background:#fff;padding:14px 12px;}
body.ysa-v2 .ysa-tree-col:hover,body.ysa-v2 .ysa-tree-prod:hover,body.ysa-v2 .ysa-tree-set:hover{background:#f7f6f3;}
body.ysa-v2 .ysa-tree-col .ysa-tree-name{font-size:15px;font-weight:600;}
body.ysa-v2 .ysa-tree-prod{background:#fff;padding-left:34px;}
body.ysa-v2 .ysa-tree-prod::before{left:19px;}
body.ysa-v2 .ysa-tree-prod .ysa-tree-name{font-size:14px;font-weight:500;color:var(--ink);}
body.ysa-v2 .ysa-tree-set{padding-left:56px;}
body.ysa-v2 .ysa-tree-set::before,body.ysa-v2 .ysa-tree-set::after{left:19px;}
body.ysa-v2 .ysa-tree-set .ysa-tree-name{font-size:13.5px;}
body.ysa-v2 .ysa-tree-set.is-default{background:#fff;}
body.ysa-v2 .ysa-tree-code{font-size:12px;color:#b3b1aa;}
body.ysa-v2 .ysa-tree-meta{font-size:12px;letter-spacing:0;text-transform:none;color:var(--mute);}
body.ysa-v2 .ysa-tree-meta.warn{color:#a3382d;}
body.ysa-v2 .ysa-pill{font-size:11px;letter-spacing:0;text-transform:none;border-radius:0;padding:2px 7px;}
body.ysa-v2 .ysa-pill-new{border:0;background:none;padding:0;color:#3f6f9a;}
body.ysa-v2 .ysa-pill-hidden{border-color:#dcc8b4;background:#fff;color:#9a5b2a;}
/* checkboxes only when you are selecting */
body.ysa-v2 .ysa-tree-check{opacity:0;transition:opacity .12s;}
body.ysa-v2 .ysa-tree-row:hover .ysa-tree-check,body.ysa-v2.has-selection .ysa-tree-check,body.ysa-v2 .ysa-tree-check:focus-visible{opacity:1;}
body.ysa-v2 .ysa-tree-row.is-selected{background:#efede8 !important;}
/* detail panel: a quiet column, not another box */
body.ysa-v2 .ysa-prod-layout{gap:32px;}
body.ysa-v2 .ysa-detail{border:0;border-left:1px solid var(--line);padding:4px 0 24px 28px;top:12px;}
body.ysa-v2 .ysa-detail-thumb{margin:0 0 16px;height:230px;background:#f4f3f0;}
body.ysa-v2 .ysa-detail-title{font-size:20px;}
@media (max-width:1099px){
  body.ysa-v2.detail-open .ysa-detail{border-left:0;padding:16px 18px 24px;background:#fff;}
  body.ysa-v2 [data-tab="products"] .ysa-card-tools{flex-wrap:wrap;}
}
body.ysa-v2 .ysa-btn{white-space:nowrap;}
body.ysa-v2 [data-tab="products"] .ysa-card-tools>*{flex:none;}
body.ysa-v2 [data-tab="products"] .ysa-card-tools .ysa-search{flex:0 1 300px;}

/* =================== ADMIN v2 — outline pass: one grid, one control style =================== */
body.ysa-v2{--ctl-h:36px;}
/* header: everything on one baseline */
body.ysa-v2 .ysa-top{align-items:center;}
body.ysa-v2 .ysa-top .actions{display:flex;align-items:center;gap:22px;}
body.ysa-v2 .ysa-whoami{font-size:13px;color:var(--mute);margin:0;}
body.ysa-v2 .ysa-top .ctl{font-size:14px;}
/* page head: title left, tools right, same left edge as the list */
body.ysa-v2 section[data-tab] .ysa-card-head{padding:0;margin:0 0 16px;gap:20px;align-items:center;}
body.ysa-v2 section[data-tab] .ysa-h{margin:0;}
/* ONE control style: 36px, 1px line, square */
body.ysa-v2 .ysa-card-tools .ysa-btn,body.ysa-v2 .ysa-card-tools select,body.ysa-v2 .ysa-card-tools .ysa-search,
body.ysa-v2 .ysa-more>summary{height:var(--ctl-h);box-sizing:border-box;display:inline-flex;align-items:center;}
body.ysa-v2 [data-tab="products"] .ysa-card-tools .ysa-search{border:1px solid var(--line);padding:0 12px 0 34px;font-size:14px;width:300px;
  background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236f6d66' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.5-3.5'/%3E%3C/svg%3E") no-repeat 12px center;}
body.ysa-v2 [data-tab="products"] .ysa-card-tools .ysa-search:focus{border-color:var(--charcoal);}
/* the two filters read as one segmented control */
body.ysa-v2 #filterIncomplete{margin-right:-9px;}
body.ysa-v2 #filterIncomplete.active+#filterHidden,body.ysa-v2 #filterHidden.active{position:relative;}
body.ysa-v2 select.ysa-search{padding:0 30px 0 12px;}
/* ---- list rhythm: 44 / 40 / 36, indent 24px per level, one hairline ---- */
body.ysa-v2 .ysa-tree{border-top:1px solid var(--line);}
body.ysa-v2 .ysa-tree-row{padding:0 12px 0 0;border-bottom:1px solid #efede9;}
body.ysa-v2 .ysa-tree-col{height:44px;padding-left:0;border-bottom-color:var(--line);}
body.ysa-v2 .ysa-tree-prod{height:40px;padding-left:24px;}
body.ysa-v2 .ysa-tree-set{height:36px;padding-left:48px;}
body.ysa-v2 .ysa-tree-prod::before,body.ysa-v2 .ysa-tree-set::before,body.ysa-v2 .ysa-tree-set::after{display:none;}
/* caret -> a real chevron in a fixed 24px column */
body.ysa-v2 .ysa-plan-caret{flex:none;width:24px;height:24px;font-size:0 !important;position:relative;display:inline-block;}
body.ysa-v2 .ysa-plan-caret::before{content:'';position:absolute;left:8px;top:8px;width:6px;height:6px;border-right:1.5px solid var(--charcoal);
  border-bottom:1.5px solid var(--charcoal);transform:rotate(-45deg);transition:transform .15s;}
body.ysa-v2 .is-open>.ysa-plan-caret::before{transform:rotate(45deg);top:6px;}
body.ysa-v2 .ysa-tree-set .ysa-tree-check{margin-left:0;}
body.ysa-v2 .ysa-tree-meta{min-width:90px;text-align:right;font-variant-numeric:tabular-nums;}
body.ysa-v2 .ysa-tree-row.is-selected{box-shadow:inset 2px 0 0 var(--charcoal);}
/* ---- detail panel: a list, not a stack of boxes ---- */
body.ysa-v2 .ysa-detail{padding:0 0 24px 28px;}
body.ysa-v2 .ysa-detail-thumb{height:240px;margin-bottom:18px;}
body.ysa-v2 .ysa-detail-kind{font-size:12px;letter-spacing:0;}
body.ysa-v2 .ysa-detail-facts{margin-top:6px;}
body.ysa-v2 .ysa-detail-sec{margin-top:18px;border-top:1px solid var(--line);}
body.ysa-v2 .ysa-detail-k{margin:22px 0 0;padding-bottom:6px;border-bottom:1px solid var(--line);font-size:12px;color:var(--mute);}
body.ysa-v2 .ysa-sw-row{padding:12px 0;}
body.ysa-v2 .ysa-sw{width:32px;height:18px;border-color:#b9b6ae;}
body.ysa-v2 .ysa-sw::after{width:12px;height:12px;background:#b9b6ae;}
body.ysa-v2 .ysa-sw.on{background:var(--charcoal);border-color:var(--charcoal);}
body.ysa-v2 .ysa-sw.on::after{left:16px;background:#fff;}
body.ysa-v2 .ysa-act{flex-direction:row;align-items:center;justify-content:space-between;border:0;border-bottom:1px solid var(--line);margin:0;padding:12px 0;background:none;}
body.ysa-v2 .ysa-act:hover{background:none;}
body.ysa-v2 .ysa-act:hover .ysa-act-l>span{text-decoration:underline;text-underline-offset:3px;}
body.ysa-v2 .ysa-act-l{display:flex;flex-direction:column;}
body.ysa-v2 .ysa-act-l small{font-size:12px;color:var(--mute);}
body.ysa-v2 .ysa-act-r{font-size:18px;color:var(--mute);}
body.ysa-v2 .ysa-act.is-done{background:none;border-color:var(--line);}
body.ysa-v2 .ysa-act.is-done .ysa-act-r{font-size:12px;color:var(--ink);}
body.ysa-v2 .ysa-detail-danger{border-top:0;margin-top:24px;padding-top:0;}
/* ---- Materials: grouped table, one control style ---- */
body.ysa-v2 .ysa-mat-dict{table-layout:fixed;border-collapse:collapse;width:100%;}
body.ysa-v2 .ysa-mat-dict col.c-sw{width:56px;} body.ysa-v2 .ysa-mat-dict col.c-code{width:22%;}
body.ysa-v2 .ysa-mat-dict col.c-used{width:90px;} body.ysa-v2 .ysa-mat-dict col.c-cat{width:160px;} body.ysa-v2 .ysa-mat-dict col.c-act{width:290px;}
body.ysa-v2 .ysa-mat-dict th{font-size:12px;font-weight:400;color:var(--mute);padding:0 10px 8px;border-bottom:1px solid var(--line);}
body.ysa-v2 .ysa-mat-dict td{padding:8px 10px;height:52px;border-bottom:1px solid #efede9;}
body.ysa-v2 .ysa-mat-dict .num{text-align:right;font-variant-numeric:tabular-nums;}
body.ysa-v2 .ysa-mat-group th{padding:26px 10px 8px;font-size:15px;font-weight:600;color:var(--ink);border-bottom:1px solid var(--line);text-align:left;}
body.ysa-v2 .ysa-mat-group th span{font-weight:400;font-size:13px;color:var(--mute);margin-left:4px;}
body.ysa-v2 .ysa-mat-dict tbody:first-of-type .ysa-mat-group th{padding-top:12px;}
body.ysa-v2 .ysa-mat-code{font-weight:600;font-size:13.5px;word-break:break-all;}
body.ysa-v2 .ysa-mat-alias{display:block;font-weight:400;font-size:12px;color:var(--mute);}
body.ysa-v2 .ysa-field{height:var(--ctl-h);box-sizing:border-box;width:100%;border:1px solid var(--line);background:#fff;padding:0 10px;
  font:inherit;font-size:14px;color:var(--ink);border-radius:0;}
body.ysa-v2 .ysa-field:focus{outline:none;border-color:var(--charcoal);}
body.ysa-v2 .ysa-mat-sw{width:36px;height:36px;border:1px solid var(--line);}
body.ysa-v2 .ysa-mat-sw.is-auto{border-style:dashed;border-color:#c9c6bf;}
body.ysa-v2 .ysa-mat-acts{text-align:right;white-space:nowrap;}
body.ysa-v2 .ysa-textbtn{background:none;border:0;padding:0;margin-left:16px;font:inherit;font-size:13px;color:var(--charcoal);cursor:pointer;}
body.ysa-v2 .ysa-textbtn:hover{color:var(--ink);text-decoration:underline;text-underline-offset:3px;}
@media (max-width:1099px){
  body.ysa-v2 .ysa-mat-dict col.c-act{width:auto;} body.ysa-v2 .ysa-mat-dict col.c-cat{width:130px;}
}
@media (max-width:760px){
  body.ysa-v2 .ysa-mat-dict,body.ysa-v2 .ysa-mat-dict tbody,body.ysa-v2 .ysa-mat-dict tr,body.ysa-v2 .ysa-mat-dict td{display:block;width:auto;height:auto;}
  body.ysa-v2 .ysa-mat-dict thead{display:none;}
  body.ysa-v2 .ysa-mat-dict tr[data-code]{display:grid;grid-template-columns:44px 1fr;gap:6px 12px;padding:12px 0;border-bottom:1px solid var(--line);}
  body.ysa-v2 .ysa-mat-dict tr[data-code] td{border:0;padding:0;}
  body.ysa-v2 .ysa-mat-swcell{grid-row:span 3;}
  body.ysa-v2 .ysa-mat-dict td.num{display:none;}
  body.ysa-v2 .ysa-mat-acts{text-align:left;} body.ysa-v2 .ysa-textbtn{margin:0 16px 0 0;}
  body.ysa-v2 [data-tab="products"] .ysa-card-tools .ysa-search{width:100%;}
}
/* the header must not move between tabs: one page width for every tab,
   and room for the scrollbar is always reserved (short vs long pages) */
html:has(body.ysa-body){scrollbar-gutter:stable;}
body.ysa-v2 .ysa-page,body.ysa-v2[data-tab] .ysa-page{max-width:1440px;}

/* =================== ADMIN v2 — audit fixes (contrast, focus, Upload & Activity) =================== */
/* contrast: the old --mute (#9a9a95) was 2.8:1 on white; these pass WCAG AA (4.5:1+) */
body.ysa-v2{--mute:#6f6d66;--faint:#8a877f;}
body.ysa-v2 .ysa-tabs button{color:#6f6d66;}
body.ysa-v2 .ysa-tabs .n,body.ysa-v2 .ysa-tree-code,body.ysa-v2 .ysa-card .ysa-h .ysa-pill{color:#7a776f;}
/* visible keyboard focus everywhere */
body.ysa-v2 :is(button,a,input,select,summary,[tabindex]):focus-visible{outline:2px solid var(--charcoal);outline-offset:2px;}
body.ysa-v2 .ysa-tree-row:focus-visible{outline:2px solid var(--charcoal);outline-offset:-2px;}
/* the sort select never gets the search icon */
body.ysa-v2 [data-tab="products"] .ysa-card-tools select.ysa-search{background-image:none;padding:0 28px 0 12px;width:auto;}
/* Upload + Activity: same type scale and cards as the rest — no 9-10px text, no pale grey */
body.ysa-v2 section.ysa-tab-upload,body.ysa-v2 section[data-tab="activity"]{border:0;border-top:1px solid var(--line);border-radius:0;padding:24px 0 8px;background:none;animation:none;}
body.ysa-v2 section[data-tab="activity"]+section[data-tab="activity"]{margin-top:8px;}
body.ysa-v2 section[data-tab] *{letter-spacing:normal;}
body.ysa-v2 section.ysa-tab-upload :is(label,small,span,p,em,i,div),body.ysa-v2 section[data-tab="activity"] :is(small,span,p,em,i,div,td,th){text-transform:none;}
body.ysa-v2 .ysa-main section[data-tab] :is(.ysa-p,.ysa-p-sub,.ysa-hint,.ysa-plan-var-meta,.ysa-usage-meta,.ysa-analytics-meta,.ysa-backup-meta){font-size:13px;color:var(--mute);}
body.ysa-v2 .ysa-main section[data-tab] :is(label,.ysa-fld label,.ysa-usage-label,.ysa-plan-k,.ysa-up-label){font-size:12px;color:var(--mute);}
body.ysa-v2 .ysa-main section.ysa-tab-upload *,body.ysa-v2 .ysa-main section[data-tab="activity"] *{min-font-size:12px;}
body.ysa-v2 .ysa-main section.ysa-tab-upload :is(span,small,em,i,label,p,div),
body.ysa-v2 .ysa-main section[data-tab="activity"] :is(span,small,em,i,p,div,td,th,button){font-size:max(12px, 1em);}
body.ysa-v2 .ysa-usage{border:0;padding:20px 0 4px;background:none;}
body.ysa-v2 .ysa-usage *{font-size:13px !important;color:var(--mute);}
body.ysa-v2 .ysa-usage-num{color:var(--ink) !important;}
body.ysa-v2 .ysa-main section.ysa-tab-upload input:not([type=radio]):not([type=checkbox]){height:var(--ctl-h);border:1px solid var(--line);border-radius:0;font-size:14px;padding:0 10px;}
body.ysa-v2 .ysa-main section.ysa-tab-upload .ysa-btn-primary{height:40px;padding:0 22px;font-size:14px;background:var(--charcoal);border-color:var(--charcoal);color:#fff;}
body.ysa-v2 .ysa-main section.ysa-tab-upload .ysa-btn-primary:disabled{background:#e6e3dd;border-color:#e6e3dd;color:#8a877f;}
/* Activity lists grow with the page — no scroll area inside the page */
body.ysa-v2 .ysa-backup-list,body.ysa-v2 .ysa-analytics-list,body.ysa-v2 .ysa-access-list{max-height:none;overflow:visible;}
body.ysa-v2 .ysa-analytics-row,body.ysa-v2 .ysa-backup-row{font-size:13.5px;padding:10px 0;}
body.ysa-v2 .ysa-analytics-meta,body.ysa-v2 .ysa-backup-meta{font-size:12px !important;letter-spacing:0;text-transform:none;}

