
/* Hallmark · genre: playful · theme: pinkyu (tweakcn) · design-system: design.md · designed-as-app */
:root{
/* ===== pinkyu, the ENTIRE registry verbatim, in tweakcn's own token names. Regenerated
   from https://tweakcn.com/r/themes/cms0ur5fd000204jl0f5546vq — the same JSON the shadcn
   CLI consumes, which cannot run here (no components.json, no Tailwind, no React). Values
   are oklch as shipped. Nothing below this line is hand-typed, so it cannot drift. ===== */
  /* surfaces */
  --background:oklch(0.9884 0.0044 359.9937);
  --foreground:oklch(0.4029 0.0250 36.9948);
  --card:oklch(1.0000 0 0);
  --card-foreground:oklch(0.4029 0.0250 36.9948);
  --popover:oklch(1.0000 0 0);
  --popover-foreground:oklch(0.4029 0.0250 36.9948);
  /* roles */
  --primary:oklch(0.8642 0.0513 4.4534);
  --primary-foreground:oklch(1.0000 0 0);
  --secondary:oklch(0.8590 0.0323 174.1791);
  --secondary-foreground:oklch(0.3695 0.0249 167.8736);
  --muted:oklch(0.9731 0.0079 357.7963);
  --muted-foreground:oklch(0.6334 0.0257 357.7990);
  --accent:oklch(0.9368 0.0381 92.2360);
  --accent-foreground:oklch(0.5291 0.0564 88.3303);
  --destructive:oklch(0.7812 0.0836 13.5094);
  --destructive-foreground:oklch(1.0000 0 0);
  /* edges */
  --border:oklch(0.9437 0.0206 358.6875);
  --input:oklch(0.9958 0.0025 345.2100);
  --ring:oklch(0.8642 0.0513 4.4534);
  /* charts */
  --chart-1:oklch(0.8642 0.0513 4.4534);
  --chart-2:oklch(0.8590 0.0323 174.1791);
  --chart-3:oklch(0.9368 0.0381 92.2360);
  --chart-4:oklch(0.8556 0.0326 301.5194);
  --chart-5:oklch(0.8901 0.0375 38.6811);
  /* sidebar */
  --sidebar:oklch(1.0000 0 0);
  --sidebar-foreground:oklch(0.4029 0.0250 36.9948);
  --sidebar-primary:oklch(0.8642 0.0513 4.4534);
  --sidebar-primary-foreground:oklch(1.0000 0 0);
  --sidebar-accent:oklch(0.9731 0.0079 357.7963);
  --sidebar-accent-foreground:oklch(0.4029 0.0250 36.9948);
  --sidebar-border:oklch(0.9437 0.0206 358.6875);
  --sidebar-ring:oklch(0.8642 0.0513 4.4534);
  /* type */
  --font-sans:'Jua', sans-serif;
  --font-serif:'Jua', serif;
  --font-mono:monospace;
  --tracking-normal:0.02em;
  --letter-spacing:0.02em;
  --spacing:0.25rem;
  /* shadow */
  --shadow-color:#b76b96;
  --shadow-opacity:1;
  --shadow-blur:0px;
  --shadow-spread:0px;
  --shadow-offset-x:4px;
  --shadow-offset-y:4px;
  --shadow-2xs:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 0.50);
  --shadow-xs:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 0.50);
  --shadow-sm:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 1.00), 4px 1px 2px -1px hsl(326.0526 34.5455% 56.8627% / 1.00);
  --shadow:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 1.00), 4px 1px 2px -1px hsl(326.0526 34.5455% 56.8627% / 1.00);
  --shadow-md:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 1.00), 4px 2px 4px -1px hsl(326.0526 34.5455% 56.8627% / 1.00);
  --shadow-lg:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 1.00), 4px 4px 6px -1px hsl(326.0526 34.5455% 56.8627% / 1.00);
  --shadow-xl:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 1.00), 4px 8px 10px -1px hsl(326.0526 34.5455% 56.8627% / 1.00);
  --shadow-2xl:4px 4px 0px 0px hsl(326.0526 34.5455% 56.8627% / 2.50);
  /* theme section (Tailwind tracking scale, derived) */
  --tracking-tighter:calc(var(--tracking-normal) - 0.05em);
  --tracking-tight:calc(var(--tracking-normal) - 0.025em);
  --tracking-wide:calc(var(--tracking-normal) + 0.025em);
  --tracking-wider:calc(var(--tracking-normal) + 0.05em);
  --tracking-widest:calc(var(--tracking-normal) + 0.1em);
  --radius:0.4rem;

/* ===== overrides, each with the ratio it buys. The theme puts WHITE on its pastels, which
   measures 1.5-2:1 — these are the only departures, and every one stays inside the palette. */
--primary-foreground:#55443f;     /* was #FFF on #f1c5ce = 1.54:1 -> 5.96:1 (the theme's own foreground) */
--muted-foreground:#7E646C;       /* was #98848a = 3.38:1 on background -> 5.17:1 */
--accent-foreground:#6B5D3B;      /* was #796a45 = 4.41:1 on accent -> 5.37:1 */
--destructive:#C4485A;            /* was #e8a2a8 + white = 2.07:1 -> 4.75:1 */
--border-strong:#BC7590;          /* the registry has no control-border token; --border is 1.15:1, WCAG 1.4.11 wants 3. This is 3.33 */
--star:#96681F;                   /* nor a rarity-gold token; 4.70:1 on background */
/* --radius is a BASE, not a value to paste everywhere. shadcn derives a scale from it and
   that scale is most of why the tweakcn demo reads as designed: measured on their page,
   cards land at 10.4px and buttons/inputs at 4.4px. Flattening all 151 radii to one 6.4px
   is what made this look uniform-but-flat. Big containers rounder, small controls tighter. */
--radius:0.4rem;
--radius-lg:calc(var(--radius) + 4px);   /* cards, stages, sheets */
--radius-sm:calc(var(--radius) - 2px);   /* INPUTS and swatches — things you type in or
                                            pick from are rectangles, in pinkyu too */
/* ...but anything you PRESS is a pill. That's the cute in this theme and it's what
   tweakcn's own chrome does: fully round buttons and tabs, rectangular fields. Deriving
   4.4px buttons from --radius was shadcn convention, not this theme's look. */
--radius-pill:999px;
/* legacy names the rest of this sheet still reads — now aliases, so there is ONE palette */
--paper:var(--background);--paper-2:#fbf4f6;--ink:var(--foreground);--ink2:var(--muted-foreground);
--coral:var(--primary);--coral-d:#B0405A;   /* accent for TEXT and icons only: 5.30:1 on the background */
--on-coral:var(--foreground);   /* filled buttons are the theme's soft primary with dark text (5.96:1), not an invented dark pink with white text. That invention was the loudest thing on the page. */
--line:var(--border);
--gold:#E0A93B;--sage:#7FB79E;
/* Scrollbar thumb. NOT --primary: the soft sakura pink measures 1.49:1 on this page,
   fainter than the grey it replaces, so a themed bar would be a usability step DOWN.
   Same hue at oklch L 0.64 -> 3.40:1, visible and still rose. Dark keeps --primary,
   which is already 7.51:1 on charcoal. */
--scroll-thumb:#ba7585;
/* The hard offset shadow is a CONTROL affordance, not a card treatment. Measured on the
   tweakcn demo: their cards are box-shadow:none and only buttons/inputs carry the 3px
   offset. Stamping it on every card is what gave this the all-over rubber-stamp look. */
--shadow-ctl:var(--shadow-2xs);   /* controls wear the theme ramp now, not my hand-rolled offset */
--shadow-s:0 1px 2px color-mix(in srgb, var(--shadow-color) 7%, transparent),0 4px 12px -6px color-mix(in srgb, var(--shadow-color) 14%, transparent);   /* surfaces stay soft: the ramp above is all hard offsets, which is a control look */
--shadow-m:0 2px 6px color-mix(in srgb, var(--shadow-color) 8%, transparent),0 14px 30px -16px color-mix(in srgb, var(--shadow-color) 20%, transparent);
--ease-out:cubic-bezier(.16,1,.3,1);
/* Jua owns display (it IS the theme's voice) but it ships weight 400 only, so body text
   set in it was all synthesised bold — thicker strokes, no real hierarchy, worst at the
   11-12px uppercase labels. Nunito carries body, Caveat keeps the handwritten card plate. */
--display:"Jua",system-ui,-apple-system,sans-serif;--hand:"Caveat","Segoe Print",system-ui,cursive;
--body:"Nunito",system-ui,-apple-system,"Segoe UI",sans-serif}
/* Dark. Driven by [data-theme], which the inline boot script sets from the saved choice or
   the system preference — one rule instead of a @media copy AND an override copy.

   NOT pinkyu's published dark half: that one is a saturated plum (#2D1B22, chroma 18) under
   near-white text at 14.7:1, and a saturated dark ground under near-white is the recipe for
   eye strain — the eye can't settle on the hue and the text haloes. This keeps the warmth
   but pulls the ground to chroma 6 and the text back to 11.8:1, with three visible elevation
   steps (bg -> card -> muted) so surfaces read as layers instead of one murky field.
   Only TOKENS flip; object colours (wood, straps, packs, auras) keep their hues — a walnut
   shelf is walnut at night too. */
:root[data-theme=dark]{
  color-scheme:dark;   /* native controls, scrollbars and form widgets follow */
  --background:#2A2427;--foreground:#E8E1E4;--card:#393135;--card-foreground:#E8E1E4;
  --popover:#393135;--popover-foreground:#E8E1E4;
  --primary:#e8a4b0;--primary-foreground:#291f23;
  --secondary:#8fb991;--secondary-foreground:#202e20;
  --muted:#443A3F;--muted-foreground:#B5A8AE;
  --accent:#d2ae56;--accent-foreground:#291f23;
  --destructive:#ce8a8a;--destructive-foreground:#291f23;
  --border:#453B40;--input:#393135;--ring:#e8a4b0;
  --border-strong:#8D7B84;--star:#E3BE7E;
  --sidebar:oklch(0.2274 0.0174 352.6907);--sidebar-foreground:oklch(0.9717 0.0100 1.9807);
  --sidebar-primary:oklch(0.7869 0.0818 7.4637);--sidebar-primary-foreground:oklch(0.2527 0.0169 352.4401);
  --sidebar-accent:oklch(0.3053 0.0203 358.0195);--sidebar-accent-foreground:oklch(0.9717 0.0100 1.9807);
  --sidebar-border:oklch(0.3511 0.0228 356.3895);--sidebar-ring:oklch(0.7869 0.0818 7.4637);
  --shadow-2xs:4px 4px 0px 2px hsl(0 0% 0% / 0.50);
  --shadow-sm:4px 4px 0px 2px hsl(0 0% 0% / 1.00), 4px 1px 2px 1px hsl(0 0% 0% / 1.00);
  --shadow-md:4px 4px 0px 2px hsl(0 0% 0% / 1.00), 4px 2px 4px 1px hsl(0 0% 0% / 1.00);
  --coral-d:#e8a4b0;--on-coral:#291f23;   /* dark: primary is light, so its text is the dark ground */   /* on dark the "press me" pink is the LIGHT one, so its text flips */
  /* The one legacy alias that was a LITERAL instead of a token: --paper-2 stayed #fbf4f6
     here, so .lblock.band painted a near-white panel in dark mode and put --foreground
     (#E8E1E4) on it at 1.19:1 — "Not a spreadsheet. A family." was invisible. Its
     siblings (--paper, --ink, --ink2, --line) all alias tokens and flipped correctly. */
  --paper-2:var(--muted);--scroll-thumb:var(--primary);
  --shadow-color:325.78 18% 10%}
/* the band's top highlight is a 55%-white hairline: a raised edge on cream, a glare on charcoal */
:root[data-theme=dark] .lblock.band{box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
/* Panel gradients that ran from a THEME TOKEN to a hardcoded light literal — half
   theme-aware, half not. In dark they faded from the dark surface to near-white, so text
   sitting on the far end lost its ground: "We'll sign you in with Google" on .finalcta was
   --ink2 over a cream stop. Each rule keeps its own light value in --grad-far; this flips
   all four at once. A contrast sweep will NOT catch these — a gradient is background-image,
   so walking up for backgroundColor sails past it to the body and reports a pass. */
:root[data-theme=dark] .vstage,
:root[data-theme=dark] .exnudge,:root[data-theme=dark] .finalcta{--grad-far:var(--card)}
/* the paper grain reads as dust on a dark ground */
:root[data-theme=dark] body{background-image:none}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
*{box-sizing:border-box;margin:0}
:focus-visible{outline:2px solid var(--coral-d);outline-offset:2px;border-radius:4px}
body{font-family:var(--body);color:var(--ink);background:var(--paper);line-height:1.55;-webkit-font-smoothing:antialiased;
  letter-spacing:var(--letter-spacing); /* pinkyu ships this and it was going unused */
  /* paper grain: fractal noise at ~4% alpha, flat cream becomes touchable paper */
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='.045'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")}
.wrap{max-width:520px;margin:0 auto;padding:48px 22px}
h1{font-family:var(--display);font-weight:600;font-size:2rem;line-height:1.1;margin-bottom:10px}
h2{font-family:var(--display);font-weight:600;font-size:1.3rem;margin-bottom:8px}
p{color:var(--ink2);margin-bottom:16px}
/* Inline prose links. There was no bare 'a' rule at all, so every link that wasn't a button or
   a card fell through to the UA's blue/purple — the one thing on the page from outside the
   palette. Bare element selector, so every .btn/.hubrow/.sclink class rule still wins. */
a{color:var(--coral-d);text-underline-offset:2px;text-decoration-thickness:1px}
a:hover{text-decoration-thickness:2px}
/* Phosphor duotone icons (icons.ts). Both layers paint currentColor, so an icon picks up
   whatever row it sits in — inactive nav grey, active coral, hub-row coral. --icon-tint and
   --icon-tint-alpha retint ONLY the filled layer, which is the knob a second theme turns:
   set them on :root for a whole theme, or on one surface for one surface. */
.ico,.navico,.bico{width:24px;height:24px;flex:none;fill:currentColor}
.ico .dt,.navico .dt,.bico .dt{fill:var(--icon-tint,currentColor);opacity:var(--icon-tint-alpha,.2)}
/* .bico rides inside a button label, so it sizes in em — one rule covers .small buttons too */
.bico{width:1.05em;height:1.05em;vertical-align:-2px;margin-right:5px}
/* flex rows already space their children, and an icon-only control has no label to sit
   beside — in both cases the label margin would just push the icon off centre */
.scstyle-t .bico,.stmenu button .bico,.stdel .bico,#reroll .bico{margin:0}
.stdel .bico{width:15px;height:15px;color:var(--coral-d)}   /* 26px round button */
#reroll .bico{width:19px;height:19px}
/* an icon quoted INSIDE a sentence ("↗ to edit, 🗑 to say goodbye") — it names a button, so
   it has to be the same mark that button wears, at the sentence's own size */
.ihint{margin:0;color:var(--coral-d);vertical-align:-3px}
/* Two-state buttons carry BOTH icons and let aria-pressed pick, so the JS that swaps the
   label never has to rebuild an SVG (textContent would delete it). */
.i-on{display:none}
[aria-pressed=true]>.i-off{display:none}
[aria-pressed=true]>.i-on{display:inline-block}
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:26px;margin-bottom:16px;box-shadow:var(--shadow-s)}
label{display:block;font-weight:700;margin-bottom:6px}
input[type=text],input[type=search]{width:100%;padding:12px 14px;border:1px solid var(--border-strong);border-radius:var(--radius-sm);font-size:1rem;background:var(--card);font-family:inherit;color:var(--ink);box-shadow:var(--shadow-ctl);transition:border-color .15s var(--ease-out),box-shadow .15s var(--ease-out)}
input[type=text]:focus,input[type=search]:focus{outline:none;border-color:var(--coral);box-shadow:inset 0 1px 2px rgba(64,52,43,.04),0 0 0 3px rgba(224,122,95,.18)}
/* native date + select wear the same rounded skin as text inputs, with a coral chevron
   instead of the stock system dropdown; keeps the cozy tone through every form row */
input[type=date],select{width:100%;padding:12px 14px;border:1px solid var(--border-strong);border-radius:var(--radius-sm);font-size:1rem;background:var(--card);font-family:inherit;color:var(--ink);box-shadow:inset 0 1px 2px rgba(64,52,43,.05)}
select{appearance:none;-webkit-appearance:none;padding-right:38px;cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1.5 L6 6.5 L11 1.5' fill='none' stroke='%23c96a4e' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 14px center}
input[type=date]:focus,select:focus{outline:none;border-color:var(--coral);box-shadow:inset 0 1px 2px rgba(64,52,43,.04),0 0 0 3px rgba(224,122,95,.18)}
.check{display:flex;gap:10px;align-items:flex-start;margin:14px 0;color:var(--ink)}
.check input{margin-top:4px;width:18px;height:18px;accent-color:var(--coral)}
.btn{display:inline-block;font-weight:700;font-size:1rem;border:0;border-radius:var(--radius-pill);padding:12px 22px;cursor:pointer;background:var(--primary);color:var(--on-coral);text-decoration:none;
  box-shadow:var(--shadow-ctl);
  transition:transform .15s var(--ease-out),box-shadow .15s var(--ease-out),background .15s var(--ease-out)}
/* THE 3D. pinkyu's entire shadow ramp is hard offsets (4px 4px 0), which is what makes a
   button read as a slab sitting above the page — so every state has to keep the offset and
   MOVE it, never blur it. Hover lifts 1px and the slab goes fully opaque (--shadow-sm);
   press travels the whole 4px offset and the slab collapses to nothing, so the button
   physically sinks onto its own shadow. The old hover replaced the offset with a soft glow
   and a #b34e3a rust that isn't in this palette — that's what flattened it. */
.btn:hover{background:color-mix(in srgb, var(--primary) 88%, var(--coral-d));transform:translate(-1px,-1px);box-shadow:var(--shadow-sm)}
.btn:active{transform:translate(4px,4px);box-shadow:none}
/* Bordered buttons are shadcn's OUTLINE variant, not its ghost one — the tweakcn demo gives
   those the same offset as a filled button. Ours were the only controls with box-shadow:none. */
.btn.ghost{background:transparent;color:var(--coral-d);border:2px solid var(--coral-d);box-shadow:var(--shadow-ctl)}
.btn.ghost:hover{background:var(--accent);color:var(--accent-foreground);border-color:var(--accent-foreground);transform:translate(-1px,-1px);box-shadow:var(--shadow-sm)}
.muted{font-size:.85rem;color:var(--ink2)}
.err{background:var(--muted);color:var(--destructive);border-radius:var(--radius);padding:8px 12px;margin-bottom:12px;font-size:.9rem}
.topbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:22px}
.brand{font-family:var(--display);font-weight:700;font-size:1.2rem;display:inline-flex;gap:.4rem;align-items:center;text-decoration:none;color:inherit;padding:8px 0}
/* the sheep, not a teddy emoji: the mark and the app icon are the same animal now */
.brandmark{width:26px;height:26px;display:block;flex:none}
/* app shell, mobile: single column + bottom nav; desktop: sidebar + wide main */
.appshell{width:100%}
.appmain{padding:20px 20px 96px;max-width:560px;margin:0 auto}
/* brand row doubles as the page's action row (← Shelf etc), a back button doesn't
   earn a whole row of its own above the content */
.mtop{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 0 4px}
.mtop .btn{flex:none;white-space:nowrap}
/* theme toggle: icon-only, so it needs a real 44px target and a visible edge */
.themetog{margin-left:auto;flex:none;width:40px;height:40px;display:grid;place-items:center;padding:0;
  border:2px solid var(--border-strong);border-radius:var(--radius-pill);background:var(--card);
  font-size:1.05rem;line-height:1;cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .15s var(--ease-out),border-color .15s}
.themetog::after{content:"";position:absolute;inset:-4px;border-radius:inherit} /* 48px hit area */
.themetog{position:relative}
.themetog .bico{width:20px;height:20px;margin:0}
/* which one shows is a THEME fact, so the same data-theme that repaints the palette picks
   the icon — the boot script sets it before first paint, so there's no swap flash */
.themetog .i-sun{display:none}
:root[data-theme=dark] .themetog .i-sun{display:block}
:root[data-theme=dark] .themetog .i-moon{display:none}
.themetog:hover{background:var(--accent);border-color:var(--coral-d);transform:translateY(-1px)}
.themetog:active{transform:translateY(1px)}
.mtop .themetog+.btn{margin-left:8px}
.sidebar{display:none}
@media(min-width:900px){
  .appshell{display:grid;grid-template-columns:238px minmax(0,1fr);min-height:100vh}
  .sidebar{display:flex;flex-direction:column;position:sticky;top:0;align-self:start;height:100vh;
    padding:26px 16px;gap:3px;border-right:1px solid var(--sidebar-border);background:var(--sidebar);color:var(--sidebar-foreground)}
  /* content fills the column right of the sidebar, capped + centered on huge screens.
     width:100% matters: on a grid item, auto margins otherwise defeat stretch and
     shrink-wrap the main column to its content. */
  .appmain{width:100%;max-width:980px;margin:0 auto;padding:36px 46px 48px}
  .mtop{display:none}
  /* the sidebar's Sign out takes over here; the profile page's copy would be the second */
  .signout-page{display:none}
  /* the sidebar owns the brand here, but a page action still needs somewhere to live */
  .mtop:has(.btn,.themetog){display:flex;justify-content:flex-end;padding:0 0 10px}
  .mtop:has(.btn,.themetog) .brand{display:none}
  .bnav{display:none!important}
  /* keep forms/text readable on the wide desktop main; the shelf stays full-width */
  .appmain .card,.appmain .story{max-width:600px;margin-left:auto;margin-right:auto}
}
/* Big desktops: 1920px screens left ~700px unused beside a 980px column, so whole rows
   of the collection sat below the fold. Widening the container adds COLUMNS of identical
   rows (the wall's 240px row-stride / plank-pitch contract is untouched), more of the
   display fits on one screen. */
@media(min-width:1440px){
  .appmain{max-width:1340px}
}
/* The shelf is a display surface, not a column of prose, so it does not want the
   reading width the rest of the app is capped to. Two caps were holding it narrow and
   only one of them was the obvious one: .wall's own 900px, and .appmain's 980px, which
   binds all the way to 1439px and is the one that actually bites on a laptop — at a
   1400px window the wall sat at 888px when the room allowed 1070px. Raising the wall
   cap alone changed nothing below 1440; the column has to open first.
   1560 is where it stops: past that the hooks stretch away from each other and a row
   reads as scattered rather than arranged. */
@media(min-width:900px){
  .appmain:has(.tbshelf){max-width:none}
  .wall{--wall-max:1560px}
}
.sbrand{font-size:1.32rem;margin:0 0 20px;padding:0 10px}
.snav{display:flex;flex-direction:column;gap:3px}
.snav-i{display:flex;align-items:center;gap:12px;padding:11px 12px;border-radius:var(--radius-pill);text-decoration:none;color:var(--ink2);font-weight:600;font-size:.98rem;transition:background .14s,color .14s}
.snav-i svg{width:22px;height:22px;flex:none}
.snav-i:hover{background:var(--sidebar-accent);color:var(--sidebar-accent-foreground)}
.snav-i[aria-current]{background:var(--sidebar-accent);color:var(--sidebar-accent-foreground);box-shadow:inset 0 0 0 1px var(--sidebar-border)}
.snav-i:focus-visible{outline:2px solid var(--coral);outline-offset:1px}
.snav-add{margin-top:14px;text-align:center;font-size:.95rem;padding:11px 12px}
.sfoot{margin-top:auto}
.snav-out{background:none;border:0;color:var(--ink2);font-weight:600;font-size:.9rem;cursor:pointer;padding:11px 12px;width:100%;text-align:left;border-radius:var(--radius)}
.snav-out:hover{background:var(--accent);color:var(--ink)}
.snav-out:focus-visible{outline:2px solid var(--coral);outline-offset:1px}
form{margin:0}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
textarea,select,input[type=date]{width:100%;padding:12px 14px;border:1px solid var(--border-strong);border-radius:var(--radius-sm);font-size:1rem;background:var(--card);font-family:inherit;color:var(--ink);box-shadow:var(--shadow-ctl)}
textarea:focus,select:focus,input[type=date]:focus{outline:none;border-color:var(--coral);box-shadow:inset 0 1px 2px rgba(64,52,43,.04),0 0 0 3px rgba(224,122,95,.18)}
textarea{min-height:82px;resize:vertical}
.field{margin-bottom:14px}
.hint{font-size:.8125rem;color:var(--ink2);margin:5px 0 0}
.hint.err{color:var(--destructive)}
.btn.block{display:block;width:100%;text-align:center;margin-top:6px}
.btn[disabled]{opacity:.5;cursor:default}
.toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:6px}
/* title block: page title + the little count caption; the action button never wraps */
.tbtitle{min-width:0}
.shelfcount{margin:2px 0 0;font-size:.85rem}
.toolbar .btn{white-space:nowrap;flex:none}
/* shelf toolbar actions: balanced pair, comfortable thumb targets, never crowd the title */
/* Shelf home on a phone: the brand row repeats what the lit "Shelf" tab in the bottom nav
   already says, so it goes and the theme toggle floats into the slot it leaves behind,
   taking 122px of chrome off the top — one more row of plushies above the fold.
   ("+ New plushie" used to be hidden here too; it is gone at every size now — the sidebar
   has "+ Add a plushie" on desktop and the centre FAB is always on screen on a phone.)
   ponytail: the ?imported= welcome banner would sit under the floated toggle; it's a
   one-shot query-param card, not worth a second selector to dodge. */
@media(max-width:899px){
  .appmain:has(.tbshelf){position:relative}
  .appmain:has(.tbshelf)>.mtop{position:absolute;top:20px;right:20px;padding:0;z-index:3}
  .appmain:has(.tbshelf)>.mtop .brand{display:none}
  .tbshelf{padding-right:52px} /* keep the title clear of the floated toggle */
  .tbshelf h1{font-size:1.6rem}
}
/* add-a-plushie: photo picker + cutout preview */
.picker{display:block;border:2px dashed var(--line);border-radius:var(--radius);padding:22px;text-align:center;background:var(--card);cursor:pointer;font-weight:700}
.picker:hover{border-color:var(--coral)}
.pickrow{display:flex;gap:10px}
.pickrow .picker{flex:1;min-width:0;padding:18px 12px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px}
/* both photo lanes weigh the same at rest, coral is reserved for a REAL selection,
   so "Take a photo" no longer reads as pre-picked next to a "placeholder" library lane */
.picker.cam{border-style:dashed}
.picker .hint{font-size:.8125rem;color:var(--ink2);margin-top:4px}
.picker:focus-within{border-color:var(--coral);box-shadow:0 0 0 3px rgba(224,122,95,.25)}
/* Dragging a photo over the page. The ring is fixed and pointer-events:none — it must not
   become the drop target itself, or the file lands on an overlay instead of the document. */
body.pb-dropping::after{content:"";position:fixed;inset:10px;z-index:60;pointer-events:none;
  border:3px dashed var(--primary);border-radius:var(--radius-lg);
  background:color-mix(in srgb,var(--primary) 8%,transparent)}
/* a pink wash over the card, NOT var(--accent): accent is gold in dark mode, which turns
   the two tiles into mustard blocks and drops the label to unreadable on them */
body.pb-dropping .picker{border-color:var(--primary);background:color-mix(in srgb,var(--primary) 14%,var(--card))}
/* drag-and-drop is a mouse affordance; a touch device is told to tap instead */
.drophint{display:none;text-align:center;color:var(--ink2);font-size:.8125rem;margin:8px 0 0}
.drophint kbd{font:inherit;font-weight:700;background:var(--muted);border:1px solid var(--border);
  border-radius:6px;padding:1px 5px;color:var(--ink)}
@media(hover:hover) and (pointer:fine){.drophint{display:block}}
/* visually hidden but still focusable/tab-reachable (keyboard + screen readers can open the picker) */
.vhidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
.preview{background-color:#f3ece2;background-image:linear-gradient(45deg,var(--border) 25%,transparent 25%),linear-gradient(-45deg,var(--border) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--border) 75%),linear-gradient(-45deg,transparent 75%,var(--border) 75%);background-size:16px 16px;background-position:0 0,0 8px,8px -8px,-8px 0;border-radius:var(--radius);padding:14px;align-items:center;justify-content:center;min-height:200px}
.preview img{max-width:100%;max-height:260px;object-fit:contain;filter:drop-shadow(0 8px 14px rgba(64,52,43,.22))}
.status{font-size:.9rem;margin:10px 0 0;min-height:1.2em;color:var(--ink2)}
.status.ok{color:#2f7d4f}.status.note{color:#a15a2a}.status.err{color:var(--destructive)}
.spin{display:inline-block;width:14px;height:14px;border:2px solid var(--coral);border-top-color:transparent;border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px;margin-right:6px}
@keyframes sp{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.spin{animation:none}}
/* Paint mode has to be unmistakable: with nothing painted yet the canvas looks exactly
   like normal, so the state lives on the STAGE (border + badge) rather than on the
   plushie, which is what the old full-canvas wash got wrong. */
/* user-select is load-bearing, not tidiness: without it a brush stroke is also a text
   selection, and once the browser has a selection under the cursor the next drag is a
   native DRAG of that selection — which is what hauls the plushie around the stage while
   you are trying to paint it. Covers the plush page's Pettable tab too; both wear this
   class. */
.furpainting{outline:3px dashed var(--coral-d);outline-offset:-6px;border-radius:14px;
  user-select:none;-webkit-user-select:none;
  animation:furpulse 1.7s ease-in-out infinite}
.furpainting::after{content:"🖌 pink = no fur — brush to add, erase to take back";position:absolute;left:12px;top:12px;z-index:6;
  background:var(--coral-d);color:#fff;font-family:var(--display);font-size:.72rem;font-weight:800;
  padding:4px 11px;border-radius:999px;pointer-events:none;box-shadow:0 2px 6px rgba(90,60,70,.3)}
@keyframes furpulse{0%,100%{outline-color:var(--coral-d)}50%{outline-color:#f6c3b4}}
@media (prefers-reduced-motion:reduce){.furpainting{animation:none}}
/* Pettable tab: the plushie's own cutout wearing a real coat */
/* width:100% is load-bearing: this is an empty flex CHILD of the flex .vstage, so with
   no content of its own it collapses to zero and the coat is never built. Every other
   pane sizes itself off content it already has. */
.furstage{display:flex;align-items:center;justify-content:center;position:relative;
  width:100%;flex:1 1 100%;min-width:0;min-height:300px}
.furctl{display:flex;flex-direction:column;gap:8px;max-width:420px;margin:0 auto}
.furfabs{display:flex;gap:6px;flex-wrap:wrap}
.furfabs .pkchip.active{border-color:var(--coral-d);background:#fdeee9;color:var(--coral-d)}
.furpaint{display:flex;gap:10px;flex-wrap:wrap;align-items:center;font-size:.8rem;font-weight:700;
  padding:8px 10px;border:2px solid #f0d9cf;border-radius:12px}
.furrow{display:flex;align-items:center;gap:10px}
.furrow>span{width:64px;font-weight:700;font-size:.8rem}
.furrow>input[type=range]{flex:1}
.furrow>b{width:36px;font-size:.78rem}
/* manual cutout editor, draw the outline (finger-friendly, full-screen) */
.editor{position:fixed;inset:0;z-index:100;background:var(--paper);display:flex;flex-direction:column;overscroll-behavior:none}
.editor[hidden]{display:none}
.editor-bar{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid var(--line);background:var(--card)}
.editor-title{font-family:var(--hand);font-weight:700;font-size:1.25rem;color:var(--coral-d);text-align:center;flex:1;min-width:0}
.btn.small{padding:8px 14px;font-size:.9rem}
.editor-stage{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:12px;touch-action:none;overscroll-behavior:none;
  background-color:#f3ece2;background-image:linear-gradient(45deg,var(--border) 25%,transparent 25%),linear-gradient(-45deg,var(--border) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--border) 75%),linear-gradient(-45deg,transparent 75%,var(--border) 75%);background-size:18px 18px;background-position:0 0,0 9px,9px -9px,-9px 0}
#edCanvas{max-width:100%;max-height:100%;touch-action:none;cursor:crosshair;border-radius:8px;box-shadow:0 10px 22px -8px rgba(64,52,43,.42)}
/* magnifier: on a phone your fingertip covers the exact edge you are aiming at, so mirror
   that patch of the clean photo just above it. content-box because the global
   *{box-sizing:border-box} would otherwise shrink the canvas behind the rim. */
.loupe{position:fixed;z-index:110;box-sizing:content-box;width:130px;height:130px;border-radius:50%;overflow:hidden;display:none;pointer-events:none;border:4px solid #fff;box-shadow:0 8px 24px rgba(64,52,43,.4)}
.loupe::after{content:"";position:absolute;left:50%;top:50%;width:20px;height:20px;transform:translate(-50%,-50%);border:2px solid var(--coral);border-radius:50%;box-shadow:0 0 0 1px rgba(255,255,255,.7)}
.loupe canvas{display:block;width:130px;height:130px}
.etool:disabled{opacity:.4;cursor:default} /* nothing to undo yet reads as "you have not slipped" */
.editor-tools{display:flex;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid var(--line);background:var(--card)}
.etool{font-family:var(--display);font-weight:600;font-size:1rem;min-width:46px;height:46px;padding:0 12px;border-radius:var(--radius-pill);border:2px solid var(--border-strong);background:var(--card);color:var(--ink);cursor:pointer;touch-action:manipulation}
.etool:hover{background:var(--accent);color:var(--accent-foreground);border-color:var(--coral)}
.etool:active{transform:translateY(1px)}
.etool.wide{min-width:auto;flex:0 1 auto}
.editor-tools .btn.done{margin-left:auto}
.editor-hint2{font-size:.8125rem;line-height:1.4;color:var(--ink2);margin:0;padding:8px 14px calc(10px + env(safe-area-inset-bottom));background:var(--card);text-align:center}
.editor-hint2 b{color:var(--coral-d);font-weight:700}
.handlechips{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 2px}
.hchip{font-family:var(--display);font-weight:600;font-size:.82rem;padding:9px 13px;min-height:40px;border-radius:var(--radius-pill);cursor:pointer;background:var(--card);border:2px solid var(--border-strong);color:var(--ink);display:inline-flex;align-items:center;touch-action:manipulation;transition:transform .15s var(--ease-out),border-color .15s var(--ease-out),background .15s var(--ease-out)}
.hchip:hover{background:var(--accent);color:var(--accent-foreground);border-color:var(--coral);transform:translateY(-1px)} .hchip.sel{border-color:var(--coral);background:var(--muted);color:var(--coral-d)}
/* ===== collectible pack card (a plushie in blister packaging), themed by --pk ===== */
/* ONE track, not five outlined pills: the group carries the border and the picked view is
   the only filled thing in it. All five labels fit a 393px phone at 12px padding (measured:
   335 of 349 available) — that's why they're wordmarks, not emoji + word, which ran 413.
   overflow-x is the safety valve for bumped-up text sizes. */
.viewtoggle{position:relative;display:flex;gap:2px;width:max-content;max-width:100%;margin:10px auto 4px;padding:6px;
  
  overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.viewtoggle::-webkit-scrollbar{display:none}
/* one pill slides between the buttons instead of five backgrounds blinking on and off —
   the movement is what tells you the views are a set you're moving through. JS writes
   left/width from the picked button's offsets (see the carousel script). */
.vt-pill{position:absolute;top:6px;bottom:6px;left:0;width:0;border-radius:var(--radius-pill);background:var(--secondary);
  box-shadow:var(--shadow-ctl);pointer-events:none;
  transition:left .4s cubic-bezier(.65,0,.35,1),width .4s cubic-bezier(.65,0,.35,1)}
.vt{position:relative;z-index:1;font-family:var(--display);font-weight:600;font-size:.85rem;padding:11px 12px;border-radius:var(--radius-pill);border:0;background:transparent;color:var(--ink2);cursor:pointer;
  flex:none;white-space:nowrap;transition:transform .15s var(--ease-out),color .25s var(--ease-out)}
.vt:hover{background:var(--accent);color:var(--accent-foreground)}
.vt.active{color:var(--secondary-foreground)}
/* ===== view carousel: one stage per view, swipeable, paged by the picker above =====
   Only the previews ride the track. Every control lives in .vctl underneath, so a
   horizontal scroller never has a slider or a text field inside it to fight over. */
.vcar{position:relative;margin:6px 0 0}
/* One axis only. overflow-x:auto alone computes overflow-y to auto (a non-visible axis
   forces the other), so this was a vertical scroller too and any slightly-off swipe slid
   the card up or down — 68px of travel, courtesy of the acrylic slide. overflow-y:clip
   with a clip-margin would be the tidy fix, but next to overflow-x:auto it computes to
   plain hidden and the margin is ignored, so the room for bleed comes from padding the
   track and pulling the same amount back off its margins: the box keeps its place in the
   layout, and the fur strands and the card cutout still paint past the stage edge.
   .vslide's 100% width is of the CONTENT box, so the padding must be vertical only. */
.vcar-track{display:flex;align-items:stretch;overflow-x:auto;overflow-y:hidden;padding:18px 0;margin:-18px 0;scroll-snap-type:x mandatory;
  scroll-behavior:smooth;overscroll-behavior-x:contain;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.vcar-track::-webkit-scrollbar{display:none}
.vslide{flex:0 0 100%;scroll-snap-align:center;scroll-snap-stop:always;display:flex;justify-content:center}
/* the shared stage: every view sits on the same panel, so paging doesn't change the
   furniture. min-height (not height) + stretch = one uniform track, nothing clipped. */
.vstage{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:var(--preview-h);
  padding:14px 10px;border-radius:var(--radius-lg);--grad-far:#F7E4EA;background:linear-gradient(var(--muted),var(--grad-far));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.6),inset 0 0 0 1px rgba(64,52,43,.06)}
/* the pack's offset shadow adds ~14px on the right — size it to the stage, not the column,
   or the packaging hangs off the edge of its own display case */
.vstage .packcard{width:min(268px,72vw)}
.vcar-nav{display:none}
@media(min-width:720px){ /* touch pages by swiping; a pointer wants something to click */
  .vcar-nav{position:absolute;top:50%;z-index:6;display:grid;place-items:center;width:36px;height:36px;margin-top:-18px;
    border:2px solid var(--border-strong);border-radius:50%;background:var(--card);color:var(--ink);
    font-size:1.3rem;line-height:1;cursor:pointer;box-shadow:var(--shadow-s);transition:border-color .15s,transform .15s var(--ease-out)}
  .vcar-nav:hover{background:var(--accent);border-color:var(--coral);transform:scale(1.06)}
  .vcar-prev{left:-14px}.vcar-next{right:-14px}
}
.vctl{width:min(430px,100%);margin:12px auto 0}
.vctl-p[hidden]{display:none}
.vctl .stkopt{text-align:center}
@media(prefers-reduced-motion:reduce){.vt-pill{transition:none}.vcar-track{scroll-behavior:auto}}
.herosticker[hidden]{display:none}
.packcard{--ink:#43291b;--pk:#e07a5f;
  --accent:var(--pk);--body:color-mix(in srgb,var(--pk) 20%,#fff);--win:color-mix(in srgb,var(--pk) 50%,#fff);--shadow:color-mix(in srgb,var(--pk) 66%,#3a2416);
  position:relative;width:min(320px,86vw);aspect-ratio:1/1.16;background:var(--body);border:5px solid var(--ink);border-radius:var(--radius-lg);overflow:hidden;container-type:inline-size;
  box-shadow:9px 11px 0 0 var(--shadow),9px 11px 0 5px var(--ink),16px 20px 24px rgba(67,41,27,.22)}
.pk-head{position:relative;background:var(--accent);border-bottom:5px solid var(--ink);padding:14px 16px 12px;display:flex;justify-content:space-between;align-items:flex-start;gap:8px;min-height:66px}
/* the editable header shows it is editable on hover/focus without a control appearing:
   the pack is the UI here. Wrapping is allowed, but max-width keeps it clear of the
   euro-slot hole punched through the middle of the header. */
.pk-logo[contenteditable]{cursor:text;border-radius:4px;outline:none;
  transition:background .15s,box-shadow .15s}
.pk-logo[contenteditable]:hover{background:rgba(255,255,255,.35);box-shadow:0 0 0 3px rgba(255,255,255,.35)}
.pk-logo[contenteditable]:focus{background:rgba(255,255,255,.6);box-shadow:0 0 0 3px rgba(255,255,255,.6)}
.pk-logo{font-family:var(--display);font-weight:700;font-size:clamp(16px,7cqw,24px);line-height:.9;color:var(--ink);text-transform:uppercase;letter-spacing:.3px;text-shadow:0 1.5px 0 rgba(255,255,255,.3);max-width:62%;overflow-wrap:anywhere}
.pk-brand{text-align:right;line-height:1.1;flex:none}
.pk-url{font-size:clamp(9px,3cqw,12px);font-weight:600;color:var(--ink);background:rgba(255,255,255,.35);border:2px solid var(--ink);border-radius:var(--radius);padding:1px 8px;display:inline-block;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pk-stars{margin-top:4px;font-size:12px;letter-spacing:3px;color:var(--star);-webkit-text-stroke:.6px var(--ink)}
.pk-window{position:absolute;left:20px;right:20px;top:86px;bottom:20px;border:5px solid var(--ink);border-radius:var(--radius);overflow:hidden;
  background:radial-gradient(120% 100% at 50% 12%, color-mix(in srgb,var(--win) 78%,#fff), var(--win));
  box-shadow:inset 0 4px 10px rgba(255,255,255,.55), inset 0 -14px 24px rgba(0,0,0,.12)}
.pk-window::before{content:"";position:absolute;inset:0;pointer-events:none;z-index:4;border-radius:inherit;
  background:linear-gradient(122deg, transparent 30%, rgba(255,255,255,.5) 37%, rgba(255,255,255,.08) 43%, transparent 49%, transparent 63%, rgba(255,255,255,.36) 69%, rgba(255,255,255,.05) 74%, transparent 79%),radial-gradient(130% 70% at 12% 6%, rgba(255,255,255,.5), transparent 44%)}
.pk-content{position:absolute;inset:0;z-index:2;display:flex;align-items:flex-end;justify-content:center}
.pk-content img.char{max-width:82%;max-height:88%;object-fit:contain;margin-bottom:5%;filter:drop-shadow(0 6px 5px rgba(67,41,27,.32))}
.pk-content span.char{font-size:clamp(70px,30cqw,120px);line-height:1}
/* plush scale + position inside the pack (grows from the feet; drag to move) */
.pk-content{transform:translate(calc(var(--px,0)*100%),calc(var(--py,0)*100%))}
.pk-content img.char,.pk-content span.char{transform:scale(var(--pz,1));transform-origin:50% 96%;transition:transform .25s var(--ease-out);cursor:grab}
@media(prefers-reduced-motion:reduce){.pk-content img.char,.pk-content span.char{transition:none}}
/* hang slit, a punched euro-slot showing the backdrop through the top */
.pk-hang{position:absolute;top:4px;left:50%;transform:translateX(-50%);z-index:7;width:54px;height:26px;pointer-events:none}
.pk-hang svg{display:block;width:100%;height:100%;filter:drop-shadow(0 1.5px 1px rgba(0,0,0,.14))}
/* corner sticker inside the display window (a badge on the packaging). Positioned by its
   CENTRE via left/top % (inline --sx/--sy from the pack config), so the owner can drag it
   anywhere in the window. Defaults sit in the top-right corner. */
.pk-sticker{position:absolute;left:88%;top:10%;transform:translate(-50%,-50%);z-index:6;font-size:1.55rem;line-height:1;filter:drop-shadow(0 2px 2px rgba(67,41,27,.26));cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}
/* window backdrop patterns (subtle, tiled over the window tint). Selector is
   .packcard .pk-window.pkbg-* (0,3,0) on purpose: the .pks-box/.pks-bag rules below use
   the background: shorthand at (0,2,0), which would otherwise silently wipe the pattern
   image, the picker looked dead in box/bag shapes. */
.pk-window{background-repeat:repeat;background-position:center}
.packcard .pk-window.pkbg-flowers{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='46' height='46'%3E%3Cg fill='%23ffffff' fill-opacity='.5'%3E%3Cg transform='translate(11 11)'%3E%3Ccircle cx='0' cy='-5' r='3'/%3E%3Ccircle cx='5' cy='-1.5' r='3'/%3E%3Ccircle cx='3' cy='4' r='3'/%3E%3Ccircle cx='-3' cy='4' r='3'/%3E%3Ccircle cx='-5' cy='-1.5' r='3'/%3E%3Ccircle cx='0' cy='0' r='2.4' fill='%23f7c948' fill-opacity='.7'/%3E%3C/g%3E%3Cg transform='translate(34 33) scale(.7)'%3E%3Ccircle cx='0' cy='-5' r='3'/%3E%3Ccircle cx='5' cy='-1.5' r='3'/%3E%3Ccircle cx='3' cy='4' r='3'/%3E%3Ccircle cx='-3' cy='4' r='3'/%3E%3Ccircle cx='-5' cy='-1.5' r='3'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}
.packcard .pk-window.pkbg-leaves{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='%23ffffff' fill-opacity='.45'%3E%3Cpath d='M11 4 C4 8 4 16 11 18 C13 11 13 11 18 8 C15 5 13 4 11 4 Z'/%3E%3Cpath d='M33 26 C26 30 26 38 33 40 C35 33 35 33 40 30 C37 27 35 26 33 26 Z'/%3E%3C/g%3E%3C/svg%3E")}
.packcard .pk-window.pkbg-sparkles{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='%23ffffff' fill-opacity='.6'%3E%3Cpath d='M10 3 L11.5 8.5 L17 10 L11.5 11.5 L10 17 L8.5 11.5 L3 10 L8.5 8.5 Z'/%3E%3Cpath d='M30 22 L31 26 L35 27 L31 28 L30 32 L29 28 L25 27 L29 26 Z'/%3E%3C/g%3E%3C/svg%3E")}
.packcard .pk-window.pkbg-gingham{background-image:linear-gradient(rgba(255,255,255,.28) 50%,transparent 50%),linear-gradient(90deg,rgba(255,255,255,.28) 50%,transparent 50%);background-size:16px 16px,16px 16px}
.packcard .pk-window.pkbg-paws{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='46' height='46'%3E%3Cg fill='%23ffffff' fill-opacity='.5'%3E%3Cg transform='translate(12 14)'%3E%3Cellipse cx='0' cy='2' rx='4' ry='3.4'/%3E%3Ccircle cx='-4' cy='-3' r='1.7'/%3E%3Ccircle cx='0' cy='-4.5' r='1.7'/%3E%3Ccircle cx='4' cy='-3' r='1.7'/%3E%3C/g%3E%3Cg transform='translate(33 34) scale(.8)'%3E%3Cellipse cx='0' cy='2' rx='4' ry='3.4'/%3E%3Ccircle cx='-4' cy='-3' r='1.7'/%3E%3Ccircle cx='0' cy='-4.5' r='1.7'/%3E%3Ccircle cx='4' cy='-3' r='1.7'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}
.packcard .pk-window.pkbg-hearts{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='%23ffffff' fill-opacity='.5'%3E%3Cpath d='M11 8 C11 5 6 4 6 9 C6 12 11 15 11 15 C11 15 16 12 16 9 C16 4 11 5 11 8 Z'/%3E%3Cpath d='M30 27 C30 25 26 24 26 28 C26 30 30 33 30 33 C30 33 34 30 34 28 C34 24 30 25 30 27 Z'/%3E%3C/g%3E%3C/svg%3E")}
.pk-sparkle{position:absolute;z-index:3;width:20px;height:20px;pointer-events:none;background:#fff;filter:drop-shadow(0 0 3px rgba(255,255,255,.95));clip-path:polygon(50% 0,58% 42%,100% 50%,58% 58%,50% 100%,42% 58%,0 50%,42% 42%)}
.pk-rarity{position:absolute;top:10px;left:50%;transform:translateX(-50%) rotate(-3deg);z-index:5;
  /* the pack's OWN ink, not white: this ribbon sits on a colour the collector picked, and
     white failed on every preset (2.38:1 on purple). The pack ink clears 4.5 on all six
     — worst is coral at 4.53 — because they're all light pastels by design. */
  font-family:var(--display);font-weight:700;font-size:clamp(11px,3.4cqw,14px);letter-spacing:.5px;color:var(--ink);
  background:var(--accent);border:2.5px solid var(--ink);border-radius:var(--radius);padding:2px 12px;white-space:nowrap;
  box-shadow:0 2px 0 var(--ink)}
.packwrap{display:flex;flex-direction:column;align-items:center;gap:2px}
/* ===== pack styles: 📦 window box · 🍬 candy bag ===== */
.pks-box{border-radius:var(--radius)}
.pks-box .pk-head{border-radius:0}
.pks-box .pk-window{left:26px;right:26px;top:94px;bottom:26px;border-radius:var(--radius);
  background:radial-gradient(120% 100% at 50% 10%, #fffdf8, color-mix(in srgb,var(--win) 42%,#fff))}
.pks-box .pk-window::before{background:linear-gradient(115deg, transparent 38%, rgba(255,255,255,.4) 44%, rgba(255,255,255,.06) 50%, transparent 56%)}
.pks-bag{border-radius:var(--radius) 12px 30px 30px}
.pks-bag .pk-head{background:repeating-linear-gradient(90deg, var(--accent) 0 12px, color-mix(in srgb,var(--accent) 82%,#40342B) 12px 24px);min-height:56px;padding:10px 16px 8px}
.pks-bag .pk-window{left:26px;right:26px;top:78px;bottom:24px;border-radius:50%/44%;
  background:radial-gradient(115% 95% at 50% 8%, color-mix(in srgb,var(--win) 70%,#fff), var(--win))}
/* ===== plush page hero card: a collectible card — art window in a paper frame, name
   plate under it, and a specular glare that tracks the pointer while it tilts ===== */
/* ===== pointer tilt, shared. No touch-action lock: a horizontal drag has to reach the
   carousel, not be eaten by the tilt; the browser taking the gesture fires pointercancel,
   which resets the lean. Used by the hero card and the pack. ===== */
.tiltbox{perspective:900px}
.tilted{transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));transform-style:preserve-3d;
  transition:transform .5s var(--ease-out)}
.tilted.tilting{transition:none} /* while the pointer drives it, easing reads as lag */
.glare{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity .3s;
  mix-blend-mode:overlay;background:radial-gradient(circle at var(--gx,50%) var(--gy,0%),rgba(255,255,255,.85),rgba(255,255,255,0) 58%)}
.tilted.tilting .glare{opacity:1}
@media(prefers-reduced-motion:reduce){.tilted{transition:none}}
.hc-tilt{width:min(74vw,340px,calc((var(--preview-h) - 20px) * .80))}
.hc-card{position:relative;display:flex;flex-direction:column;gap:9px;padding:9px 9px 11px;border-radius:var(--radius-lg);
  background:linear-gradient(158deg,var(--card),var(--muted));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),0 0 0 1px rgba(64,52,43,.10),0 22px 42px -26px rgba(64,52,43,.55)}
.hc-plate{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:0 8px;padding:0 7px 1px}
/* two classes: .hand is declared further down the sheet and would otherwise win at 2.7rem */
.hc-plate .hc-name{grid-column:1;font-size:clamp(1.32rem,6.2vw,1.75rem);line-height:1.05;margin:0;overflow-wrap:anywhere}
.hc-meta{grid-column:1;margin:1px 0 0;font-size:.68rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--ink2)}
.hc-stars{grid-column:2;grid-row:1/3;font-size:.95rem;letter-spacing:.06em;color:var(--star);text-shadow:0 1px 0 rgba(120,80,20,.25)}
/* the card owns the plushie's name, so the loose hand-written title under the preview
   would just say it twice — it comes back for every other view */
.packview.v-card~h1{display:none}
.hc-stage{position:relative;display:flex;align-items:center;justify-content:center;
  width:100%;aspect-ratio:1/1.06;border-radius:var(--radius-lg);
  box-shadow:inset 0 0 0 1px rgba(64,52,43,.05),inset 0 2px 12px rgba(255,255,255,.5),0 10px 22px -16px rgba(64,52,43,.45)}
.hc-stage::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(120deg,transparent 44%,rgba(255,255,255,.28) 51%,transparent 60%)}
/* absolutely positioned on purpose: in flow, a PORTRAIT cutout's intrinsic height wins
   over the window's aspect-ratio (flex min-height:auto) and stretches the whole card
   into a tower. Out of flow, the window keeps its ratio and the plush just bleeds. */
.hc-cut{position:absolute;left:50%;top:50%;z-index:2;width:106%;max-width:none;max-height:106%;object-fit:contain;
  filter:drop-shadow(0 18px 18px rgba(64,52,43,.3));transform:translate(-50%,-53%) rotate(-6deg);
  user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.hc-cut.hc-emoji{font-size:120px;line-height:1;width:auto}
.hc-shadow{position:absolute;z-index:1;bottom:7%;left:50%;width:56%;height:16px;border-radius:50%;
  background:radial-gradient(ellipse at center,rgba(46,38,30,.32),transparent 68%);transform:translateX(-50%);pointer-events:none}
/* both classes, because .pm-swatches' own margin-top is declared later in this sheet */
.pm-swatches.hc-sw{margin-top:0;justify-content:center}
/* sticker view controls, outline swatches + aura, inside the pane */
.stkopt{margin-top:2px;text-align:center}
.stkdots{display:flex;gap:11px;justify-content:center;flex-wrap:wrap}
.stk-sw{width:32px;height:32px;border-radius:var(--radius-sm);border:0;cursor:pointer;padding:0;position:relative;background:var(--c);
  touch-action:manipulation}
.stk-sw::after{content:"";position:absolute;inset:-6px;border-radius:var(--radius-sm)} /* 44px hit area */
.stk-sw:hover{transform:scale(1.08)}
/* the colour wheel sits in the same row as the presets and matches their box, but its
   swatch is the live value, so it reads as "and anything else" rather than a control */
.stk-wheel{width:32px;height:32px;padding:0;border:0;border-radius:var(--radius-sm);cursor:pointer;
  background:conic-gradient(#ff8fa8,#ffd66e,#8fd6a4,#8fc4f0,#c9a3f0,#ff8fa8);position:relative;
  box-shadow:0 0 0 2px #0e0e10;-webkit-appearance:none;appearance:none}
.stk-wheel::-webkit-color-swatch-wrapper{padding:0}
.stk-wheel::-webkit-color-swatch{border:0;border-radius:var(--radius-sm);opacity:0}
.stk-wheel.on{box-shadow:0 0 0 2px #0e0e10,0 0 0 4px var(--c,#0e0e10)}
.stkc-white{--c:#fff}.stkc-cream{--c:#fdf3e0}.stkc-black{--c:#4a4640}.stkc-pink{--c:#f4b8cc}
.stkc-mint{--c:#b7ddc6}.stkc-sky{--c:#aecbe6}.stkc-coral{--c:#f0a48a}
.hero-cutout.stc-cream{filter:url(#pbst-cream) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
/* any colour the wheel picks: one filter whose flood the editor rewrites live */
.hero-cutout.stc-custom{filter:url(#pbst-custom) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
.hero-cutout.stc-white{filter:url(#pbst-white) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
.hero-cutout.stc-black{filter:url(#pbst-black) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
.hero-cutout.stc-pink{filter:url(#pbst-pink) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
.hero-cutout.stc-mint{filter:url(#pbst-mint) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
.hero-cutout.stc-sky{filter:url(#pbst-sky) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
.hero-cutout.stc-coral{filter:url(#pbst-coral) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
/* whole-photo plushies sit in the pack window as a rounded print, not a hard rectangle */
.pk-content img.char.charwhole{border-radius:var(--radius)}
/* pack customizer controls (owner), cute toy-shop chips + a size slider */
.packopts{width:100%;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:12px 16px 16px;box-shadow:var(--shadow-s)}
/* sectioned customizer sheet, a soft tab rail, one pane at a time (design-inspo/package.png) */
.pktabs{display:flex;gap:3px;justify-content:center;flex-wrap:wrap;margin:8px 0 12px;
  background:var(--paper);border:1px solid rgba(64,52,43,.12);border-radius:var(--radius);padding:4px}
.pktabb{border:0;background:transparent;font-family:var(--body);font-weight:800;font-size:.79rem;color:var(--ink2);
  padding:8px 11px;min-height:36px;border-radius:var(--radius);cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:background .15s var(--ease-out),color .15s var(--ease-out)}
.pktabb.active{background:var(--muted);color:var(--coral-d)}
@media(max-width:460px){
  .pktabs{display:grid;grid-template-columns:repeat(4,1fr);border-radius:var(--radius)}
  .pktabb{padding:8px 2px;font-size:.72rem;border-radius:var(--radius)}
}
.pkpane{min-height:64px;display:flex;flex-direction:column;justify-content:center}
.pkpane[hidden]{display:none}
.pkoptlbl{font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink2);font-weight:700;margin:12px 0 6px;text-align:center}
.pkstyles{display:flex;gap:8px;flex-wrap:wrap;justify-content:center}
.pkchip{font-family:var(--display);font-weight:600;font-size:.85rem;padding:9px 13px;border-radius:var(--radius-pill);cursor:pointer;
  background:var(--card);border:2px solid var(--border-strong);color:var(--ink);transition:transform .15s var(--ease-out),border-color .15s,background .15s;touch-action:manipulation}
.pkchip.small{font-size:.78rem;padding:7px 11px}
.pkchip:hover{background:var(--accent);color:var(--accent-foreground);border-color:var(--coral);transform:translateY(-1px)}
.pkchip:active{transform:translateY(1px)}
.pkchip.active{border-color:var(--coral);background:var(--muted);color:var(--coral-d)}
/* colour swatches, full-bleed rounded squares. Picked state = the shared swatch
   rules at the bottom of this sheet (--c is the swatch's own colour). */
.pkcolor{width:38px;height:38px;border-radius:var(--radius-sm);border:0;cursor:pointer;position:relative;padding:0;background:var(--c);
  touch-action:manipulation}
.pkcolor::after{content:"";position:absolute;inset:-5px;border-radius:var(--radius-sm)} /* 48px hit area */
.pkcolor:hover{transform:scale(1.08)}
.pkcolor:active{transform:scale(1.02)}
.pkc-aura{border-radius:50%} /* the "match my aura" rainbow stays a disc, it's the odd one out on purpose */
.pkc-aura{--c:#ff78aa;background:conic-gradient(from 0deg,#ff78aa,#f5be5a,#82cd9b,#78aaff,#be96f0,#ff78aa)}
.pkc-coral{--c:#e07a5f}.pkc-pink{--c:#ff9ec4}.pkc-green{--c:#9cc77e}
.pkc-purple{--c:#b99ce0}.pkc-blue{--c:#7fb2e6}.pkc-yellow{--c:#f2c94c}
/* corner-sticker picker */
.pkstk{width:42px;height:42px;border-radius:var(--radius-pill);border:2px solid var(--border-strong);background:var(--card);cursor:pointer;font-size:1.2rem;line-height:1;
  display:flex;align-items:center;justify-content:center;transition:transform .15s var(--ease-out),border-color .15s;touch-action:manipulation}
.pkstk:hover{background:var(--accent);border-color:var(--coral);transform:translateY(-1px)}
.pkstk:active{transform:translateY(1px)}
.pkstk.active{border-color:var(--coral);background:var(--muted);box-shadow:0 0 0 2px rgba(224,122,95,.25)}
.sizerow{display:flex;align-items:center;gap:10px;width:100%;font-size:1.1rem;margin:0}
.sizerow input[type=range]{flex:1;accent-color:var(--coral-d);height:44px;cursor:pointer}
/* bulk add grid */
.bulkgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;margin:16px 0}
.bulkcard{position:relative;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:8px;box-shadow:var(--shadow-s)}
.bulkcard img{width:100%;aspect-ratio:1;object-fit:contain;background:#f6efe6;border-radius:var(--radius);display:block}
.bulkcard input{width:100%;margin-top:8px;padding:8px 10px;font-size:16px;border:1px solid var(--border);border-radius:var(--radius);background:var(--card);color:var(--ink)}
.bulkcard input:focus{outline:none;border-color:var(--coral);box-shadow:0 0 0 3px rgba(224,122,95,.18)}
.bulkdel{position:absolute;top:-8px;right:-8px;z-index:2;width:30px;height:30px;border-radius:50%;border:2px solid #e6b9ae;background:var(--card);color:var(--destructive);cursor:pointer;font-size:.85rem;box-shadow:var(--shadow-s)}
.bulkdel:hover{background:var(--muted)}.bulkdel:active{transform:translateY(1px)}
/* a strapline printed on the pack front, the way blister packaging carries one. Absolute,
   so it contributes no height and cannot push the card out of its stage. */
.pk-tagline{position:absolute;left:22px;right:22px;bottom:26px;z-index:5;text-align:center;
  font-family:var(--hand);font-weight:700;font-size:clamp(12px,4.4cqw,17px);line-height:1.15;color:var(--ink);
  background:rgba(255,255,255,.78);border:2.5px solid var(--ink);border-radius:var(--radius-pill);
  padding:2px 10px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* plush page */
.tag{display:inline-block;font-size:.78rem;background:#f4e7df;color:#a15a2a;border-radius:var(--radius);padding:4px 12px;margin-top:6px;font-weight:700}
.specsheet{padding:6px 20px;margin:12px auto;max-width:400px}
.specrow{display:flex;justify-content:space-between;gap:14px;padding:9px 0;border-bottom:1px dashed var(--line)}
.specrow:last-child{border-bottom:0}
.speck{font-weight:700;color:var(--ink2);font-size:.9rem;flex:none}
.specv{color:var(--ink);text-align:right;overflow-wrap:anywhere}
.detailform label{font-size:.9rem}
.detailform .field{margin-bottom:12px}
.factsug{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 10px}
.factrow{display:flex;gap:8px;align-items:center;margin-bottom:8px}
.factrow input{flex:1;min-width:0}
.factrow input:first-child{flex:0 0 40%}
.factdel{flex:none;width:40px;height:40px;border-radius:var(--radius);border:2px solid #e6b9ae;background:var(--card);color:var(--destructive);cursor:pointer;font-size:.9rem}
.factdel:hover{background:var(--muted)}
.hand{font-family:var(--hand);font-weight:700;font-size:2.7rem;line-height:1;margin:16px 0 8px}
.chips{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
.chip{background:var(--card);border:2px solid var(--border-strong);border-radius:var(--radius-pill);padding:5px 12px;font-size:.85rem;font-weight:600;color:var(--ink2)}
.namecard{margin:12px 0 6px}
.suggestions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
.chip.pick{cursor:pointer;background:var(--card);border:2px solid var(--border-strong);color:var(--ink);font-weight:700}
.chip.pick:hover{border-color:var(--coral);background:#fff6f2}
.linkbox{flex:1;min-width:0;overflow:auto;padding:9px 12px;background:var(--card);border:2px solid var(--border);border-radius:var(--radius);font-family:ui-monospace,SFMono-Regular,monospace;font-size:.85rem;color:var(--coral-d);text-decoration:none;white-space:nowrap}
.gotcha{color:var(--ink2);font-weight:600;margin:8px 0 16px}
.story{margin-top:16px}
.story h3{font-family:var(--display);font-weight:600;font-size:1.05rem;margin-bottom:6px}
.story p{color:var(--ink);margin:0;white-space:pre-line}
/* destructive, but not shouting: a quiet link under the shelf controls where people go
   looking for it. The goodbye confirm is what actually guards the delete. */
.linkdanger{display:block;width:100%;margin:12px 0 0;padding:8px 0 2px;border:0;border-top:1px solid var(--line);
  background:none;font:700 .82rem/1.4 var(--body);color:var(--destructive);text-align:center;cursor:pointer;border-radius:0}
.linkdanger:hover{color:#7d2a17;text-decoration:underline}
.btn.danger{background:transparent;color:var(--destructive);border:2px solid #e6b9ae;box-shadow:none}
.btn.danger:hover{transform:none;box-shadow:none}
.btn.danger:hover{background:var(--muted)}
/* ===== display wall: each plushie hangs as its pack from a metal bracket seated in an
   undercut groove between panel planks. The back-panel design is themeable, a .wt-*
   class sets --panel1/2, --groove, --grain. Depth = drop-shadow + translateZ (no mirror
   reflection on packaging). ===== */
/* --gapy: the phase of the plank-gap pattern against the PADDING box, i.e. a gap covers
   --gapy + n*--pitch .. +12px. The drag gap-guides read this same var, so the guides and
   the painted gaps can't drift apart (it shifts on narrow screens, see the 560px query). */
/* --maskh: height of the plaque mask (layer 1). A var, not a background-size list, because
   --grain is 1 layer on most themes and 2 on Cork — any background-* list short of the real
   layer count REPEATS onto the extras (CSS Backgrounds 3 §3.10), so the narrow-screen
   override used to hand the base panel fill a 208px tile and seam it across the planks. */
.wall{--gapy:46px;--maskh:290px;
  --panel1:var(--muted);--panel2:var(--muted);--groove:rgba(122,96,60,.30);--gap:#71583f;--grain:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0));--pitch:60px;
  /* --pkw is what a pack actually occupies: .wallpack .packcard is width:300px at zoom:.46.
     It has to be the floor of the track, or the grid hands packs a cell narrower than the
     card and they overlap — 44% alone drops to 109px at a 320px screen and did exactly
     that, in Auto, before anyone touched "per row". Only packs use this rule; at-keychain
     and at-case replace grid-template-columns wholesale below. */
  --pkw:138px;
  display:grid;grid-template-columns:repeat(auto-fill,minmax(max(var(--pkw),min(150px,44%)),1fr));gap:58px 14px;
  margin:16px auto 6px;max-width:var(--wall-max,900px);padding:40px 16px 42px;border-radius:var(--radius);overflow:hidden;
  position:relative; /* containing block for .gapguide, which is placed in wall coords */
  /* horizontal slat planks with real dark gaps between them (see slat-inspo). The maths:
     row stride = 22 padding + 160.08 card + 58 row-gap ≈ 240px, and --pitch is only ever
     a divisor of 240 (40/48/60/80/120), so a gap lands behind EVERY row's hooks whatever
     panel size the user picks. Gap height is FIXED at 12px (the hook clamp is built for
     it). Period order is gap → lit lip → plank face → shade, phased so a gap centres on
     y52 of each row, starting the period on the gap means the wall's top edge can never
     show an orphan highlight line. */
  background:
    /* top mask: the whole plaque zone (padding 40 + 182 plaque row) is clean panel, no
       partial gap slivers behind or beside the plaque, whatever the pitch. The first
       visible gap is the one the first row of hooks clamps into. */
    linear-gradient(var(--panel1),var(--panel1)) 0 0/100% var(--maskh) no-repeat,
    repeating-linear-gradient(to bottom,
      var(--gap) 0 12px,
      rgba(255,255,255,.4) 12px 14px,
      transparent 14px calc(var(--pitch) - 2px),
      rgba(0,0,0,.16) calc(var(--pitch) - 2px) var(--pitch)) 0 var(--gapy),
    var(--grain),
    linear-gradient(var(--panel1),var(--panel2));
  /* mask covers the side padding, no stripe slivers at the edges. Five values for four
     layers: the spare is ignored on normal themes and catches Cork's second grain layer,
     which would otherwise cycle back to border-box. */
  background-origin:border-box,padding-box,padding-box,padding-box,padding-box;
  box-shadow:inset 0 2px 14px rgba(64,52,43,.16),inset 0 -3px 10px rgba(64,52,43,.08);
  border:1px solid rgba(64,52,43,.14)}
/* keychains hang at a normal, readable size in Auto (the plush isn't tiny). Users who want
   more per row pick an explicit count (.wall-cpr, below). ONLY the column axis changes; the
   58px row-gap and the 182px keychain row height are untouched, so hooks land on their gaps. */
.wall.at-keychain{grid-template-columns:repeat(auto-fill,minmax(min(112px,30%),1fr));column-gap:8px}
.wall.at-keychain .charm-cut{max-width:calc(96px * var(--kcz,1));max-height:calc(106px * var(--kcz,1))}
.wall.at-keychain .charm-emoji{font-size:76px}
/* Acrylic column axis only; the 182px case row height and plank stride are untouched, so
   hooks still land on their gaps.
   --casemin is a hard floor, not a preference: --maskpadx reserves the dilated rim on both
   sides, so a constant ~24px of every cell is rim before any plushie is drawn. 66px is the
   narrowest cell that still reads as a plushie in a case, and unlike a keychain — whose
   artwork scales freely — an acrylic cannot shrink past its own rim.
   The Auto target is clamped at BOTH ends, because the original min(66px,18%) was wrong at
   both. Unbounded below, 18% let a cell fall to 46px on a 304px phone: 19px of plushie in a
   46px case, almost entirely edge. And pinned to 66px above, since 18% of an 866px desktop
   wall is 156px, so min() picked 66 and packed TWELVE 44px charms across a wall where a
   keychain gets seven. 112px is the keychain's own target, so the two charm formats now read
   at the same scale.
   An explicit "per row" still floors at --casemin, not at this target — asking for a count
   should be honoured further than Auto chooses on your behalf. */
.wall.at-case{--casemin:66px;
  grid-template-columns:repeat(auto-fill,minmax(clamp(var(--casemin),18%,112px),1fr));column-gap:5px}
/* 88%, not a bare 64px: .cs-body carries the acrylic MASK and is only ever as wide as the
   cell, so artwork wider than it gets sliced off square at the mask edge — a flat vertical
   cut through the white shell on any plushie whose silhouette is wide enough to reach it.
   Below ~73px of cell (a 5-across row on a ~330px phone) a fixed 64px always overhung, by
   8px a side on the shelves I measured. The .wall-cpr rule below already scales by
   percentage for the same reason; Auto simply never got it. min() keeps 64px wherever the
   cell is wide enough, so nothing changes above that. */
/* 88px, not 64px: 64 was sized for the old 5-across-on-a-phone grid and left the artwork
   filling barely half of the 123px cells the clamped Auto target now hands out on a desktop.
   88 = the 112px target cell minus the rim's constant 24px, i.e. the artwork fills its cell
   the way the keychain's 96px does. Phones are unaffected — their content box is far under
   88px, so the 100% branch stays the binding one. */
.wall.at-case .cs-cut{max-width:calc(min(88px,100%) * var(--kcz,1));max-height:calc(80px * var(--kcz,1))}
.wall.at-case .cs-emoji{font-size:52px}
/* user-chosen "per row": an exact column count overrides the per-type auto grid. The
   cutouts switch to a % of their cell so they scale with the column width (bigger when
   fewer per row, smaller when more), capped so they never balloon past their old size. */
.wall.wall-cpr{grid-template-columns:repeat(var(--cpr,3),minmax(0,1fr))!important}
.wall.wall-cpr.at-keychain .charm-cut{max-width:calc(min(112px,86%) * var(--kcz,1))}
/* 100%, not 88%: the 12px --maskpadx on .cs-body now reserves the rim explicitly, so the
   old percentage haircut (which was standing in for that reserve) would charge for it twice
   and shrink every charm for nothing. The content box IS the room the artwork may fill. */
.wall.wall-cpr.at-case .cs-cut{max-width:calc(min(118px,100%) * var(--kcz,1))}
/* Packs are the one format whose art does NOT scale with its cell: .packcard is a fixed
   138px (the shop-card internals — window insets, head height, hang tab — are laid out in
   px), so the two rules above have no pack equivalent and an explicit count just slides
   the cards over each other. Measured at 390px: 3 per row overlaps by 28px, 4 by 56px,
   5 by 72px, and it's the same on any narrow column, not just a phone.
   So honour the count only while the columns it asks for still clear a card — past that,
   auto-fill caps it at what actually fits, which is the number the user wanted anyway.
   The floor is the auto grid's own expression, so the cap can never land below what
   "Auto" would have picked. The 14px is .wall's column-gap — keep the two in step. */
.wall.wall-cpr:not(.at-keychain):not(.at-case){
  grid-template-columns:repeat(auto-fill,minmax(max(var(--pkw),calc((100% - (var(--cpr,3) - 1)*14px)/var(--cpr,3))),1fr))!important}
/* Acrylics get the same treatment for the same reason — their floor is the rim rather than a
   fixed card, but it is just as immovable. An explicit count is honoured only while the cells
   it asks for still clear --casemin. 5px is .at-case's column-gap; keep the two in step. */
.wall.wall-cpr.at-case{
  grid-template-columns:repeat(auto-fill,minmax(max(var(--casemin),calc((100% - (var(--cpr,3) - 1)*5px)/var(--cpr,3))),1fr))!important}
/* The HARDWARE has to grow with the cutout. It did not: --lug/--hole/--cl-* were pinned to
   .wall, so an explicit column count nearly doubled the artwork (64px -> 118px) while the
   tab stayed 20px. At 17% of the body the lobe dissolves into the shell's own dilated top
   edge — it reads as a flared shoulder with a dip in it rather than a tab, and the 4px hole
   vanishes inside it. Sized together here, next to the rule that resizes the cutout, so the
   two cannot drift apart again. --hole stays under --lug * 0.2, which is what keeps a collar
   of plastic above the drilling. */
.wallpack{position:relative;display:block;text-decoration:none;color:inherit;padding-top:22px;
  -webkit-tap-highlight-color:transparent;
  /* long-press arms the reorder drag, don't let iOS pop the link preview sheet */
  -webkit-touch-callout:none;user-select:none;-webkit-user-select:none}
/* These three are exactly what wallSwingScript rotates — it picks whichever of them is
   the visible format and writes transform:rotate() to it on every frame of the pendulum.
   Without a compositing layer of its own, each frame re-RASTERISES the whole thing at a
   new angle: for the acrylic shell that means re-running #cshell's 15 primitives, and for
   the strap it means re-blurring its drop-shadows. Traced while swiping a thumb across
   the wall, raster went keychain 46ms -> 2ms, acrylic 12ms -> 1ms, pack 16ms -> 1ms, with
   paint falling to zero in all three — the compositor now just rotates a cached texture.
   Promote the element that MOVES, not its filtered child: a layer on .cs-body (inside the
   shell) did nothing for movement, because the rotation happens on the parent.
   Layer count is bounded by format, not by collection size — the two formats that aren't
   on show are display:none, and display:none elements never get a layer. */
.case-shell,.pk-charm,.packcard{will-change:transform}
/* wall images must be pointer-transparent so a long-press lands on the <a> (which has
   touch-callout:none), not the <img>, which would pop the browser's "save/copy image"
   menu and block dragging the plushie around. Applies to every format (keychain + packs). */
.wall img{pointer-events:none;-webkit-touch-callout:none;-webkit-user-drag:none;user-select:none;-webkit-user-select:none}
/* the shop plaque is MOUNTED ON the wall (top row, exactly 182px = one pack row), so it
   travels with the wall when shared. The first pack row then starts one full 240px
   stride below and every hook still lands on a plank gap. */
.wallsignrow{grid-column:1/-1;height:182px;display:flex;align-items:center;justify-content:center}
.wallsign{--sg1:#f6aebf;--sg2:#ee9ab0;--trim:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0));--wood:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0));
  position:relative;display:flex;align-items:center;gap:18px;
  width:min(700px,97%);min-height:128px;padding:16px 28px;border:3px solid #43291b;border-radius:var(--radius);
  background:var(--trim),var(--wood),linear-gradient(180deg,var(--sg1),var(--sg2));
  box-shadow:0 4px 0 rgba(67,41,27,.32),0 12px 22px -8px rgba(64,52,43,.35),inset 0 2px 0 rgba(255,255,255,.35)}
/* ── free-form fastener pieces: pinned anywhere on the banner by inline left/top %, added
   + dragged by the owner while the drawer is open. The graphics reuse the swappable
   plaque-fastener look above, but each is its own draggable piece (--fr = tilt). A
   transparent pad gives every piece a comfortable touch target; ::before is the visual. */
.ws-fast{position:absolute;transform:translate(-50%,-50%) rotate(var(--fr,0deg)) scale(var(--fsc,1));
  transform-origin:50% 50%;z-index:3;padding:13px;line-height:0;pointer-events:none;
  -webkit-user-select:none;user-select:none;transition:opacity .15s var(--ease-out)}
.ws-fast::before{content:"";display:block}
/* the small round pieces need extra pad to keep a ≥44px drag target (parity with the
   emoji badges); tape/ribbon are already wide enough at the base 13px pad */
.fk-screw,.fk-tack{padding:17px}
.fk-screw::before{width:10px;height:10px;border-radius:50%;
  background:radial-gradient(circle at 35% 30%,#e8ebf0,#8b919c);
  box-shadow:inset 0 -1px 1px rgba(0,0,0,.35),0 1px 1px rgba(255,255,255,.4)}
.fk-tack::before{width:15px;height:15px;border-radius:50%;
  background:radial-gradient(circle at 34% 30%,#ff9aa8,#d94b60 68%);
  box-shadow:0 2px 3px rgba(64,52,43,.42),inset 0 -2px 2px rgba(150,20,40,.5),inset 0 2px 2px rgba(255,255,255,.5)}
.fk-tape::before{width:64px;height:26px;
  background:repeating-linear-gradient(45deg,rgba(255,255,255,.62) 0 7px,rgba(255,255,255,.42) 7px 14px);
  border:1px solid rgba(255,255,255,.5);box-shadow:0 1px 3px rgba(64,52,43,.24)}
.fk-ribbon::before{width:74px;height:40px;
  background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 74 40'><defs><linearGradient id='lp' x1='0' x2='0' y1='0' y2='1'><stop offset='0' stop-color='%23f0977f'/><stop offset='.55' stop-color='%23e07a5f'/><stop offset='1' stop-color='%23c85a44'/></linearGradient><linearGradient id='tl' x1='0' x2='0' y1='0' y2='1'><stop offset='0' stop-color='%23e07a5f'/><stop offset='1' stop-color='%23b34e3a'/></linearGradient></defs><path d='M34 19 C27 26 22 32 17 38 L24 38 C28 33 32 27 36 21 Z' fill='url(%23tl)'/><path d='M40 19 C47 26 52 32 57 38 L50 38 C46 33 42 27 38 21 Z' fill='url(%23tl)'/><path d='M36 15 C28 4 12 2 8 10 C4 18 16 24 34 19 Z' fill='url(%23lp)' stroke='%23a84531' stroke-width='1'/><path d='M38 15 C46 4 62 2 66 10 C70 18 58 24 40 19 Z' fill='url(%23lp)' stroke='%23a84531' stroke-width='1'/><path d='M33 16 C26 9 16 7.5 12 11' fill='none' stroke='rgba(255,255,255,.4)' stroke-width='1.6' stroke-linecap='round'/><path d='M41 16 C48 9 58 7.5 62 11' fill='none' stroke='rgba(255,255,255,.4)' stroke-width='1.6' stroke-linecap='round'/><rect x='32.4' y='13.4' width='9.2' height='9.4' rx='3.2' fill='%23c85a44' stroke='%23a84531' stroke-width='1'/><rect x='34' y='14.6' width='3' height='3.4' rx='1.4' fill='rgba(255,255,255,.42)'/></svg>") center/contain no-repeat;
  filter:drop-shadow(0 2px 2px rgba(64,52,43,.35))}
body.wtopen .ws-fast{pointer-events:auto;cursor:grab;touch-action:none}
body.wtopen .ws-fast:active{cursor:grabbing}
.ws-fast.bye{--fsc:.7;opacity:.5}
/* rotate handle: tap a piece while editing to reveal a ↻ knob above it, then drag the knob
   around the piece to spin it. It rides inside the rotated piece, so it orbits like a
   design-tool rotation handle. Only the selected piece (and only while editing) shows one. */
.ws-rot{position:absolute;left:50%;top:-32px;transform:translateX(-50%);width:32px;height:32px;
  display:none;align-items:center;justify-content:center;cursor:grab;touch-action:none;z-index:6}
.ws-rot:active{cursor:grabbing}
body.wtopen .ws-fast.sel .ws-rot{display:flex}
.ws-rot::before{content:"↻";font-size:14px;font-weight:700;line-height:1;width:23px;height:23px;border-radius:50%;
  background:var(--card,#fff);border:2px solid var(--coral-d,#e07a5f);color:var(--coral-d,#e07a5f);
  display:flex;align-items:center;justify-content:center;box-shadow:0 2px 5px rgba(64,52,43,.3)}
/* ✓ sits opposite the ↻ knob and means "this one's set": it drops the selection, so both
   handles go away and the piece can't be spun by a stray tap. Nothing is saved here —
   every move and spin already wrote through save() — it just settles the piece. */
.ws-done{position:absolute;left:50%;bottom:-32px;transform:translateX(-50%);width:32px;height:32px;
  display:none;align-items:center;justify-content:center;cursor:pointer;touch-action:none;z-index:6}
body.wtopen .ws-fast.sel .ws-done{display:flex}
.ws-done .bico{margin:0;width:23px;height:23px;padding:4px;border-radius:50%;
  background:var(--card,#fff);border:2px solid var(--coral-d,#e07a5f);color:var(--coral-d,#e07a5f);
  box-shadow:0 2px 5px rgba(64,52,43,.3)}
.ws-done:active .bico{transform:translateY(1px)}
.ws-mid{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:7px;text-align:center;
  padding:0 40px} /* keep long titles clear of the emoji badge */
.ws-title{font-family:var(--display);font-weight:700;font-size:clamp(1.5rem,5.4vw,2.5rem);line-height:1.05;
  /* break-word, not anywhere: 'anywhere' shrinks min-content to one glyph and the flex
     column happily wraps PHOEB/E'S, break-word only breaks a word that truly overflows */
  letter-spacing:.04em;color:#43291b;overflow-wrap:break-word}
.ws-by{background:#fffdf6;border:2px solid rgba(67,41,27,.45);border-radius:var(--radius);padding:3px 13px;
  font-family:var(--body);font-weight:800;font-size:.85rem;color:#43291b}
/* emoji badges are pinned anywhere on the sign by inline left/top % (dragged by the
   owner while the design drawer is open); translate keeps each centred on its pin */
.ws-emoji{position:absolute;transform:translate(-50%,-50%);font-size:2.1rem;line-height:1;z-index:4;
  padding:10px; /* ≥44px touch target at every width, translate keeps the glyph on its pin */
  filter:drop-shadow(0 2px 2px rgba(67,41,27,.3));-webkit-user-select:none;user-select:none}
/* badges are draggable ONLY while the design drawer is open, otherwise a swipe that
   lands on one must scroll the page, not silently move the badge */
body.wtopen .ws-emoji{cursor:grab;touch-action:none}
body.wtopen .ws-emoji:active{cursor:grabbing;filter:drop-shadow(0 4px 4px rgba(67,41,27,.36))}
/* dragging a badge over the bin: it shrinks back as a "let go and it's gone" preview */
.ws-emoji.bye{opacity:.5;transform:translate(-50%,-50%) scale(.7)}
/* the drop-to-remove pill, hidden until a badge is actually moving, so it never sits
   in the way of the wall. Fixed to the viewport: the plaque can be scrolled anywhere. */
.embin{position:fixed;left:50%;bottom:calc(26px + env(safe-area-inset-bottom));z-index:230;display:flex;align-items:center;gap:8px; /* above the wall-design sheet (220) or it's an invisible drop target */
  padding:11px 18px;border-radius:var(--radius);background:var(--card);color:var(--ink2);
  border:2px dashed rgba(64,52,43,.32);box-shadow:0 10px 24px rgba(64,52,43,.22);
  font-family:var(--body);font-weight:800;font-size:.86rem;white-space:nowrap;
  opacity:0;pointer-events:none;transform:translate(-50%,16px);
  transition:opacity .18s var(--ease-out),transform .18s var(--ease-out),background .15s,border-color .15s}
.embin .embin-i{display:inline-flex;font-size:1.15rem;line-height:1}
.embin .embin-i .bico{margin:0;width:1.15em;height:1.15em}
.embin.hot .embin-i{color:var(--coral-d)}
.embin.on{opacity:1;transform:translate(-50%,0)}
.embin.hot{background:#ffe6df;border-color:var(--coral-d);border-style:solid;color:var(--coral-d);
  transform:translate(-50%,0) scale(1.07)}
@media(prefers-reduced-motion:reduce){.embin{transition:opacity .01s}}
/* on phones the bottom tab bar owns the bottom edge, lift the bin clear of it */
@media(max-width:899px){.embin{bottom:calc(76px + env(safe-area-inset-bottom))}}
@media(max-width:560px){.ws-emoji{font-size:1.5rem}.wallsign{gap:10px;padding:12px 16px;min-height:110px}
  /* tighter plaque zone: padding 40→20 + sign row 182→140 = 62px reclaimed. The plank
     pattern phase (46px) and the top mask (250px) shift by the SAME 62 so every hook
     still clamps onto a plank gap at every pitch. */
  .wall{padding-top:20px;--gapy:-16px;--maskh:208px}
  .wallsignrow{height:140px}}
/* Gap guides: while a pack is being dragged, every plank gap its hook can clamp into
   lights up down its own column, and the one the current drop height would pick lights
   up solid. The gaps are the whole affordance and nothing pointed at them before.
   Ladder length + spacing come from --pitch, so it tracks the panel-size slider live.
   z:0 keeps it painted on the WALL, behind every pack (restack gives those 1..N). */
.gapguide{position:absolute;pointer-events:none;z-index:0;border-radius:4px;
  background:repeating-linear-gradient(to bottom,rgba(255,255,255,.5) 0 12px,transparent 12px var(--pitch,60px));
  box-shadow:0 0 0 1px rgba(224,122,95,.25) inset}
/* keep the 12px band height in step with the wall's own gap height above */
.gapguide::after{content:"";position:absolute;left:0;right:0;top:var(--sel,0);height:12px;border-radius:4px;
  background:var(--primary);opacity:0;box-shadow:0 0 0 2px rgba(255,255,255,.6),0 2px 9px rgba(224,122,95,.65);
  transition:top .12s var(--ease-out),opacity .12s}
.gapguide.lit::after{opacity:.92}
@media (prefers-reduced-motion:reduce){.gapguide::after{transition:none}}
/* sign colourways */
.wsc-pink{--sg1:#ffd6e6;--sg2:#f49ec0}
.wsc-coral{--sg1:#ffc9ae;--sg2:#f08a6c}
.wsc-sunny{--sg1:#ffefad;--sg2:#ffce68}
.wsc-mint{--sg1:#d2f2e2;--sg2:#9ddcbe}
.wsc-sage{--sg1:#e2efc9;--sg2:#b2d49e}
.wsc-sky{--sg1:#c8e6fb;--sg2:#9ec9ef}
.wsc-lilac{--sg1:#e7d7fa;--sg2:#c6aded}
.wsc-cream{--sg1:#fdf4dc;--sg2:#f3dfae}
.wsc-snow{--sg1:#ffffff;--sg2:#eee7db}
/* a wooden plank sign, horizontal grain over warm wood. Kept a few shades lighter
   than the dark wall themes (walnut/charcoal) so the plaque reads on them. */
.wsc-cocoa{--sg1:#d9ab74;--sg2:#bd8a50}
.wsc-cocoa .ws-by{background:#f7ecd8}
/* sign trims, overlays that work on any colourway */
.wst-stitch{outline:2.5px dashed rgba(67,41,27,.45);outline-offset:-9px}
.wst-dots{--trim:radial-gradient(rgba(255,255,255,.5) 2.4px,transparent 2.9px) 4px 4px/24px 24px}
.wst-gingham{--trim:repeating-linear-gradient(0deg,rgba(255,255,255,.22) 0 10px,transparent 10px 24px),repeating-linear-gradient(90deg,rgba(255,255,255,.22) 0 10px,transparent 10px 24px)}
/* a deliberately empty spot on the wall, invisible at rest, a drop target mid-drag.
   Rest outline is TRANSPARENT and transitions in: mode classes fade the targets in/out
   instead of flashing them, and the dz fold/unfold glides instead of snapping the wall
   182px taller/shorter at the start and end of every phone drag. */
.wallcell{min-height:182px;border-radius:var(--radius);outline:2.5px dashed transparent;outline-offset:-10px;
  transition:outline-color .18s ease,background-color .18s ease}
/* CONSTANT GEOMETRY: the drop-room cells hold a small strip at ALL times (rest, drag,
   place), they used to be 0 at rest and unfold on drag, which grew the wall ~one row
   the instant you grabbed a plushie and slid the whole page ("the panels are scrolling").
   Reserving the strip permanently means the wall never changes height, so a drag can't
   move anything. Only the outline/glow toggles on drag; height stays put. */
.wallcell.dz{min-height:44px}
.wall.reordering .wallcell{outline-color:rgba(64,52,43,.28)}
.celltarget{outline:3px dashed var(--coral-d) !important;outline-offset:-8px;border-radius:var(--radius);background:rgba(224,122,95,.08)}
/* placing mode (wall tray armed or tray drag in flight): every empty spot glows coral so
   legal targets are visible WITHOUT hover (mobile). No height change, the reserved dz
   strip is already a tappable target, and growing it here slid the page too.
   STATIC outline: an infinite pulse on the whole wall reads as "flashing" on a phone. */
.wall.placing .wallcell{outline-color:var(--coral-d);background:rgba(224,122,95,.07)}
@media (prefers-reduced-motion:reduce){.wallcell{transition:none}}
/* just-added pack: a coral ring pulses three times, then fades away (reduced-motion: steady halo) */
.wp-new{position:relative}
.wp-new::after{content:"";position:absolute;inset:-7px;z-index:6;pointer-events:none;border:3px solid var(--coral-d);border-radius:var(--radius);animation:wpnew 1.1s ease-in-out 3,wpnewfade .6s ease 3.5s forwards}
@keyframes wpnew{0%,100%{opacity:.45;transform:scale(.985)}50%{opacity:1;transform:scale(1.015)}}
@keyframes wpnewfade{to{opacity:0}}
@media (prefers-reduced-motion:reduce){.wp-new::after{animation:none;opacity:.85}}
/* ---- wall tray: storage plushies waiting for a spot (under the Display wall) ---- */
.wtray{margin:16px 0 0;padding:10px 12px 12px;border-radius:var(--radius);background:var(--card);border:1px solid var(--border);box-shadow:var(--shadow-m)}
.wtray[hidden]{display:none}
.wtray-head{display:flex;justify-content:space-between;align-items:baseline;gap:4px 12px;flex-wrap:wrap;font:700 .84rem var(--body);color:var(--ink)}
.wtray-tip{font:600 .74rem var(--body);color:var(--ink2)}
/* the inline desktop tray needs no close; the mobile bottom-sheet gets a persistent X
   pinned top-right so it's reachable even after the storage row is scrolled. */
.wtray-close{display:none;align-items:center;justify-content:center;width:30px;height:30px;border:0;border-radius:50%;
  background:var(--paper);color:var(--ink2);font-size:.85rem;cursor:pointer;box-shadow:0 1px 3px rgba(64,52,43,.2);-webkit-tap-highlight-color:transparent}
.wtray-close:hover{background:#ffe6df;color:var(--coral-d)}
@media(max-width:719px){.wtray-close{display:flex;position:absolute;top:12px;right:12px;z-index:3}.wtray-head{padding-right:40px}}
.wtray-row{display:flex;gap:10px;overflow-x:auto;padding:10px 2px 2px;-webkit-overflow-scrolling:touch;scrollbar-width:thin}
.wtray-item{flex:0 0 auto;width:76px;min-height:96px;display:flex;flex-direction:column;align-items:center;gap:5px;padding:8px 6px;
  border:2px dashed rgba(64,52,43,.18);border-radius:var(--radius);background:var(--paper);cursor:pointer;
  touch-action:pan-x;-webkit-tap-highlight-color:transparent;font-family:inherit;color:inherit;
  transition:transform .15s var(--ease-out),border-color .15s,background .15s,box-shadow .15s}
.wtray-item:active{transform:scale(.96)}
.wtray-pic{width:56px;height:56px;display:grid;place-items:center}
.wtray-pic img{max-width:100%;max-height:100%;object-fit:contain;filter:drop-shadow(0 3px 4px rgba(64,52,43,.22));
  pointer-events:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.wtray-emoji{font-size:34px;line-height:1}
.wtray-name{max-width:100%;font:700 .68rem var(--body);color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wtray-item.lift{border-style:solid;border-color:var(--coral-d);background:var(--card);box-shadow:0 8px 18px -8px rgba(200,90,68,.55)}
.wtghost{position:fixed;z-index:99999;width:64px;height:64px;object-fit:contain;pointer-events:none;transform:translate(-50%,-72%);filter:drop-shadow(0 10px 14px rgba(64,52,43,.4))}
/* ---- mobile: the tray becomes a bottom drawer (same pattern as the wall-design
   picker). The wall is tall; a strip at its foot means scrolling past every row to
   reach storage, and placing into a high spot was near-impossible. Now: a floating
   "🧺 N in storage" pill opens the drawer over the page; tapping a friend collapses
   it to a "Placing {name}" chip so the WHOLE wall stays scrollable + tappable. ---- */
.wtrayfab{position:fixed;right:14px;bottom:calc(14px + env(safe-area-inset-bottom));z-index:110;
  display:none;align-items:center;gap:7px;padding:11px 15px;min-height:44px;border-radius:var(--radius);
  border:1px solid var(--border-strong);background:var(--card);box-shadow:0 10px 24px rgba(64,52,43,.3);
  font:800 .85rem var(--body);color:var(--ink);cursor:pointer;-webkit-tap-highlight-color:transparent}
.wtrayfab b{color:var(--coral-d)}
.wtrayscrim{position:fixed;inset:0;z-index:115;background:rgba(43,32,23,.34);opacity:0;pointer-events:none;transition:opacity .22s ease}
.wtrayscrim.show{opacity:1;pointer-events:auto}
@media (min-width:720px){.wtrayfab,.wtrayscrim{display:none !important}}
@media (max-width:719px){
  .wtray{position:fixed;left:0;right:0;bottom:0;z-index:120;margin:0;max-height:48vh;overflow-y:auto;
    border-radius:var(--radius) 20px 0 0;border-bottom:0;padding:8px 12px calc(14px + env(safe-area-inset-bottom));
    transform:translateY(106%);transition:transform .24s var(--ease-out);box-shadow:0 -12px 34px rgba(64,52,43,.28)}
  .wtray.open{transform:none}
  .wtray::before{content:"";display:block;width:44px;height:5px;border-radius:3px;background:var(--line);margin:0 auto 8px} /* grab handle */
  .wtrayfab.show{display:inline-flex}
  /* the fixed bottom nav owns the bottom ~64px+safe-area, the fab and placing chip
     must float above it, not on top of the Saved/Profile tabs */
  .wtrayfab{bottom:calc(88px + env(safe-area-inset-bottom))}
  .wallpane{padding-bottom:140px} /* fab clearance, ONLY the wall pane, not the toolbar (it made a huge top gap) */
}
.wallpack:focus-visible{outline:none}
.wallpack:focus-visible .packcard{outline:3px solid var(--coral-d);outline-offset:5px;border-radius:var(--radius)}
.wallpack .packcard{width:300px;zoom:.46;margin:0 auto;position:relative;top:11px;z-index:1;transform:translateZ(0); /* sits low enough that the hook visibly hooks the euro-slot */
  transform-origin:50% 17px; /* the prong through the hang hole, the pendulum fulcrum */
  transition:transform .26s var(--ease-out);
  filter:drop-shadow(0 13px 15px rgba(64,52,43,.34))}
.wallpack .pk-tagline{display:none}
.wallpack .pk-content img.char,.wallpack .pk-content span.char{cursor:inherit}
/* touch/hover physics: a damped pendulum swing about the hook (no lift, a hung pack
   can't float off its groove, it can only swing) */
@keyframes pkswing{
  0%{transform:translateZ(0) rotate(0)}
  14%{transform:translateZ(0) rotate(6.5deg)}
  34%{transform:translateZ(0) rotate(-4.8deg)}
  54%{transform:translateZ(0) rotate(3deg)}
  72%{transform:translateZ(0) rotate(-1.6deg)}
  87%{transform:translateZ(0) rotate(.7deg)}
  100%{transform:translateZ(0) rotate(0)}
}
.wallpack .packcard.swing{animation:pkswing 1.5s ease-in-out}
.wallpack:active .packcard{transform:translateZ(0) translateY(1px)}
@media(prefers-reduced-motion:reduce){.wallpack .packcard.swing{animation:none}}
/* the display hook, like a real slat-wall peg (see shelf-inspo refs): a folded metal
   clamp grips the plank lip at the groove, and its prong runs down BEHIND the pack,
   so the strap is hidden by the packaging (z below the card) while .peg-tip shows the
   prong passing THROUGH the punched hang hole (z above the card, sized to the hole). */
.peg{position:absolute;top:3px;left:50%;transform:translateX(-50%);width:22px;height:38px;z-index:0;pointer-events:none;
  background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 38'><defs><linearGradient id='f' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23eef1f5'/><stop offset='.45' stop-color='%23c3c8d1'/><stop offset='1' stop-color='%23a4abb6'/></linearGradient><linearGradient id='s' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23dfe3e9'/><stop offset='.5' stop-color='%23b6bcc6'/><stop offset='1' stop-color='%23979ea9'/></linearGradient></defs><rect x='4' y='4' width='14' height='13' rx='2.5' fill='url(%23f)' stroke='%236b7280' stroke-width='1'/><rect x='3' y='1.5' width='16' height='4' rx='2' fill='%23b9bfc9' stroke='%236b7280' stroke-width='1'/><circle cx='11' cy='10.5' r='1.7' fill='%23848b96'/><rect x='8.6' y='16' width='4.8' height='21' rx='2.2' fill='url(%23s)' stroke='%236b7280' stroke-width='.8'/></svg>") center/contain no-repeat;
  filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.28))}
/* on the wall, the punched hang hole is a REAL hole, you see the chosen back
   panel through it (theme vars cascade down from .wall), not paper */
.wallpack .pk-hang path{fill:var(--panel2,var(--muted))}
/* the bit of prong visible through the pack's euro-slot hole */
/* the hook rests AT the rounded upper border of the hole: a small nub hooked over the
   top inner edge (not a prong running to the bottom of the slot). z:8 so it sits in front
   of the hole's back-panel fill. */
.peg-tip{position:absolute;top:31px;left:50%;transform:translateX(-50%);width:8px;height:7px;z-index:8;pointer-events:none;border-radius:4px 4px 5px 5px;
  background:linear-gradient(90deg,#dfe3e9,#b6bcc6 50%,#979ea9);box-shadow:0 0 0 .8px #6b7280,0 1.5px 2px rgba(0,0,0,.3)}
/* hook finishes, same clamp-into-gap + prong-through-hole geometry, different metal */
.hk-noir .peg{background-image:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 38'><defs><linearGradient id='f' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23555962'/><stop offset='.45' stop-color='%2333363d'/><stop offset='1' stop-color='%2320222a'/></linearGradient><linearGradient id='s' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%234a4d55'/><stop offset='.5' stop-color='%232b2d34'/><stop offset='1' stop-color='%23191b20'/></linearGradient></defs><rect x='4' y='4' width='14' height='13' rx='1.2' fill='url(%23f)' stroke='%230c0d10' stroke-width='1'/><rect x='3' y='1.5' width='16' height='4' rx='1' fill='%232e3138' stroke='%230c0d10' stroke-width='1'/><circle cx='11' cy='10.5' r='1.7' fill='%23111318'/><rect x='8.6' y='16' width='4.8' height='21' rx='2.2' fill='url(%23s)' stroke='%230c0d10' stroke-width='.8'/></svg>")}
.hk-noir .peg-tip{background:linear-gradient(90deg,#4a4d55,#2b2d34 50%,#191b20);box-shadow:0 0 0 .8px #0c0d10,0 1px 1.5px rgba(0,0,0,.35)}
.hk-brass .peg{background-image:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 38'><defs><linearGradient id='f' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23f7e6b0'/><stop offset='.45' stop-color='%23d9b45e'/><stop offset='1' stop-color='%23b08c34'/></linearGradient><linearGradient id='s' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23eed9a0'/><stop offset='.5' stop-color='%23caa54d'/><stop offset='1' stop-color='%23a17d2c'/></linearGradient></defs><rect x='4' y='4' width='14' height='13' rx='2.5' fill='url(%23f)' stroke='%23705a14' stroke-width='1'/><rect x='3' y='1.5' width='16' height='4' rx='2' fill='%23cfa953' stroke='%23705a14' stroke-width='1'/><circle cx='11' cy='10.5' r='1.7' fill='%23907022'/><rect x='8.6' y='16' width='4.8' height='21' rx='2.2' fill='url(%23s)' stroke='%23705a14' stroke-width='.8'/></svg>")}
.hk-brass .peg-tip{background:linear-gradient(90deg,#eed9a0,#caa54d 50%,#a17d2c);box-shadow:0 0 0 .8px #705a14,0 1px 1.5px rgba(0,0,0,.25)}
.hk-gold .peg{background-image:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 38'><defs><linearGradient id='f' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23fff4c2'/><stop offset='.45' stop-color='%23f2cf52'/><stop offset='1' stop-color='%23cfa41f'/></linearGradient><linearGradient id='s' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23ffeaa8'/><stop offset='.5' stop-color='%23e8c243'/><stop offset='1' stop-color='%23bf941c'/></linearGradient></defs><rect x='4' y='4' width='14' height='13' rx='2.5' fill='url(%23f)' stroke='%238a6a10' stroke-width='1'/><rect x='3' y='1.5' width='16' height='4' rx='2' fill='%23f0cb4b' stroke='%238a6a10' stroke-width='1'/><circle cx='11' cy='10.5' r='1.7' fill='%23a8801a'/><rect x='8.6' y='16' width='4.8' height='21' rx='2.2' fill='url(%23s)' stroke='%238a6a10' stroke-width='.8'/></svg>")}
.hk-gold .peg-tip{background:linear-gradient(90deg,#ffeaa8,#e8c243 50%,#bf941c);box-shadow:0 0 0 .8px #8a6a10,0 1px 1.5px rgba(0,0,0,.25)}
.hk-rosegold .peg{background-image:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 38'><defs><linearGradient id='f' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23ffd9cf'/><stop offset='.45' stop-color='%23e8ac9b'/><stop offset='1' stop-color='%23c98776'/></linearGradient><linearGradient id='s' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='%23f9cdc0'/><stop offset='.5' stop-color='%23dfa08e'/><stop offset='1' stop-color='%23b87d6d'/></linearGradient></defs><rect x='4' y='4' width='14' height='13' rx='2.5' fill='url(%23f)' stroke='%238f5a4a' stroke-width='1'/><rect x='3' y='1.5' width='16' height='4' rx='2' fill='%23e8ab99' stroke='%238f5a4a' stroke-width='1'/><circle cx='11' cy='10.5' r='1.7' fill='%23a86c5b'/><rect x='8.6' y='16' width='4.8' height='21' rx='2.2' fill='url(%23s)' stroke='%238f5a4a' stroke-width='.8'/></svg>")}
.hk-rosegold .peg-tip{background:linear-gradient(90deg,#f9cdc0,#dfa08e 50%,#b87d6d);box-shadow:0 0 0 .8px #8f5a4a,0 1px 1.5px rgba(0,0,0,.25)}
/* ── keychain mode: the plushie leaves the package and hangs as a plush keychain,
   a metal split ring (the wall hook passes THROUGH it) → a fabric strap sewn to the
   plushie → the plushie itself, with a soft drop-shadow so it reads as hanging under a
   light from above. No die-cut outline. Swing pivots at the ring/hook. ── */
.pk-charm{display:none}
.at-keychain .packcard,.at-keychain .peg-tip{display:none} /* hide the blister pack */
/* keep the SAME 182px row box as hook mode so the plank gaps stay behind EVERY hook,
   on every row; the plushie is free to dangle past the box into the gap below */
.at-keychain .wallpack{height:182px;padding-top:0;overflow:visible}
.at-keychain .pk-charm{display:flex;flex-direction:column;align-items:center;width:100%;position:relative;
  transform-origin:50% 44px; /* the ring the hook passes through, the pendulum fulcrum */
  transition:transform .26s var(--ease-out)}
/* the wall hook sits IN the plank gap (peg unchanged from hook mode) and is drawn in
   FRONT of the keychain so it clearly holds the ring */
.at-keychain .peg{z-index:8}
/* metal split ring, hanging on the hook, fat, glossy boutique hardware (see
   design-inspo/keychaindesigns.png): thick band, bright top-left specular, split tab */
.charm-ring{margin-top:28px;width:33px;height:33px;flex-shrink:0;z-index:1;position:relative;
  background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><defs><linearGradient id='r' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><circle cx='16' cy='16' r='11' fill='none' stroke='url(%23r)' stroke-width='5.2'/><circle cx='16' cy='16' r='11' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1'/><circle cx='10.2' cy='8.4' r='1.7' fill='rgba(255,255,255,.9)'/><rect x='19.8' y='3.4' width='2.8' height='7.4' rx='1.4' fill='%23848c98' transform='rotate(32 16 16)'/></svg>") center/contain no-repeat;
  filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
.hk-brass .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.7) saturate(2.4) hue-rotate(2deg)}
.hk-noir .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) brightness(.42)}
.hk-gold .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.85) saturate(3.2) hue-rotate(-6deg) brightness(1.06)}
.hk-rosegold .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.5) saturate(2) hue-rotate(-28deg)}
/* fabric strap: a named tag at the top + a thin tail (::after) that runs down BEHIND the
   plushie, so however the cutout was framed the strap always reaches and reads as sewn on */
.charm-strap{margin-top:-9px;width:26px;min-height:50px;display:flex;align-items:flex-start;justify-content:center;padding:11px 0 8px;z-index:1;position:relative;
  background:
    repeating-linear-gradient(90deg,rgba(58,43,31,.42) 0 2.5px,transparent 2.5px 5px) center top 7px/68% 1.5px no-repeat,
    repeating-linear-gradient(180deg,rgba(255,255,255,.55) 0 3px,transparent 3px 7px) left 3px top/1.5px 100% no-repeat,
    repeating-linear-gradient(180deg,rgba(255,255,255,.55) 0 3px,transparent 3px 7px) right 3px top/1.5px 100% no-repeat,
    linear-gradient(90deg,rgba(0,0,0,.08),rgba(255,255,255,.16) 48%,rgba(0,0,0,.08)),var(--oc,#aecbe6);
  border-radius:5px 5px 4px 4px;box-shadow:inset 0 0 0 1px rgba(255,255,255,.28),0 1px 2px rgba(64,52,43,.26)}
/* default 0, NOT a length: the tail may only exist once pbTrimCharm has measured that
   the plush is tall enough to hide it. The old 52px fallback meant every charm whose
   trim had not run yet (image still loading, zero-size box, tainted canvas) dangled
   a tail below a short plush — the rule was enforced by JS while CSS broke it. */
.charm-strap::after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%);width:15px;height:var(--tail,0px);
  background:
    repeating-linear-gradient(180deg,rgba(255,255,255,.45) 0 3px,transparent 3px 7px) left 2px top/1.2px 100% no-repeat,
    repeating-linear-gradient(180deg,rgba(255,255,255,.45) 0 3px,transparent 3px 7px) right 2px top/1.2px 100% no-repeat,
    linear-gradient(90deg,rgba(0,0,0,.08),rgba(255,255,255,.16) 48%,rgba(0,0,0,.08)),var(--oc,#aecbe6);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.2)}
/* ── the "strap loops through the ring" illusion, done as THREE LAYERS (no bent
   fabric anywhere): back = the whole hardware (z1, behind the strap) · middle = the
   strap with its rounded cap reaching into the opening (z above the back) · front =
   ONLY the hardware's bottom arc, repainted on the charm's pseudo and clipped below
   the split line (z4). The cap visibly descends into the hole and vanishes behind
   the front arc exactly where the metal band starts. ── */
.pk-charm.att-strap{--hw-art:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><defs><linearGradient id='r' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><circle cx='16' cy='16' r='11' fill='none' stroke='url(%23r)' stroke-width='5.2'/><circle cx='16' cy='16' r='11' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1'/><circle cx='10.2' cy='8.4' r='1.7' fill='rgba(255,255,255,.9)'/><rect x='19.8' y='3.4' width='2.8' height='7.4' rx='1.4' fill='%23848c98' transform='rotate(32 16 16)'/></svg>");--hw-w:33px;--hw-h:33px;--hw-top:28px;--hw-split:72%}
.pk-charm.att-ring{--hw-art:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 42'><defs><linearGradient id='g' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><circle cx='16' cy='13' r='9.6' fill='none' stroke='url(%23g)' stroke-width='4.8'/><circle cx='16' cy='13' r='9.6' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1'/><circle cx='11' cy='7.6' r='1.6' fill='rgba(255,255,255,.9)'/><rect x='13.9' y='24.4' width='4.2' height='5.6' rx='1.8' fill='url(%23g)'/><rect x='14.9' y='25.2' width='1' height='4' rx='.5' fill='rgba(255,255,255,.55)'/><circle cx='16' cy='35.4' r='4' fill='none' stroke='url(%23g)' stroke-width='2.8'/></svg>");--hw-w:36px;--hw-h:47px;--hw-top:23px;--hw-split:88%}
.pk-charm.att-heart{--hw-art:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 42'><defs><linearGradient id='h' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><path d='M16 25.5 C10.8 21 5 16.4 5 10.4 C5 6.3 8 3.9 10.9 3.9 C13.2 3.9 15 5.4 16 7.6 C17 5.4 18.8 3.9 21.1 3.9 C24 3.9 27 6.3 27 10.4 C27 16.4 21.2 21 16 25.5 Z' fill='none' stroke='url(%23h)' stroke-width='4.8' stroke-linejoin='round' stroke-linecap='round'/><path d='M16 25.5 C10.8 21 5 16.4 5 10.4 C5 6.3 8 3.9 10.9 3.9 C13.2 3.9 15 5.4 16 7.6 C17 5.4 18.8 3.9 21.1 3.9 C24 3.9 27 6.3 27 10.4 C27 16.4 21.2 21 16 25.5 Z' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1' stroke-linejoin='round'/><circle cx='10.6' cy='8.2' r='1.6' fill='rgba(255,255,255,.9)'/><rect x='13.9' y='26.2' width='4.2' height='5.6' rx='1.8' fill='url(%23h)'/><rect x='14.9' y='27' width='1' height='4' rx='.5' fill='rgba(255,255,255,.55)'/><circle cx='16' cy='36.4' r='4' fill='none' stroke='url(%23h)' stroke-width='2.8'/></svg>");--hw-w:36px;--hw-h:47px;--hw-top:23px;--hw-split:90%}
.pk-charm.att-strap::after,.pk-charm.att-ring::after,.pk-charm.att-heart::after{content:"";position:absolute;
  top:var(--hw-top);left:50%;transform:translateX(-50%);width:var(--hw-w);height:var(--hw-h);z-index:4;pointer-events:none;
  background:var(--hw-art) center/contain no-repeat;clip-path:inset(var(--hw-split) 0 0 0);
  filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
/* the front arc wears the same metal tint as the back */
.pk-charm.kr-gold::after{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.8) saturate(3) hue-rotate(4deg)}
.pk-charm.kr-noir::after{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) brightness(.42)}
.pk-charm.kr-rose::after{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.6) saturate(2.4) hue-rotate(-26deg) brightness(1.06)}
/* the FOLD: a narrow dome of fabric hanging LOW in the opening, most of the loop is
   implied behind the front arc, so only this peek is visible. Same material as the
   strap (the dome runs down behind the arc and meets the strap body with overlap,
   no gap), with a light top edge for thickness and a shadowed base where it dives
   under the rim (occlusion). Plenty of clearance: it never fills the hole. */
.charm-strap::before{content:"";position:absolute;bottom:calc(100% - 1px);left:50%;transform:translateX(-50%);
  width:calc(8px * var(--ksw,1));height:12px;border-radius:100px 100px 0 0; /* smooth bend, not a pill */
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--oc,#aecbe6) 86%, #fff) 0%,          /* lit crest of the bend */
    var(--oc,#aecbe6) 34%,
    color-mix(in srgb, var(--oc,#aecbe6) 78%, #4a3620) 78%,      /* falling into the rim's shade */
    color-mix(in srgb, var(--oc,#aecbe6) 62%, #4a3620) 100%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.35)}
/* the wide strap starts BEHIND the front arc, it must never show inside the hole
   beside the fold (that reads as "glued on top of the ring") */
.att-strap .charm-strap{margin-top:-4px}
/* clips end in a SMALL link, the fold narrows to thread it, strap tucks under the link */
.att-ring .charm-strap,.att-heart .charm-strap{margin-top:-2px}
.att-ring .charm-strap::before,.att-heart .charm-strap::before{width:calc(4px * var(--ksw,1));height:10px}
.charm-strap>span{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--body);font-weight:800;font-size:.56rem;
  letter-spacing:.05em;color:#3a2b1f;white-space:nowrap;max-height:30px;overflow:hidden;text-transform:uppercase}
.pk-charm.oc-black .charm-strap>span{color:#faf4e2} /* scoped: a legacy wall-level oc-black must not recolour every charm's label */
/* the plushie: no outline; a soft drop-shadow (light from top → shadow below) sells the hang.
   It sits IN FRONT of the strap tail and tucks just under the tag so the strap reads as attached */
.charm-cut{margin-top:-6px;width:auto;max-width:calc(122px * var(--kcz,1));height:auto;max-height:calc(132px * var(--kcz,1));object-fit:contain;z-index:2;position:relative;
  filter:drop-shadow(0 11px 9px rgba(64,52,43,.36))}
.charm-emoji{font-size:100px;line-height:1;margin-top:-6px;z-index:2;position:relative;filter:drop-shadow(0 11px 9px rgba(64,52,43,.32))}
/* Nothing hangs perfectly flat — the same rule the acrylic below is built on. The plush
   leans on the tab it is sewn to; the pivot is 50% 0, the sewn point, so however far it
   leans the tuck under the strap stays exact. On .charm-cut, NOT on .pk-charm: the whole
   charm's transform belongs to the pendulum swing, and the hardware above the seam hangs
   straight anyway. Three angles across the wall so nine charms don't read as one skew. */
/* --kcfit is the measured correction (see pbTrimCharm): a scale, not a cap, because a
   cap the artwork never reaches does nothing — on the wall the cell decides the width.
   Origin at the top centre so a corrected charm still hangs from its strap. */
.charm-cut{transform-origin:50% 0;transform:rotate(var(--lean,-2.4deg)) scale(var(--kcfit,1))}
.at-keychain .wallpack:nth-child(3n+2) .pk-charm{--lean:1.8deg}
.at-keychain .wallpack:nth-child(3n) .pk-charm{--lean:-1deg}
/* per-plushie charm customization: strap thickness (--ksw), ring colour (kr-*).
   No per-plushie size, every charm hangs at one scale so the wall stays consistent. */
.charm-strap{width:calc(26px * var(--ksw,1))}
.charm-strap::after{width:calc(15px * var(--ksw,1))}
.charm-strap>span{max-height:82px}
.pk-charm.kr-silver .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
.pk-charm.kr-gold .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.8) saturate(3) hue-rotate(4deg)}
.pk-charm.kr-noir .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) brightness(.42)}
.pk-charm.kr-rose .charm-ring{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.6) saturate(2.4) hue-rotate(-26deg) brightness(1.06)}
/* ── attachment hardware: strap (default) · ring clip · heart clip. The named fabric
   strap is part of EVERY style, the clip is the hardware between hook and strap. ── */
.charm-heart{display:none}
.att-heart .charm-ring{display:none}
.att-heart .charm-heart{display:block}
/* ring clip: same clip anatomy as the heart (ring → swivel barrel → link) but a circle;
   the strap hangs from the link below */
.att-ring .charm-ring{width:36px;height:47px;margin-top:23px;
  background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 42'><defs><linearGradient id='g' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><circle cx='16' cy='13' r='9.6' fill='none' stroke='url(%23g)' stroke-width='4.8'/><circle cx='16' cy='13' r='9.6' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1'/><circle cx='11' cy='7.6' r='1.6' fill='rgba(255,255,255,.9)'/><rect x='13.9' y='24.4' width='4.2' height='5.6' rx='1.8' fill='url(%23g)'/><rect x='14.9' y='25.2' width='1' height='4' rx='.5' fill='rgba(255,255,255,.55)'/><circle cx='16' cy='35.4' r='4' fill='none' stroke='url(%23g)' stroke-width='2.8'/></svg>") center/contain no-repeat}
/* heart CLIP at the top, fat open heart ring like real boutique hardware, with the
   swivel barrel + small link ring underneath (design-inspo/keychaindesigns.png) */
/* margin-top mirrors the split ring's hang: the ring sits at 28px with its hole centred
   on the hook tip (~44px, the pendulum fulcrum), the heart's top loop must land on the
   same tip, so it starts at 31px and the hook visibly passes through the loop */
.charm-heart{width:36px;height:47px;margin-top:23px;flex-shrink:0;z-index:1;position:relative; /* hook prong tip (y≈41) lands mid-opening, the clip reads as hooked ON, not floating under */
  background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 42'><defs><linearGradient id='h' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><path d='M16 25.5 C10.8 21 5 16.4 5 10.4 C5 6.3 8 3.9 10.9 3.9 C13.2 3.9 15 5.4 16 7.6 C17 5.4 18.8 3.9 21.1 3.9 C24 3.9 27 6.3 27 10.4 C27 16.4 21.2 21 16 25.5 Z' fill='none' stroke='url(%23h)' stroke-width='4.8' stroke-linejoin='round' stroke-linecap='round'/><path d='M16 25.5 C10.8 21 5 16.4 5 10.4 C5 6.3 8 3.9 10.9 3.9 C13.2 3.9 15 5.4 16 7.6 C17 5.4 18.8 3.9 21.1 3.9 C24 3.9 27 6.3 27 10.4 C27 16.4 21.2 21 16 25.5 Z' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1' stroke-linejoin='round'/><circle cx='10.6' cy='8.2' r='1.6' fill='rgba(255,255,255,.9)'/><rect x='13.9' y='26.2' width='4.2' height='5.6' rx='1.8' fill='url(%23h)'/><rect x='14.9' y='27' width='1' height='4' rx='.5' fill='rgba(255,255,255,.55)'/><circle cx='16' cy='36.4' r='4' fill='none' stroke='url(%23h)' stroke-width='2.8'/></svg>") center/contain no-repeat;
  filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
/* the ring colour tints every metal part */
.pk-charm.kr-gold .charm-heart{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.8) saturate(3) hue-rotate(4deg)}
.pk-charm.kr-noir .charm-heart{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) brightness(.42)}
.pk-charm.kr-rose .charm-heart{filter:drop-shadow(0 2px 1.5px rgba(64,52,43,.34)) sepia(.6) saturate(2.4) hue-rotate(-26deg) brightness(1.06)}
/* keychain + acrylic hang, so they start at the top of the shared stage rather than
   floating in the middle of it */
.vslide[data-pane=keychain] .vstage,.vslide[data-pane=acrylic] .vstage{align-items:flex-start;padding-top:20px}
/* The acrylic was sized for the WALL, where charms are thumbnails — on the shared stage
   it sat as a stamp in an empty case. The keychain gets there by JS (fitCharm scales to
   the box); this one is a fixed shape, so it just needs stage-sized numbers. */
/* Every hardware size is ABSOLUTE px on .case-shell, which is right on the wall (charms are
   64-118px thumbs there) and wrong here, where the same charm renders ~300px: the lug came
   out at 10% of the charm and the drill at 2.9%, so the hole was SMALLER THAN THE RING
   covering it and read as a smudge instead of an opening. Restated as a share of the charm,
   per the reference proportions: tab 15%, hole 6.7%, jump ring 9%, clip 23%. */
.vslide[data-pane=acrylic] .case-shell{--cl-top:8px;--cw-fb:262px}
.vslide[data-pane=acrylic] .cs-emoji{font-size:180px}
.kcfit{transform-origin:50% 0;will-change:transform}
/* the editor's footer: the receipt line, and an escape hatch that only appears once there
   is something to escape from */
.kcsaverow{display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;margin:6px 0 0}
.kcsaverow .btn{flex:none}
.kcsaverow [hidden]{display:none}
/* The size ruler: two of the owner's other charms, at their own saved size, flanking the
   one being edited. Ghosted so it is obvious which one you are changing, and pushed behind
   it so a big neighbour never hides the subject. They are inert — a reference, not a
   picker — and they hang from the same top edge as the real charm so the comparison is
   between the plushies, not between where two charms happen to start. */
.kcghosts{position:absolute;inset:0;display:flex;align-items:flex-start;justify-content:center;gap:clamp(10px,7vw,54px);
  pointer-events:none;z-index:0}
.kcghost{opacity:.26;filter:grayscale(.55)}
.kcstar{position:relative;z-index:1}
@media(max-width:420px){.kcghost:nth-child(2){display:none}}   /* one ruler is enough on a narrow phone */ /* JS scales this to keep the whole charm inside the box */
.vstage .pk-charm{display:flex;flex-direction:column;align-items:center;position:relative;width:auto;transition:none}
/* the wall editor's way IN to per-plushie styling: each thumb is a link to that plushie's
   own page, where its charm, acrylic and pack are set */
.kcpick{display:flex;gap:6px;flex-wrap:wrap;margin:2px 0 8px}
.kcthumb{width:44px;height:44px;border-radius:var(--radius);border:2px solid var(--border-strong);background:var(--card);padding:2px;cursor:pointer;overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:22px;flex:none}
.kcthumb img{width:100%;height:100%;object-fit:contain;pointer-events:none}
.kcthumb:hover{border-color:var(--coral-d)}
.krcolor{width:34px;height:34px;border-radius:var(--radius-sm);border:0;cursor:pointer;padding:0}
.krcolor:hover{transform:scale(1.08)}
/* metals are gradients, so --c is the mid stop, the one the ring should echo */
.krc-silver{--c:#c3c9d2;background:linear-gradient(135deg,#f1f3f6,#c3c9d2 55%,#969da9)}
.krc-gold{--c:#d9b45e;background:linear-gradient(135deg,#f7e6b0,#d9b45e 55%,#b08c34)}
.krc-noir{--c:#33363d;background:linear-gradient(135deg,#5a5d64,#33363d 55%,#1c1e24)}
.krc-rose{--c:#e3a89b;background:linear-gradient(135deg,#f6d6cf,#e3a89b 55%,#c98074)}
.at-keychain .wallpack:focus-visible .pk-charm{transform:rotate(-3.5deg)} /* hover swing is physics now */
.wallpack .pk-charm.swing{animation:pkswing 1.5s ease-in-out}
@media(prefers-reduced-motion:reduce){.wallpack .pk-charm.swing{animation:none}}
/* ── acrylic case mode: the cutout run through the #cshell SVG filter (molded clear
   rim + sheen) hanging from a gold heart clasp, rendered live, nothing stored ── */
.at-case .packcard,.at-case .peg-tip{display:none}
.at-case .wallpack{height:182px;padding-top:0;overflow:visible}
.at-case .peg{z-index:8} /* hook in front, it holds the case's clasp loop */
.case-shell{display:none}
.at-case .case-shell{display:flex;flex-direction:column;align-items:center;width:100%;position:relative;z-index:1;
  transform-origin:50% 36px; /* the clasp loop on the hook, the pendulum fulcrum */
  transform:rotate(-1.6deg); /* acrylic-keychain-gpt.md: nothing hangs perfectly flat */
  transition:transform .26s var(--ease-out)}
/* Clasp geometry as variables on the SHELL, because two things have to agree on it: the
   clasp element and the front-arc pass that repaints its bottom through the hole. One
   source or they drift apart at different sizes. The chain runs UPWARD from --anchor (the
   hole): ring on the hole, clasp on the ring. */
.case-shell{
  /* ONE geometry at every size. All hardware is a share of the MEASURED artwork width
     (--charmw, published by pbTrimCharm) — the px era kept two systems, and the anchor
     rewrite that fixed the editor silently blew up the wall, where heart-clip hardware
     sized for the editor met 64px charms. --cw-fb covers pre-trim moments and emoji. */
  --cw:var(--charmw,var(--cw-fb,96px));
  --cl-w:calc(var(--cw) * 0.28);--cl-h:calc(var(--cw) * 0.37);
  --lug:calc(var(--cw) * 0.17);--hole:calc(var(--cw) * 0.034);
  --cl-top:25px;--cl-split:90%;
  /* the jump ring is a SEPARATE part from the connector above it, in silver rather than the
     connector's white, and it is what actually links hardware to charm */
  /* the connector between clasp and charm is a SOLID LINK now — the jump ring drew a
     second circle in the one place already crowded with them (bead loop above, drilled
     hole below), and the stack read as a bullseye however it was layered. */
  --link:calc(var(--cl-h) * 0.22);--linkw:calc(var(--cl-h) * 0.09);
  /* How far the shell rides up under the clasp — the knob that brings the RING to the hole
     rather than moving the hole to the ring. A charm hangs OFF a jump ring: the wire passes
     through the hole and the charm's weight rests the hole on the ring's lower inner arc, so
     the collar of plastic above the hole sits inside the ring's opening. Lining the hole up
     with the ring's CENTRE instead buries the lower half of the ring in plastic, which is
     what read as the tab being swallowed. 0.132 of the clasp's height is where the loop's
     centre sits above the shell's box top (measured off the render; the art's own cy says
     0.157, contain-fitting and the border box account for the rest). The charm now hangs a
     jump ring's worth lower, because there is a part between them: 0.8 of a jump ring puts
     the connector's link inside its top and the charm's hole on its bottom wire. */
  /* ===== ONE anchor: the drilled hole. Everything hangs off it, upward.
     This used to run the other way — a --cl-bite that slid the whole shell up under a
     fixed clasp, while the hole's own y was derived separately from --headroom. Two
     coordinate systems for one physical joint, so the ring and the hole could drift
     apart and did. Now: the tab's geometry fixes the hole, the ring is placed on the
     hole, the clasp is placed on the ring. Nothing chases anything. ===== */
  --lug-h:calc(var(--lug) * 0.92);
  /* clearance above the artwork for the hardware, so the clasp's top lands at y=0 */
  --hang:calc(var(--cl-top) + var(--cl-h) * 0.94 + var(--link) + var(--holeup));
  /* the hole's centre, in .case-shell space. The visible artwork starts at --hang
     (see .cs-body's margin), and the hole sits in the crown of the tab above it. */
  /* How far above the artwork the hole sits. The floor is a safety property, not a tweak:
     if the tab is ever small relative to the drill, the hole would land ON the plush, so
     keep it its own radius plus 2px clear. --anchor and --holey BOTH read this, which is
     what stops the ring and the hole disagreeing. */
  --holeup:max(calc(var(--lug-h) * 0.55),calc(var(--hole) + 2px));
  --anchor:calc(var(--hang) - var(--holeup));
  --cl-tint:drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
/* the acrylic clasp tints with the ring colour (shared with the keychain), so the acrylic
   editor's "Clasp" picker is meaningful. Default gold SVG, retinted per kr-*. On the shell
   as a variable, so the front-arc wears the same metal as the back without repeating it. */
.case-shell.kr-silver{--cl-tint:saturate(.06) brightness(1.14) drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
.case-shell.kr-noir{--cl-tint:brightness(.42) drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
.case-shell.kr-rose{--cl-tint:sepia(.6) saturate(2.4) hue-rotate(-26deg) brightness(1.06) drop-shadow(0 2px 1.5px rgba(64,52,43,.34))}
/* per-plushie keychain + acrylic editor previews on the plushie page */
.kcase-prev{display:flex;justify-content:center;align-items:flex-start;width:100%}
.kced .kcrow,.acred .kcrow{display:flex;align-items:center;gap:10px;margin:8px 0 0;flex-wrap:wrap}
.kced .kcrow .pkoptlbl,.acred .kcrow .pkoptlbl{margin:0;min-width:44px}
/* z-index 0, NOT 2: the clasp has to pass BEHIND the shell so the front-arc pass below can
   bring only its bottom back out in front. At 2 it painted over the shell and covered the
   drilled hole entirely — the hole was there, you just could never see it. */
.cs-clasp{position:absolute;left:50%;transform:translateX(-50%);
  top:calc(var(--anchor) - var(--link) - var(--cl-h) * 0.94);
  width:var(--cl-w);height:var(--cl-h);z-index:0;
  background:var(--hw-art) center/contain no-repeat;
  filter:var(--cl-tint)}
/* The ring PASSES THROUGH the hole; it does not sit on top of it. Exactly the two-pass trick
   the plush keychain already uses (--hw-split, above): the whole clasp paints behind, then
   ONLY its bottom arc is repainted in front, clipped below the split line, so the shell's
   tab visibly swallows the ring where the metal enters the hole. One art source and a clip
   rather than two half-rings, so the halves cannot drift out of register. */
/* THE LINK — hook → clasp → solid metal bar → through the drilled hole. It paints BEHIND
   the shell (z0) and the hole is a real void, so the bar shows through the opening: that
   see-through moment is what says "attached", with one shape instead of the old two-pass
   jump ring, whose extra circle stacked into a bullseye with the bead and the hole. */
.cs-jump{position:absolute;left:50%;transform:translateX(-50%);
  /* Ends ON the hole's upper rim (anchor is the hole's CENTRE, so the rim is one radius
     up) with 2px of bite, and paints in FRONT of the shell — behind it, the contact point
     is buried under the tab and the link just looks amputated. The void below the tip stays
     open, so the eye reads: bar hooked over the top edge of a real hole. */
  top:calc(var(--anchor) - var(--link));
  /* Dips 0.65 of a radius PAST the rim — about a third of the opening — and ends in a
     semicircular cap (999px bottom corners clamp to half the width). A flat tip stopping
     level with the rim reads as resting ON the hole; a round one sunk slightly inside it
     reads as going INTO it, and the remaining void below still shows the background. */
  width:var(--linkw);height:calc(var(--link) - var(--hole) * 0.35);
  border-radius:2px 2px 999px 999px;z-index:3;pointer-events:none;
  background:linear-gradient(90deg,#f1f3f6,#c3c9d2 55%,#969da9);
  filter:var(--cl-tint)}
.at-case .case-shell::after,.kcase-prev .case-shell::after{content:"";position:absolute;
  top:calc(var(--anchor) - var(--link) - var(--cl-h) * 0.94);
  left:50%;transform:translateX(-50%);width:var(--cl-w);height:var(--cl-h);
  z-index:3;pointer-events:none;background:var(--hw-art) center/contain no-repeat;
  clip-path:inset(var(--cl-split) 0 0 0);filter:var(--cl-tint)}
/* identical hardware to the strap keychain — same ring, same bead, same lower loop */
.case-shell.att-ring{--hw-art:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 42'><defs><linearGradient id='g' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><circle cx='16' cy='13' r='9.6' fill='none' stroke='url(%23g)' stroke-width='4.8'/><circle cx='16' cy='13' r='9.6' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1'/><circle cx='11' cy='7.6' r='1.6' fill='rgba(255,255,255,.9)'/><rect x='13.9' y='24.4' width='4.2' height='5.6' rx='1.8' fill='url(%23g)'/><rect x='14.9' y='25.2' width='1' height='4' rx='.5' fill='rgba(255,255,255,.55)'/><circle cx='16' cy='35.4' r='4' fill='none' stroke='url(%23g)' stroke-width='2.8'/></svg>");--cl-split:88%}
.case-shell.att-heart{--hw-art:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 42'><defs><linearGradient id='h' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%23f7f9fc'/><stop offset='.45' stop-color='%23c9cfd8'/><stop offset='1' stop-color='%238b93a0'/></linearGradient></defs><path d='M16 25.5 C10.8 21 5 16.4 5 10.4 C5 6.3 8 3.9 10.9 3.9 C13.2 3.9 15 5.4 16 7.6 C17 5.4 18.8 3.9 21.1 3.9 C24 3.9 27 6.3 27 10.4 C27 16.4 21.2 21 16 25.5 Z' fill='none' stroke='url(%23h)' stroke-width='4.8' stroke-linejoin='round' stroke-linecap='round'/><path d='M16 25.5 C10.8 21 5 16.4 5 10.4 C5 6.3 8 3.9 10.9 3.9 C13.2 3.9 15 5.4 16 7.6 C17 5.4 18.8 3.9 21.1 3.9 C24 3.9 27 6.3 27 10.4 C27 16.4 21.2 21 16 25.5 Z' fill='none' stroke='rgba(255,255,255,.5)' stroke-width='1.1' stroke-linejoin='round'/><circle cx='10.6' cy='8.2' r='1.6' fill='rgba(255,255,255,.9)'/><rect x='13.9' y='26.2' width='4.2' height='5.6' rx='1.8' fill='url(%23h)'/><rect x='14.9' y='27' width='1' height='4' rx='.5' fill='rgba(255,255,255,.55)'/><circle cx='16' cy='36.4' r='4' fill='none' stroke='url(%23h)' stroke-width='2.8'/></svg>");--cl-split:90%}
/* margin-top 0, NOT -12px. The hole is drilled at an offset from .cs-body's top and the
   artwork has to be at a KNOWN offset from that same top or the two disagree. The trim
   already zeroes this inline once it has measured a photo, so 0 is what every measured
   charm has always rendered with — the -12px only ever applied to cutouts the trim
   skipped (tight crops, where top===0), which put their artwork 12px above where the
   hole assumed it was, and the hole punched through the plushie's head. */
.cs-cut{transform-origin:50% 50%;transform:scale(var(--kcfit,1));margin-top:0;width:auto;height:auto;max-width:calc(124px * var(--kcz,1));max-height:calc(126px * var(--kcz,1));object-fit:contain;position:relative;
  /* raw (uncut) photos become rounded acrylic photo blocks; true cutouts already have
     transparent corners, so the radius changes nothing for them */
  border-radius:var(--radius);
  /* the shell filter and its shadows now live on .cs-body, which unions the lug in */
  }
/* the tint picks its filter; --shellf is on the SHELL so the cutout inherits it */
.case-shell.ac-clear{--shellf:url(#cshell-clear);--acryl:#fdfbfa}
.case-shell.ac-sky{--shellf:url(#cshell-sky);--acryl:#cfe6f5}
.case-shell.ac-blush{--shellf:url(#cshell-blush);--acryl:#fbd9e4}
.case-shell.ac-mint{--shellf:url(#cshell-mint);--acryl:#d2ecdd}
.case-shell.ac-lilac{--shellf:url(#cshell-lilac);--acryl:#e0d8f2}
.case-shell.ac-butter{--shellf:url(#cshell-butter);--acryl:#fbeec6}
/* swatches for the picker: a pale tint on white, so they read as translucent plastic */
.krcolor.acr-clear{--c:#fdfbfa;background:linear-gradient(135deg,#ffffff,#f0eeec)}
.krcolor.acr-sky{--c:#cfe6f5;background:linear-gradient(135deg,#eaf5fc,#cfe6f5)}
.krcolor.acr-blush{--c:#fbd9e4;background:linear-gradient(135deg,#fdeef3,#fbd9e4)}
.krcolor.acr-mint{--c:#d2ecdd;background:linear-gradient(135deg,#eaf7f0,#d2ecdd)}
.krcolor.acr-lilac{--c:#e0d8f2;background:linear-gradient(135deg,#f1edfa,#e0d8f2)}
.krcolor.acr-butter{--c:#fbeec6;background:linear-gradient(135deg,#fdf7e4,#fbeec6)}
/* ===== the punched hole. A real acrylic charm is DRILLED and the ring goes through it,
   so this is a ring-shaped cut (inner shadow + lit lower lip) sitting on the shell, with
   the clasp's lower loop crossing it. Positioned off the clasp, not the cutout, because
   the cutout's offset moves with each photo's transparent headroom. ===== */
/* ===== The acrylic cutline, generated the way a laser cutter would: the artwork's alpha
   and the hanger lug's alpha are UNIONED first (both live inside .cs-body, which is what
   carries the filter), then #cshell offsets and smooths that union into one continuous
   silhouette, then the hole is masked out of the result. Bolting a tab on top of the
   finished shell is what left a visible seam — this fuses because the rim never knows
   there were two shapes. Artwork stays untouched; only the cutline changes. ===== */
/* width:100% matters — shrink-to-fit here removes the definite width the cutout sizes
   against, and an SVG/indeterminate image then collapses to the lug's 36px. */
/* --holey: the hole is drilled in the TAB, not at the top of the artwork. --headroom is
   where the plushie's first visible pixel sits, so drilling there put the hole's whole
   lower half inside its head. The lug spans headroom ± lug/2, which means the half above
   the artwork is free plastic — drill in the middle of THAT. lug*0.3 clears the artwork by
   lug*0.3-hole and leaves lug*0.2-hole of plastic above the hole, so both --lug/--hole
   pairs below need hole <= lug*0.2 (they are 4/20 and 7/36); the filter's ~8px rim is
   further plastic on top of that. */
.cs-body{position:relative;display:flex;flex-direction:column;align-items:center;width:100%;
  /* Drill in the middle of the TAB: the lug sits 0.85 of its height above the artwork, so
     its centre — and the hole — is 0.35 of a lug up from the artwork's top edge. That is a
     definition, not a tuned number, so the hole tracks the lobe at any size. The second term
     floor lives in --holeup and keeps the hole its own radius plus 2px clear of the plushie
     whatever the lug does. The hardware is brought to this hole, never the other way round:
     the tab is the fixed geometry every charm shares, so the metal anchors to it. */
  /* Put the VISIBLE artwork top at --hang in shell space; the transparent padding above
     it is pulled off, not designed around. That is the whole job of this margin now. */
  /* --maskpad exists because mask-clip:no-clip is a lie in Chromium — neither the standard
     nor the -webkit- property implements it, so the mask always clips paint to the BORDER
     BOX. The shell paints outside that box (tab dome above the artwork, dilated rim and
     drop-shadow below), and Chrome amputated both: a flat cut across the top and bottom of
     every tall charm. So the box is grown to contain the paint — that is a guarantee no
     engine can decline — and every box-relative number below is shifted by the same pad. */
  --maskpad:60px;
  /* ...and the SIDES need the same treatment, for the same reason. The pad above was only
     ever vertical, so the dilated rim kept being amputated square on the left and right of
     any charm whose silhouette reached the box — a flat vertical cut down the white edge,
     worst at high column counts where the box is narrowest.
     The rim's width is a constant of #cshell, NOT a property of the plushie: the filter
     dilates by blurring SourceAlpha at stdDeviation 4 and thresholding it (d8 cuts at
     alpha .0147 ≈ 2.2σ), so the outermost paint sits ~9px out from the silhouette at every
     charm size — stdDeviation is in user units and does not scale. That is what makes this
     fixable without knowing how wide any given charm will be: reserve the constant.
     box-sizing is border-box, so this shrinks the content box and leaves the border box on
     the cell — the rim paints into the padding, inside the box the mask clips to.
     Symmetric, so the centred lug and the 50%-positioned mask are undisturbed. */
  --maskpadx:12px;
  padding:var(--maskpad) var(--maskpadx);
  margin-top:calc(var(--hang) - var(--headroom,0px) - var(--maskpad));
  /* the hole, in THIS box's space — same definition as --anchor, one lug up from the art */
  --holey:calc(var(--maskpad) + var(--headroom,0px) - var(--holeup));
  /* NO drop-shadow. There were two — drop-shadow(0 3px 5px) for contact and (0 14px 18px)
     for ambient depth — and both were a bad deal twice over.
     Visually: a drop-shadow is a fixed px offset and blur, but a charm is not a fixed
     size. It shrinks with every extra column ("per row", and the auto grid packs acrylic
     ~5 across on a phone), so the shadow does not shrink with it — it grows relative to
     the charm until neighbouring shadows in a row merge into one grey band behind the
     whole shelf. That is the "box" behind every keychain, and it is worst exactly where
     it was reported: several acrylics in a row.
     Cost: each chained drop-shadow is another full-region CPU blur stacked on #cshell's
     own 15 primitives. The pair was the most expensive thing on the wall — more than the
     shell filter it was decorating.
     The charm does not need one. It is a slab of clear acrylic: the shell's own dilated
     rim, its white ring highlight and its specular glint already give it thickness. */
  filter:var(--shellf,url(#cshell-clear));
  /* no-clip, and it is not optional. mask-clip defaults to border-box, so the mask that
     drills the hole was ALSO amputating everything that overflows this box — which is the
     hanger tab, drawn above the artwork's top edge. The lug rendered correctly and was then
     cut off at the silhouette, so the tab read as folded flat into the charm and the jump
     ring appeared to float above a shell it was not attached to. A cutout with transparent
     headroom hid the bug (its lug sits nearer the box) which is why one charm in nine always
     looked right. */
  /* The mask is sized and placed around the HOLE, not around this box. A gradient sized to
     the border box repeats outside it, so once --holey went negative — which it does for
     every tight-cropped cutout, the hole being drilled in the tab ABOVE the artwork — the
     hole fell into a repeated tile and was never punched at all. One charm in nine had
     enough transparent headroom to keep --holey positive and was the only one with a hole.
     A single oversized no-repeat circle centred on the drill point has no tiles to fall
     between. --maskr just has to exceed the biggest charm — and 300px did NOT: the mask is
     a finite tile, so anything past hole+300px was silently sliced off, which is a flat
     horizontal cut across the shell's bottom on a tall enough photo. A gradient is
     resolution-independent; a huge radius costs nothing. */
  --maskr:1000px;
  -webkit-mask-image:radial-gradient(circle at center,transparent 0 var(--hole),#000 var(--hole));
  mask-image:radial-gradient(circle at center,transparent 0 var(--hole),#000 var(--hole));
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:calc(var(--maskr) * 2) calc(var(--maskr) * 2);
  mask-size:calc(var(--maskr) * 2) calc(var(--maskr) * 2);
  -webkit-mask-position:50% calc(var(--holey) - var(--maskr));
  mask-position:50% calc(var(--holey) - var(--maskr));
}
/* the lug only needs ALPHA — the filter reads SourceAlpha, so its colour is incidental.
   The tint is there so the visible plastic matches the shell it fuses into. */
/* NO drilled-edge ring here. There used to be one, to make the hole read as a cut rather
   than a gap — but the mask already removes real pixels, so the wall genuinely shows
   through, and a ring drawn around it is simply a second circle. Stacked with the jump
   ring and the connector's link that made a bullseye of concentric rings sitting on the
   plushie. The hole is an opening; it does not need to be outlined to prove it. */
/* The lug has to PROTRUDE, not straddle. Centred on the artwork's top edge (top: -lug/2)
   it was half-buried, and the dilation fused it into a wide shoulder — a lump, not a tab,
   with the hole drilled into the silhouette's own top rather than into a lobe that exists
   to carry it. Sitting 0.85 of its height above the edge leaves 15% overlap, which the
   ~8px dilation turns into a neck, and puts the whole lobe in clear air. */
.cs-lug{position:absolute;left:50%;top:calc(var(--maskpad,0px) + var(--headroom,0px) - var(--lug) * 0.85);
  transform:translateX(-50%);width:var(--lug);height:calc(var(--lug) * 0.92);
  /* a circle here reads as a disc stuck on the plushie; a real tab has a rounded
     crown and squarer shoulders where it runs into the charm */
  border-radius:calc(var(--lug) * 0.46) calc(var(--lug) * 0.46) calc(var(--lug) * 0.16) calc(var(--lug) * 0.16);
  background:color-mix(in srgb,var(--acryl,#fdfbfa) 62%,transparent)}
/* wall charms are thumbnails: the lug and its hole scale with them or they swallow the plush */
/* On the wall the CLASP is real hardware, identical to the strap keychain's (art was
   already shared; box and hang point now too), so a peg grips either charm the same way.
   Tab and drill stay proportional to the charm — plastic scales, metal does not. */
.wall .case-shell{--cl-w:36px;--cl-h:47px;--cl-top:23px}
.cs-emoji{font-size:92px;line-height:1}
/* The acrylic takes the same clip choice as the keychain (default art is the heart; this
   is its ring twin) and gains a DRILLED HOLE where the lower loop meets the shell — the
   detail that makes it read as threaded through the plastic rather than resting on it. */
/* ===== one preview height for every view, so swiping does not resize the plushie =====
   The wall shows charms as thumbnails; here each view fills the same box instead. */
/* an explicit height, not min-height: while the stage grew to fit the TALLEST slide,
   --preview-h stayed a fixed cap, so on a big screen every preview sat small inside a
   box it could not see. Now the box is a known size and each view is sized off it. */
.vstage{--preview-h:clamp(320px,min(56vh,96vw),470px)}
/* The peel reserves ~half of its canvas as room for the sticker to lift and curl. That is
   fixed: outline width and shadow size make no difference to it (measured, three option
   sets, identical). Enlarging the canvas to win the space back does not work either — the
   library anchors the art off-centre once the canvas is much bigger than the source, so it
   drifts up and left and clips. The canvas therefore matches the stage, and the size win
   comes from cropping the source's transparent padding before the library ever sees it. */
.vstage .herosticker{width:min(var(--preview-h),84vw);height:var(--preview-h);max-width:none;margin:0}
/* the cutout itself was still on its wall-sized height, so the sticker sat small
   inside a stage the other views fill */
.vstage .hero-cutout{height:calc(var(--preview-h) - 34px);max-width:100%;width:auto}
.vstage .packcard{width:min(calc((var(--preview-h) - 30px) / 1.16),76vw)}
.vslide[data-pane=acrylic] .cs-cut{max-width:min(300px,74vw);max-height:calc(var(--preview-h) - 168px)}
/* ===== scrollbar, themed, EVERYWHERE. Firefox gets the two-value form; WebKit needs the
   pseudo elements, where the transparent border + background-clip is what insets the thumb
   without shrinking the track's hit area.

   This used to be scoped to .pedit-panel, so one panel wore the theme and every other
   scroller — the avatar picker, the emoji grid, the page itself — rendered the OS default
   grey. Now global, and the two properties need different reach: scrollbar-color IS
   inherited, so :root carries it to every descendant, but scrollbar-width is NOT (measured:
   a plain div under a themed :root still computed auto), so it needs the universal
   selector to match WebKit's 11px in Firefox. The WebKit pseudos inherit nothing, hence
   the bare element rules.

   The carousels that deliberately hide their bar (.viewtoggle, .vcar-track, .marquee-zone)
   keep doing so, and not by specificity — a declaration on the element itself always beats
   a value it would otherwise inherit, whatever the selectors score. The WebKit side does
   come down to specificity, and .foo::-webkit-scrollbar outranks the bare pseudo.
   Thumb is --primary, so it flips with the theme on its own. ===== */
:root{scrollbar-color:var(--scroll-thumb) transparent}
*{scrollbar-width:thin}
::-webkit-scrollbar{width:11px;height:11px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--scroll-thumb);border-radius:var(--radius-pill);
  border:3px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--coral-d);background-clip:content-box}
::-webkit-scrollbar-corner{background:transparent}

/* an uncut (whole) photo can't be a die-cut charm, in keychain mode it falls back to
   its acrylic photo block, hanging from the same hook as everyone else */
.at-keychain .wallpack.uncut .pk-charm{display:none}
.at-keychain .wallpack.uncut .case-shell{display:flex;flex-direction:column;align-items:center;width:100%;position:relative;z-index:1;
  transform-origin:50% 36px;transition:transform .26s var(--ease-out)}
.at-keychain .wallpack.uncut{height:182px;padding-top:0;overflow:visible}
.at-case .wallpack:focus-visible .case-shell{transform:rotate(-3.5deg)} /* hover swing is physics now */
.wallpack .case-shell.swing{animation:pkswing 1.5s ease-in-out}
@media(prefers-reduced-motion:reduce){.wallpack .case-shell.swing{animation:none}}
/* ===== Vitrine wall: cubby boxes on a 12-col grid (port of artifacts/vitrine-wall.html).
   Cells carry desktop rect vars (--c1..--r2) AND mobile rect vars (--mc1..--mr2); the
   media query flips which set the grid uses. Six colourways; "crossing" turns the whole
   wall into AC-style wooden cubbies with five toon palettes (data-pal). ===== */
.pvit{position:relative;margin-top:14px}
.pvgrid{position:relative;display:grid;grid-template-columns:repeat(12,1fr);grid-auto-rows:var(--rh,120px);
  gap:8px;padding:10px;border-radius:var(--radius);touch-action:pan-y;
  background:var(--vf,#e7e4dd);box-shadow:0 24px 50px rgba(64,52,43,.16), inset 0 1px 0 #fff, inset 0 0 0 1px var(--vl,#cdc9c0)}
.vc{position:relative;grid-column:var(--c1)/var(--c2);grid-row:var(--r1)/var(--r2)}
@media(max-width:719px){
  .pvgrid{grid-auto-rows:var(--rhm,96px);gap:6px;padding:7px}
  .vc{grid-column:var(--mc1)/var(--mc2);grid-row:var(--mr1)/var(--mr2)}
}
/* colourways (prototype values) */
.pvit[data-vth=ivory]{--vf:#e7e4dd;--vl:#cdc9c0;--vb:var(--card);--vp:#e3ded3;--vled:#fff3d8}
.pvit[data-vth=blush]{--vf:#f2dad9;--vl:#dcb3b1;--vb:#f7cbc7;--vp:#f5e0df;--vled:#ffd2c8}
.pvit[data-vth=nude]{--vf:#e8dac9;--vl:#d2bda4;--vb:#f2e2cf;--vp:#ecdfce;--vled:#ffe6bf}
.pvit[data-vth=slate]{--vf:#d9dde1;--vl:#bcc2c8;--vb:#eef2f6;--vp:#e0e5ea;--vled:#d9e9ff}
.pvit[data-vth=sage]{--vf:#dce3d7;--vl:#bfc9b8;--vb:#ebf1e7;--vp:#e1e8dc;--vled:#e4f2d6}
.pvit[data-vth=butter]{--vf:#f2e8c9;--vl:#dccf9f;--vb:#faf1d6;--vp:#f0e6c4;--vled:#fff0b8}
.pvit[data-vth=lavender]{--vf:#e4ddef;--vl:#c8bcdd;--vb:#f0eaf9;--vp:#e6dff1;--vled:#e9dcff}
.pvit[data-vth=peach]{--vf:#f6e0d2;--vl:#e3bda6;--vb:#fce9dc;--vp:#f4dfd0;--vled:#ffdfc2}
.pvit[data-vth=mist]{--vf:#dde5e4;--vl:#bccbc9;--vb:#ecf3f2;--vp:#e2eae9;--vled:#d8f2ee}
/* realistic niches: lit back panel + soft bevel + strip light; panels: lacquered solids */
.vc-n{border-radius:var(--radius);overflow:hidden;
  background:radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,.85), rgba(255,255,255,.25) 46%, transparent 78%), var(--vb);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.8), inset 0 0 0 1px rgba(120,110,90,.18), inset 0 -2px 3px -1px rgba(90,80,60,.14)}
.vc-led::before{content:"";position:absolute;z-index:1;left:6%;right:6%;top:2px;height:4px;border-radius:3px;
  background:var(--vled);box-shadow:0 2px 12px 2px var(--vled);pointer-events:none}
.vc-p{border-radius:var(--radius);
  background:repeating-linear-gradient(90deg, rgba(255,255,255,.28) 0 1px, transparent 1px 3px),
    linear-gradient(160deg, color-mix(in srgb, var(--vp) 84%, #fff), var(--vp) 62%, color-mix(in srgb, var(--vp) 82%, #000));
  box-shadow:0 7px 15px rgba(60,50,35,.30), inset 0 1.5px 0 rgba(255,255,255,.9), inset 0 -3px 6px -2px rgba(90,78,58,.20)}
.vc-arch{border-radius:48% 48% 10px 10px / 30% 30% 10px 10px}
.vc-hit{position:absolute;inset:0;z-index:2;display:grid;place-items:center;touch-action:pan-y;
  -webkit-tap-highlight-color:transparent;text-decoration:none} /* pan-y: a filled cubby must not trap page scroll (drags arm via press-and-hold) */
.vc-plush{max-width:84%;max-height:80%;pointer-events:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;
  filter:drop-shadow(0 4px 5px rgba(60,50,35,.35))}
.vc-plush.vc-emoji{font-size:46px;line-height:1}
.vc-n.over{outline:3px solid var(--gold,#e8b84b);outline-offset:-3px;z-index:3}
@media(hover:hover) and (prefers-reduced-motion:no-preference){
  .vc-hit:hover .vc-plush{transform:translateY(-3px)}
}
/* --- crossing: toon wooden cubbies (toon mode owns the style; realism layers off).
   --tf* frame wood · --tc* cubby interior · --tpa/b/c the candy-lined alt cubbies --- */
.pvit.toon{--tf1:#8a5a33;--tf2:#7c4f2e;--tf3:#6b4224;--tc1:#bd8358;--tc2:#9c6539;--tc3:#7a4a26;--tglow:rgba(255,220,170,.28);
  --tpa:#ffe9f2;--tpb:#ffd9e8;--tpc:#f0b8cd}
.pvit.toon[data-pal=sakura]{--tf1:#d98a9c;--tf2:#c4738a;--tf3:#a95c74;--tc1:#f4bcc8;--tc2:#e394a8;--tc3:#cd7892;--tglow:rgba(255,235,240,.35);
  --tpa:#fff9ef;--tpb:#fdeed6;--tpc:#ecd3ae}
.pvit.toon[data-pal=ocean]{--tf1:#6a94b8;--tf2:#527a9e;--tf3:#40648a;--tc1:#a8cbe0;--tc2:#82aacb;--tc3:#6490b4;--tglow:rgba(225,244,255,.35);
  --tpa:#f2fbf8;--tpb:#daf3ea;--tpc:#b2dccf}
.pvit.toon[data-pal=matcha]{--tf1:#8aa860;--tf2:#75904c;--tf3:#5f7a3c;--tc1:#bcd698;--tc2:#a3c17c;--tc3:#87a960;--tglow:rgba(244,255,225,.35)}
.pvit.toon[data-pal=honey]{--tf1:#c99a4a;--tf2:#b38436;--tf3:#96692a;--tc1:#f0cd8a;--tc2:#e0b268;--tc3:#c99a4e;--tglow:rgba(255,240,205,.4);
  --tpa:#fffdf6;--tpb:#fdf3dd;--tpc:#f0dcb0}
.pvit.toon[data-pal=berry]{--tf1:#9b7bb8;--tf2:#8a68a8;--tf3:#755590;--tc1:#cbb3e0;--tc2:#b295cf;--tc3:#9678b4;--tglow:rgba(240,230,255,.35);
  --tpa:#fdeff6;--tpb:#f8d9ea;--tpc:#e5b4cf}
.pvit.toon[data-pal=vanilla]{--tf1:#cfbfa8;--tf2:#bfae94;--tf3:#a8957c;--tc1:#f2e7d4;--tc2:#e4d4bb;--tc3:#cbb99d;--tglow:rgba(255,250,235,.45)}
.pvit.toon[data-pal=night]{--tf1:#4a5578;--tf2:#3d4766;--tf3:#303852;--tc1:#7482a8;--tc2:#5d6b92;--tc3:#49577e;--tglow:rgba(200,215,255,.25);
  --tpa:#e8e6f8;--tpb:#d5d2ef;--tpc:#b0abd8}
.pvit.toon .pvgrid{border:3px solid #4a2f17;border-radius:var(--radius);
  background:
    radial-gradient(130% 90% at 24% 0%, var(--tglow), transparent 55%),
    repeating-linear-gradient(0deg, rgba(58,32,14,.16) 0 2px, transparent 2px 30px),
    repeating-linear-gradient(90deg, rgba(58,32,14,.10) 0 2px, transparent 2px 90px),
    linear-gradient(180deg,var(--tf1) 0%,var(--tf2) 55%,var(--tf3) 100%);
  box-shadow:inset 0 4px 0 rgba(255,220,170,.30), inset 0 -10px 0 rgba(50,28,12,.35),
             0 10px 0 rgba(40,24,12,.35), 0 24px 44px rgba(64,52,43,.4)}
.pvit.toon .vc-n{border:3px solid #4a2f17;border-radius:var(--radius);
  background:
    radial-gradient(75% 55% at 50% 20%, rgba(255,224,178,.30), transparent 70%),
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='90' height='90' viewBox='0 0 90 90'><circle cx='16' cy='20' r='1.3' fill='%23e8c29a' opacity='.5'/><circle cx='70' cy='12' r='1.1' fill='%23e8c29a' opacity='.4'/><circle cx='44' cy='72' r='1.2' fill='%23e8c29a' opacity='.45'/><path d='M64 44 l1.6 3.3 3.3 1.6 -3.3 1.6 -1.6 3.3 -1.6-3.3 -3.3-1.6 3.3-1.6 z' fill='%23eec9a0' opacity='.5'/></svg>") 0 0/90px 90px,
    radial-gradient(120% 105% at 50% 26%, var(--tc1) 0%, var(--tc2) 52%, var(--tc3) 100%);
  box-shadow:inset 0 6px 12px rgba(50,28,12,.42), inset 0 -3px 0 rgba(255,214,164,.14),
             0 5px 0 rgba(50,28,12,.5), 0 12px 18px rgba(30,17,8,.35)}
.pvit.toon .vc:nth-child(3n+1) .vc-n::after{content:"";position:absolute;z-index:1;right:6px;top:6px;width:20px;height:20px;pointer-events:none;
  background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 1.5 L14.6 9.4 L22.5 12 L14.6 14.6 L12 22.5 L9.4 14.6 L1.5 12 L9.4 9.4 Z' fill='%23ffd76a' stroke='%234a2f17' stroke-width='1.7' stroke-linejoin='round'/></svg>") center/contain no-repeat;
  filter:drop-shadow(0 2px 0 rgba(50,28,12,.4))}
.pvit.toon .vc-led::before{display:none}
.pvit.toon .vc-p{border:3px solid #4a2f17;border-radius:var(--radius);
  background:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='46' height='46' viewBox='0 0 46 46'><circle cx='9' cy='11' r='1.4' fill='%23ffffff' opacity='.7'/><circle cx='37' cy='7' r='1.2' fill='%23ffffff' opacity='.7'/><circle cx='24' cy='38' r='1.3' fill='%23ffffff' opacity='.7'/><path d='M31 24 l1.8 3.7 3.7 1.8 -3.7 1.8 -1.8 3.7 -1.8-3.7 -3.7-1.8 3.7-1.8 z' fill='%23ffffff' opacity='.55'/></svg>") 0 0/46px 46px,
    linear-gradient(180deg,var(--tpa,#ffe9f2),var(--tpb,#ffd9e8) 55%,var(--tpc,#f0b8cd));
  box-shadow:inset 0 4px 0 rgba(255,255,255,.55), inset 0 -6px 0 rgba(74,47,23,.14),
             0 5px 0 rgba(50,28,12,.5), 0 12px 16px rgba(30,17,8,.32)}
/* candy-lined ALT cubbies: the old pink panels, but open, a real box a plushie can sit in */
.pvit.toon .vc-n.vc-alt{
  background:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='46' height='46' viewBox='0 0 46 46'><circle cx='9' cy='11' r='1.4' fill='%23ffffff' opacity='.7'/><circle cx='37' cy='7' r='1.2' fill='%23ffffff' opacity='.7'/><circle cx='24' cy='38' r='1.3' fill='%23ffffff' opacity='.7'/><path d='M31 24 l1.8 3.7 3.7 1.8 -3.7 1.8 -1.8 3.7 -1.8-3.7 -3.7-1.8 3.7-1.8 z' fill='%23ffffff' opacity='.55'/></svg>") 0 0/46px 46px,
    radial-gradient(120% 105% at 50% 26%, var(--tpa,#ffe9f2) 0%, var(--tpb,#ffd9e8) 52%, var(--tpc,#f0b8cd) 100%);
  box-shadow:inset 0 6px 12px rgba(50,28,12,.28), inset 0 -3px 0 rgba(255,255,255,.30),
             0 5px 0 rgba(50,28,12,.5), 0 12px 18px rgba(30,17,8,.35)}
/* realism themes: alt cubbies get a soft tinted lining (same panel colour, still usable) */
.vc-n.vc-alt{background:radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,.85), rgba(255,255,255,.25) 46%, transparent 78%), color-mix(in srgb, var(--vp,#e3ded3) 72%, #fff)}
.pvit.toon .vc-arch{border-radius:46% 46% 16px 16px / 26% 26% 16px 16px}
.pvit.toon .vc-plush{filter:drop-shadow(0 3px 0 rgba(50,28,12,.35))}
.pvit.toon .vc-n.over{outline-color:#ffd76a}
@media(hover:hover) and (prefers-reduced-motion:no-preference){
  .pvit.toon .vc-hit:hover .vc-plush{transform:rotate(-3deg)}
}
/* --- owner strip (unplaced plushies) + drag ghost + style bar --- */
.pvstrip{display:flex;align-items:center;gap:10px;margin-top:10px;padding:8px 10px;border-radius:var(--radius);
  background:rgba(120,110,90,.10);box-shadow:inset 0 0 0 1px rgba(120,110,90,.18);
  overflow-x:auto;-webkit-overflow-scrolling:touch}
.pvstrip.over{box-shadow:inset 0 0 0 2px var(--gold,#e8b84b)}
.pvs-hint{flex:0 0 auto;color:#8f857a;font:500 11px system-ui}
.pvs-item{flex:0 0 auto;width:52px;height:52px;display:grid;place-items:center;border-radius:var(--radius);
  background:var(--card);box-shadow:0 2px 5px rgba(64,52,43,.18), inset 0 0 0 1px rgba(120,110,90,.16);
  touch-action:pan-x;-webkit-tap-highlight-color:transparent} /* pan-x: the strip must stay swipe-scrollable on touch */
.pvs-item img{max-width:86%;max-height:86%;pointer-events:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.pvs-emoji{font-size:30px;line-height:1}
/* storage friends ride along in the strip, solid border + 🧺 badge says "not out yet";
   dragging one into a box places AND displays them */
.pvs-item{position:relative}
.pvs-item.pvs-stored{background:#fbf6ec;box-shadow:0 2px 5px rgba(64,52,43,.18), inset 0 0 0 2px rgba(64,52,43,.24)}
.pvs-tag{position:absolute;right:-4px;bottom:-4px;font-size:14px;line-height:1;filter:drop-shadow(0 1px 1px rgba(64,52,43,.3))}
.pvghost{position:fixed;z-index:100;width:72px;height:72px;object-fit:contain;pointer-events:none;
  transform:translate(-50%,-50%);filter:drop-shadow(0 10px 14px rgba(0,0,0,.45))}
.pvbar{margin:0 0 8px}
.pvbar>summary{display:inline-block;list-style:none;cursor:pointer}
.pvbar>summary::-webkit-details-marker{display:none}
.pvbar-body{display:grid;grid-template-columns:auto 1fr;gap:6px 10px;align-items:center;max-width:340px;
  margin-top:8px;padding:10px 12px;border-radius:var(--radius);background:rgba(120,110,90,.10);
  box-shadow:inset 0 0 0 1px rgba(120,110,90,.18)}
.pvbar-body .lbl{margin:0}
.pvbar-body select{width:100%;padding:6px 8px;border-radius:8px;border:1px solid rgba(120,110,90,.3);background:var(--card);font:600 12px system-ui}
.pv-palwrap{display:contents}
.pv-palwrap[hidden]{display:none} /* display:contents defeats the hidden attribute, restore it */

/* ===== Shelf view: plushies on cushions under glass cases, standing on wooden boards.
   PERSPECTIVE CONTRACT: --sv-squash is the foreshortening of EVERY horizontal surface here
   (board, cushion top, glass lid), so they can never disagree about the camera angle.
   LIGHT CONTRACT: one direction drives every highlight/shadow, baked from angle 278 /
   height 16, a low raking light from the left. Unlike the dark mock-up, this sits on the
   app's cream paper, so the glass reads through DARK edges rather than bright speculars. */
.shelfrows{--sv-squash:.085;--sv-taper:7%;
  --sv-sheen:98deg;--sv-lit-t:.139;--sv-lit-b:0;--sv-lit-l:.99;--sv-lit-r:0;--sv-elev:.178;--sv-li:1;
  --sv-sx:22.51;--sv-sy:3.16;
  /* 78 not 30: cdepth*squash was 2.55px of lid on a ~130px-wide case whose board claims
     188px of depth — too thin to read as a top face at all. 78*.085 = 6.6px. */
  --sv-cdepth:78px;--sv-gh:118px;--sv-pfh:17px;--sv-cush-top:8px;--sv-sit:13px;--sv-cushion:#efdcc2;
  --sv-row:182px;
  /* stated the way the demo states them: a board --sv-depth deep renders depth*squash
     tall, and --sv-floor is how much of it stays in FRONT of the cases */
  --sv-depth:188px;--sv-floor:3px;
  --sv-board:calc(var(--sv-depth) * var(--sv-squash));   /* 188 * .085 = 16px */
  --sv-seat:calc(var(--sv-board) - var(--sv-floor));     /* 16 - 3 = 13px */
  --sv-base:12px;    /* cushion bottom above the card's bottom edge */
  --sv-pad:6px;      /* the container's own padding-top, which the boards must clear */
  /* positioned: the ::before shelf-light layer below is position:absolute, and with no
     positioned ancestor it resolved against the VIEWPORT and tiled warm bands down the
     whole page instead of sitting under the boards. */
  position:relative;
  /* 132 not 138: two tracks + the 18px gap must fit 288px (a 360px phone minus the app,
     cabinet and grid padding), or the shelf drops to a single letterboxed column on every
     Galaxy-class handset. 138 needed 294. */
  display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:0 18px;
  /* bottom padding + border-box clip: the board tile's lip shade runs 32px below the band
     top but the last tile only has 19px left, so the bottom plank's shadow was severed on
     a hard line. The padding gives it somewhere to fade out. */
  padding:6px 4px 14px;
  /* the boards ARE the background: one band per --sv-row, pitched so every cushion lands on
     one by construction. --sv-seat pulls the band up so cases sit onto the surface. */
  /* four stops: surface back -> surface front -> edge top -> edge bottom, PLUS a bright
     back-edge catchlight and a rounded-lip shade under the front edge so the board reads
     as thick timber, not a stripe. A material class swaps colours; geometry never changes. */
  background-image:linear-gradient(to bottom,
    rgba(255,255,255,.30) 0,rgba(255,255,255,0) 2px,
    transparent calc(var(--sv-board) + 3px),rgba(255,255,255,.14) calc(var(--sv-board) + 3px),
    rgba(255,255,255,0) calc(var(--sv-board) + 5px),transparent calc(var(--sv-board) + 5px)),
    linear-gradient(to bottom,
    var(--sv-w1) 0,var(--sv-w2) var(--sv-board),
    var(--sv-w3) var(--sv-board),var(--sv-w4) calc(var(--sv-board) + 4px),
    var(--sv-wsh) calc(var(--sv-board) + 4px),transparent calc(var(--sv-board) + 16px));
  background-size:100% var(--sv-row),100% var(--sv-row);
  /* band top = where a cushion sits, pulled up by --sv-seat so the case stands ON the
     surface with --sv-seat of board behind it and the rest in front */
  background-position:0 calc(var(--sv-pad) + var(--sv-row) - var(--sv-base) - var(--sv-seat));
  background-clip:border-box}
/* ---- the cabinet ----
   Rather than darkening the whole app, the shelf sits in its own recessed case. That is
   what a display cabinet is anyway, and it gives the glass a dark ground to read against
   (white rims on cream paper are nearly invisible; on night they carry the whole effect). */
/* bottom padding clears the LAST board's front edge + its lip shadow, or the bottom shelf
   gets sliced off by the cabinet's own rounded corner */
.shelfroom{border-radius:var(--radius);padding:12px 12px 18px;transition:background .3s}
.shelfroom.night{--sv-grgb:255,255,255;--sv-gb:.10;--sv-gs:.5;--sv-gtop:.55;--sv-name:#f4ebdf;
  --sv-brgb:255,238,214;--sv-bb:.13;   /* back wall: a warm lit panel behind each plushie */
  /* dark-walnut cabinet interior: warm top-lit gradient with a corner vignette, not a
     flat black rectangle */
  background:
    radial-gradient(140% 100% at 50% 108%,transparent 55%,rgba(0,0,0,.4) 100%),
    radial-gradient(120% 90% at 50% 0,#2b2119 0,#191310 62%,#120e0b 100%);
  box-shadow:inset 0 3px 22px rgba(0,0,0,.55),inset 0 -14px 30px rgba(0,0,0,.35),0 12px 26px rgba(64,52,43,.2)}
/* ---- PAPER (light) ----
   Paper used to be night with the colours inverted: transparent cabinet, white sheens on
   cream, edge gain dropped to .2. That loses BOTH of night's signals at once — a white
   specular is invisible on cream, and a .09-.29 alpha hairline is not an object. The cases
   read as flat grey cards.
   A light ground needs the OTHER half of how acrylic actually looks: a faint cool tint in
   the body, visible wall thickness at the edges, and a cast shadow. Those need something to
   sit on, so paper gets a real cabinet too — a warm off-white recess, not nothing. */
.shelfroom.paper{--sv-grgb:64,52,43;--sv-gb:.16;--sv-gs:.5;--sv-gtop:.9;--sv-name:var(--ink);
  /* interior LIGHTER than the cabinet: a lit vitrine. Same trick as night's warm panel,
     opposite polarity, and it is what gives each case a defined field to read against. */
  --sv-brgb:255,253,246;--sv-bb:.66;
  background:
    radial-gradient(150% 100% at 50% 108%,transparent 58%,rgba(120,96,64,.14) 100%),
    linear-gradient(180deg,#fbf4e8 0,#f2e7d6 58%,#ebdcc7 100%);
  box-shadow:inset 0 3px 14px rgba(120,96,64,.17),inset 0 -10px 22px rgba(120,96,64,.10),
    0 10px 24px rgba(64,52,43,.10)}
/* NIGHT ONLY: a warm bloom spilling from under each board's front edge, the strip light a
   real cabinet has. Its own layer so the board gradient stack above stays untouched, and
   it repeats on the same --sv-row pitch, so it lands under every shelf by construction. */
.shelfroom.night .shelfrows::before{content:"";position:absolute;inset:0;pointer-events:none;z-index:0;
  background-image:linear-gradient(to bottom,
    transparent calc(var(--sv-board) + 4px),
    rgba(255,226,178,.20) calc(var(--sv-board) + 6px),
    rgba(255,226,178,0) calc(var(--sv-board) + 32px));
  background-size:100% var(--sv-row);
  background-position:0 calc(var(--sv-pad) + var(--sv-row) - var(--sv-base) - var(--sv-seat))}

/* shelf materials, the default is walnut, in the family of the app's .wt-walnut wall */
.shelfrows,.shelfrows.sm-walnut{--sv-w1:#6f4a2c;--sv-w2:#9a6b42;--sv-w3:#5b3a20;--sv-w4:#4a2e18;--sv-wsh:rgba(35,22,10,.45)}
.shelfrows.sm-oak{--sv-w1:#a97a45;--sv-w2:#d2a469;--sv-w3:#875d2f;--sv-w4:#6d4a25;--sv-wsh:rgba(60,38,14,.4)}
.shelfrows.sm-birch{--sv-w1:#cbb392;--sv-w2:#eaddc2;--sv-w3:#ad9370;--sv-w4:#8f7757;--sv-wsh:rgba(80,60,32,.32)}
.shelfrows.sm-charcoal{--sv-w1:#35353a;--sv-w2:#55555c;--sv-w3:#26262a;--sv-w4:#1a1a1e;--sv-wsh:rgba(0,0,0,.5)}
.shelfrows.sm-marble{--sv-w1:#cfcbc4;--sv-w2:#f1efeb;--sv-w3:#aba69e;--sv-w4:#918c84;--sv-wsh:rgba(60,52,43,.32)}
/* Charcoal is the one material tuned for the wrong ground: on night its #1a1a1e front edge
   is 3 channels off the #191310 cabinet and its shadow is black on near-black, so the plank
   loses all thickness and floats as a grey stripe. Warm the neutral and make the edge
   LIGHTER than the cabinet, so thickness reads without a shadow that cannot show. */
.shelfroom.night .shelfrows.sm-charcoal{--sv-w1:#3c3934;--sv-w2:#5e5a53;--sv-w3:#44403a;--sv-w4:#34302b;
  --sv-wsh:rgba(0,0,0,.42)}
.shelfmats{display:flex;gap:6px;align-items:center}
/* Shelf appearance disclosure. Above 560px it ISN'T one: the trigger is hidden and the body
   sits inline, so the desktop toolbar is exactly what it always was. Below 560px it folds,
   which is what buys back a row of phone chrome before the first shelf board.
   Deliberately not a <details>: Chrome hides details content via ::details-content, which
   a display:flex on the body cannot override, so desktop lost its chips entirely. */
.scstyle{flex:none;display:contents}
.scstyle-t{display:none}
.scstyle-body{display:flex;align-items:center;gap:8px}
@media(max-width:560px){
  .scstyle{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:none}
  .scstyle-t{display:inline-flex;align-items:center;gap:6px;cursor:pointer;border:0;
    font-family:var(--body);font-weight:800;font-size:.85rem;color:var(--ink);
    padding:7px 12px;min-height:38px;border-radius:var(--radius);
    background:var(--paper);box-shadow:inset 0 0 0 1px rgba(64,52,43,.14);-webkit-tap-highlight-color:transparent}
  .scstyle-t::after{content:"▾";font-size:.7em;opacity:.7}
  .scstyle-t[aria-expanded=true]{background:var(--muted);color:var(--coral-d);box-shadow:inset 0 0 0 1px var(--coral)}
  .scstyle-t[aria-expanded=true]::after{content:"▴"}
  .scstyle-body{display:none}
  .scstyle.open{flex:1 1 100%}
  .scstyle.open .scstyle-body{display:flex;flex:1 1 100%;flex-wrap:wrap;padding-top:2px}
}
.smchip{position:relative;width:24px;height:24px;padding:0;border-radius:50%;border:2px solid var(--border-strong);cursor:pointer;
  background:linear-gradient(160deg,var(--c2) 0 52%,var(--c3) 52% 100%)}
/* the view's primary customization control at 24px on a 30px pitch: grow the target, not
   the dot (the 6px .shelfmats gap means the pad can only go ±3px sideways) */
.smchip::after{content:"";position:absolute;inset:-10px -3px;border-radius:inherit}
.smchip.on{border-color:var(--coral);box-shadow:0 0 0 2px var(--paper),0 0 0 4px var(--coral)}
.smchip.sm-walnut{--c2:#9a6b42;--c3:#5b3a20}
.smchip.sm-oak{--c2:#d2a469;--c3:#875d2f}
.smchip.sm-birch{--c2:#eaddc2;--c3:#ad9370}
.smchip.sm-charcoal{--c2:#55555c;--c3:#26262a}
.smchip.sm-marble{--c2:#f1efeb;--c3:#aba69e}

.shelfcard{position:relative;z-index:1;height:var(--sv-row);display:block;text-decoration:none;color:inherit;
  -webkit-user-drag:none}   /* z-index: stay above the shelf-light bloom layer */
/* The case's BACK WALL. Without it a plushie sits against the raw cabinet gradient and the
   dark ones dissolve into it; a plane behind them gives every silhouette something to read
   against. Lit from the same --sv-sheen direction as every other surface here. */
.sc-back{position:absolute;left:0;right:0;bottom:var(--sv-pfh);height:var(--sv-gh);z-index:0;
  border-radius:6px 6px 4px 4px;transition:background .3s;
  background:linear-gradient(var(--sv-sheen),
    rgba(var(--sv-brgb),var(--sv-bb)) 0,rgba(var(--sv-brgb),calc(var(--sv-bb) * .35)) 62%,rgba(var(--sv-brgb),0) 100%)}
/* absolutely placed, never flex-shrunk, the cushion's bottom edge is the contact point
   and must line up with the board band by construction */
.scase{position:absolute;left:50%;bottom:var(--sv-base);transform:translateX(-50%);
  /* max-width so a wide track (a near-empty shelf, or the last row) can't stretch a case
     into a squat letterbox — the glass height is fixed, so width has to be capped too */
  width:86%;max-width:150px;height:calc(var(--sv-pfh) + var(--sv-gh))}
/* soft square cushion: square footprint, domed squishy top the plush settles into */
.sc-face{position:absolute;left:0;right:0;bottom:0;height:var(--sv-pfh);z-index:4;overflow:hidden;
  border-radius:3px 3px 7px 7px;
  background:linear-gradient(180deg,var(--card),var(--sv-cushion)); /* matte-ivory pedestal, not flat cream */
  box-shadow:0 3px 7px rgba(64,52,43,.34)}
.sc-face::before{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(255,255,255,.5) 0,rgba(0,0,0,.05) 34%,rgba(0,0,0,.26) 100%)}
.sc-face::after{content:"";position:absolute;inset:0;
  background:linear-gradient(var(--sv-sheen),rgba(255,255,255,.55) 0,rgba(255,255,255,0) 72%);
  opacity:calc(var(--sv-li) * (.35 + .45 * var(--sv-elev)))}
.sc-top{position:absolute;left:0;right:0;bottom:var(--sv-pfh);z-index:1;height:var(--sv-cush-top);
  background:var(--sv-cushion);border-radius:42% 42% 3px 3px / 82% 82% 10% 10%;
  box-shadow:inset 0 2px 3px rgba(255,255,255,.6)}
.sc-dimple{position:absolute;left:50%;bottom:calc(var(--sv-sit) - 5px);z-index:1;width:46%;height:9px;
  transform:translateX(-50%);
  background:radial-gradient(ellipse at 50% 50%,rgba(64,52,43,.3),transparent 72%)}
/* --sv-scale is the plushie's REAL size (from their recorded size, see sizeScale), so a
   16" Squishmallow towers over a 4" keychain instead of both filling the case. 1 when
   nobody recorded a size, which is exactly the old fixed cap. */
.sc-plush{position:absolute;left:50%;bottom:var(--sv-sit);z-index:2;transform:translateX(-50%);
  /* 100/90 not 92/82: the case is 118px of glass and the old cap left a band of dead air
     over every plushie's head. The tallest still clears the lid (13px sit + 100 < 129). */
  max-height:calc(100px * var(--sv-scale,1));max-width:calc(90% * var(--sv-scale,1));
  width:auto;height:auto;object-fit:contain;
  filter:drop-shadow(calc(var(--sv-sx) * .22px) calc(var(--sv-sy) * .22px) 4px rgba(64,52,43,.4));
  transition:transform .3s var(--ease-out)}
/* a brown shadow is invisible on a near-black cabinet, so night gets its own, or every
   friend floats a few millimetres above their cushion */
.shelfroom.night .sc-plush{filter:drop-shadow(calc(var(--sv-sx) * .22px) calc(var(--sv-sy) * .22px) 5px rgba(0,0,0,.62))}
.sc-emoji{font-size:calc(64px * var(--sv-scale,1));line-height:1}
/* an uncut (whole) photo stands in the case / vitrine box as a little framed print */
.sc-plush.framed,.vc-plush.framed{border:5px solid var(--card);border-bottom-width:14px;border-radius:3px;
  background:var(--card);box-shadow:0 4px 9px rgba(64,52,43,.35),inset 0 0 0 1px rgba(64,52,43,.1)}
/* Inside a case the frame gets the light contract like everything else: a bright left rail,
   a shaded right one, and NO second shadow. (The old straight-down box-shadow was re-cast
   4.95px right by .sc-plush's drop-shadow filter, which applies to the element's own
   shadow too, and its brown is the exact colour the night cabinet swallows.) Scoped under
   .shelfrows so the vitrine's .vc-plush.framed, which has no --sv-sx/--sv-sy, is untouched. */
.shelfrows .sc-plush.framed{
  box-shadow:inset 3px 0 0 -1px rgba(255,255,255,.85),inset -3px 0 0 -1px rgba(64,52,43,.16),
    inset 0 0 0 1px rgba(64,52,43,.12)}
.shelfroom.paper .sc-plush.framed{box-shadow:inset 3px 0 0 -1px rgba(255,255,255,.9),
  inset -3px 0 0 -1px rgba(64,52,43,.2),inset 0 0 0 1px rgba(64,52,43,.16)}
/* the case. On cream paper glass reads as DARK edges plus a soft sheen, not white rims. */
.sc-glass{position:absolute;left:0;right:0;bottom:var(--sv-pfh);height:var(--sv-gh);z-index:3;
  transition:transform .45s cubic-bezier(.485,-.34,.635,1.33),opacity .45s}
.sc-front{position:absolute;inset:0;border-radius:6px 6px 4px 4px;
  /* layered acrylic (glass-case-gpt-ideas): the light-contract sheen, PLUS a narrow
     diagonal streak (position varies per case via --sv-strx) and a faint horizontal
     falloff, three gradients read as reflections on real material, one reads as tint */
  background:
    linear-gradient(105deg,transparent calc(var(--sv-strx,18%) - 12%),rgba(255,255,255,.16) var(--sv-strx,18%),transparent calc(var(--sv-strx,18%) + 10%)),
    linear-gradient(var(--sv-sheen),rgba(255,255,255,.4) 0,rgba(255,255,255,.26) 8%,rgba(255,255,255,.15) 17%,
    rgba(255,255,255,.08) 28%,rgba(255,255,255,.045) 42%,rgba(255,255,255,.03) 62%,
    rgba(255,255,255,.1) 82%,rgba(255,255,255,.26) 100%);
  /* NO backdrop blur: it fuzzed every plushie behind the glass, the sheen gradients
     alone read as acrylic while the friends inside stay pin-sharp */
  /* On PAPER the glass reads through dark edges; on NIGHT through bright speculars.
     --sv-grgb flips which, --sv-gb is the ambient edge and --sv-gs the lit-edge gain. */
  border:1px solid;
  border-top-color:rgba(var(--sv-grgb),calc(var(--sv-gb) + var(--sv-gs) * var(--sv-lit-t)));
  border-bottom-color:rgba(var(--sv-grgb),calc(var(--sv-gb) + var(--sv-gs) * .85 * var(--sv-lit-b)));
  border-left-color:rgba(var(--sv-grgb),calc(var(--sv-gb) + var(--sv-gs) * var(--sv-lit-l)));
  border-right-color:rgba(var(--sv-grgb),calc(var(--sv-gb) + var(--sv-gs) * var(--sv-lit-r)));
  /* thick-edge glow: lit side catches a soft inner wash, far side shades, 6–8mm walls */
  box-shadow:inset 0 1px 0 rgba(255,255,255,var(--sv-gtop)),
    inset 7px 0 16px rgba(255,255,255,calc(.05 * var(--sv-li))),
    inset -7px 0 16px rgba(0,0,0,.12),
    0 2px 6px rgba(35,22,10,.16);
  transition:border-color .3s,box-shadow .3s,background .3s}
/* PAPER: swap the white speculars (invisible on cream) for what acrylic actually shows
   against a light ground — a cool green-grey tint in the body, 3px of visible wall
   thickness at each edge, one bright window streak, and a cast shadow the light contract
   throws down-right. This is the rule that makes a case read as a box rather than a card. */
.shelfroom.paper .sc-front{
  background:
    linear-gradient(105deg,transparent calc(var(--sv-strx,18%) - 12%),rgba(255,255,255,.62) var(--sv-strx,18%),transparent calc(var(--sv-strx,18%) + 10%)),
    linear-gradient(var(--sv-sheen),rgba(206,222,224,.34) 0,rgba(206,222,224,.14) 26%,
    rgba(214,226,228,.07) 52%,rgba(255,255,255,.20) 78%,rgba(255,255,255,.44) 100%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,var(--sv-gtop)),
    inset 3px 0 0 rgba(64,52,43,.09),
    inset -3px 0 0 rgba(64,52,43,.14),
    inset 0 -3px 0 rgba(64,52,43,.11),
    calc(var(--sv-sx) * .16px) calc(var(--sv-sy) * .9px) 10px -2px rgba(64,52,43,.24),
    0 1px 2px rgba(64,52,43,.16)}
/* every case is subtly its own: the streak sits differently on each, the way no two
   acrylic boxes catch a room's light identically. Keyed to position, which is fine for a
   reflection — unlike case HEIGHT, which is a property of the box (see --sv-gh below). */
.shelfcard:nth-child(3n) .sc-front{--sv-strx:34%}
.shelfcard:nth-child(3n+1) .sc-front{--sv-strx:16%}
.shelfcard:nth-child(4n+2) .sc-front{--sv-strx:58%}
.sc-lid{position:absolute;left:0;right:0;bottom:100%;height:calc(var(--sv-cdepth) * var(--sv-squash));
  /* The lid is the case's TOP face, so it stays a lit surface on both grounds — it was
     invisible on paper only because the paper cabinet was transparent, i.e. the lid was
     white-on-cream. Now that paper has a warm cabinet it reads there too. Direction comes
     from --sv-sheen rather than a hard-coded 180deg, like every other surface here. */
  /* It read as a separate sheet laid on the box for two reasons, both geometry-then-tone:
     the trapezoid's bottom corners were square while .sc-front's top corners are rounded
     6px, so the lid overhung into little ears at each side; and .72 white against the
     front's .4 top edge made it a brighter MATERIAL, not a lit face of the same one.
     Bottom corners inset to meet the radius, and the tone now starts just above the
     front's own top stop so the two surfaces read continuous across the seam. */
  background:linear-gradient(var(--sv-sheen),rgba(255,255,255,.48),rgba(255,255,255,.2));
  /* The back edge was two hard vertices, so the case read as a folded card even once the
     tone matched. A polygon cannot curve, but at this height (cdepth * squash ≈ 7px) four
     extra points per corner are past the point where the eye can tell a fine chamfer from
     an arc — and they cost nothing. Radii track the front's 6px so the whole box is one
     rounded solid rather than a rounded front with a sharp hat. */
  clip-path:polygon(
    calc(var(--sv-taper) + 6px) 0,
    calc(100% - var(--sv-taper) - 6px) 0,
    calc(100% - var(--sv-taper) - 2.4px) 12%,
    calc(100% - var(--sv-taper) - 0.4px) 34%,
    calc(100% - var(--sv-taper) + 0.6px) 62%,
    calc(100% - 5px) 100%,
    5px 100%,
    calc(var(--sv-taper) - 0.6px) 62%,
    calc(var(--sv-taper) + 0.4px) 34%,
    calc(var(--sv-taper) + 2.4px) 12%);
  border-top:1px solid rgba(var(--sv-grgb),calc(var(--sv-gb) + .08))}
/* the label sits ON the cushion front, so it travels with the case when it moves.
   .09em of tracking costs ~15% of the width, which is what was ellipsing the long names,
   so a long name buys its letters back by giving up tracking and a little size. */
/* flex, not a plain nowrap box: the stars are appended last, so with a single ellipsised
   line they were always the FIRST thing clipped — the museum touch vanished on exactly the
   plushies a collector bothered to rate. Now the NAME ellipses and the stars always survive. */
.sc-plate{position:absolute;left:3%;right:3%;bottom:3px;z-index:5;
  display:flex;align-items:baseline;justify-content:center;gap:3px;
  font-size:.55rem;font-weight:800;letter-spacing:.07em;text-transform:uppercase;
  color:#6d4f30;pointer-events:none;transition:color .3s}
.sc-plate>.sc-nm{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sc-plate.long{font-size:.48rem;letter-spacing:.015em}
.sc-rar{flex:none;color:#c9962e;letter-spacing:.02em;text-shadow:0 1px 1px rgba(255,255,255,.5)}
/* museum spotlight: a warm pool from the shelf lighting above, night mode only, where
   there is darkness for it to read against */
.shelfroom.night .scase::before{content:"";position:absolute;left:-12%;right:-12%;top:-16px;height:74%;
  z-index:0;pointer-events:none;
  background:radial-gradient(62% 58% at 50% 0%,rgba(255,238,204,.15),transparent 72%)}

/* an empty pitch = a real, empty glass case with a + button waiting to be filled.
   :has(.sc-add) scopes every affordance to the OWNER — a visitor's copy of the same gap
   ships no controls and no script, so a pointer cursor and a lid that flies off just read
   as a plushie that failed to load. */
.shelfcard.empty:has(.sc-add){cursor:pointer}
.shelfcard.empty:not(:has(.sc-add)):hover .sc-glass{transform:none;opacity:.72}
.shelfcard.empty:not(:has(.sc-add)):hover .scase{transform:translateX(-50%)}
.shelfcard.empty .sc-glass{opacity:.72}
.sc-add{position:absolute;left:50%;top:42%;transform:translate(-50%,-50%);z-index:6;
  width:40px;height:40px;border-radius:50%;border:none;cursor:pointer;
  font:700 24px/1 system-ui;color:var(--sv-name);
  background:rgba(var(--sv-grgb),.15);box-shadow:inset 0 0 0 1px rgba(var(--sv-grgb),.3);
  transition:background .15s,transform .15s}
.sc-add:hover{background:rgba(var(--sv-grgb),.28);transform:translate(-50%,-50%) scale(1.08)}
/* remove-this-case button, always available on an empty case (no need for reorder mode) */
.sc-del{position:absolute;top:6px;right:9px;z-index:7;width:22px;height:22px;border-radius:50%;
  border:none;cursor:pointer;font:700 15px/1 system-ui;color:var(--sv-name);
  display:flex;align-items:center;justify-content:center;
  background:rgba(var(--sv-grgb),.16);box-shadow:inset 0 0 0 1px rgba(var(--sv-grgb),.3);
  transition:background .15s,color .15s,opacity .15s}
.sc-del:hover{background:var(--coral);color:#fff;box-shadow:none}
/* 22px is half the touch guidance, it is the ONLY remove path, it is destructive with no
   undo, and every other pixel of the same card opens the picker — so a near miss does the
   wrong thing. Grow the hit area without growing the dot (same trick as .wtchip). */
.sc-del::after{content:"";position:absolute;inset:-11px;border-radius:50%}
/* parked in the corner of a dark cabinet, a always-on × reads as "close the shelf". Hide
   it until the case is hovered — but ONLY where hover exists, or touch loses the control. */
@media(hover:hover){
  .sc-del{opacity:0}
  .shelfcard.empty:hover .sc-del,.sc-del:focus-visible{opacity:1}
}

/* pick-a-plushie sheet, opened from an empty case's + button */
/* z-index 130, not 60: .bnav is ALSO 60 and is emitted after <main>, so on the tie the
   bottom nav painted over the sheet's last row — taps landed on its + and navigated to
   /add. 130 clears .wtray (120) and stays under .pmodal (400). */
.shelfpick{position:fixed;inset:0;z-index:130;display:flex;align-items:flex-end;justify-content:center;
  background:rgba(20,15,10,.5)}
.shelfpick[hidden]{display:none}
.sp-sheet{background:var(--card);border-radius:var(--radius) 18px 0 0;width:min(560px,100%);max-height:76vh;
  overflow:auto;padding:16px 16px calc(26px + env(safe-area-inset-bottom));box-shadow:0 -8px 30px rgba(64,52,43,.28)}
.sp-head{display:flex;align-items:center;justify-content:space-between;font-weight:800;color:var(--ink);margin-bottom:12px}
.sp-head button{background:none;border:none;font-size:22px;line-height:1;cursor:pointer;color:var(--ink2)}
.sp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(94px,1fr));gap:10px}
.sp-item{display:flex;flex-direction:column;align-items:center;gap:6px;padding:9px 4px;
  border:1px solid var(--border-strong);border-radius:var(--radius);background:var(--paper);cursor:pointer;
  font-size:.72rem;font-weight:700;color:var(--ink)}
.sp-item img{width:58px;height:58px;object-fit:contain}
.sp-item span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sp-item:hover{border-color:var(--coral)}
.sp-empty{grid-column:1/-1;text-align:center;color:var(--ink2);padding:22px 8px;font-size:.9rem;line-height:1.5}
/* open the case on hover, same gesture as the mock-up, and the whole case leans in a
   touch (scale + deeper ground shadow) so hovering feels like picking it up */
.scase{transition:transform .3s var(--ease-out)}
/* :focus-visible gets the SAME opening as :hover — the wall already pairs them this way,
   and without it a keyboard user can never see into a case. */
.shelfcard:hover .scase,.shelfcard:focus-visible .scase{transform:translateX(-50%) scale(1.025)}
.shelfcard:hover .sc-face{box-shadow:0 6px 13px rgba(64,52,43,.42)}
.shelfcard:hover .sc-glass,.shelfcard:focus-visible .sc-glass{transform:translateY(-116%);opacity:0}
.shelfcard:hover .sc-plush,.shelfcard:focus-visible .sc-plush{transform:translateX(-50%) scale(1.05)}
.shelfcard:focus-visible{outline:none}
.shelfcard:focus-visible .scase{outline:3px solid var(--coral-d);outline-offset:5px;border-radius:var(--radius)}
/* touch has no hover, so the case never opened on a phone: nudge the glass on press so a
   tap still feels like lifting the lid before the plushie's card takes over */
@media(hover:none){
  .shelfcard:active .sc-glass{transform:translateY(-14%);opacity:.5}
  .shelfcard:active .scase{transform:translateX(-50%) scale(1.02)}
}
/* reorder mode cancels the lift, on hover AND focus — the lift itself is keyed to both, so
   a hover-only cancel left a keyboard-focused card flying its lid off mid-reorder */
.shelfrows.reorderable .shelfcard:hover .scase,
.shelfrows.reorderable .shelfcard:focus-visible .scase{transform:translateX(-50%)}
.shelfrows.reorderable .shelfcard:hover .sc-glass,
.shelfrows.reorderable .shelfcard:focus-visible .sc-glass{transform:none;opacity:1}
/* pan-y, not none: .reorderable is a persistent MODE, not a live gesture, so touch-action:
   none froze scrolling for as long as the user stayed in it — on a tall shelf even the Done
   button became unreachable. The drag suppresses panning itself once it actually arms
   (touchmove {passive:false} in shelfReorderScript), the way the wall has always done it. */
.shelfrows.reorderable{touch-action:pan-y}
.shelfrows.reorderable .shelfcard{cursor:grab;-webkit-user-select:none;user-select:none}
/* :not(.empty) — the card is the drag handle, but empty cases are never handles (pointerdown
   bails on them), and blanking their children made the × and + inert in reorder mode: three
   visible affordances, zero behaviour, and no way to remove a case without leaving the mode. */
.shelfrows.reorderable .shelfcard:not(.empty) *{pointer-events:none}
.shelfcard.dragging{opacity:.4;pointer-events:none}
/* the drop target a case will land in, so a touch drag says WHERE before you let go */
.shelfcard.slotmark::after{content:"";position:absolute;inset:8px 2px;border-radius:var(--radius);z-index:8;
  pointer-events:none;outline:2px dashed var(--coral-d);outline-offset:-2px;background:rgba(224,122,95,.10)}
@media(prefers-reduced-motion:reduce){
  .sc-glass,.sc-plush,.scase,.shelfroom,.sc-front,.sc-back,.sc-plate{transition:none}}
/* strap fabric colour presets (set --oc) */
.oc-white{--oc:#f2efe8}.oc-cream{--oc:#efe1c2}.oc-black{--oc:#4a4640}.oc-pink{--oc:#f4b8cc}
.oc-mint{--oc:#b7ddc6}.oc-sky{--oc:#aecbe6}.oc-coral{--oc:#f0a48a}
/* strap colour swatches, the chip IS the fabric colour (no dot-in-a-pill) */
.oc-sw{width:38px;height:38px;padding:0;border:0;border-radius:var(--radius-sm);--c:var(--oc,#efe1c2);background:var(--c);flex:none}
.oc-sw:hover{transform:scale(1.08);border:0;background:var(--c)}
.oc-sw.active,.oc-sw[aria-pressed=true]{border:0;background:var(--c)}
/* back-panel designs, each sets the vars the wall + swatch read (woods, colours, materials, botanical) */
.wt-birch{--panel1:var(--muted);--panel2:var(--muted);--groove:rgba(122,96,60,.30);--gap:#71583f}
.wt-oak{--panel1:#e9d2a9;--panel2:#dcbf92;--groove:rgba(92,62,30,.42);--gap:#4a2f16}
.wt-walnut{--panel1:#7c563a;--panel2:#684125;--groove:rgba(24,14,6,.55);--gap:#1d0e04}
.wt-honey{--panel1:#f1daa6;--panel2:#e7c67f;--groove:rgba(120,78,26,.42);--gap:#5c3d15}
.wt-cream{--panel1:#faf4e8;--panel2:#f1e8d5;--groove:rgba(64,52,43,.2);--gap:#8a7a64}
.wt-sage{--panel1:#dce6d2;--panel2:#cad9b9;--groove:rgba(70,92,52,.32);--gap:#4c5c38}
.wt-blush{--panel1:#f7dee2;--panel2:#efccd3;--groove:rgba(150,80,92,.28);--gap:#7e4a55}
.wt-sky{--panel1:#d7e7f3;--panel2:#c3ddef;--groove:rgba(52,88,120,.3);--gap:#3c5872}
.wt-lilac{--panel1:#e6ddf2;--panel2:#d8cbea;--groove:rgba(96,72,132,.3);--gap:#584672}
.wt-charcoal{--panel1:#48443f;--panel2:#3a3732;--groove:rgba(10,8,6,.55);--gap:#14110d}
.wt-cork{--panel1:#dcbc95;--panel2:#d0aa7c;--groove:rgba(110,74,38,.3);--gap:#5c3e20;--grain:radial-gradient(rgba(120,84,44,.22) 1px,transparent 1.6px) 0 0/8px 8px,radial-gradient(rgba(150,110,66,.15) 1px,transparent 1.6px) 4px 5px/11px 11px}
/* drag a pack along the pegs to reorder, same lift/wiggle mechanic as the icon grid */
.wall.reordering{touch-action:none}
.wall.reordering .wallpack{touch-action:none}
.wallpack.dragging{opacity:.22}
/* NO pack wiggle while .reordering: every pack animating at once for the duration of a
   phone drag is the "flashing" users feel. The ghost + hot-cell outline already say
   what's happening. Wiggle stays in .editing only, a deliberate, static mode. */
.wall-ghost{position:fixed;z-index:999;margin:0;padding-top:18px;pointer-events:none;transform:translate(-50%,-52%) scale(1.05);opacity:.97}
/* drop-to-storage bar: invisible until a wall drag is live, then slides up from the
   bottom edge, drop a pack on it to move that friend off the display */
.walldrop{position:fixed;left:10px;right:10px;bottom:calc(10px + env(safe-area-inset-bottom));transform:translateY(150%);z-index:220;
  display:flex;align-items:center;justify-content:center;min-height:64px;text-align:center;
  font-family:var(--body);font-weight:800;font-size:1.02rem;color:var(--ink);
  background:var(--card);border:3px dashed var(--line);border-radius:var(--radius);padding:10px 18px;
  box-shadow:0 10px 26px -8px rgba(64,52,43,.4);pointer-events:none;opacity:0;
  transition:transform .2s var(--ease-out),opacity .2s var(--ease-out),border-color .15s,background .15s}
body.walldragging .walldrop{transform:translateY(0);opacity:1;pointer-events:auto}
.walldrop.hot{border-color:var(--coral-d);background:var(--muted);color:var(--coral-d);transform:translateY(0) scale(1.02)}
@media(prefers-reduced-motion:reduce){.walldrop{transition:opacity .2s}}
@media(prefers-reduced-motion:reduce){.wallpack .packcard{transition:none}}
/* Wall / Grid view toggle */
.homehead{display:flex;align-items:center;justify-content:space-between;gap:10px 12px;flex-wrap:wrap;margin:0 0 10px}
/* the wall-design picker + Edit button dock into the header row (one row of chrome,
   not three); hidden in storage view via [data-homeview] */
.homehead .displaytop{margin:0;max-width:none;flex:none}
.homehead .displaytop .walldesign{margin:0}
.displaytop[hidden]{display:none} /* display:flex above would otherwise defeat the hidden attribute */
/* footer shortcuts: three equal pills on one row at every width */
.footlinks{display:flex;gap:10px;max-width:560px;margin:14px auto 16px}
.footlinks .btn{flex:1 1 0;white-space:nowrap;padding:12px 8px;font-size:.92rem;text-align:center}
/* mobile: slim the header controls so view toggle + wall design + Edit share one row */
@media(max-width:560px){
  .hvbtn{font-size:.8rem;padding:7px 11px;min-height:32px}
  .homehead .walldesign>summary{padding:7px 11px;font-size:.85rem;min-height:44px}
  /* glass-case controls (mat chips + Case/Night/Reorder) can't fit one row, wrap, don't overflow */
  .homehead .displaytop{flex:1 1 100%;flex-wrap:wrap;justify-content:flex-start}
  /* Decorate is the only thing left in here now that the pin is gone, and one small pill
     fits beside the two view tabs — so it rides the same row, hard right, instead of
     claiming a 44px row of its own. The wall's own toolbars (glass case controls) still
     wrap below on their own line via .displaytop above. */
  .homehead .homeactions{flex:0 0 auto;margin-left:auto;justify-content:flex-end;gap:8px 10px}
}
.homeactions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* Same component as the plushie page's .viewtoggle — one track, one filled pill, same
   colours and metrics. Two switchers that look different read as two different ideas. */
.homeview{display:inline-flex;gap:2px}
.hvbtn{border:0;background:transparent;font-family:var(--display);font-weight:600;font-size:.86rem;color:var(--ink2);
  padding:7px 14px;min-height:34px;border-radius:var(--radius-pill);cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:background .16s var(--ease-out),color .16s var(--ease-out),transform .12s var(--ease-out)}
.hvbtn:hover{background:var(--accent);color:var(--accent-foreground)}
/* --shadow-ctl is the theme's hard 4px offset. On a 34px fully-round pill its own pill
   shape pokes out past the corners and reads as a misprint, not a shadow — it's the only
   hard offset in the header. The soft surface shadow keeps the pill lifted without it. */
/* The selected view is a pressed control, so it wears the control shadow (hard offset), not
   --shadow-s — that soft one is for card surfaces and read as a smudge under a mint pill. */
.hvbtn.on,.hvbtn[aria-pressed=true]{background:var(--secondary);color:var(--secondary-foreground);box-shadow:var(--shadow-ctl)}
.hvbtn:active{transform:translateY(1px)}
.tiny{font-size:.82rem}
/* Display top row: the wall-design trigger */
.displaytop{display:flex;align-items:center;justify-content:space-between;gap:8px;max-width:900px;margin:0 auto}
.displaytop .walldesign{margin:0 0 8px}
.wallpack.wp-off{display:none} /* storage-only pack kept in the owner's wall DOM for instant show/hide */
/* ===== Storage: friends OFF display only (on-display cards hide, one home at a time) ===== */
/* every friend shows here, the tag badge says who's out on display vs resting */
.stgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;max-width:780px;margin:0 auto 8px}
@media(max-width:560px){.stgrid{grid-template-columns:repeat(2,1fr);gap:10px}}
.stcard{position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;padding:12px 10px 11px;border:0;cursor:pointer;
  background:linear-gradient(180deg,var(--card),var(--paper));border-radius:var(--radius);-webkit-tap-highlight-color:transparent;
  box-shadow:var(--shadow-s),inset 0 0 0 1px rgba(64,52,43,.06);transition:transform .15s var(--ease-out),box-shadow .15s var(--ease-out)}
.stcard{min-width:0}   /* or a long name widens the column instead of ellipsising */
.stcard:active{transform:translateY(1px)}
.stcard.sel{box-shadow:0 0 0 2.5px var(--coral-d),var(--shadow-m)}
.stcard-pic{position:relative;width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center;border-radius:var(--radius);overflow:hidden;
  background:radial-gradient(120% 100% at 50% 8%,rgba(255,255,255,.55),rgba(64,52,43,.05))}
/* Absolutely positioned, not a percentage-sized flex child: max-height:88% had to
   resolve against an aspect-ratio box, and a percentage height against a container
   whose own height is derived can resolve to 0 in a later layout pass — the photo
   loads, then collapses to nothing. Against the padding box of a positioned parent the
   inset is unambiguous, and object-fit keeps the same letterboxing as before. */
.stcard-pic img{position:absolute;inset:6%;width:88%;height:88%;object-fit:contain;filter:drop-shadow(0 4px 5px rgba(64,52,43,.22));
  /* press-and-hold on these cards means REARRANGE, so the photo must not be the thing
     under the finger — a phone hit-testing an <img> opens its own copy/download sheet
     over the drag. pointer-events:none hands the press to the card; the callout and
     select rules cover browsers that still hit-test through it. */
  pointer-events:none;-webkit-touch-callout:none;user-select:none;-webkit-user-select:none}
/* Until a photo has bytes — first paint, a revalidation, or the renderer dropping a decode
   on a phone — the frame shimmers instead of sitting empty, so a slow shelf reads as
   loading rather than broken. .imgok comes from the capturing load listener; a card with
   no photo at all shows its emoji, not a shimmer that never ends. */
.stcard-pic::before{content:"";position:absolute;inset:0;border-radius:inherit;
  background:linear-gradient(90deg,var(--muted) 25%,var(--card) 50%,var(--muted) 75%);
  background-size:200% 100%;animation:pbshimmer 1.4s ease-in-out infinite}
.stcard-pic.imgok::before,.stcard-pic:has(.stemoji)::before{content:none}
@keyframes pbshimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
@media(prefers-reduced-motion:reduce){.stcard-pic::before{animation:none}}
.wtray-item img,.scs-item img,.wallpack img{-webkit-touch-callout:none;user-select:none;-webkit-user-select:none}
.stemoji{font-size:54px;line-height:1}
.stcard-name{font-family:var(--body);font-weight:800;font-size:.92rem;color:var(--ink);max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stcard-tag{font-family:var(--body);font-weight:800;font-size:.66rem;padding:2px 10px;border-radius:var(--radius);
  background:rgba(64,52,43,.09);color:var(--ink2)}
.stcard.on .stcard-tag{background:#e3f4ea;color:#1f8a3b}
.stcard-tag{display:inline-flex;align-items:center;gap:5px}
.stcard-tag .bico{margin:0;width:1.1em;height:1.1em}
.stcard.on .stcard-tag .i-on{display:inline-block}
.stcard.on .stcard-tag .i-off{display:none}
.stcard-check{position:absolute;top:8px;right:8px;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  background:var(--primary);color:var(--on-coral);font-size:.78rem;font-weight:800;opacity:0;transform:scale(.5);transition:opacity .14s,transform .14s}
.stcard.sel .stcard-check{opacity:1;transform:none}
/* open → plush page (re-cut lives there) */
.stopen{position:absolute;top:7px;left:7px;z-index:2;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  background:var(--paper);color:var(--ink2);font-size:.9rem;font-weight:800;text-decoration:none;box-shadow:0 1px 3px rgba(64,52,43,.22);opacity:.8}
.stopen .bico{margin:0;width:15px;height:15px}
.stopen:hover{opacity:1;color:var(--coral-d)}
.stdel{position:absolute;top:7px;right:7px;z-index:2;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  border:0;cursor:pointer;background:var(--paper);font-size:.8rem;box-shadow:0 1px 3px rgba(64,52,43,.22);opacity:.8;-webkit-tap-highlight-color:transparent}
.stdel:hover{opacity:1;background:#ffe6df}
/* ── the "say goodbye" confirm, one cute card on a soft scrim, nothing scary ── */
/* shared cute popup, sticker-book card (slime-maker recipe) in PlushBase's readable font */
.byebox,.cutepop{position:fixed;inset:0;z-index:300;display:flex;align-items:center;justify-content:center;padding:20px;
  background:rgba(43,32,23,.4);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}
.byebox[hidden],.cutepop[hidden]{display:none}
.cutepop-scrim{position:absolute;inset:0}
/* long-press / right-click menu for the compact storage surfaces (wall tray, case strip).
   Fixed + z-index above the bottom nav (60) and the wall tray (120), below .pmodal (400). */
.stmenu{position:fixed;z-index:300;min-width:190px;padding:6px;border-radius:14px;
  background:var(--card);box-shadow:0 12px 30px -6px rgba(64,52,43,.34),inset 0 0 0 1px rgba(64,52,43,.12)}
.stmenu-h{padding:6px 10px 8px;font-weight:800;font-size:.82rem;color:var(--ink2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:230px}
.stmenu button{display:flex;align-items:center;gap:8px;width:100%;min-height:42px;padding:8px 10px;
  border:0;border-radius:10px;background:transparent;cursor:pointer;text-align:left;
  font-family:var(--body);font-weight:700;font-size:.9rem;color:var(--ink)}
.stmenu button:hover,.stmenu button:focus-visible{background:var(--paper);outline:none}
.stmenu button.stmenu-bad{color:var(--coral-d)}
.stmenu button.stmenu-bad:hover,.stmenu button.stmenu-bad:focus-visible{background:#fdeee9}
.bye-card,.cutepop-card{position:relative;width:min(340px,92vw);text-align:center;
  background:var(--card) radial-gradient(rgba(244,158,192,.12) 2.5px,transparent 2.5px) 0 0/22px 22px;
  border:3px solid var(--ink);border-radius:var(--radius-lg);padding:22px 22px 18px;
  box-shadow:6px 6px 0 var(--ink),0 18px 44px rgba(43,32,23,.3);animation:byepop .28s var(--ease-out)}
@keyframes byepop{0%{transform:scale(.88) translateY(8px);opacity:0}100%{transform:none;opacity:1}}
@media(prefers-reduced-motion:reduce){.bye-card,.cutepop-card{animation:none}}
.bye-card b,.cutepop-card b{font-family:var(--display);font-weight:600;font-size:1.2rem;color:var(--ink)}
.bye-pic,.cutepop-pic{display:flex;align-items:center;justify-content:center;width:84px;height:84px;margin:0 auto 10px;border-radius:50%;
  font-size:44px;overflow:hidden;background:#FFF8FB;border:2.5px solid var(--ink);box-shadow:2.5px 2.5px 0 rgba(90,74,94,.25)}
.bye-pic img,.cutepop-pic img{width:80%;height:80%;object-fit:contain}
/* ── desktop editor: hero on the left, all editor cards in a scrollable right panel ── */
@media(min-width:960px){
  .pedit-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:26px;align-items:start;max-width:1000px;margin:0 auto}
  .pedit-hero{position:sticky;top:14px}
  .pedit-panel{max-height:calc(100vh - 40px);overflow-y:auto;overscroll-behavior:contain;padding-right:6px;scrollbar-width:thin}
  .pedit-panel>.card,.pedit-panel>form{margin-bottom:14px}
}
/* the gentle "saved" toast, floats above the bottom nav, fades away on its own */
/* The toast was a small dark-brown pill from before this theme existed — easy to miss and
   the only thing on the page wearing that colour. It's a pinkyu control now: the card
   surface, the coral edge and the same hard offset every button has, at a size you notice
   without it blocking the thing you just changed. */
#pbtoast{position:fixed;left:50%;bottom:calc(88px + env(safe-area-inset-bottom));z-index:240;
  transform:translate(-50%,10px) scale(.96);opacity:0;pointer-events:none;
  display:flex;align-items:center;gap:8px;
  background:var(--card);color:var(--coral-d);font:800 .95rem var(--body);padding:12px 20px;
  border:2px solid var(--coral-d);border-radius:var(--radius-pill);
  box-shadow:var(--shadow-md);white-space:nowrap;
  transition:opacity .22s var(--ease-out),transform .22s var(--ease-out)}
#pbtoast .bico{margin:0;width:1.15em;height:1.15em;flex:none}
#pbtoast.show{opacity:1;transform:translate(-50%,0) scale(1)}
@media(prefers-reduced-motion:reduce){#pbtoast{transition:opacity .2s linear}#pbtoast.show{transform:translate(-50%,0)}}
@media(min-width:900px){#pbtoast{bottom:26px}}
/* ── Photo & cutout card on the plush page ── */
.photocard>summary{cursor:pointer;font-weight:800;color:var(--ink);list-style:none}
.photocard>summary::-webkit-details-marker{display:none}
.photocard>summary::after{content:" ›";opacity:.5}
.photorow{display:flex;gap:14px;justify-content:center;margin:12px 0 2px}
.photocol{margin:0;display:flex;flex-direction:column;align-items:center;gap:6px}
.photocol figcaption{font-family:var(--body);font-weight:700;font-size:.76rem;color:var(--ink2)}
.photothumb{width:132px;height:132px;object-fit:contain;border-radius:var(--radius);background:var(--card);box-shadow:inset 0 0 0 1px rgba(64,52,43,.08)}
.photocol .photocut{display:flex;align-items:center;justify-content:center;width:132px;height:132px;border-radius:var(--radius);
  background:repeating-conic-gradient(#f0ece4 0% 25%,#fff 0% 50%) 50%/18px 18px} /* checkerboard = transparency */
.photocol .photocut .photothumb{width:100%;height:100%;background:transparent;box-shadow:none}
.stbar{position:sticky;bottom:14px;z-index:40;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
  max-width:600px;margin:14px auto 0;padding:12px 16px;border-radius:var(--radius);background:var(--paper);
  box-shadow:0 10px 26px -8px rgba(64,52,43,.42),inset 0 0 0 1px rgba(64,52,43,.1)}
.stbar[hidden]{display:none}
/* on mobile the fixed bottom nav owns the bottom ~64px+safe-area, lift the sticky
   move bar above it so it's never hidden behind the nav */
@media(max-width:899px){.stbar{bottom:calc(76px + env(safe-area-inset-bottom))}}
.stbar-count{font-family:var(--body);font-weight:800;color:var(--ink)}
.stbar-acts{display:flex;gap:8px;flex-wrap:wrap}
/* Wall design docks as a panel and the wall REFLOWS beside it so edits stay visible.
   Desktop: right drawer; the left nav collapses and the main content shifts left so the
   wall sits fully in the space beside the panel. Mobile: a bottom sheet, wall above it. */
.walldesign[open] #wtpanel{position:fixed;z-index:220;background:var(--paper);
  overflow-y:auto;-webkit-overflow-scrolling:touch}
/* desktop right drawer + reflow */
@media(min-width:900px){
  .walldesign[open] #wtpanel{top:0;right:0;bottom:0;width:340px;
    border-left:1.5px solid rgba(64,52,43,.14);box-shadow:-14px 0 36px rgba(64,52,43,.28);
    padding:14px 16px 24px;animation:wtslide .22s var(--ease-out)}
  .appshell,.appmain{transition:grid-template-columns .24s var(--ease-out),padding .24s var(--ease-out)}
  .sidebar{transition:opacity .18s var(--ease-out)}
  body.wtopen .appshell{grid-template-columns:0 minmax(0,1fr)}
  body.wtopen .sidebar{opacity:0;pointer-events:none}
  body.wtopen .appmain{padding-right:372px}
}
/* mobile bottom sheet + reflow (wall sits in the space above the sheet) */
@media(max-width:899px){
  /* half-sheet: covers ~48% of the screen so the wall + banner stay visible above it and
     edits show live (Reddit-style bottom sheet). The panel scrolls internally. */
  .walldesign[open] #wtpanel{left:0;right:0;bottom:0;top:auto;height:48vh;max-height:48vh;
    border-top:1.5px solid rgba(64,52,43,.14);border-radius:var(--radius) 20px 0 0;
    box-shadow:0 -14px 36px rgba(64,52,43,.3);
    padding:8px 16px calc(20px + env(safe-area-inset-bottom));animation:wtsheet .24s var(--ease-out)}
  .walldesign[open] #wtpanel::before{content:"";display:block;width:38px;height:4px;margin:2px auto 8px;
    border-radius:var(--radius);background:rgba(64,52,43,.22)} /* grabber */
  /* lift the shelf just enough to clear the half-sheet, no giant empty gap above it */
  body.wtopen .wrap,body.wtopen .appmain{padding-bottom:50vh}
}
@keyframes wtslide{from{transform:translateX(40px);opacity:.4}to{transform:translateX(0);opacity:1}}
@keyframes wtsheet{from{transform:translateY(40px);opacity:.5}to{transform:translateY(0);opacity:1}}
@media(prefers-reduced-motion:reduce){.walldesign[open] #wtpanel{animation:none}.appshell,.appmain,.sidebar{transition:none}}
.wtpanel-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 10px;
  font-family:var(--body);font-size:1rem;color:var(--ink)}
#wtsaved{margin-left:auto;font-size:.72rem;white-space:nowrap}
/* chunk the long option list into three scannable groups: wall · hanging · plaque */
.wtsect{font-family:var(--display);font-weight:600;font-size:.95rem;color:var(--coral-d);
  margin:16px 0 6px;padding-top:12px;border-top:1px dashed var(--line)}
.wtnav+.wtsect{margin-top:2px;padding-top:0;border-top:0}
/* sticky section jumper, the drawer is long; one tap lands on Looks/Wall/Hang/Plaque */
/* banner title promoted to the very top of the panel for one-tap access */
.wtbanner-top{margin:0 0 8px}
/* The jump bar pins to the top of the scrolling panel. It has to be an opaque BAR, not a
   row of floating chips: the old fade-to-transparent background (and a 4px bleed against
   the panel's 16px padding) let the swatch grid scroll through the gaps between chips and
   past both edges, so the row read as debris on top of the options instead of a header. */
/* top is NEGATIVE by the panel's own top padding, with that much added back as padding:
   pinned at 0 the bar stopped short of the scrollport and options slid through the gap
   above it. Per breakpoint because the sheet and the drawer pad differently. */
.wtnav{position:sticky;top:-8px;z-index:8;display:flex;gap:6px;
  margin:0 -16px;padding:16px 16px 9px;
  background:var(--paper);border-bottom:1px solid var(--border);
  box-shadow:0 8px 10px -9px rgba(64,52,43,.5)}
@media(min-width:900px){.wtnav{top:-14px;padding-top:22px}}
.wtnav-b{flex:1 1 0;font-family:var(--body);font-weight:800;font-size:.78rem;padding:8px 4px;min-height:36px;
  border:1.5px solid var(--line);border-radius:var(--radius);background:var(--card);color:var(--ink);cursor:pointer;white-space:nowrap;
  -webkit-tap-highlight-color:transparent}
.wtnav-b{display:inline-flex;align-items:center;justify-content:center;gap:5px}
.wtnav-b .bico{margin:0;width:1.05em;height:1.05em}
.wtnav-b.on .bico{--icon-tint-alpha:.45}   /* selected: the tint carries the state, same as the nav tabs */
.wtnav-b:hover{border-color:var(--coral)}
.wtnav-b.on{background:var(--coral);border-color:var(--coral-d);color:#fff}
.wtnav-b:active{transform:translateY(1px)}
/* active Popular look wears a badge, like a chosen sticker */
.wpreset{position:relative}
.wpreset[aria-pressed=true]::after{content:"✓";content:"✓" / "";position:absolute;top:-7px;right:-7px;width:22px;height:22px;
  display:grid;place-items:center;font-size:.8rem;font-weight:800;color:#fff;background:var(--primary);
  border-radius:50%;box-shadow:0 1px 3px rgba(64,52,43,.4),0 0 0 2px var(--paper)}
/* trim chips carry a live mini-preview strip of the pattern on the current plaque colour */
.trimchip{display:inline-flex;align-items:center;gap:7px}
.trimprev{width:36px;height:15px;border-radius:4px;flex:none;border:1px solid rgba(67,41,27,.5);
  background:var(--trim,linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0))),linear-gradient(180deg,var(--sg1,#f6aebf),var(--sg2,#ee9ab0))}
.trimprev.wst-stitch{outline:1.5px dashed rgba(67,41,27,.55);outline-offset:-4px}
/* one-tap bundled looks: each card is a live mini board, wall gradient (wt-* vars),
   a plaque strip in the look's sign colour (wsc-* vars), its emoji, and a name */
/* 20 looks would push the rest of the drawer off-screen, same capped scroller the
   emoji grid uses, so Wall/Hang/Plaque stay one flick away */
.wpresets{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;max-height:310px;overflow-y:auto;
  padding:8px 8px 4px 2px;scrollbar-width:thin;overscroll-behavior:contain; /* top/right padding: the ✓ badge hangs -7px outside its tile */
  /* fade the last sliver of tiles so the cut row reads as "scroll for more", not breakage */
  -webkit-mask-image:linear-gradient(180deg,#000 calc(100% - 22px),transparent);
  mask-image:linear-gradient(180deg,#000 calc(100% - 22px),transparent)}
@media(max-width:390px){.wpresets{grid-template-columns:repeat(3,1fr)}}
.wpreset{min-width:0;border:2px solid rgba(64,52,43,.16);border-radius:var(--radius);background:var(--card);cursor:pointer;
  padding:4px 4px 6px;display:flex;flex-direction:column;gap:4px;-webkit-tap-highlight-color:transparent;
  transition:transform .15s var(--ease-out),border-color .15s var(--ease-out)}
.wpreset:hover{border-color:var(--coral);transform:translateY(-1px)}
.wpreset:active{transform:translateY(1px)}
.wpreset[aria-pressed=true]{border-color:var(--coral-d);box-shadow:0 0 0 2px rgba(224,122,95,.4)}
.wpr-board{position:relative;height:44px;border-radius:var(--radius);overflow:hidden;
  background:var(--grain,linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0))),linear-gradient(var(--panel1,var(--muted)),var(--panel2,var(--muted)));
  box-shadow:inset 0 -5px 7px -5px rgba(0,0,0,.25),inset 0 5px 7px -5px rgba(255,255,255,.35)}
.wpr-em{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:.9rem;line-height:1;
  letter-spacing:1px;filter:drop-shadow(0 1px 1px rgba(64,52,43,.3))}
/* two lines, so "🍄 Cottage Woods" reads as itself instead of "Cottag…", and
   overflow-wrap so a long single word ("Marshmallow") breaks instead of clipping mid-glyph */
.wpr-name{font-family:var(--body);font-weight:800;font-size:.66rem;line-height:1.2;text-align:center;color:var(--ink);
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:2.4em;overflow-wrap:anywhere}
@media(prefers-reduced-motion:reduce){.wpreset,.wpreset:hover{transition:none;transform:none}}
.wtclose{border:1.5px solid rgba(64,52,43,.2);background:var(--paper);color:var(--ink);width:38px;height:38px;
  border-radius:50%;font-size:1rem;font-weight:800;cursor:pointer;-webkit-tap-highlight-color:transparent}
.wtclose:active{transform:translateY(1px)}
/* inside the drawer the theme swatches wrap instead of side-scrolling */
.walldesign[open] .wtrow{flex-wrap:wrap;overflow-x:visible;padding-bottom:4px}
.walldesign[open] .wtslider{max-width:100%}
/* back-panel design picker */
.walldesign{max-width:900px;margin:0 auto 8px}
/* Decorate wears .btn.ghost.small like Pinned next to it — same pill, same 2px coral edge,
   same hard offset — so the two controls in that row read as one pair instead of a filled
   rectangle beside a pill. Only the bits a <summary> needs are set here; everything else
   (radius, shadow, hover lift, press) comes from .btn, so it can't drift again. */
.walldesign>summary.ghost{display:inline-flex;align-items:center;gap:6px;list-style:none;
  background:var(--muted);border-color:var(--coral)}   /* the resting look: filled, quiet */
/* the brush inherits the button's coral, so it reads as part of the label. The flex row
   already has a 6px gap, so the icon drops its label margin. */
.walldesign>summary .wd-pen{display:inline-flex;font-size:.95rem}
.walldesign>summary .bico,.wtpanel-head .bico{margin:0;width:1.15em;height:1.15em}
.wtpanel-head .bico{margin-right:6px}
.walldesign>summary::-webkit-details-marker{display:none}
.walldesign[open]>summary.ghost,.walldesign[open]>summary.ghost:hover{background:var(--primary);color:var(--on-coral);border-color:var(--coral-d)}
/* keep the Decorate button hugging the RIGHT of the header row (not full-width below) */
.homehead .displaytop[data-homeview=wall]{flex:0 0 auto;margin-left:auto}
.walldesign[open]>summary{margin-bottom:10px}
.wtrow{display:flex;gap:10px;overflow-x:auto;padding:4px 2px 10px;scrollbar-width:thin;-webkit-overflow-scrolling:touch}
.wtsw{flex:0 0 auto;width:76px;height:58px;border-radius:var(--radius);border:2px solid rgba(64,52,43,.16);cursor:pointer;position:relative;overflow:hidden;padding:0;
  display:flex;align-items:flex-end;justify-content:center;-webkit-tap-highlight-color:transparent;
  background:var(--grain,linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0))),linear-gradient(var(--panel1,var(--muted)),var(--panel2,var(--muted)));
  box-shadow:inset 0 -6px 8px -6px rgba(0,0,0,.25),inset 0 6px 8px -6px rgba(255,255,255,.4)}
.wtsw[aria-pressed=true]{border-color:var(--coral-d)}
.wtsw:active{transform:translateY(1px)}
/* panel-size / spacing / hook option rows under the swatches */
.wtopt{display:flex;align-items:center;gap:10px;margin:6px 0;flex-wrap:wrap}
.wtoptlbl{font-family:var(--body);font-weight:800;font-size:.78rem;color:var(--ink);width:62px;flex-shrink:0}
.wtchips{display:flex;gap:6px;flex-wrap:wrap}
.wtchip{border:1.5px solid rgba(64,52,43,.18);background:var(--paper);color:var(--ink);border-radius:var(--radius-pill);
  font-family:var(--body);font-weight:700;font-size:.78rem;padding:7px 12px;min-height:34px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;transition:background .15s var(--ease-out),border-color .15s var(--ease-out)}
.wtchip[aria-pressed=true]{background:var(--coral);border-color:var(--coral-d);color:#fff}
/* Hook-finish chips carry a real metal dot, painted from the same colours as the peg it
   selects — they used to carry ⚪⚫🟡🪸, which drew a ring-inside-a-ring on the pale ones
   and, in coral's case, wasn't a metal at all. */
.hkchip{display:inline-flex;align-items:center;gap:7px}
.hkdot{width:14px;height:14px;border-radius:50%;flex:none;
  background:linear-gradient(135deg,var(--m1),var(--m2) 55%,var(--m3));
  box-shadow:inset 0 0 0 .9px var(--mk),0 1px 1.5px rgba(64,52,43,.25)}
.hkd-silver{--m1:#dfe3e9;--m2:#b6bcc6;--m3:#979ea9;--mk:#6b7280}
.hkd-noir{--m1:#555962;--m2:#33363d;--m3:#20222a;--mk:#0c0d10}
.hkd-brass{--m1:#f7e6b0;--m2:#d9b45e;--m3:#b08c34;--mk:#705a14}
.hkd-gold{--m1:#fff4c2;--m2:#f2cf52;--m3:#cfa41f;--mk:#8a6a10}
.hkd-rosegold{--m1:#ffd9cf;--m2:#e8ac9b;--m3:#c98776;--mk:#8f5a4a}
/* on the selected chip (coral fill) the pale metals need their own edge or they melt in */
.hkchip[aria-pressed=true] .hkdot{box-shadow:inset 0 0 0 .9px var(--mk),0 0 0 1.5px rgba(255,255,255,.75)}
.wtchip:active{transform:translateY(1px)}
/* a "per row" count this screen can't honour — dimmed and inert rather than removed, so a
   choice made on a desktop is still visible here, with .wtcprhint saying why. See syncCpr(). */
.wtchip[disabled]{opacity:.38;cursor:default}
.wtchip[disabled]:active{transform:none}
.wtcprhint{flex:1 1 100%;margin:4px 0 0}
/* sliders for panel size + spacing */
.wtslider{flex:1;min-width:130px;max-width:260px;height:34px;margin:0;accent-color:var(--coral-d);cursor:pointer}
.wtval{font-family:var(--body);font-weight:800;font-size:.78rem;color:var(--ink);min-width:52px;text-align:right}
/* sign controls: text input (16px font so iOS doesn't zoom), colour chips, mascot thumbs */
.wtinput{flex:1;min-width:150px;max-width:280px;font-family:var(--body);font-weight:700;font-size:16px;color:var(--ink);
  background:var(--paper);border:1.5px solid rgba(64,52,43,.22);border-radius:var(--radius);padding:9px 13px;min-height:40px}
.wtinput:focus-visible{outline:2px solid var(--coral-d);outline-offset:1px}
/* stacked option (emoji picker) + popular-emoji grid with search */
.wtopt-col{flex-direction:column;align-items:stretch;gap:8px}
.emojigrid{display:flex;flex-wrap:wrap;gap:3px;max-height:156px;overflow-y:auto;padding:4px 2px;
  background:rgba(64,52,43,.04);border-radius:var(--radius);-webkit-overflow-scrolling:touch}
.emojibtn{border:0;background:transparent;font-size:1.35rem;line-height:1;width:38px;height:38px;border-radius:var(--radius);cursor:pointer;
  display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent}
@media(hover:hover){.emojibtn:hover{background:var(--paper)}}
.emojibtn.on{background:rgba(224,122,95,.2);box-shadow:inset 0 0 0 2px var(--coral-d)}
.emojibtn:active{transform:translateY(1px)}
/* the Outline row only shows when the keychain hanger is selected */
/* display-style tiles: five mini renders of the real display objects (charm, card, box,
   bag, acrylic case), each a tap target, the wall behind is the live preview */
.disptiles{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;width:100%}
.disptile{min-width:0;border:2px solid rgba(64,52,43,.16);border-radius:var(--radius);background:var(--card);cursor:pointer;
  padding:4px 4px 6px;display:flex;flex-direction:column;gap:4px;position:relative;-webkit-tap-highlight-color:transparent;
  transition:transform .15s var(--ease-out),border-color .15s var(--ease-out)}
.disptile:hover{border-color:var(--coral);transform:translateY(-1px)}
.disptile:active{transform:translateY(1px)}
.disptile[aria-pressed=true]{border-color:var(--coral-d);box-shadow:0 0 0 2px rgba(224,122,95,.4)}
.disptile[aria-pressed=true]::after{content:"✓";content:"✓" / "";position:absolute;top:-7px;right:-7px;width:22px;height:22px;
  display:grid;place-items:center;font-size:.8rem;font-weight:800;color:#fff;background:var(--primary);
  border-radius:50%;box-shadow:0 1px 3px rgba(64,52,43,.4),0 0 0 2px var(--paper);z-index:2}
.dt-stage{height:92px;border-radius:var(--radius);overflow:hidden;display:flex;align-items:flex-start;justify-content:center;
  background:radial-gradient(120% 100% at 50% 0%,#fff8ee,#f1e6d2)}
/* per-type fit: each preview renders at natural size, then scales to sit whole inside
   the 92px stage (pack cards are ~300×370 natural, the charm ~120×255, the case ~124×183) */
.dt-fit{transform-origin:top center;pointer-events:none;position:relative;flex:none}
.disptile.at-keychain .dt-fit{transform:scale(.34)}
.disptile.at-card .dt-fit,.disptile.at-box .dt-fit,.disptile.at-bag .dt-fit{transform:scale(.24)}
.disptile.at-case .dt-fit{transform:scale(.48)}
.dt-name{font-family:var(--body);font-weight:800;font-size:.66rem;line-height:1.2;text-align:center;color:var(--ink);
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}
/* Pack's shape row, only while a pack display is active */
.packsub{display:none}
#wtpanel.showpackage .packsub{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;width:100%;margin-top:2px}
.disptile.mini .dt-stage{height:64px}
.disptile.mini.at-card .dt-fit,.disptile.mini.at-box .dt-fit,.disptile.mini.at-bag .dt-fit{transform:scale(.165)}
/* ===== catalog filter bar: sticky search + a momentum marquee of species chips =====
   The bar bleeds to the edges of .appmain's padding so it reads as a toolbar, and stays
   put while a 100+ page grid scrolls under it. */
.catbar{margin:0 -20px 12px;padding:10px 20px 8px;
  background:var(--paper);background:color-mix(in srgb,var(--paper) 90%,transparent);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
/* sticky on desktop only: search + chips + active filters run ~250px tall, which is a third
   of a phone screen to surrender permanently. On mobile it scrolls away like the rest. */
@media(min-width:900px){.catbar{position:sticky;top:0;z-index:20;margin:0 -46px 14px;padding:12px 46px 10px}}
.catsearch{display:flex;gap:8px;align-items:center}
.catsearch input{flex:1;min-width:0}
.catsearch .btn{flex:none}
@media(max-width:420px){.catsearch .btn{padding:12px 16px}}
/* "All" is pinned outside the marquee: clearing filters must never be a moving target */
.fchiprow{display:flex;align-items:center;gap:8px;min-width:0;margin-top:9px}
.fchip{flex:none;display:inline-block;text-decoration:none;white-space:nowrap;cursor:pointer;
  padding:7px 15px;border-radius:var(--radius);font-size:.85rem;font-weight:700;line-height:1.35;
  background:var(--card);border:1.5px solid var(--line);color:var(--ink2);-webkit-tap-highlight-color:transparent;
  transition:transform .3s cubic-bezier(.34,1.56,.64,1),background .2s,color .2s,border-color .2s}
.fchip:hover{border-color:var(--coral);background:#fff6f2;color:var(--coral-d)}
.fchip.on{background:var(--primary);border-color:var(--coral-d);color:#fff}
.fchip.on:hover{background:#b34e3a;color:#fff}
.fchip.pop{transform:scale(1.12)} /* spring pop on select, css.md */
.fchip:active{transform:scale(.95)}
/* Momentum marquee: the track drifts left forever and is duplicated so -50% loops with no
   seam. Hover, keyboard focus, or a finger on the row parks it; underneath it is still a
   real scroller, so a flick throws it with the platform's own momentum. */
.marquee-zone{flex:1;min-width:0;overflow-x:auto;overflow-y:hidden;padding:3px 0;container-type:inline-size;
  scrollbar-width:none;-webkit-overflow-scrolling:touch;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 34px),transparent);
          mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 34px),transparent)}
.marquee-zone::-webkit-scrollbar{display:none}
.marquee-track{display:flex;width:max-content;animation:marquee-scroll 45s linear infinite}
/* min-width keeps the loop seamless even when a thin catalog has too few chips to fill the
   row. Left-aligned on purpose: a centring justify-content clips the head of an overflowing
   scroller out of reach. */
.marquee-half{display:flex;gap:8px;padding-right:8px;min-width:100cqw}
.marquee-zone:hover .marquee-track,.marquee-zone:focus-within .marquee-track,
.marquee-zone.parked .marquee-track{animation-play-state:paused}
@keyframes marquee-scroll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@media(prefers-reduced-motion:reduce){.marquee-track{animation:none}.fchip{transition:none}}
/* active filters, held still so they can be un-picked without chasing the drift */
.catsel{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:9px}
.catsel[hidden]{display:none}
.catsel .hint{margin:0}
.catsel .fchip{font-size:.8rem;padding:7px 13px} /* narrower than the marquee's, but the same height to stay a comfortable tap target */
.catclear{font-size:.8rem;font-weight:700;color:var(--coral-d);margin-left:2px}
#catresults{transition:opacity .16s var(--ease-out)}
#catresults.busy{opacity:.45;pointer-events:none}
.catpage{display:flex;align-items:center;gap:10px;margin-top:20px}
.catpage>span:first-child{flex:1}
.catpage>span:last-child{flex:1;text-align:right}
.catpage>.hint{flex:none;margin:0}
/* catalog: dead hotlinks swap to this tile (global error handler); 2-line name clamp */
.catfb{width:100%;aspect-ratio:1;display:grid;place-items:center;background:#f8f1e7;border-radius:var(--radius);font-size:2.6rem}
/* catalog card: the whole tile still opens the entry, with Save / + Add sitting on top so a
   collector can claim or wishlist a plushie without leaving the grid.
   Tiles are sized for a real look at the plushie: ~7 columns on a wide desktop, 2 on a
   phone, instead of the 11 thumbnail-sized columns the old 108px floor produced. */
.catgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(158px,1fr));gap:14px}
@media(max-width:560px){.catgrid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}}
.catcard{position:relative;border:1px solid var(--line,#e5ded2);border-radius:var(--radius);padding:9px;background:var(--card);display:flex;flex-direction:column;
  transition:transform .18s var(--ease-out),box-shadow .18s var(--ease-out),border-color .18s var(--ease-out)}
.catcard:hover{transform:translateY(-2px);border-color:#e0d2c0;box-shadow:var(--shadow-s)}
@media(prefers-reduced-motion:reduce){.catcard{transition:none}.catcard:hover{transform:none}}
.catcard-go{text-decoration:none;color:inherit;display:flex;flex-direction:column;flex:1}
.catcard .catimg,.catcard .catfb{background:#fbf5ec;border-radius:var(--radius)}
.catcard.added{box-shadow:0 0 0 2px var(--coral-d,var(--coral-d))}
/* the little "yes, they're yours" burst when a catalog card lands on the shelf. The wrapper
   is the burst origin (card is position:relative), so each particle's --tx/--ty is a plain
   offset from that point. JS spawns them, sets the vars, removes the wrapper on animationend. */
.confetti-burst{position:absolute;left:50%;top:42%;z-index:5;pointer-events:none}
.confetti-particle{position:absolute;width:6px;height:6px;border-radius:2px;
  box-shadow:0 1px 2px rgba(64,52,43,.18);
  animation:confetti-fly 1.15s cubic-bezier(.16,1,.3,1) forwards}
/* transform is only named at 100%, so it eases across the whole run; opacity holds full for
   the first half and fades at the end. Without the hold, the ease-out dumps most of the fade
   in the first ~150ms and you see a faint smudge instead of confetti. */
@keyframes confetti-fly{0%,55%{opacity:1}to{transform:translate(var(--tx),var(--ty)) rotate(var(--rot));opacity:0}}
/* the card itself flinches — a burst nobody saw start reads as decoration, not confirmation */
.catcard.added{animation:card-pop .5s cubic-bezier(.16,1,.3,1)}
@keyframes card-pop{35%{transform:scale(1.05)}to{transform:scale(1)}}
/* particles fly past the card's edges; without this the next card in the grid paints over them */
.catcard:has(.confetti-burst){z-index:6}
@media(prefers-reduced-motion:reduce){.confetti-burst{display:none}.catcard.added{animation:none}}
.catact{display:flex;gap:6px;align-items:center;margin-top:auto;padding-top:8px;flex-wrap:wrap}
.catadd{flex:1;min-width:0;border:0;cursor:pointer;border-radius:var(--radius);padding:7px 8px;font:inherit;font-weight:800;font-size:.76rem;
  color:#fff;background:var(--coral-d,var(--coral-d));white-space:nowrap}
.catadd:hover{filter:brightness(1.06)}
.catadd[disabled]{opacity:.75;cursor:default}
/* .savebtn is the big pill used on shelves; on a catalog card it shrinks to a heart chip.
   Double class so these win over the .savebtn base rules further down the sheet. */
.savebtn.catsave{flex:none;width:34px;height:32px;padding:0;border-radius:var(--radius);border:1px solid var(--line,#e5ded2);
  background:var(--card);cursor:pointer;display:grid;place-items:center;font-size:.95rem;color:#cbc2b4;line-height:1;gap:0}
.savebtn.catsave.on{color:#fff;border-color:var(--coral-d,var(--coral-d));background:var(--coral-d,var(--coral-d))}
.savebtn.catsave:hover{transform:scale(1.06)}
/* size chips appear in place when an entry comes in more than one size */
.catsizes{display:flex;flex-wrap:wrap;gap:5px;width:100%}
.catsize{border:1px solid var(--line,#e5ded2);background:var(--card);border-radius:8px;padding:5px 8px;font:inherit;font-size:.72rem;font-weight:700;cursor:pointer;color:#4a3620}
.catsize:hover{border-color:var(--coral-d,var(--coral-d));color:var(--coral-d,var(--coral-d))}
.savedh{font-size:1.05rem;margin:0 0 2px}
/* min-height reserves both clamp lines, so the brand/size caption sits on one baseline
   across the row instead of stair-stepping with each name's length */
.catname{font-size:.83rem;font-weight:700;margin-top:6px;line-height:1.25;min-height:2.5em;
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}
/* pack/case preview cell in the wall editor, same cream box as the keychain preview */
.wssw{background:linear-gradient(180deg,var(--sg1),var(--sg2));border-color:rgba(64,52,43,.3)}
.wssw[aria-pressed=true]{color:#43291b;background:linear-gradient(180deg,var(--sg1),var(--sg2));box-shadow:0 0 0 2px var(--coral-d)}
.wtlbl{font-family:var(--body);font-size:.66rem;font-weight:800;background:rgba(255,255,255,.85);color:#43291b;border-radius:6px;padding:1px 5px;margin-bottom:6px;pointer-events:none;white-space:nowrap}
.wt-walnut .wtlbl,.wt-charcoal .wtlbl{background:transparent;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.45)}
/* ===== the furry wordmark, plush pillow letters + hearts/paws to play with ===== */
.furlogo{position:relative;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:2px 10px;
  margin:6px auto 24px;max-width:760px;padding:10px 16px}
.furlword{display:flex;align-items:center;gap:14px} /* halo+fringe widen each glyph ~13px — tight gaps merge letters */
/* Letter height is a variable, not a literal, because plushLogo's letter overlap is a
   FRACTION of it. The lab tunes spacing against a fixed 119px letter; the page scales
   letters with the viewport, so a fixed -12px margin that reads as a 10% overlap on
   desktop becomes a 20% overlap at 430px and mashes the word into one pink smear. */
/* Letter height is derived from the CONTAINER, not the viewport, so PLUSHBASE stays on one
   line at every width. Measured with a forced re-mount at each size, one letter's box —
   glyph plus the fur that overhangs it — is a steady 0.66 of the letter height, so nine of
   them plus the word gap need ~6.03H before overlap. The letters overlap by 0.1043H per
   side, which claws back 18 x that, so the real cost is ~4.14H and the height can be a
   much larger share of the box. vw could not do this job: it sits in
   .wrap's padding, so the same viewport gives it different room on different pages.
   flex-wrap stays on as the backstop — if a font swap ever widens the glyphs it wraps
   rather than overflowing the page. */
.furlogo{container-type:inline-size;--furl-h:min(119px,22.5cqw)}
.furl{display:block;height:var(--furl-h);width:34px} /* width self-sizes per glyph via /fur.js */
.furlogo.compact{margin:0 auto 10px;padding:4px 8px;--furl-h:clamp(44px,11vw,60px)}
.furlogo .furdeco{display:block;position:absolute} /* .furlogo scope outranks .fur's position:relative */
/* grounding shadow options, the cheap half of "looks 3D".
   soft = sits on the page · cast = floats with a thrown studio shadow (seedream ref) */
.furlogo.sh-soft .fur-canvas{filter:drop-shadow(0 7px 9px rgba(133,63,95,.26))}
.furlogo.sh-cast .fur-canvas{filter:drop-shadow(-13px 17px 14px rgba(139,77,101,.32)) drop-shadow(-3px 5px 4px rgba(139,77,101,.18))}
/* glow = backlit rim: a white halo hugging the silhouette + a whisper of ground shadow
   (the tone-on-tone look, pair with the nub texture and a pale pink) */
.furlogo.sh-glow .fur-canvas{filter:drop-shadow(0 0 3px rgba(255,255,255,.95)) drop-shadow(0 0 9px rgba(255,255,255,.65)) drop-shadow(0 10px 12px rgba(170,100,125,.18))}
.furdeco.fd-h1{width:46px;height:42px;left:-16px;top:-22px;rotate:-14deg}
.furdeco.fd-h2{width:34px;height:31px;right:-6px;bottom:-16px;rotate:11deg}
.furdeco.fd-p1{width:42px;height:42px;right:-18px;top:-24px;rotate:14deg}
.furdeco.fd-p2{width:36px;height:36px;left:-4px;bottom:-20px;rotate:-10deg}
@media(max-width:460px){.furdeco.fd-h1{left:-14px}.furdeco.fd-p1{right:-14px}}
/* ===== fun share overlay: tap a wall plushie, sneaker-style product stage ===== */
.pmodal{position:fixed;inset:0;z-index:400}
.pmodal[hidden]{display:none}
/* light, opaque pastel world, no dark dim, no white modal box (sneaker.md) */
.pmodal-scrim{position:absolute;inset:0;background:var(--background);opacity:0;transition:opacity .24s}
.pmodal.on .pmodal-scrim{opacity:1}
/* the world tints to the active plushie's gradient while swiping (sneaker.md) */
.pm-bg{position:absolute;inset:0;opacity:0;pointer-events:none}
/* the plushie is the hero, a soft aura spotlight, cutout floating with a shadow, sparkles */
/* ===== swipe overlay, the cutout BURSTS out of its gradient panel (sneaker-style) ===== */
.pcar{position:relative;z-index:1;height:100%;overflow:hidden;touch-action:pan-y;
  opacity:0;transform:translateY(18px) scale(.97);transition:transform .28s var(--ease-out),opacity .24s}
.pmodal.on .pcar{transform:none;opacity:1}
/* the slime-maker backdrop, exactly: #FFF3F7 with a 38px grid of #FFD8E6 dots */
.pmodal-scrim::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:var(--background) radial-gradient(var(--border) 12%,transparent 13%);background-size:38px 38px}
.pcar-track{display:flex;height:100%;will-change:transform}
.pcard{flex:0 0 100%;box-sizing:border-box;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
  overflow:hidden; /* each slide clips its own jump-out, neighbours never peek into the corners */
  padding:max(24px,env(safe-area-inset-top)) 20px max(60px,calc(env(safe-area-inset-bottom) + 44px))}
/* the wrapper vanishes on mobile, stage + info lay out exactly as before */
.pcard-box{display:contents}
/* the gradient "box" the plushie jumps out of, background set inline per plushie */
.pcard-stage{position:relative;display:flex;align-items:center;justify-content:center;
  width:min(78vw,330px);aspect-ratio:1/1.06;border-radius:var(--radius-lg);
  border:2px dashed #FF9EC4;   /* thinner than 3px: at 3 it competed with the plushie */
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.3),inset 0 8px 20px -12px rgba(64,52,43,.28)}
.pcard-stage::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(120deg,transparent 44%,rgba(255,255,255,.28) 51%,transparent 60%)}
/* centred and CONTAINED: the plushie sits inside its washi-framed box like a proper
   product card (the old 136% burst spilled past every edge). */
.pm-cut{position:relative;z-index:2;width:88%;max-width:none;max-height:90%;object-fit:contain;
  filter:drop-shadow(0 18px 18px rgba(64,52,43,.3));transform-origin:50% 70%;will-change:transform;
  user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.pm-cut.pm-emoji{font-size:150px;line-height:1;width:auto}
/* soft contact shadow, lags the cutout during swipes */
.pm-shadow{position:absolute;z-index:1;bottom:7%;left:50%;width:56%;height:18px;border-radius:50%;
  background:radial-gradient(ellipse at center,rgba(46,38,30,.34),transparent 68%);
  transform:translateX(-50%);will-change:transform;pointer-events:none}
.pcard-info{width:min(78vw,330px);text-align:left;display:flex;flex-direction:column;align-items:flex-start;gap:2px}
/* metadata: only fields with real data, so the row hides itself when a plushie has none */
.pcard-meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 12px;margin-top:8px;padding-top:8px;
  width:100%;border-top:1px dashed var(--border-strong);
  font-family:var(--body);font-weight:700;font-size:.78rem;color:var(--muted-foreground)}
.pcard-m{display:inline-flex;align-items:center;gap:5px}
.pcard-m b{font-weight:800;color:var(--ink);font-size:.72rem;text-transform:uppercase;letter-spacing:.06em}
.pcard-actions{width:100%;margin-top:12px}
.pcard-actions .savebtn,.pcard-actions .btn{width:100%;justify-content:center;padding:14px 18px;font-size:1.02rem}
/* filled, because in the modal this is the only action — on a grid card the same button is
   one of many and stays outlined so a wall of them does not shout */
.pcard-actions .savebtn{background:var(--primary);border-color:var(--coral-d);color:var(--foreground);box-shadow:var(--shadow-ctl)}
.pcard-actions .savebtn:hover{background:var(--accent);color:var(--accent-foreground)}
.pcard-actions .savebtn.on{background:var(--secondary);border-color:var(--secondary-foreground);color:var(--secondary-foreground)}
.pcard-cat{display:inline-block;font-family:var(--body);font-weight:800;font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--ink);background:var(--muted);border:2px solid var(--ink);border-radius:var(--radius);padding:3px 12px;box-shadow:2px 2px 0 var(--ink)}
.pcard-name{font-family:var(--display);font-weight:600;font-size:clamp(1.9rem,7.5vw,2.5rem);letter-spacing:0;line-height:1.05;margin:2px 0 0;color:var(--ink);text-shadow:3px 3px 0 var(--background),5px 5px 0 var(--primary)}
.pcard-rar{font-family:var(--body);font-weight:700;font-size:.92rem;color:var(--star);display:flex;align-items:center;gap:6px}
.pcard-rar.muted{color:var(--muted-foreground)}
.pcard-stars{color:var(--star);letter-spacing:.08em;font-size:1.05rem;text-shadow:0 1px 0 rgba(120,80,20,.25)}
/* gradient swatches, rounded squares, one gap+coral ring when picked */
.pm-swatches{display:flex;gap:10px;margin-top:16px}
/* invisible hit-area expansion to ~44px on small controls, expansion is mostly
   vertical so adjacent controls in a row don't steal each other's taps */
.hvbtn,.wtchip,.wtnav-b,.emojibtn,.pm-sw{position:relative}
.hvbtn::after,.wtnav-b::after{content:"";position:absolute;inset:-6px 0;border-radius:inherit}   /* single rows: grow down/up only */
.wtchip::after{content:"";position:absolute;inset:-3px -2px;border-radius:inherit}               /* wrapped rows, 6px gap: meet in the middle */
.emojibtn::after{content:"";position:absolute;inset:-1px;border-radius:inherit}                  /* 3px grid gap, anything bigger steals taps */
.pm-sw::after{content:"";position:absolute;inset:-9px -4px;border-radius:inherit}
.pm-sw{width:26px;height:26px;border-radius:var(--radius-sm);border:0;cursor:pointer;padding:0;-webkit-tap-highlight-color:transparent}
.pm-sw:hover{transform:scale(1.08)}
.pme-sw{margin:2px 0 10px}
/* position dots, the only hint there's more to swipe to */
.pcar-dots{position:absolute;z-index:2;left:0;right:0;bottom:max(16px,env(safe-area-inset-bottom));display:flex;gap:6px;justify-content:center;flex-wrap:wrap;padding:0 16px;pointer-events:none}
.pcar-dot{width:6px;height:6px;border-radius:50%;background:var(--border);transition:background .2s,transform .2s}
.pcar-dot{background:#EFD9E2}
.pcar-dot.on{background:#FF9EC4;transform:scale(1.3)}
/* ===== desktop: the full-screen burst is too much, a compact floating card instead.
   Same clean cutout + changeable gradient + swatches + swipe, zero buttons.
   (Must come AFTER the mobile base rules so equal-specificity overrides win.) ===== */
@media(min-width:720px){
  /* a true modal: page dimmed behind, pastel world + backdrop tint are mobile-only */
  .pmodal-scrim{background:rgba(43,32,23,.45);-webkit-backdrop-filter:blur(7px) saturate(.9);backdrop-filter:blur(7px) saturate(.9)}
  .pmodal-scrim::after{display:none} /* dots are part of the mobile pastel world */
  .pm-bg{display:none}
  .pcard-box{display:block;width:min(400px,92vw);max-height:calc(100vh - 96px);box-sizing:border-box;
    background:var(--card);
    border-radius:var(--radius);padding:24px 24px 20px;
    border:3px solid var(--ink);
    box-shadow:6px 6px 0 var(--ink),0 24px 60px -18px rgba(64,52,43,.3)}
  .pcard-stage{width:min(100%,44vh);margin:0 auto}
  /* contained on desktop, a tidy product shot, not a burst */
  .pm-cut{width:86%;max-height:92%}
  .pm-cut.pm-emoji{font-size:110px}
  .pcard-info{width:100%;margin-top:14px}
  .pcard-name{font-size:2rem}
}
/* close ✕, always visible (a modal you can't see how to leave is a trap on mobile) */
.pm-close{display:grid;position:fixed;top:max(14px,env(safe-area-inset-top));right:14px;z-index:460;width:46px;height:46px;border-radius:50%;
  border:2px solid var(--ink);background:var(--card);box-shadow:2px 2px 0 var(--ink);
  font:800 1.05rem/1 var(--body);color:var(--ink);cursor:pointer;place-items:center;
  transition:transform .1s var(--ease-out),box-shadow .1s var(--ease-out)}
.pm-close:hover{background:var(--muted)}
.pm-close:active{transform:translate(2px,2px);box-shadow:0 0 0 var(--ink)}
/* owner ✏️ Edit chip inside the overlay card */
.pcard-actions{display:flex;gap:10px;justify-content:center;margin-top:12px}
.pcard-actions .btn{text-decoration:none}
.pm-edit{display:inline-flex;align-items:center;gap:6px;margin-top:14px;padding:9px 20px;min-height:42px;
  border-radius:var(--radius);border:2px solid var(--ink);background:#FFF8FB;
  font-family:var(--body);font-weight:800;font-size:.9rem;color:var(--ink);cursor:pointer;
  box-shadow:2px 2px 0 var(--ink);
  transition:transform .1s var(--ease-out),box-shadow .1s var(--ease-out)}
.pm-edit:hover{background:var(--muted)}
.pm-edit:active{transform:translate(2px,2px);box-shadow:0 0 0 var(--ink)}
/* quick-edit panel, floats above the overlay, centered bottom sheet */
@keyframes pmpackin{0%{transform:translateY(16px) scale(.82) rotate(-4deg);opacity:0}100%{transform:none;opacity:1}}
.pmedit{position:fixed;z-index:470;left:50%;bottom:max(20px,env(safe-area-inset-bottom));transform:translateX(-50%);
  width:min(340px,calc(100vw - 32px))}
.pmedit form{background:var(--card);
  border:3px solid var(--ink);border-radius:var(--radius);
  box-shadow:6px 6px 0 var(--ink),0 24px 60px -16px rgba(64,52,43,.35);padding:16px 18px 14px;
  animation:pmpackin .35s cubic-bezier(.25,1.5,.4,1) both}
.pmedit input[type=text]{background:#FFF8FB;border:3px solid var(--ink);border-radius:var(--radius);font-family:var(--body);font-weight:700;color:var(--ink)}
.pmedit input[type=text]:focus{border-color:#FF9EC4;box-shadow:none}
.pmedit b{font-family:var(--display);font-weight:600;font-size:1.2rem;color:var(--ink)}
.pmedit[hidden]{display:none}
.pmedit b{display:block;font:700 1.1rem var(--hand);margin-bottom:10px}
.pmedit label{display:block;font:800 .68rem var(--body);letter-spacing:.08em;text-transform:uppercase;color:var(--ink2);margin:8px 0 4px}
.pmedit input{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:var(--radius);border:1.5px solid var(--line);
  background:var(--paper);font:600 16px var(--body);color:var(--ink)}   /* 16px: under it, iOS zooms the page on focus and stays zoomed */
.pmedit input:focus{outline:none;border-color:var(--coral)}
.pmedit-row{display:flex;gap:8px;align-items:center;margin-top:14px;flex-wrap:wrap}
.pmedit .sclink{margin-left:auto;font-size:.84rem}
.pmedit .status{margin:8px 0 0;font:700 .84rem var(--body);color:var(--ink2)}
/* ♥ Save / wishlist toggle */
.savebtn{display:inline-flex;align-items:center;justify-content:center;gap:7px;font-family:var(--body);font-weight:800;font-size:1rem;
  border:2px solid var(--coral);border-radius:var(--radius);padding:10px 20px;background:var(--paper);color:var(--coral-d);cursor:pointer;
  -webkit-tap-highlight-color:transparent;transition:background .15s var(--ease-out),transform .1s var(--ease-out)}
.savebtn .savebtn-i{font-size:1.08em;line-height:1}
.savebtn.on{background:var(--primary);border-color:var(--coral-d);color:#fff}
.savebtn:active{transform:translateY(1px)}
@keyframes savepop{0%{transform:scale(1)}40%{transform:scale(1.32)}100%{transform:scale(1)}}
.savebtn.on .savebtn-i{animation:savepop .32s var(--ease-out)}
/* wishlist cards: you collect a die-cut STICKER of each saved friend (not the plushie) */
.savedcard{text-decoration:none;color:inherit}
.savedcard .stcard-pic{background:radial-gradient(120% 100% at 50% 0%,rgba(255,255,255,.6),rgba(64,52,43,.03))}
.savedcard .stickercut{max-width:82%;max-height:82%;width:auto;height:auto;object-fit:contain;
  filter:drop-shadow(2.5px 0 0 #fff) drop-shadow(-2.5px 0 0 #fff) drop-shadow(0 2.5px 0 #fff) drop-shadow(0 -2.5px 0 #fff) drop-shadow(1.8px 1.8px 0 #fff) drop-shadow(-1.8px -1.8px 0 #fff) drop-shadow(1.8px -1.8px 0 #fff) drop-shadow(-1.8px 1.8px 0 #fff) drop-shadow(0 6px 6px rgba(64,52,43,.28));
  transform:rotate(-2.5deg)}
.savedcard:nth-child(even) .stickercut{transform:rotate(2.5deg)}
.savedcard:nth-child(3n) .stickercut{transform:rotate(1.2deg)}
/* ===== floating plushie shelf, shadows + reflections + tactile motion ===== */
.shelfarea{position:relative;margin:16px auto 24px;max-width:720px}
@media(min-width:900px){
  /* desktop shelf: a proper centered stage, bigger friends, stronger shelf line */
  .shelfarea{margin-top:30px}
  .slot{width:156px}
  .pbook{height:250px;padding-bottom:26px}
  .pcutout{height:clamp(120px,13vw,168px);max-width:158px}
  .pemoji{font-size:96px}
  .plabel{font-size:1.5rem}
  .shelfrow{background-size:100% 250px;background-position:0 172px;padding-bottom:70px;gap:6px 22px}
}
.shelfrow{list-style:none;margin:0;padding:0 0 54px;display:flex;flex-wrap:wrap;justify-content:center;gap:4px 16px;
  /* a wooden plank under every wrapped row: lit top face, front edge, drop shadow */
  background-image:linear-gradient(to bottom,
    #e9cba4 0px,#dcb27f 5px,               /* top face catches the light */
    #c08d57 5px,#a8743f 19px,              /* front edge, a real plank, not a line */
    #96662f 19px,#8a5c28 22px,             /* underside lip */
    rgba(64,52,43,.16) 22px,rgba(64,52,43,0) 38px,  /* soft cast shadow */
    transparent 38px);
  background-size:100% 210px;background-position:0 138px;background-repeat:repeat-y}
.slot{width:120px;display:flex;min-width:0}
.pbook{position:relative;width:100%;height:210px;padding-bottom:22px;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;
  text-decoration:none;color:inherit;cursor:pointer;outline:none;-webkit-tap-highlight-color:transparent}
.pbook:focus-visible{outline:2px solid var(--coral-d);outline-offset:4px;border-radius:var(--radius)}
.pfloat{display:block;animation:pfloat 3.4s ease-in-out infinite;animation-delay:calc(var(--i,0)*-0.5s)}
.plift{display:block;transition:transform .45s cubic-bezier(.25,1.5,.5,1)}
.pcutout{display:block;width:auto;height:clamp(94px,17vw,138px);max-width:128px;object-fit:contain;
  filter:url(#pb-sticker) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 12px 14px rgba(64,52,43,.28));
  transition:transform .18s ease-out;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;pointer-events:none}
.pemoji{font-size:74px;line-height:1;filter:none}
.pbook:hover .plift,.pbook:focus-visible .plift{transform:translateY(-16px) scale(1.08)}
.pbook:hover .pfloat,.pbook:focus-visible .pfloat{animation-play-state:paused}
.pbook:hover .pcutout,.pbook:focus-visible .pcutout{filter:url(#pb-sticker) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 20px 22px rgba(64,52,43,.26))}
.plabel{font-family:var(--hand);font-weight:700;font-size:1.3rem;line-height:1;text-align:center;color:var(--ink);margin-top:26px;overflow-wrap:anywhere;transition:transform .4s cubic-bezier(.25,1.5,.5,1)}
.pbook:hover .plabel,.pbook:focus-visible .plabel{transform:scale(1.06)}
@keyframes pfloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-9px)}}
@keyframes pdrop{0%{transform:translateY(-34px);opacity:0}70%{transform:translateY(5px)}100%{transform:translateY(0);opacity:1}}
.anim .plift{animation:pdrop .6s cubic-bezier(.25,1.5,.5,1) backwards;animation-delay:calc(var(--i,0)*70ms)}
@keyframes psquish{0%{transform:translateY(-16px) scale(1.08)}35%{transform:translateY(-4px) scaleY(.82) scaleX(1.16)}70%{transform:translateY(-14px) scaleY(1.05) scaleX(.97)}100%{transform:translateY(-16px) scale(1.08)}}
.pbook.squish .plift{animation:psquish .32s ease}
/* press feedback on every secondary control (design.md: controls press) */
.vt:active,.factdel:active,.chip.pick:active,.snav-out:active{transform:translateY(1px)}
.bnav-i:active{opacity:.55}
.picon:active .pitile{transform:translateY(1px)}
@media (prefers-reduced-motion:reduce){
  .vt,.factdel{transition:none}
  .pfloat{animation:none}.plift{transition:none}.anim .plift{animation:none}.pbook.squish .plift{animation:none}.pcutout{transition:none}
  .pbook:hover .plift,.pbook:focus-visible .plift{transform:translateY(-6px)}
  .cubby .ph,.plabel,.aurapick label,.snav-i{transition:none}
}
/* ===== per-plushie aura (glow finish) ===== */
@property --gh{syntax:'<angle>';inherits:false;initial-value:0deg}
.aura-pink{--glow:drop-shadow(0 0 16px rgba(255,120,170,.62))}
.aura-coral{--glow:drop-shadow(0 0 16px rgba(224,122,95,.55))}
.aura-gold{--glow:drop-shadow(0 0 18px rgba(245,190,90,.64))}
.aura-mint{--glow:drop-shadow(0 0 16px rgba(130,205,155,.6))}
.aura-blue{--glow:drop-shadow(0 0 18px rgba(120,170,255,.6))}
.aura-lavender{--glow:drop-shadow(0 0 16px rgba(190,150,240,.6))}
.aura-halo{--glow:drop-shadow(0 0 26px rgba(255,255,255,.8))}
.aura-holo{--glow:drop-shadow(0 0 22px hsl(var(--gh) 92% 72% / .72));animation:aurahue 4.5s linear infinite}
@keyframes aurahue{to{--gh:360deg}}
/* The Sticker view IS the interactive peel. The WebGL host fills .herosticker; the library
   renders the sticker at ~58% of the host's height, so the host is sized ~1.7x the target
   sticker so the peel lands at the same size as the Card/Pack views (and matches on mobile). */
.herosticker{position:relative;display:flex;align-items:center;justify-content:center;width:min(430px,90vw);height:min(510px,92vw);margin:10px auto 6px}
.hero-cutout{display:block;height:min(300px,52vw);width:auto;object-fit:contain;
  filter:url(#pb-sticker) var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 16px 18px rgba(64,52,43,.26))}
/* peel canvas fills the sized host; the static cutout sits behind as the pre-load placeholder */
.peelhost{position:absolute;inset:0;touch-action:none}
.peelhost:focus,.peelhost :focus{outline:none} /* peel is a pointer gesture; drop the WebGL host's focus ring */
.peelhost canvas{display:block;width:100%;height:100%}
.aurapick{display:flex;flex-wrap:wrap;gap:11px;justify-content:center;align-items:center;margin:8px 0 0}
.aurapick input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.aurapick label{width:34px;height:34px;border-radius:var(--radius);cursor:pointer;display:block;position:relative}
.aurapick label::after{content:"";position:absolute;inset:-6px;border-radius:var(--radius)} /* invisible 46px hit area */
.aurapick label:hover{transform:scale(1.1)}
.aurapick input:focus-visible+label{outline:2px solid var(--coral-d);outline-offset:3px}
@media (prefers-reduced-motion:reduce){.aura-holo{animation:none}}
/* ===== mobile bottom navigation ===== */
.bnav{position:fixed;left:0;right:0;bottom:0;z-index:60;display:none;grid-template-columns:repeat(5,1fr);align-items:end;
  background:color-mix(in srgb, var(--card) 94%, transparent);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  border-top:1px solid var(--line);padding:7px 6px calc(7px + env(safe-area-inset-bottom))}
.bnav-i{display:flex;flex-direction:column;align-items:center;gap:3px;text-decoration:none;color:var(--ink2);
  font-size:.7rem;font-weight:700;padding:5px 0 2px;-webkit-tap-highlight-color:transparent}
.bnav-i[aria-current]{color:var(--coral-d)}
/* The active tab is the one place the tint earns being louder than .2 — it's what makes a
   selected tab read as selected at a glance, without a second colour or a pill behind it. */
.bnav-i[aria-current] .navico{--icon-tint-alpha:.42}
.bnav-add .fab{display:flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:50%;
  background:var(--primary);color:var(--on-coral);font-size:2rem;line-height:1;margin-top:-20px;box-shadow:0 7px 16px -5px rgba(200,90,68,.65)}
.bnav-add:active .fab{transform:scale(.94)}
/* the bar owns navigation all the way up to where the sidebar takes over, no dead zone */
@media(max-width:899px){.bnav{display:grid}.wrap{padding-bottom:94px}}
/* profile */
.profhead{display:flex;gap:14px;align-items:center;margin-bottom:18px}
.avatar{width:62px;height:62px;border-radius:50%;flex:none;object-fit:cover;background:var(--coral);color:#fff;
  display:flex;align-items:center;justify-content:center;font-family:var(--display);font-weight:700;font-size:1.7rem}
.avatar.sm{width:40px;height:40px;font-size:1rem}
/* Avatar picker. The plushie grid reuses .sp-grid/.sp-item (the sticker picker) — same
   job, same shape — so this is only the bits that grid doesn't already have. */
.avpick>summary{cursor:pointer;font-weight:800;color:var(--ink);list-style:none;padding:2px 0}
.avpick>summary::-webkit-details-marker{display:none}
.avpick>summary::after{content:" ›";opacity:.5}
.avpick[open]>summary::after{content:" ⌄"}
.avpick input[type=file]{width:100%;margin:8px 0 10px;font:inherit;font-size:.9rem;color:var(--ink2)}
.avpick input[type=file]::file-selector-button{font:inherit;font-weight:700;font-size:.85rem;cursor:pointer;
  margin-right:10px;padding:9px 14px;border:2px solid var(--border-strong);border-radius:var(--radius-pill);
  background:var(--card);color:var(--ink)}
.avopt{padding:5px;border:2px solid var(--border);border-radius:var(--radius);background:var(--card);cursor:pointer}
.avopt img{width:100%;aspect-ratio:1;object-fit:contain;border-radius:calc(var(--radius) - 3px)}
.avopt:hover{border-color:var(--coral-d)}
.avopt[aria-current=true]{border-color:var(--coral-d);box-shadow:var(--shadow-ctl)}
/* profile hub: secondary destinations as one tidy list instead of scattered buttons */
.hublist{display:flex;flex-direction:column}
.hubrow{display:flex;align-items:center;gap:12px;padding:13px 2px;text-decoration:none;color:var(--ink);
  font-weight:700;border-bottom:1px solid var(--line)}
.hubrow:last-child{border-bottom:0}
.hubrow:hover{color:var(--coral-d)}
.hubrow .hubi{font-size:1.15rem;width:26px;text-align:center;flex:none;color:var(--coral-d)}
.hubrow .hubi svg{vertical-align:-5px}
.hubrow .hubc{flex:1;min-width:0}
.hubrow .hubs{display:block;font-weight:400;font-size:.82rem;color:var(--ink2)}
.hubrow::after{content:"›";color:var(--ink2);font-size:1.3rem;line-height:1;flex:none}
/* ===== explore: the directory of shared shelves ===== */
.exnudge{--grad-far:#fdf3ee;background:linear-gradient(180deg,var(--card),var(--grad-far))}
.exgrid{display:grid;grid-template-columns:1fr;gap:12px;margin-top:4px}
@media(min-width:640px){.exgrid{grid-template-columns:repeat(auto-fill,minmax(300px,1fr))}}
.excard{display:flex;flex-direction:column;gap:10px;padding:14px;text-decoration:none;color:inherit;
  background:var(--card);border:1px solid var(--border-strong);border-radius:var(--radius);box-shadow:var(--shadow-s);
  transition:transform .16s var(--ease-out),box-shadow .16s var(--ease-out),border-color .16s var(--ease-out)}
.excard:hover{transform:translateY(-2px);border-color:var(--coral);box-shadow:var(--shadow-m)}
.excard:active{transform:translateY(0)}
.exstrip{display:flex;align-items:flex-end;gap:6px;height:92px;padding:8px 10px 6px;border-radius:var(--radius);
  background:radial-gradient(120% 100% at 50% 0%,#fffefb,#f2e6d3);box-shadow:inset 0 0 0 1px rgba(64,52,43,.06)}
.excut{flex:1 1 0;min-width:0;height:100%;display:flex;align-items:flex-end;justify-content:center}
.excut img{max-width:100%;max-height:100%;object-fit:contain;filter:drop-shadow(0 4px 4px rgba(64,52,43,.24))}
.excut.exemoji{font-size:2rem;align-items:center;opacity:.75}
.exmore{flex:none;align-self:center;font-size:.78rem;font-weight:800;color:var(--ink2)}
.exmeta{display:flex;align-items:baseline;justify-content:space-between;gap:10px;padding:0 2px}
.exname{font-family:var(--display);font-size:1.05rem;overflow-wrap:anywhere}
@media(prefers-reduced-motion:reduce){.excard{transition:none}}
/* ===== admin dashboard ===== */
.adlist{display:flex;flex-direction:column;gap:10px}
.adrow{display:grid;grid-template-columns:1.7fr 1fr 1fr auto;gap:12px;align-items:center;padding:12px 14px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius)}
.adwho{display:flex;align-items:center;gap:10px;min-width:0}
.adwho b{overflow-wrap:anywhere}
.admeta,.addates{display:flex;flex-direction:column;gap:2px;font-size:.85rem}
.admeta span{white-space:nowrap}
.adact{justify-self:end}
.small{font-size:.75rem}
.adrow.banned{opacity:.65}
.banbadge{display:inline-block;background:#b3402a;color:#fff;font-size:.6rem;font-weight:800;letter-spacing:.04em;padding:2px 7px;border-radius:6px;vertical-align:middle}
@media(max-width:640px){.adrow{grid-template-columns:1fr auto}.admeta,.addates{grid-column:1/-1;flex-direction:row;flex-wrap:wrap;gap:6px 14px}}
/* admin moderation: per-user image grid */
.adbtns{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.adbtns form{margin:0}
.modgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:12px}
.modcard{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:8px;display:flex;flex-direction:column;gap:6px}
.modimg{width:100%;aspect-ratio:1;border-radius:var(--radius);overflow:hidden;background:#f0e8da;display:flex;align-items:center;justify-content:center}
.modimg img{width:100%;height:100%;object-fit:cover}
.modemoji{font-size:44px}
.modname{font-weight:700;font-size:.85rem;text-align:center;overflow-wrap:anywhere}
/* ===== marketing homepage ===== */
.hero{text-align:center;padding:6px 0 8px}
.hero h1{font-size:2.15rem;line-height:1.1;margin-bottom:12px;text-wrap:balance;font-weight:700}
.lede{font-size:1.05rem;color:var(--ink2);max-width:40ch;margin:0 auto 22px}
.ba{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;margin:10px 0 22px}
.ba figure{margin:0;display:flex;flex-direction:column;align-items:center;gap:7px}
.ba figcaption{font-family:var(--hand);font-weight:700;font-size:1.2rem;color:var(--ink2)}
/* the "your photo" print: tilted, white-bordered, held down by a strip of washi */
.ba-before{position:relative;transform:rotate(-2.5deg)}
.ba-before::before{content:"";position:absolute;top:-10px;left:50%;transform:translateX(-50%) rotate(-4deg);width:72px;height:22px;border-radius:2px;z-index:2;
  background-color:rgba(214,167,92,.5);background-image:radial-gradient(rgba(255,255,255,.55) 1.5px,transparent 2px);background-size:10px 10px;box-shadow:0 2px 4px rgba(64,52,43,.14)}
.ba-after{transform:rotate(1.5deg)}
.ba-before img{width:150px;height:auto;border-radius:4px;box-shadow:0 10px 20px -8px rgba(64,52,43,.34);border:6px solid #fff;border-bottom-width:20px}
/* No #pb-sticker die-cut edge here. The filter floods with var(--card), so on the dark
   theme the "sticker border" comes out charcoal — a dark line drawn around the plushie
   rather than a white one — and dilating the alpha put every jagged step in the cutout
   under a magnifying glass. The glow carries the sticker read on its own. */
.ba-after img{height:190px;width:auto;object-fit:contain;filter:var(--glow,drop-shadow(0 0 0 transparent)) drop-shadow(0 12px 16px rgba(64,52,43,.24))}
.ba-arrow{font-size:1.6rem;color:var(--coral-d);font-weight:700}
.cta{font-size:1.05rem;padding:14px 22px}
.lblock{margin:38px 0}
.lblock h2{text-align:center;font-size:1.5rem;margin-bottom:18px}
/* hand-drawn coral swash under marketing section titles, the scrapbook signature */
.lblock h2::after,.finalcta h2::after{content:"";display:block;width:92px;height:10px;margin:6px auto 0;
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 92 10'%3E%3Cpath d='M2 7 C 18 3, 34 9, 47 5 S 76 3, 90 6' fill='none' stroke='%23E07A5F' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center/contain}
.lblock.band{background:var(--paper-2);border-radius:var(--radius);padding:30px 22px;box-shadow:inset 0 1px 0 rgba(255,255,255,.55),inset 0 -1px 0 rgba(64,52,43,.04)}
.steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}
.steps li{display:flex;gap:14px;align-items:flex-start;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow-s)}
.stepn{font-size:1.8rem;line-height:1;flex:none}
/* the step badge WAS the emoji, so its 1.8rem font-size was the icon's size — an SVG needs
   it spelled out, and the tint carries the step's colour instead of Apple's palette */
.stepn .bico{width:1.75rem;height:1.75rem;margin:0;color:var(--coral-d)}
.steps b{display:block;margin-bottom:2px}
.steps p{margin:0;font-size:.92rem;color:var(--ink2)}
.why{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.whyc{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow-s)}
.whyc b{display:block;margin-bottom:4px;font-size:.98rem}
.whyc p{margin:0;font-size:.86rem;color:var(--ink2)}
.finalcta{position:relative;text-align:center;--grad-far:#fbf4ec;background:linear-gradient(150deg,var(--muted),var(--grad-far));border:1px solid var(--border);border-radius:var(--radius);padding:30px 20px;box-shadow:var(--shadow-m)}
.finalcta::before{content:"";position:absolute;top:-11px;left:50%;transform:translateX(-50%) rotate(-3deg);width:88px;height:24px;border-radius:2px;
  background:repeating-linear-gradient(45deg,rgba(122,158,127,.45) 0 8px,rgba(122,158,127,.22) 8px 16px);box-shadow:0 2px 4px rgba(64,52,43,.12)}
.finalcta h2{margin-bottom:16px}
.lfoot{text-align:center;color:var(--ink2);font-size:.8rem;margin:26px 0 6px}
/* pettable fur heart (canvas fur; see /fur.js) */
.petbox{margin:30px auto 4px;text-align:center}
.furheart{width:min(190px,46vw);aspect-ratio:1/1;margin:0 auto;cursor:grab}
.furheart:active{cursor:grabbing}
.petcap{font-family:var(--hand);font-weight:700;font-size:1.35rem;color:var(--ink2);margin:14px 0 0}
.fur{position:relative;display:block;overflow:visible;touch-action:pan-y;transition:transform 420ms cubic-bezier(.32,.72,0,1)}
.fur:hover{transform:scale(1.015) rotate(-.4deg)}
.fur-canvas{position:absolute;pointer-events:none}
/* Hands off while a tool is live. The hover flourish above is charm on a shelf and a bug
   under a brush: the coat scales 1.5% and rotates, the paint overlay is a SIBLING canvas
   that does not, so every stroke lands slightly off the fur you aimed at — and letting go
   to click a button drops the hover and animates the plushie back over 420ms, which reads
   as it being dragged away and springing back. */
.furtool .fur{transition:none}
.furtool .fur:hover{transform:none}
@media(prefers-reduced-motion:reduce){.fur{transition:none}.fur:hover{transform:none}}
/* legal pages */
.legal h2{font-size:1.05rem;margin:22px 0 6px}
.legal p{color:var(--ink2);font-size:.95rem}
.lfoot a{text-decoration:none}
@media(max-width:520px){.why{grid-template-columns:1fr}.hero h1{font-size:1.9rem}.ba-after img{height:160px}}
.sclink{color:var(--coral-d);font-weight:700;text-decoration:none;font-size:.95rem}
.sclink:hover{text-decoration:underline}
/* showcase display switcher, chips flip the stage between the real display renderers */
.scsw{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:4px 0 16px}
.scstage{max-width:980px;margin:0 auto}
.scstage [data-scpane][hidden]{display:none}
.scthemes{display:flex;gap:12px;justify-content:center;margin-top:14px}
.scth{width:30px;height:30px;border-radius:var(--radius);border:0;box-shadow:inset 0 0 0 1px rgba(64,52,43,.14),inset 0 1px 0 rgba(255,255,255,.4);
  cursor:pointer;padding:0;position:relative;transition:transform .15s var(--ease-out),box-shadow .15s;touch-action:manipulation}
.scth::after{content:"";position:absolute;inset:-7px;border-radius:var(--radius)} /* 44px hit area */
.scth:hover{transform:scale(1.1)}
.scth.on{box-shadow:0 0 0 2px var(--paper),0 0 0 4px var(--coral-d),inset 0 0 0 1px rgba(64,52,43,.14)}
.scstories{display:flex;flex-direction:column;gap:12px;margin:0 0 8px}
.sccard{display:flex;gap:14px;align-items:flex-start;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;scroll-margin-top:16px;box-shadow:var(--shadow-s)}
.scdot{width:26px;height:26px;border-radius:50%;flex:none;margin-top:5px}
.sccard b.hand{font-size:1.7rem;display:block;line-height:1;margin:0}
.scmeta{font-size:.8125rem;color:var(--ink2);font-weight:600;text-transform:capitalize}
.sccard p{margin:6px 0 0;font-size:.9rem;color:var(--ink2)}
/* landing, desktop: a wide, balanced canvas (mobile keeps the centered column) */
.wrap.wrapwide{max-width:1040px}
@media(min-width:640px){
  .wrapwide .hero{padding:24px 0 12px}
  .wrapwide .hero h1{font-size:3rem;max-width:15ch;margin-inline:auto}
  .wrapwide .lede{font-size:1.15rem;max-width:48ch}
  .wrapwide .ba{gap:44px;margin:22px 0 30px}
  .wrapwide .ba-before img{width:220px}
  .wrapwide .ba-after img{height:280px}
  .wrapwide .ba-arrow{font-size:2.4rem}
  .wrapwide .cta{max-width:440px;margin-inline:auto;font-size:1.1rem}
  .wrapwide .lblock{max-width:960px;margin-left:auto;margin-right:auto}
  .wrapwide .lblock h2{font-size:1.9rem;margin-bottom:24px}
  .wrapwide .steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}
  .wrapwide .why{grid-template-columns:repeat(4,minmax(0,1fr))}
  .wrapwide .finalcta{max-width:760px;margin-left:auto;margin-right:auto;padding:36px}
}
/* ===== every colour picker, one picked-state look: 2px near-black ring always,
   and when picked it springs to 1.2x, gains a second ring in the swatch's own
   colour (--c), and shows a ✓. Packs, sticker edge, ring/clasp, strap, card
   gradient, aura. Last in the sheet so it beats the per-component rules above. ===== */
.pkcolor,.stk-sw,.krcolor,.oc-sw,.pm-sw,.aurapick label{
  box-shadow:0 0 0 2px #0e0e10;
  transition:transform .5s cubic-bezier(.34,1.56,.64,1),box-shadow .3s}
.krcolor,.oc-sw{position:relative} /* the rest already are; the ✓ needs a containing block */
.pkcolor.active,.krcolor.active,.oc-sw.active,.oc-sw[aria-pressed=true],.stk-sw.on,.pm-sw.on,.aurapick input:checked+label{
  transform:scale(1.2);
  box-shadow:0 0 0 2px #0e0e10,0 0 0 4px var(--c) !important} /* !important: aura swatches carry their glow inline, which would swallow the ring */
/* ✓ rides ::before, most of these swatches already spend ::after on their hit area */
.pkcolor::before,.krcolor::before,.oc-sw::before,.stk-sw::before,.pm-sw::before,.aurapick label::before{
  content:"✓";position:absolute;inset:0;display:grid;place-items:center;pointer-events:none;
  font-size:.95rem;font-weight:800;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.55);opacity:0;transition:opacity .3s}
.pkcolor.active::before,.krcolor.active::before,.oc-sw.active::before,.oc-sw[aria-pressed=true]::before,.stk-sw.on::before,.pm-sw.on::before,.aurapick input:checked+label::before{opacity:1}
@media(prefers-reduced-motion:reduce){
  .pkcolor,.stk-sw,.krcolor,.oc-sw,.pm-sw,.aurapick label{transition:none}}

/* ===== gotcha-day calendar, pinkyu. The native date input stays in the DOM as the form
   value and the no-JS fallback; it is only visually replaced, never removed. ===== */
.dp{position:relative}
.dpnative{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;padding:0;border:0}
.dpbtn{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;
  padding:12px 14px;border:1px solid var(--border-strong);border-radius:var(--radius-sm);
  background:var(--card);color:var(--ink);font:inherit;cursor:pointer;box-shadow:var(--shadow-ctl)}
.dpbtn:hover{background:var(--accent);color:var(--accent-foreground)}
.dppop{position:fixed;z-index:200;min-width:280px;max-width:min(320px,94vw);padding:12px;
  background:var(--popover);color:var(--popover-foreground);border:1px solid var(--border);
  border-radius:var(--radius-lg);box-shadow:var(--shadow-m)}
.dppop[hidden]{display:none}
.dphead{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.dphead b{font-family:var(--display);font-weight:600;font-size:.98rem}
.dpnav{width:30px;height:30px;display:grid;place-items:center;border:0;border-radius:var(--radius-pill);
  background:transparent;color:var(--ink);font-size:1.1rem;line-height:1;cursor:pointer}
.dpnav:hover{background:var(--accent);color:var(--accent-foreground)}
.dpgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
.dpdow{display:grid;place-items:center;height:28px;font-family:var(--display);font-size:.74rem;color:var(--muted-foreground)}
.dpday{height:34px;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--ink);
  font:inherit;font-size:.88rem;cursor:pointer;transition:background .12s,color .12s}
.dpday:hover{background:var(--accent);color:var(--accent-foreground)}
.dpday.today{box-shadow:inset 0 0 0 1.5px var(--border-strong)}
/* selected uses the theme's own primary + its readable foreground, not white on pastel */
.dpday.on{background:var(--primary);color:var(--primary-foreground);font-weight:700}
.dpfoot{display:flex;justify-content:space-between;margin-top:8px;padding-top:8px;border-top:1px solid var(--border)}
.dplink{border:0;background:none;color:var(--coral-d);font:inherit;font-size:.85rem;font-weight:700;cursor:pointer;padding:4px 6px;border-radius:var(--radius-pill)}
.dplink:hover{background:var(--accent);color:var(--accent-foreground)}
@media(prefers-reduced-motion:reduce){.dpday{transition:none}}
